Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,53 @@
/* global BaseIcon */
'use strict';
(function(exports) {
var BatteryIcon = function(manager) {
BaseIcon.call(this, manager);
};
BatteryIcon.prototype = Object.create(BaseIcon.prototype);
BatteryIcon.prototype.name = 'BatteryIcon';
BatteryIcon.prototype.update = function() {
if (!this.element) {
return;
}
var battery = this.manager._battery;
if (!battery || !Number.isFinite(battery.level)) {
this.hide();
return;
}
this.show();
var icon = this.element;
var previousLevel = parseInt(icon.dataset.level, 10);
var previousCharging = (icon.dataset.charging === 'true');
icon.dataset.charging = battery.charging;
var level = Math.floor(battery.level * 10) * 10;
this.debug('previous level: ', previousLevel);
this.debug('current level:', level);
if (previousLevel === level &&
previousCharging === battery.charging) {
return;
}
icon.dataset.level = level;
navigator.mozL10n.setAttributes(
icon,
battery.charging ? 'statusbarBatteryCharging' : 'statusbarBattery',
{level: level}
);
this.previousCharging = battery.charging;
this.publish('changed');
};
BatteryIcon.prototype.view = function view() {
return `<div id="statusbar-battery"
class="sb-icon sb-icon-battery"
role="listitem">
</div>`;
};
exports.BatteryIcon = BatteryIcon;
}(window));

View file

@ -0,0 +1,263 @@
'use strict';
/* global SettingsListener, SettingsURL, CallscreenWindow, applications */
/* global Service, LazyLoader, toneUpgrader */
/* r=? dialer+system peers for changes in this file. */
(function(exports) {
/**
*
* The delay between an incoming phone call and the first ring or vibration
* needs to be as short as possible.
*
* This simple module keeps the ringtone (blob) around and starts alerting the
* user as soon as a new incoming call is detected via the mozTelephony API.
* And it opens an AttentionWindow with the preloaded callscreen app inside.
*
* We also listen for the sleep and volumedown hardware buttons to provide
* the user with an easy way to stop the ringing.
*
* @example
* var dialerAgent = new DialerAgent();
* dialerAgent.start(); // Attach the event listeners.
* dialerAgent.stop(); // Deattach the event listeners.
*
* @class DialerAgent
* @requires SettingsListener
* @requires SettingsURL
* @requires Service
* @requires LazyLoader
*
**/
var DialerAgent = function DialerAgent() {
var telephony = navigator.mozTelephony;
if (!telephony) {
return;
}
this._telephony = telephony;
this._started = false;
this._shouldVibrate = true;
this._alerting = false;
this._vibrateInterval = null;
var player = new Audio();
this._player = player;
// XXX: This will need to be updated for bug 961967
// (audio competing in system app)
player.mozAudioChannelType = 'ringer';
player.preload = 'metadata';
player.loop = true;
this.freeCallscreenWindow = this.freeCallscreenWindow.bind(this);
this.makeFakeNotification = this.makeFakeNotification.bind(this);
};
DialerAgent.prototype.freeCallscreenWindow = function() {
/* Free the callscreen window unless there are active calls or the
* callscreen is visible. */
if (this._callscreenWindow && !this.onCall() &&
!this._callscreenWindow.isVisible()) {
this._callscreenWindow.free();
}
};
DialerAgent.prototype.start = function da_start() {
if (!this._telephony) {
return;
}
if (this._started) {
throw 'Instance should not be start()\'ed twice.';
}
this._started = true;
SettingsListener.observe('audio.volume.notification', 7, function(value) {
this._playRing();
}.bind(this));
SettingsListener.observe('dialer.ringtone', '', function(value) {
LazyLoader.load(['shared/js/settings_url.js']).then(function() {
var phoneSoundURL = new SettingsURL();
this._player.pause();
this._player.src = phoneSoundURL.set(value);
this._playRing();
}.bind(this)).catch((err) => {
console.error(err);
});
}.bind(this));
// We have new default ringtones in 2.0, so check if the version is upgraded
// then execute the necessary migration.
if (Service.query('justUpgraded')) {
LazyLoader.load('js/tone_upgrader.js').then(() => {
toneUpgrader.perform('ringtone');
});
}
SettingsListener.observe('vibration.enabled', true, function(value) {
this._shouldVibrate = !!value;
}.bind(this));
this._telephony.addEventListener('callschanged', this);
window.addEventListener('sleep', this);
window.addEventListener('wake', this);
window.addEventListener('volumedown', this);
window.addEventListener('mozmemorypressure', this.freeCallscreenWindow);
this._callscreenWindow = new CallscreenWindow();
this._callscreenWindow.hide();
if (applications && applications.ready) {
this.makeFakeNotification();
} else {
window.addEventListener('applicationready', this.makeFakeNotification);
}
Service.registerState('onCall', this);
return this;
};
DialerAgent.prototype.makeFakeNotification = function() {
window.removeEventListener('applicationready', this.makeFakeNotification);
this._callscreenWindow && this._callscreenWindow.makeNotification();
};
DialerAgent.prototype.stop = function da_stop() {
if (!this._started) {
return;
}
this._started = false;
this._telephony.removeEventListener('callschanged', this);
window.removeEventListener('sleep', this);
window.removeEventListener('wake', this);
window.removeEventListener('volumedown', this);
window.removeEventListener('mozmemorypressure', this.freeCallscreenWindow);
Service.unregisterState('onCall', this);
// TODO: should remove the settings listener once the helper
// allows it.
// See bug 981373.
};
DialerAgent.prototype.handleEvent = function da_handleEvent(evt) {
if (evt.type === 'volumedown') {
this._stopAlerting();
return;
}
if ((evt.type === 'sleep') && this._alerting) {
this._stopAlerting();
return;
}
if ((evt.type === 'sleep') && this.onCall()) {
// Hangup all calls
this._telephony.calls.forEach(call => call.hangUp());
if (this._telephony.conferenceGroup.calls.length > 0) {
this._telephony.conferenceGroup.hangUp();
}
return;
}
if ((evt.type === 'wake') && this.onCall()) {
Service.request('turnScreenOn');
return;
}
if (evt.type !== 'callschanged') {
return;
}
var calls = this._telephony.calls;
if (calls.length === 0) {
Service.request('turnScreenOn');
return;
}
var calling = calls.some(function(call) {
if (['incoming', 'dialing', 'alerting', 'connected', 'held'].indexOf(call.state) !== -1) {
return true;
}
});
if (calling) {
this.openCallscreen();
}
var incomingCall = calls[calls.length - 1];
if (this._alerting || incomingCall.state !== 'incoming') {
return;
}
var self = this;
self._startAlerting();
incomingCall.addEventListener('statechange', function callStateChange() {
incomingCall.removeEventListener('statechange', callStateChange);
self._stopAlerting();
});
};
DialerAgent.prototype._playRing = function da_playRing () {
// Notice that, even we are in the vibration mode, we would still play the
// silence ringer. That is because when the incoming call is coming, other
// playing sound should be paused. Therefore, we need to a silence ringer
// to compete for the audio output.
if (this._alerting) {
this._player.play();
}
};
DialerAgent.prototype._startAlerting = function da_startAlerting() {
this._alerting = true;
if ('vibrate' in navigator && this._shouldVibrate) {
this._vibrateInterval = window.setInterval(function vibrate() {
navigator.vibrate([200]);
}, 600);
navigator.vibrate([200]);
}
this._playRing();
};
DialerAgent.prototype._stopAlerting = function da_stopAlerting() {
var player = this._player;
this._alerting = false;
if (player && player.readyState > player.HAVE_NOTHING) {
player.pause();
player.currentTime = 0;
}
window.clearInterval(this._vibrateInterval);
};
DialerAgent.prototype.openCallscreen = function() {
if (this._callscreenWindow) {
this._callscreenWindow.ensure();
this._callscreenWindow.requestOpen();
}
};
DialerAgent.prototype.onCall = function() {
var numOpenLines = this._telephony.calls.length +
(this._telephony.conferenceGroup.calls.length ? 1 : 0);
return (numOpenLines > 0);
};
exports.DialerAgent = DialerAgent;
}(window));

View file

@ -0,0 +1,87 @@
/**
Copyright 2012, Mozilla Foundation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* globals IACHandler */
/**
* Forward all relevant events in System to the LockScreen frame.
* System --> LockScreen
*/
'use strict';
(function(exports) {
var LockScreenAgent = function(frame) {
this.configs = {
listens: [
'lockscreen-notification-request-append',
'lockscreen-notification-request-clear',
'emergency-call-leave',
'screenchange',
'timeformatchange',
'iac-mediacomms',
'appterminated', // for the music playback widget
'bluetoothprofileconnectionchange',
'lockscreen-request-mediacommand',
'lockscreen-appclosed' // necessary for state manager
]
};
this.frame = frame;
};
LockScreenAgent.prototype.start = function() {
this.startEventListeners();
};
LockScreenAgent.prototype.startEventListeners = function() {
this.configs.listens.forEach((ename) => {
window.addEventListener(ename, this);
});
};
LockScreenAgent.prototype.handleEvent = function(event) {
// XXX: This is a special case: it do not forward System events to
// LockScreen frame. Instead of, the event flow is:
//
// LockScreen -> System -> (this agent) -> IAC
//
// This is because WHEN we're in the inner iframe, comapre to the real
// app frame, LockScreen can't get the IAC port to communicate with
// media apps. So we need an agent to forward it.
//
// @see LockScreenMediaPlaybackWidget#handleEvent
if ('lockscreen-request-mediacommand' === event.type) {
var port = IACHandler ?
IACHandler.getPort('mediacomms') : null;
if (!port) {
return;
}
port.postMessage( { command: event.detail } );
return;
}
// XXX: Before we make LockScreen really as an iframe,
// do nothing and let events pass through.
// After we put LockScreen in an iframe, re-create the event and
// dispatch it to LockScreen's window.
};
LockScreenAgent.prototype.stop = function() {
this.configs.listens.forEach((ename) => {
window.removeEventListener(ename, this);
});
};
exports.LockScreenAgent = LockScreenAgent;
})(window);

View file

@ -0,0 +1,290 @@
/* globals LockScreenAgent, LazyLoader */
/* global Service */
'use strict';
(function(exports) {
var AppWindow = window.AppWindow;
/**
* This window is inherit the AppWindow, and modifies some properties
* different from the later.
*
* @constructor LockScreenWindow
* @augments AppWindow
*/
var LockScreenWindow = function() {
// Before we make lockscreen as an app (Bug 898348 ), which would
// own its own manifest, we must mock a manifest for him.
this.configs = {
url: window.location.href,
manifest: {
orientation: ['default']
},
name: 'Lockscreen',
// No manifestURL + no chrome would cause a default chrome app
manifestURL: window.location.href.replace('system', 'lockscreen') +
'/manifest.webapp',
origin: window.location.origin.replace('system', 'lockscreen'),
inputWindow: {
resizeMode: false
}
};
this.iframe = this.createFrame();
if (!this.iframe || !this.iframe.parentNode) {
// XXX: Lockscreen DOM elements are not copied.
return;
}
LazyLoader.load(['js/lockscreen_agent.js']).then(() => {
this.lockScreenAgent = new LockScreenAgent(this.iframe);
this.lockScreenAgent.start();
}).catch((err) => {
console.error(err);
});
AppWindow.call(this, this.configs);
window.dispatchEvent(new CustomEvent('lockscreen-frame-bootstrap', {
detail: this
}));
};
/**
* @borrows AppWindow.prototype as LockScreenWindow.prototype
* @memberof LockScreenWindow
*/
LockScreenWindow.prototype = Object.create(AppWindow.prototype);
LockScreenWindow.prototype.constructor = LockScreenWindow;
LockScreenWindow.prototype.isLockscreen = true;
LockScreenWindow.SUB_COMPONENTS = {
'transitionController': 'AppTransitionController',
'statusbar': 'AppStatusbar'
};
LockScreenWindow.REGISTERED_EVENTS = AppWindow.REGISTERED_EVENTS;
/**
* We still need this before we put the lockreen inside an iframe.
*
* @type LockScreen
* @memberof LockScreenWindow
*/
LockScreenWindow.prototype.lockscreen = null;
LockScreenWindow.prototype.HIERARCHY_MANAGER = 'LockScreenWindowManager';
/**
* We would maintain our own events by other components.
*
* @type string
* @memberof LockScreenWindow
*/
LockScreenWindow.prototype.eventPrefix = 'lockscreen-app';
/**
* Different animation from the original window.
*
* @type string
* @memberof LockScreenWindow
*/
LockScreenWindow.prototype.openAnimation = 'immediate';
/**
* Different animation from the original window.
*
* @type string
* @memberof LockScreenWindow
*/
LockScreenWindow.prototype.closeAnimation = 'fade-out';
LockScreenWindow.prototype._DEBUG = false;
/**
* LockScreenWindow has its own styles.
*
* @type string
* @memberof LockScreenWindow
*/
LockScreenWindow.prototype.CLASS_LIST = 'appWindow lockScreenWindow';
LockScreenWindow.prototype.CLASS_NAME = 'LockScreenWindow';
LockScreenWindow.prototype._resize = function aw__resize() {
var height, width;
// We want the lockscreen to go below the Statusbar
height = Service.query('LayoutManager.height') || window.innerHeight;
width = Service.query('LayoutManager.width') || window.innerWidth;
this.width = width;
this.height = height;
this.element.style.width = this.width + 'px';
this.element.style.height = this.height + 'px';
this.resized = true;
/**
* Fired when the app is resized.
*
* @event LockScreenWindow#lockscreen-appresize
*/
this.publish('resize');
};
/**
* Create the iframe and load it.
*
* @this {LockScreenWindow}
* @memberof LockScreenWindow
*/
LockScreenWindow.prototype.createFrame =
function lsw_createFrame() {
// XXX: Before we can make LockScreen as a real app,
// we need these.
var frame = document.getElementById('lockscreen-frame');
if (!frame) {
// XXX: This means we failed to build the system app.
// So from now on we will try to go without lockscreen.
frame = document.createElement('div');
}
frame.setVisible = function() {};
// XXX: real mozbrowser iframes would own these methods.
frame.addNextPaintListener = function(cb) {
cb();
};
frame.removeNextPaintListener = function() {};
frame.getScreenshot = function() {
// Mock the request.
return {
get onsuccess() {return null;},
set onsuccess(cb) {
var mockEvent = {
target: {result: null}
};
cb(mockEvent);
}
};
};
frame.removeAttribute('hidden');
return frame;
};
LockScreenWindow.prototype.getNotificationContainer =
function lsw_getNotificationContainer() {
// XXX: After we make LockScreen as an app, needn't this anymore.
return document.getElementById(
'notifications-lockscreen-container');
};
LockScreenWindow.prototype._resize =
function lsw__resize() {
var height, width;
this.debug('force RESIZE...');
if (this.inputWindow && typeof this.inputWindow.isActive === 'function' &&
this.inputWindow.isActive()) {
/**
* The event is dispatched on the app window only when keyboard is up.
*
* @access private
* @event LockScreenWindow~_withkeyboard
*/
this.broadcast('withkeyboard');
} else {
/**
* The event is dispatched on the LockScreen window only
* when keyboard is hidden.
*
* @access private
* @event LockScreenWindow~_withoutkeyboard
*/
this.broadcast('withoutkeyboard');
}
height = this.layoutHeight();
width = this.layoutWidth();
this.width = width;
this.height = height;
this.element.style.width = this.width + 'px';
this.element.style.height = this.height + 'px';
this.browser.element.style.width = '';
this.browser.element.style.height = '';
this.resized = true;
if (this.screenshotOverlay) {
this.screenshotOverlay.style.visibility = '';
}
/**
* Fired when the app is resized.
*
* @event LockScreenWindow#lockscreen-appresize
*/
this.publish('resize');
this.debug('W:', this.width, 'H:', this.height);
};
// XXX Bug 1085226: Before we make LockScreen use real keyboard, we need this.
LockScreenWindow.prototype.layoutHeight =
function lwm_layoutHeight() {
// Whether we can resize or not (depends on if the content
// is inside an iframe or not).
if (!this.configs.inputWindow.resizeMode) {
return window.innerHeight;
}
var softwareButtonHeight = this.isActive() ?
0 : (Service.query('SoftwareButtonManager.height') || 0);
var inputWindowHeight = 0;
if (this.states.instance && this.states.instance.inputWindow &&
typeof this.states.instance.inputWindow.isActive === 'function' &&
this.states.instance.inputWindow.isActive()) {
inputWindowHeight = this.configs.inputWindow.height;
}
var height = window.innerHeight -
inputWindowHeight -
softwareButtonHeight;
// Normalizing the height so that it always translates to an integral
// number of device pixels
var dpx = window.devicePixelRatio;
if ((height * dpx) % 1 !== 0) {
height = Math.ceil(height * dpx) / dpx;
}
return height;
};
LockScreenWindow.prototype.layoutWidth =
function() {
return window.innerWidth;
};
LockScreenWindow.prototype.lockOrientation =
function() {
// XXX: When we turn the screen on, try to lock the orientation
// until it works. It may fail at the moment the screenchange
// event has been fired, so we may need to try it several times.
var tryLockOrientation = () => {
if (screen.mozLockOrientation('portrait-primary')) {
if (!this.orientationLockID) {
throw new Error('No orientation ID. This function should only' +
'be invoked as a interval callback');
}
window.clearInterval(this.orientationLockID);
this.orientationLockID = null;
}
};
if (Service.query('isOnRealDevice')) {
if (this.orientationLockID) {
// The previous one still present and was not cleared,
// so do nothing.
return;
}
this.orientationLockID =
window.setInterval(tryLockOrientation, 4);
// 4ms is the minimum interval according to W3C#setTimeout standard.
}
};
exports.LockScreenWindow = LockScreenWindow;
})(window);

View file

@ -0,0 +1,834 @@
/* global LazyLoader, MediaPlaybackWidget, Service,
SettingsListener, SettingsURL, toneUpgrader, mozIntl */
'use strict';
var NotificationScreen = {
name: 'NotificationScreen',
TOASTER_TIMEOUT: 3500,
TRANSITION_FRACTION: 0.30,
TRANSITION_DURATION: 200,
SWIPE_INERTIA: 100,
TAP_THRESHOLD: 10,
SCROLL_THRESHOLD: 10,
CLEAR_DELAY: 80,
_notification: null,
_containerWidth: null,
_touchStartX: 0,
_touchStartY: 0,
_touchPosX: 0,
_touching: false,
_isTap: false,
_toasterTimeout: null,
lockscreenPreview: true,
silent: false,
vibrates: true,
isResending: false,
resendReceived: 0,
resendExpecting: 0,
/* These applications' notifications will be added in the "priority"
* notification group
*/
PRIORITY_APPLICATIONS: [
window.location.origin.replace('system.', 'network-alerts.') +
'/manifest.webapp'
],
/* These applications' notifications will not be notified, it means we won't
* have:
* - the banner
* - the vibration
* - the sound
*/
SILENT_APPLICATIONS: [
window.location.origin.replace('system.', 'network-alerts.') +
'/manifest.webapp'
],
getLockScreenContainer: function ns_getLockScreenContainer() {
// XXX: Bug 1057198 add this as a workaround before we truly
// make LockScreen as an app.
var lw = Service.query('LockScreenWindowManager.getInstance');
if (lw && lw.getNotificationContainer()) {
return lw.getNotificationContainer();
} else {
return null;
}
},
start: function() {
window.addEventListener('mozChromeNotificationEvent', this);
this.notificationsContainer =
document.getElementById('notifications-container');
this.container =
document.getElementById('desktop-notifications-container');
this.lockScreenContainer = this.getLockScreenContainer();
this.toaster = document.getElementById('notification-toaster');
this.ambientIndicator = document.getElementById('ambient-indicator');
this.toasterIcon = document.getElementById('toaster-icon');
this.toasterTitle = document.getElementById('toaster-title');
this.toasterDetail = document.getElementById('toaster-detail');
this.clearAllButton = document.getElementById('notification-clear');
['tap', 'touchstart', 'touchmove', 'touchend', 'touchcancel', 'wheel'].
forEach(function(evt) {
this.container.addEventListener(evt, this);
this.toaster.addEventListener(evt, this);
}, this);
this.clearAllButton.addEventListener('click', this.clearAll.bind(this));
// will hold the count of external contributors to the notification
// screen
this.externalNotificationsCount = 0;
this.unreadNotifications = [];
window.addEventListener('utilitytrayshow', this);
// Since UI expect there is a slight delay for the opened notification.
window.addEventListener('lockscreen-appclosed', this);
window.addEventListener('visibilitychange', this);
window.addEventListener('ftuopen', this);
window.addEventListener('ftudone', this);
window.addEventListener('desktop-notification-resend', this);
this._sound = 'style/notifications/ringtones/notifier_firefox.opus';
// set up the media playback widget, but only if |MediaPlaybackWidget| is
// defined (we don't define it in tests)
LazyLoader.load(['js/media_playback.js']).then(function() {
this.mediaPlaybackWidget = new MediaPlaybackWidget(
document.getElementById('media-playback-container'),
{nowPlayingAction: 'openapp'});
}.bind(this))['catch'](function(err) {
console.error(err);
});
var self = this;
SettingsListener.observe('notification.ringtone', '', function(value) {
LazyLoader.load(['shared/js/settings_url.js']).then(function() {
if (!self.ringtoneURL) {
self.ringtoneURL = new SettingsURL();
}
self._sound = self.ringtoneURL.set(value);
})['catch'](function(err) {
console.error(err);
});
});
// We have new default ringtones in 2.0, so check if the version is upgraded
// then execute the necessary migration.
if (Service.query('justUpgraded')) {
LazyLoader.load('js/tone_upgrader.js').then(function() {
toneUpgrader.perform('alerttone');
})['catch'](function(err) {
console.error(err);
});
}
Service.register('clearAll', this);
Service.register('addUnreadNotification', this);
Service.register('removeUnreadNotification', this);
this.resendStoredNotifications();
},
handleEvent: function ns_handleEvent(evt) {
switch (evt.type) {
case 'mozChromeNotificationEvent':
var detail = evt.detail;
switch (detail.type) {
case 'desktop-notification':
this.addNotification(detail);
if (this.isResending) {
this.resendReceived++;
this.isResending = (this.resendReceived < this.resendExpecting);
}
break;
case 'desktop-notification-close':
this.removeNotification(detail.id);
break;
}
break;
case 'tap':
var target = evt.target;
this.tap(target);
break;
case 'touchstart':
this.touchstart(evt);
break;
case 'touchmove':
this.touchmove(evt);
break;
case 'touchend':
this.touchend(evt);
break;
case 'touchcancel':
this.touchcancel(evt);
break;
case 'wheel':
this.wheel(evt);
break;
case 'utilitytrayshow':
this.updateTimestamps();
this.hideNotificationIndicator();
break;
case 'visibilitychange':
//update timestamps in lockscreen notifications
if (!document.hidden) {
this.updateTimestamps();
}
break;
case 'ftuopen':
this.toaster.removeEventListener('tap', this);
break;
case 'ftudone':
this.toaster.addEventListener('tap', this);
this.updateNotificationIndicator();
break;
case 'desktop-notification-resend':
this.resendExpecting = evt.detail.number;
if (this.resendExpecting) {
this.isResending = true;
}
break;
case 'lockscreen-appclosed':
// UX require to delay to clear notifications from
// LockScreen for the actionable LockScreen notifications.
setTimeout((function() {
this.clearLockScreen();
}).bind(this), 400);
break;
}
},
cancelSwipe: function ns_cancelSwipe() {
var notification = this._notification;
this._notification = null;
// If the notification has been moved, animate it back to its original
// position.
if (this._touchPosX) {
notification.addEventListener('transitionend',
function trListener() {
notification.removeEventListener('transitionend', trListener);
notification.classList.remove('snapback');
});
notification.classList.add('snapback');
}
notification.style.transform = '';
},
// Swipe handling
touchstart: function ns_touchstart(evt) {
if (evt.touches.length !== 1) {
if (this._touching) {
this._touching = false;
this.cancelSwipe();
}
return;
}
var target = evt.touches[0].target;
if (!target.dataset.notificationId) {
return;
}
this._notification = target;
this._containerWidth = this.container.clientWidth;
this._touchStartX = evt.touches[0].pageX;
this._touchStartY = evt.touches[0].pageY;
this._touchPosX = 0;
this._touchStartTime = evt.timeStamp;
this._touching = true;
this._isTap = true;
},
touchmove: function ns_touchmove(evt) {
if (!this._touching) {
return;
}
var touchDiffY = evt.touches[0].pageY - this._touchStartY;
// The notification being touched is the toast
if (this._notification.classList.contains('displayed')) {
if (touchDiffY < 0) {
this.closeToast();
}
return;
}
// Otherwise, we're swiping on a notification, which should only be allowed
// when the utility tray is fully open.
if (evt.touches.length !== 1 ||
!Service.query('UtilityTray.shown') ||
(this._isTap && Math.abs(touchDiffY) >= this.SCROLL_THRESHOLD)) {
this._touching = false;
this.cancelSwipe();
return;
}
this._touchPosX = evt.touches[0].pageX - this._touchStartX;
if (Math.abs(this._touchPosX) >= this.TAP_THRESHOLD) {
this._isTap = false;
this.notificationsContainer.style.overflow = 'hidden';
}
if (!this._isTap) {
evt.preventDefault();
evt.stopPropagation(); // Prevent this event from bubbling up to the tray.
this._notification.style.transform =
'translateX(' + this._touchPosX + 'px)';
}
},
touchend: function ns_touchend(evt) {
if (!this._touching) {
return;
}
evt.preventDefault();
this._touching = false;
this.notificationsContainer.style.overflow = '';
if (this._isTap) {
var event = new CustomEvent('tap', {
bubbles: true,
cancelable: true
});
this._notification.dispatchEvent(event);
this._notification = null;
return;
}
// Taking speed into account to detect swipes
var deltaT = evt.timeStamp - this._touchStartTime;
var speed = this._touchPosX / deltaT;
var inertia = speed * this.SWIPE_INERTIA;
var finalDelta = Math.abs(this._touchPosX + inertia);
if (finalDelta >
this._containerWidth * this.TRANSITION_FRACTION) {
if (this._touchPosX < 0) {
this._notification.classList.add('left');
}
var durationLeft = (1 - (finalDelta / this._containerWidth)) *
this.TRANSITION_DURATION;
this.swipeCloseNotification(durationLeft);
} else {
this.cancelSwipe();
}
},
touchcancel: function ns_touchcancel(evt) {
if (this._touching) {
evt.preventDefault();
this._touching = false;
this.cancelSwipe();
}
},
wheel: function ns_wheel(evt) {
if (evt.deltaMode === evt.DOM_DELTA_PAGE && evt.deltaX) {
this._notification = evt.target;
this.swipeCloseNotification();
}
},
tap: function ns_tap(node) {
var notificationId = node.dataset.notificationId;
var notificationNode = this.container.querySelector(
'[data-notification-id="' + notificationId + '"]');
var event = document.createEvent('CustomEvent');
event.initCustomEvent('mozContentNotificationEvent', true, true, {
type: 'desktop-notification-click',
id: notificationId
});
window.dispatchEvent(event);
window.dispatchEvent(new CustomEvent('notification-clicked', {
detail: {
id: notificationId
}
}));
// Desktop notifications are removed when they are clicked (see bug 890440)
if (notificationNode &&
notificationNode.dataset.type === 'desktop-notification' &&
notificationNode.dataset.obsoleteAPI === 'true') {
this.closeNotification(notificationNode);
}
if (node == this.toaster) {
this.closeToast();
} else {
Service.request('UtilityTray:hide');
}
},
updateTimestamps: function ns_updateTimestamps() {
var timestamps = [...document.querySelectorAll('.timestamp')];
var formatter = mozIntl._gaia.RelativeDate(navigator.languages, {
style: 'short'
});
timestamps.forEach(timestamp => {
formatter.formatElement(
timestamp, new Date(timestamp.dataset.timestamp));
});
},
updateToaster: function ns_updateToaster(detail, type, dir) {
if (detail.icon) {
this.toasterIcon.src = detail.icon;
this.toasterIcon.hidden = false;
} else {
this.toasterIcon.hidden = true;
}
this.toaster.dataset.notificationId = detail.id;
this.toaster.dataset.type = type;
this.toaster.lang = detail.lang;
this.toasterTitle.textContent = detail.title;
this.toasterTitle.dir = dir;
this.toasterDetail.textContent = detail.text;
this.toasterDetail.dir = dir;
},
addNotification: function ns_addNotification(detail) {
// LockScreen window may not opened while this singleton got initialized.
this.lockScreenContainer = this.getLockScreenContainer();
var manifestURL = detail.manifestURL || '';
var behavior = detail.mozbehavior || {};
var isPriorityNotification =
this.PRIORITY_APPLICATIONS.indexOf(manifestURL) !== -1;
var dir = detail.dir === 'ltr' || detail.dir === 'rtl' ?
detail.dir : 'auto';
var notificationContainer =
(isPriorityNotification) ?
this.container.querySelector('.priority-notifications') :
this.container.querySelector('.other-notifications');
// We need to animate the ambient indicator when the toast
// timesout, so we skip updating it here, by passing a skip bool
this.addUnreadNotification(detail.id, true);
var notificationNode = document.createElement('div');
notificationNode.classList.add('notification');
notificationNode.setAttribute('role', 'link');
notificationNode.setAttribute('tabindex', 0);
notificationNode.dataset.notificationId = detail.id;
notificationNode.dataset.noClear = behavior.noclear ? 'true' : 'false';
notificationNode.lang = detail.lang;
notificationNode.dataset.obsoleteAPI = 'false';
if (typeof detail.id === 'string' &&
detail.id.indexOf('app-notif-') === 0) {
notificationNode.dataset.obsoleteAPI = 'true';
}
var type = detail.type || 'desktop-notification';
notificationNode.dataset.type = type;
notificationNode.dataset.manifestURL = manifestURL;
var icon;
if (detail.icon) {
icon = document.createElement('img');
icon.src = detail.icon;
icon.setAttribute('role', 'presentation');
notificationNode.appendChild(icon);
}
var titleContainer = document.createElement('div');
titleContainer.classList.add('title-container');
var title = document.createElement('div');
title.classList.add('title');
title.textContent = detail.title;
title.dir = dir;
titleContainer.appendChild(title);
var time = document.createElement('span');
var timestamp = detail.timestamp ? new Date(detail.timestamp) : new Date();
time.classList.add('timestamp');
time.dataset.timestamp = timestamp;
var formatter = mozIntl._gaia.RelativeDate(navigator.languages, {
style: 'short'
});
formatter.formatElement(time, timestamp);
titleContainer.appendChild(time);
notificationNode.appendChild(titleContainer);
var message = document.createElement('div');
message.classList.add('detail');
var messageContent = document.createElement('div');
messageContent.classList.add('detail-content');
messageContent.textContent = detail.text;
messageContent.dir = dir;
message.appendChild(messageContent);
notificationNode.appendChild(message);
var notifSelector = '[data-notification-id="' + detail.id + '"]';
var oldNotif = notificationContainer.querySelector(notifSelector);
if (oldNotif) {
// The whole node cannot be replaced because CSS animations are re-started
oldNotif.replaceChild(titleContainer,
oldNotif.querySelector('.title-container'));
oldNotif.replaceChild(message, oldNotif.querySelector('.detail'));
var oldIcon = oldNotif.querySelector('img');
if (icon) {
oldIcon ? oldIcon.src = icon.src : oldNotif.insertBefore(icon,
oldNotif.firstChild);
} else if (oldIcon) {
oldNotif.removeChild(oldIcon);
}
// but we still need to update type and lang.
oldNotif.dataset.type = type;
oldNotif.lang = detail.lang;
notificationNode = oldNotif;
} else {
notificationContainer.insertBefore(notificationNode,
notificationContainer.firstElementChild);
}
var event = document.createEvent('CustomEvent');
event.initCustomEvent('mozContentNotificationEvent', true, true, {
type: 'desktop-notification-show',
id: detail.id
});
window.dispatchEvent(event);
// We turn the screen on if needed in order to let
// the user see the notification toaster unless we're on a call
if (!Service.query('screenEnabled') &&
!behavior.noscreen &&
!Service.query('onCall') &&
(!Service.query('locked') || this.lockscreenPreview)) {
Service.request('turnScreenOn');
}
var notify = !('noNotify' in detail) &&
// don't notify for network-alerts notifications
(this.SILENT_APPLICATIONS.indexOf(manifestURL) === -1);
// Notification toaster
if (notify) {
this.updateToaster(detail, type, dir);
if (this.lockscreenPreview || !window.Service.query('locked')) {
this.toaster.classList.add('displayed');
if (this._toasterTimeout) {
clearTimeout(this._toasterTimeout);
}
this._toasterTimeout = setTimeout((function() {
this.closeToast();
this._toasterTimeout = null;
}).bind(this), this.TOASTER_TIMEOUT);
}
}
// Adding it to the lockscreen if locked and the privacy setting
// does not prevent it.
if (Service.query('locked') && this.lockscreenPreview) {
this.addLockScreenNotification(detail.id,
notificationNode.cloneNode(true));
}
if (notify && !this.isResending &&
(!Service.query('locked') || this.lockscreenPreview)) {
if (!this.silent && !behavior.nosound) {
var ringtonePlayer = new Audio();
var telephony = window.navigator.mozTelephony;
ringtonePlayer.src = behavior.soundFile || this._sound;
if (telephony && telephony.active) {
ringtonePlayer.mozAudioChannelType = 'telephony';
ringtonePlayer.volume = 0.3;
} else {
ringtonePlayer.mozAudioChannelType = 'notification';
}
ringtonePlayer.play();
window.setTimeout(function smsRingtoneEnder() {
ringtonePlayer.pause();
ringtonePlayer.removeAttribute('src');
ringtonePlayer.load();
}, 4000);
}
if (this.vibrates && !behavior.novibrate) {
var pattern = [200, 200, 200];
if (behavior.vibrationPattern && behavior.vibrationPattern.length &&
behavior.vibrationPattern[0] > 0) {
pattern = behavior.vibrationPattern;
}
// bug 1030310: disable vibration for the email app when asleep
// bug 1050023: disable vibration for downloads when asleep
var vibrateWhenAsleep =
type.indexOf('download-notification-downloading') === -1 &&
manifestURL.indexOf('email.gaiamobile.org') === -1;
if (vibrateWhenAsleep || !document.hidden) {
navigator.vibrate(pattern);
}
}
}
// must be at least one notification now
this.clearAllButton.disabled = false;
return notificationNode;
},
/**
* Give a notification node and add it to LockScreen via event.
*/
addLockScreenNotification: function ns_addLockScreenNotification(id, node) {
window.dispatchEvent(
new window.CustomEvent('lockscreen-notification-request-append', { detail:
{
id: id,
node: node
}
}));
},
swipeCloseNotification: function ns_swipeCloseNotification(duration) {
var notification = this._notification;
this._notification = null;
var toaster = this.toaster;
var self = this;
notification.addEventListener('transitionend', function trListener() {
notification.removeEventListener('transitionend', trListener);
self.closeNotification(notification);
if (notification != toaster) {
return;
}
// Putting back the toaster in a clean state for the next notification
toaster.style.display = 'none';
setTimeout(function nextLoop() {
toaster.style.MozTransition = '';
toaster.style.MozTransform = '';
toaster.classList.remove('displayed');
toaster.classList.remove('disappearing');
setTimeout(function nextLoop() {
toaster.style.display = 'block';
});
});
});
notification.classList.add('disappearing');
duration = duration || this.TRANSITION_DURATION;
notification.style.transitionDuration = duration + 'ms';
notification.style.transform = '';
},
addUnreadNotification: function ns_addUnreadNotification(id, skipUpdate) {
if (Service.query('UtilityTray.shown')) {
return;
}
this.unreadNotifications.push(id);
if (!skipUpdate) {
this.updateNotificationIndicator();
}
},
removeUnreadNotification: function ns_removeUnreadNotification(id) {
var notifIndex = this.unreadNotifications.indexOf(id);
if (notifIndex > -1) {
this.unreadNotifications.splice(notifIndex, 1);
}
this.updateNotificationIndicator();
},
hideNotificationIndicator: function ns_hideNotificationIndicator() {
if (this.unreadNotifications.length > 0) {
this.unreadNotifications = [];
}
this.updateNotificationIndicator();
},
updateNotificationIndicator: function ns_updateNotificationIndicator() {
if (this.unreadNotifications.length) {
// If the ftu is running we should not show the ambient indicator
// because the statusbar is not accessible
if (Service.query('isFtuRunning')) {
return;
}
this.ambientIndicator.classList.add('unread');
navigator.mozL10n.setAttributes(
this.ambientIndicator,
'statusbarNotifications-unread',
{n: this.unreadNotifications.length}
);
} else {
this.ambientIndicator.classList.remove('unread');
this.ambientIndicator.removeAttribute('aria-label');
}
Service.request('UtilityTray:updateNotificationCount');
},
closeToast: function ns_closeToast() {
this.toaster.classList.remove('displayed');
this.updateNotificationIndicator();
},
closeNotification: function ns_closeNotification(notificationNode) {
var notificationId = notificationNode.dataset.notificationId;
this.removeNotification(notificationId);
},
removeLockScreenNotification:
function ns_removeLockScreenNotification(notificationId) {
var lockScreenNotificationNode;
var notifSelector = '[data-notification-id="' + notificationId + '"]';
this.lockScreenContainer = this.getLockScreenContainer();
if (this.lockScreenContainer) {
lockScreenNotificationNode =
this.lockScreenContainer.querySelector(notifSelector);
}
if (lockScreenNotificationNode) {
var lockScreenNotificationParentNode =
lockScreenNotificationNode.parentNode;
lockScreenNotificationParentNode.removeChild(lockScreenNotificationNode);
window.dispatchEvent(
new window.CustomEvent('lockscreen-notification-request-remove', {
containerEmpty: !lockScreenNotificationParentNode.firstElementChild
}));
}
},
removeNotification: function ns_removeNotification(notificationId) {
var notifSelector = '[data-notification-id="' + notificationId + '"]';
var notificationNode = this.container.querySelector(notifSelector);
if (notificationNode) {
notificationNode.remove();
}
var event = document.createEvent('CustomEvent');
event.initCustomEvent('mozContentNotificationEvent', true, true, {
type: 'desktop-notification-close',
id: notificationId
});
window.dispatchEvent(event);
// UX require to give a tiny delay for actionable notification on
// LockScreen.
setTimeout((function() {
this.removeLockScreenNotification(notificationId);
}).bind(this), 400);
this.removeUnreadNotification(notificationId);
if (!this.container.querySelector('.notification')) {
// no notifications left
this.clearAllButton.disabled = true;
}
},
clearAll: function ns_clearAll() {
var notifications = this.container.querySelectorAll('.notification');
var clearable = [].slice.apply(notifications)
.filter(function isClearable(notification) {
return notification.dataset.noClear !== 'true';
});
var notification;
this.clearAllButton.disabled = true;
// When focus is on a disabled element, Gecko will not dispatch any keyboard
// events to it (the disabled element) and its parent.
// The clearAll Button is focused but disabled right away after
// user click it. We need to blur the focus immediately, otherwise System
// app could not receive any keyboard events. See more detail on
// http://bugzil.la/1106844
this.clearAllButton.blur();
if (!clearable.length) {
return;
}
// Adding a callback to the last cleared notification to defer
// the destroying of the notifications after the last disappear
var lastClearable = clearable[clearable.length - 1];
var removeAll = (function removeAll() {
lastClearable.removeEventListener('transitionend', removeAll);
for (var notification of clearable) {
this.closeNotification(notification);
}
}).bind(this);
lastClearable.addEventListener('transitionend', removeAll);
var disappearingClass = document.dir === 'ltr' ?
'disappearing-via-clear-all' : 'disappearing-via-clear-all-left';
for (var index = 0, max = clearable.length; index < max; index++) {
notification = clearable[index];
notification.style.transitionDelay = (this.CLEAR_DELAY * index) + 'ms';
notification.classList.add(disappearingClass);
notification.style.transform = '';
}
},
clearLockScreen: function ns_clearLockScreen() {
// The LockScreenWindow may not be instantiated yet.
if (!this.lockScreenContainer) {
return;
}
while (this.lockScreenContainer.firstElementChild) {
var element = this.lockScreenContainer.firstElementChild;
this.lockScreenContainer.removeChild(element);
}
window.dispatchEvent(
new window.CustomEvent('lockscreen-notification-request-clear'));
},
resendStoredNotifications: function() {
if ('mozSettings' in navigator && navigator.mozSettings) {
var key = 'notifications.resend';
var req = navigator.mozSettings.createLock().get(key);
req.onsuccess = req.onerror = function onsuccess() {
var resendEnabled = req.result[key] || false;
if (!resendEnabled) {
return;
}
var resendCallback = (function(number) {
window.dispatchEvent(
new CustomEvent('desktop-notification-resend',
{ detail: { number: number } }));
}).bind(this);
if ('mozChromeNotifications' in navigator) {
navigator.mozChromeNotifications.
mozResendAllNotifications(resendCallback);
}
};
}
}
};
SettingsListener.observe(
'lockscreen.notifications-preview.enabled', true, function(value) {
NotificationScreen.lockscreenPreview = value;
});
SettingsListener.observe('audio.volume.notification', 7, function(value) {
NotificationScreen.silent = (value === 0);
});
SettingsListener.observe('vibration.enabled', true, function(value) {
NotificationScreen.vibrates = value;
});

View file

@ -0,0 +1,690 @@
'use strict';
/* globals SettingsListener, Service,
ScreenBrightnessTransition, ScreenWakeLockManager,
ScreenAutoBrightness, LazyLoader */
(function(exports) {
var ScreenManager = {
name: 'ScreenManager',
/*
* return the current screen status
* Must not mutate directly - use toggleScreen/turnScreenOff/turnScreenOn.
* Listen to 'screenchange' event to properly handle status changes
* This value can be "out of sync" with real mozPower value,
* we do this to give screen some time to flash before actual turn off.
*/
screenEnabled: false,
/**
* If user is unlocking, postpone the timeout counter.
*/
_unlocking: false,
/*
* before idle-screen-off, invoke a nice dimming to the brightness
* to notify the user that the screen is about to be turn off.
* The user can cancel the idle-screen-off by touching the screen
* and by pressing a button (trigger onactive callback on Idle API)
*
*/
_inTransition: false,
/*
* Whether the device light is enabled or not
* sync with setting 'screen.automatic-brightness'
*/
_deviceLightEnabled: true,
/*
* Preferred brightness without considering device light nor dimming
* sync with setting 'screen.brightness'
*/
_userBrightness: 1,
_savedBrightness: 1,
/*
* This property will host a ScreenBrightnessTransition instance
* and control the brightness transition for us.
* Eventually we want to move all brightness controls
* (including auto-brightness toggle and calculation) out of this module.
*/
_screenBrightnessTransition: null,
/*
* ScreenAutoBrightness instance
* manages the devicelight events and adjusts the screen brightness
* automatically
*/
_screenAutoBrightness: null,
/**
* Timeout to black the screen when locking.
*/
LOCKING_TIMEOUT: 10,
/*
* Wait for _dimNotice milliseconds during idle-screen-off
*/
_dimNotice: 10 * 1000,
/*
* We track the value of the idle timeout pref in this variable.
*/
_idleTimeout: 0,
_idleTimerId: 0,
/*
* To track the reason caused screen off?
*/
_screenOffBy: null,
_screenOffTimeout: 0,
/*
* Request wakelock during in_call state.
* To ensure turnScreenOff by proximity event is protected by wakelock for
* early suspend only.
*/
_cpuWakeLock: null,
/*
* A screen reader feature to allow screen brightness to remain 0
* regardless of the enabled state of the screen. This allows the user to
* conserve battery and have more privacy while still using the touchscreen.
*/
_screenShade: false,
start: function() {
window.addEventListener('attentionopening', this);
window.addEventListener('attentionopened', this);
window.addEventListener('sleep', this);
window.addEventListener('wake', this);
window.addEventListener('nfc-tech-discovered', this);
window.addEventListener('nfc-tech-lost', this);
window.addEventListener('requestshutdown', this);
// User is unlocking by sliding or other methods.
window.addEventListener('unlocking-start', this);
window.addEventListener('unlocking-stop', this);
// When secure app is on, do not turn the screen off.
// And when it's down, reset the timeout.
window.addEventListener('secure-appopened', this);
window.addEventListener('secure-appterminated', this);
window.addEventListener('logohidden', this);
// User is actively using the screen reader.
window.addEventListener('accessibility-action', this);
this.screen = document.getElementById('screen');
LazyLoader.load([
'js/screen_auto_brightness.js',
'js/screen_brightness_transition.js'
]).then(function() {
this._screenBrightnessTransition = new ScreenBrightnessTransition();
this._screenAutoBrightness = new ScreenAutoBrightness();
this._screenAutoBrightness.onbrightnesschange = function(brightness) {
this.setScreenBrightness(brightness, true);
}.bind(this);
this._screenBrightnessTransition.ontransitionbegin =
this._screenAutoBrightness.pause.bind(this._screenAutoBrightness);
this._screenBrightnessTransition.ontransitionend =
this._screenAutoBrightness.resume.bind(this._screenAutoBrightness);
}.bind(this))['catch'](function(err) {
console.error(err);
});
var self = this;
var power = navigator.mozPower;
// Start the screen wake lock manager so it will monitor screen wake lock
// for us. We will need to re-config the screen timeout
// when the lock state is changed.
//
// Noted that getting a lock while the screen is off will not
// turn on the screen, since no frame is considered visible by Gecko when
// the screen is off. See discussion in bug 818840.
this._wakeLockManager = new ScreenWakeLockManager();
this._wakeLockManager.onwakelockchange =
this._reconfigScreenTimeout.bind(this);
this._wakeLockManager.start();
this._firstOn = false;
SettingsListener.observe('screen.timeout', 60,
function screenTimeoutChanged(value) {
if (typeof value !== 'number') {
value = parseInt(value);
}
self._idleTimeout = value;
self._setIdleTimeout(self._idleTimeout);
if (!self._firstOn) {
self._firstOn = true;
// During boot up, the brightness was set by bootloader as 0.5,
// Let's set the API value to that so setScreenBrightness() can
// dim nicely to value set by user.
power.screenBrightness = 0.5;
// Turn screen on with dim.
self.turnScreenOn(false);
}
});
SettingsListener.observe('screen.automatic-brightness', true,
function deviceLightSettingChanged(value) {
self.setDeviceLightEnabled(value);
});
SettingsListener.observe('screen.brightness', 1,
function brightnessSettingChanged(value) {
// Profiles from the legacy range control may contain numeric strings.
var brightness = Number(value);
if (!Number.isFinite(brightness)) return;
self._userBrightness = Math.max(0, Math.min(1, brightness));
// Follow the slider immediately; lock/wake transitions keep their fades.
self.setScreenBrightness(self._userBrightness, true);
});
var telephony = window.navigator.mozTelephony;
if (telephony) {
telephony.addEventListener('callschanged', this);
}
Service.registerState('screenEnabled', this);
Service.register('turnScreenOn', this);
Service.register('turnScreenOff', this);
Service.register('turnShadeOn', this);
Service.register('turnShadeOff', this);
},
_getNumOfCalls: function scm_getNumOfCalls() {
if (navigator.mozTelephony) {
return navigator.mozTelephony.calls.length +
(navigator.mozTelephony.conferenceGroup.calls.length ? 1 : 0);
} else {
return 0;
}
},
handleEvent: function scm_handleEvent(evt) {
var telephony = window.navigator.mozTelephony;
var call;
switch (evt.type) {
case 'attentionopening':
case 'attentionopened':
if (!this.enabled) {
this.turnScreenOn();
}
break;
case 'devicelight':
if (!this._deviceLightEnabled || !this.screenEnabled ||
this._inTransition) {
return;
}
this._screenAutoBrightness &&
this._screenAutoBrightness.autoAdjust(evt.value);
break;
case 'sleep':
// If calls are present then the power button will be handled by the
// dialer agent.
if (this._getNumOfCalls() === 0) {
this.turnScreenOff(true, 'powerkey');
}
break;
case 'wake':
// If calls are present then the power button will be handled by the
// dialer agent.
if (this._getNumOfCalls() === 0) {
this.turnScreenOn();
}
break;
case 'accessibility-action':
this._reconfigScreenTimeout();
break;
case 'nfc-tech-discovered':
case 'nfc-tech-lost':
if (this._inTransition) {
this.turnScreenOn();
} else {
this._reconfigScreenTimeout();
}
break;
case 'unlocking-start':
this._setUnlocking();
break;
// When secure app is on, do not turn the screen off.
// And when it's down, reset the timeout.
case 'secure-appopened':
case 'secure-appterminated':
this._reconfigScreenTimeout();
break;
case 'unlocking-stop':
this._resetUnlocking();
break;
case 'userproximity':
if (Service.query('Bluetooth.isSCOProfileConnected') ||
telephony.speakerEnabled ||
Service.query('isHeadsetConnected')) {
// XXX: Remove this hack in Bug 868348
// We shouldn't access headset status from statusbar.
if (this._screenOffBy == 'proximity') {
this.turnScreenOn();
}
break;
}
if (evt.near) {
this.turnScreenOff(true, 'proximity');
} else {
this.turnScreenOn();
}
break;
case 'callschanged':
if (this._getNumOfCalls() === 0) {
this._uninstallProximityListener();
break;
}
// If the _cpuWakeLock is already set we are in a multiple
// call setup, the user will be notified by a tone.
if (this._cpuWakeLock) {
break;
}
// Enable the user proximity sensor once the call is connected.
call = telephony.calls[0];
if (call.state === 'dialing') {
this._installProximityListener();
} else {
call.addEventListener('statechange', this);
}
break;
case 'statechange':
call = evt.target;
if (['connected', 'alerting'].indexOf(call.state) === -1) {
break;
}
// The call is connected (MT call) or alerting/dialing (MO call).
// Remove the statechange listener and enable the user proximity
// sensor.
call.removeEventListener('statechange', this);
this._installProximityListener();
break;
// Reconfig screen time out after booting.
case 'logohidden':
window.removeEventListener('logohidden', this);
this._reconfigScreenTimeout();
break;
case 'lockscreen-appclosing' :
case 'lockpanelchange' :
window.removeEventListener('lockscreen-appclosing', this);
window.removeEventListener('lockpanelchange', this);
// Prevent racing: wakeLockManager may broadcast the message
// before this event, so one lockscreen event is actually representing
// different meanings depends on that.
//
// This happens partly because this handler doesn't enter the
// '_reconfigScreenTimeout' as other handlers may do, but in fact
// the centralized design of putting all different timeouts in one
// management function is not a good idea, because that fuzzes the
// real intention of each branch. So maybe a more complete solution
// is to decentralize that function, and make every small timeout
// configuring functions more clear.
//
// Another reason why just to set the timeout is bad because it
// depends on the event order heavily without checking any status
// at the moment the event comes. To perform such check can prevent
// that, too.
if (!this._wakeLockManager.isHeld) {
this._setIdleTimeout(this._idleTimeout, false);
}
break;
case 'requestshutdown':
this.turnScreenOn();
if (evt.detail && evt.detail.startPowerOff) {
evt.detail.startPowerOff(false);
}
break;
}
},
toggleScreen: function scm_toggleScreen() {
if (this.screenEnabled) {
// Currently there is no one used toggleScreen, so just set reason as
// toggle. If it is used by someone in the future, we can rename it.
this.turnScreenOff(true, 'toggle');
} else {
this.turnScreenOn();
}
},
turnShadeOn: function scm_turnShadeOn() {
this._screenShade = true;
// Remember the current screen brightness. We will restore it when
// we turn the shade back on.
this._savedBrightness = navigator.mozPower.screenBrightness;
this.setScreenBrightness(0, true);
},
turnShadeOff: function scm_turnShadeOff() {
// Bug 1185036: If the screen was disable with the shade on, we won't
// the screen won't turn on again until the user puts their finger on the
// screen again.
var brightness = this._savedBrightness || this._userBrightness || 1;
this._screenShade = false;
this.setScreenBrightness(brightness, true);
},
turnScreenOff: function scm_turnScreenOff(instant, reason) {
if (!this.screenEnabled) {
return false;
}
var self = this;
if (reason) {
this._screenOffBy = reason;
}
// Remember the current screen brightness. We will restore it when
// we turn the screen back on.
self._savedBrightness = navigator.mozPower.screenBrightness;
// Remove the cpuWakeLock and listening of proximity event, if screen is
// turned off by power key.
if (this._cpuWakeLock != null && this._screenOffBy == 'powerkey') {
window.removeEventListener('userproximity', this);
this._cpuWakeLock.unlock();
this._cpuWakeLock = null;
}
var screenOff = function scm_screenOff() {
self._setIdleTimeout(0);
if (self._deviceLightEnabled) {
window.removeEventListener('devicelight', self);
}
window.removeEventListener('lockscreen-appclosing', self);
window.removeEventListener('lockpanelchange', self);
self.screenEnabled = false;
self._inTransition = false;
clearTimeout(self._screenOffTimeout);
self._screenOffTimeout = setTimeout(function realScreenOff() {
self.setScreenBrightness(0, true);
// Sometimes the ScreenManager.screenEnabled and
// mozPower.screenEnabled values are out of sync.
// Since the rest of the world relies only on
// the value of ScreenManager.screenEnabled it can be some situations
// where the screen is off but ScreenManager think it is on... (see
// bug 822463). Ideally a callback should have been used, like
// ScreenManager.getScreenState(function(value) { ...} ); but there
// are too many places to change that for now.
self.screenEnabled = false;
navigator.mozPower.screenEnabled = false;
navigator.mozPower.keyLightEnabled = false;
self._screenAutoBrightness && self._screenAutoBrightness.pause();
}, 20);
self.fireScreenChangeEvent();
};
if (instant) {
screenOff();
return true;
}
this.setScreenBrightness(0.1, false);
this._inTransition = true;
setTimeout(function noticeTimeout() {
if (!self._inTransition) {
return;
}
screenOff();
}, self._dimNotice);
return true;
},
turnScreenOn: function scm_turnScreenOn(instant) {
clearTimeout(this._screenOffTimeout);
if (this.screenEnabled) {
if (this._inTransition) {
// Cancel the dim out
this._inTransition = false;
this.setScreenBrightness(this._savedBrightness, true);
this._reconfigScreenTimeout();
}
return false;
}
// Actually turn the screen on.
var power = navigator.mozPower;
if (power) {
power.screenEnabled = true;
power.keyLightEnabled = true;
}
this.screenEnabled = true;
// Set the brightness before the screen is on.
this.setScreenBrightness(this._savedBrightness, instant);
// Resume auto brightness, which is paused when the screen is turned off.
this._screenAutoBrightness && this._screenAutoBrightness.resume();
// If we are in a call or a conference call and there
// is no cpuWakeLock, we would get one here.
var telephony = window.navigator.mozTelephony;
var ongoingConference = telephony && telephony.conferenceGroup &&
telephony.conferenceGroup.calls.length;
if (!this._cpuWakeLock && telephony &&
(telephony.calls.length || ongoingConference)) {
var connected = telephony.calls.some(function(call) {
if (call.state == 'connected') {
return true;
}
return false;
});
if (connected || ongoingConference) {
this._cpuWakeLock = navigator.requestWakeLock('cpu');
window.addEventListener('userproximity', this);
}
}
// Attaching the event listener effectively turn on the hardware
// device light sensor, which _must be_ done after power.screenEnabled.
if (this._deviceLightEnabled) {
window.addEventListener('devicelight', this);
}
this._reconfigScreenTimeout();
this.fireScreenChangeEvent();
return true;
},
_reconfigScreenTimeout: function scm_reconfigScreenTimeout() {
// Remove idle timer if screen wake lock is acquired or
// if no app has been displayed yet.
if (this._wakeLockManager.isHeld ||
(!Service.query('AppWindowManager.getActiveWindow') &&
!Service.query('locked'))) {
this._setIdleTimeout(0);
// The screen should be turn off with shorter timeout if
// it was never unlocked.
} else if (!this._unlocking) {
if (Service.query('getTopMostWindow') &&
Service.query('getTopMostWindow').CLASS_NAME ===
'LockScreenWindow') {
this._setIdleTimeout(this.LOCKING_TIMEOUT, true);
window.addEventListener('lockscreen-appclosing', this);
window.addEventListener('lockpanelchange', this);
} else {
this._setIdleTimeout(this._idleTimeout, false);
}
}
},
/**
* If user is unlocking, postpone the timeout counter.
*
* @this {ScreenManager}
*/
_setUnlocking: function scm_setUnlocking() {
this._unlocking = true;
// Need to cancel it: the last set timeout would still be triggered.
window.clearIdleTimeout(this._idleTimerId);
},
/**
* Reset the state of user unlocking.
*
* @this {ScreenManager}
*/
_resetUnlocking: function scm_resetUnlocking() {
this._unlocking = false;
this._reconfigScreenTimeout();
},
setScreenBrightness: function scm_setScreenBrightness(brightness, instant) {
// Don't touch the brightness if the screen shade is on.
if (this._screenShade && !!brightness) {
return;
}
this._targetBrightness = brightness;
var power = navigator.mozPower;
if (!power) {
return;
}
// Stop the current transition
if (this._screenBrightnessTransition &&
this._screenBrightnessTransition.isRunning) {
this._screenBrightnessTransition.abort();
}
if (typeof instant !== 'boolean') {
instant = true;
}
if (instant) {
power.screenBrightness = brightness;
return;
}
if (this._screenBrightnessTransition) {
this._screenBrightnessTransition.transitionTo(this._targetBrightness);
} else {
power.screenBrightness = this._targetBrightness;
}
},
setDeviceLightEnabled: function scm_setDeviceLightEnabled(enabled) {
if (!enabled && this._deviceLightEnabled) {
// Disabled -- set the brightness back to preferred brightness
this.setScreenBrightness(this._userBrightness, false);
}
this._deviceLightEnabled = enabled;
this._screenAutoBrightness &&
this._screenAutoBrightness.reset();
if (!this.screenEnabled) {
return;
}
// Disable/enable device light sensor accordingly.
// This will also toggle the actual hardware, which
// must be done while the screen is on.
if (enabled) {
window.addEventListener('devicelight', this);
} else {
window.removeEventListener('devicelight', this);
}
},
_setIdleTimeout: function scm_setIdleTimeout(time, instant) {
window.clearIdleTimeout(this._idleTimerId);
// Reset the idled state back to false.
this._idled = false;
// 0 is the value used to disable idle timer by user and by us.
if (time === 0) {
return;
}
var self = this;
var idleCallback = function idle_proxy() {
self.turnScreenOff(instant, 'idle_timeout');
};
var activeCallback = function active_proxy() {
self.turnScreenOn(true);
};
var finalTimeout =
instant ? time * 1000 : (time * 1000) - this._dimNotice;
this._idleTimerId = window.setIdleTimeout(idleCallback,
activeCallback, finalTimeout);
},
fireScreenChangeEvent: function scm_fireScreenChangeEvent() {
var detail = { screenEnabled: this.screenEnabled };
// Tell others the cause of screen-off.
detail.screenOffBy = this._screenOffBy;
var evt = new CustomEvent('screenchange',
{ bubbles: true, cancelable: false,
detail: detail });
window.dispatchEvent(evt);
},
_installProximityListener: function() {
if (this._cpuWakeLock) {
return;
}
this._cpuWakeLock = navigator.requestWakeLock('cpu');
window.addEventListener('userproximity', this);
},
_uninstallProximityListener: function() {
window.removeEventListener('userproximity', this);
if (this._cpuWakeLock) {
this._cpuWakeLock.unlock();
this._cpuWakeLock = null;
}
}
};
exports.ScreenManager = ScreenManager;
}(window));

View file

@ -0,0 +1,441 @@
/* global LazyLoader, Service, SettingsListener,
WifiIcon, WifiWakeLockManager */
'use strict';
(function(exports) {
var Wifi = {
name: 'Wifi',
// Sync with mozSettings wifi.enabled key with an observer.
// Note that there is a timing difference with _enabled and enabled.
wifiEnabled: true,
// True if we have disabled the wifi so we would remember to quietly turn
// it back on.
// (TODO: this should be named wifiDisabledByTimeout)
wifiDisabledByWakelock: false,
// Without an wifi wake lock, wait for screenOffTimeout milliseconds
// to turn wifi off after the conditions are met.
// If it's set to 0, wifi will never be turn off.
// This is in sync with mozSettings wifi.screen_off_timeout.
screenOffTimeout: 0,
// When wifiSleepMode is true and the screenOffTimeout is not zero,
// Wi-Fi will be automatically turned off during sleep to save battery
// power.
// This is in sync with mozSettings wifi.sleepMode.
wifiSleepMode: false,
// if Wifi is enabled but disconnected, try to scan for networks every
// kScanInterval ms.
kScanInterval: 20 * 1000,
_scanTimer: null,
// Sync with the hardware (mozWifiManager) state by listening to the
// two events.
_enabled: false,
get enabled() {
return this._enabled;
},
// true if there is an alarm or if there is an alarm being set.
_hasAlarm: false,
// keep the id returned by mozAlarms API
_alarmId: null,
start: function() {
if (!window.navigator.mozSettings) {
console.error('Wifi: No mozSettings!');
return;
}
if (!window.navigator.mozWifiManager) {
console.error('Wifi: No mozWifiManager!');
return;
}
this.wifiManager = window.navigator.mozWifiManager;
LazyLoader.load(['js/captive_portal.js']);
Service.request('stepReady', '#wifi').then(function() {
return LazyLoader.load(['js/wifi_icon.js']);
}.bind(this)).then(function() {
this.icon = new WifiIcon(this);
this.icon.start();
}.bind(this))['catch'](function(err) { // XXX: workaround gjslint
console.error(err);
});
window.addEventListener('screenchange', this);
var battery = window.navigator.battery;
battery.addEventListener('chargingchange', this);
// If wifi is turned off by us and phone got rebooted,
// bring wifi back.
var name = 'wifi.disabled_by_wakelock';
var req = SettingsListener.getSettingsLock().get(name);
req.onsuccess = function gotWifiDisabledByWakelock() {
if (!req.result[name]) {
return;
}
// Re-enable wifi and reset wifi.disabled_by_wakelock
// SettingsListener.getSettingsLock() always return invalid lock
// in our usage here.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=793239
var lock = navigator.mozSettings.createLock();
lock.set({ 'wifi.enabled': true });
lock.set({ 'wifi.disabled_by_wakelock': false });
};
this._wakeLockManager = new WifiWakeLockManager();
this._wakeLockManager.onwakelockchange = this.maybeToggleWifi.bind(this);
this._wakeLockManager.start();
var self = this;
var wifiManager = window.navigator.mozWifiManager;
// when wifi is really enabled, emit event to notify QuickSettings
wifiManager.onenabled = function onWifiEnabled() {
self._enabled = true;
var evt = document.createEvent('CustomEvent');
evt.initCustomEvent('wifi-enabled',
/* canBubble */ true, /* cancelable */ false, null);
window.dispatchEvent(evt);
self.icon && self.icon.update();
};
// when wifi is really disabled, emit event to notify QuickSettings
wifiManager.ondisabled = function onWifiDisabled() {
this._enabled = false;
var evt = document.createEvent('CustomEvent');
evt.initCustomEvent('wifi-disabled',
/* canBubble */ true, /* cancelable */ false, null);
window.dispatchEvent(evt);
if (self.icon) {
self.icon.update();
}
};
// when wifi status change, emit event to notify Statusbar/UpdateManager
wifiManager.onstatuschange = function onWifiDisabled() {
var evt = document.createEvent('CustomEvent');
evt.initCustomEvent('wifi-statuschange',
/* canBubble */ true, /* cancelable */ false, null);
window.dispatchEvent(evt);
self.icon && self.icon.update();
};
wifiManager.onconnectioninfoupdate = function() {
self.icon && self.icon.update();
};
SettingsListener.observe(
'wifi.screen_off_timeout', 600000, function(value) {
self.screenOffTimeout = value;
});
SettingsListener.observe('wifi.sleepMode', true, function(value) {
self.wifiSleepMode = value;
});
Service.registerState('enabled', this);
// Track the wifi.enabled mozSettings value
SettingsListener.observe('wifi.enabled', true, function(value) {
if (!wifiManager && value) {
self.wifiEnabled = false;
// roll back the setting value to notify the UIs
// that wifi interface is not available
if (value) {
SettingsListener.getSettingsLock().set({
'wifi.enabled': false
});
}
return;
}
self.wifiEnabled = value;
if (self.icon) {
self.icon && self.icon.update();
}
clearTimeout(self._scanTimer);
if (!value) {
return;
}
// If wifi is enabled but disconnected.
// we would need to call getNetworks() continuously
// so we could join known wifi network
self._scanTimer = setInterval(function wifi_scan() {
if (wifiManager.connection.status == 'disconnected') {
wifiManager.getNetworks();
}
}, self.kScanInterval);
});
},
handleEvent: function wifi_handleEvent(evt) {
this.maybeToggleWifi();
},
/**
* Check the status of screen, wifi wake lock, and power source
* and turn on/off wifi accordingly.
*
* You can think of this as a gigantic state machine where the input
* is the states and the output is triggering the timer to turn wifi off,
* or turning the wifi back on (if it is turned off by us).
*
* |--------------------------------|---------------------|
* | Conditions | |
* |--------|-----------|-----------| End state |
* | Screen | Power | Wake lock | |
* |--------|-----------|-----------|---------------------|
* | off | unplugged | not hold | set timer to off (1)|
* |--------|-----------|-----------|---------------------|
* | off | unplugged | hold | turn back on |
* | | | | if turned off by |
* | | | | timer (2) |
* |--------|-----------|-----------|---------------------|
* | off | plugged | not hold | turn back on |
* | | | | if turned off by |
* | | | | timer (3) |
* |--------|-----------|-----------|---------------------|
* | off | plugged | hold | turn back on |
* | | | | if turned off by |
* | | | | timer (4) |
* |--------|-----------|-----------|---------------------|
* | on | unplugged | not hold | turn back on |
* | | | | if turned off by |
* | | | | timer (5) |
* |--------|-----------|-----------|---------------------|
* | on | unplugged | hold | turn back on |
* | | | | if turned off by |
* | | | | timer (6) |
* |--------|-----------|-----------|---------------------|
* | on | plugged | not hold | turn back on |
* | | | | if turned off by |
* | | | | timer (7) |
* |--------|-----------|-----------|---------------------|
* | on | plugged | hold | turn back on |
* | | | | if turned off by |
* | | | | timer (8) |
* |--------|-----------|-----------|---------------------|
*
* The rest of the code are housekeepers making sure we keep an CPU lock
* during the operations, and try not to set the timer twice etc.
*
* It is also assumed setting mozSettings w/ the same value would be a
* no-op with no side effects so we don't really protect that.
*
* As the entry point to of the state machine, it is very important to
* keep this function making the right decisions at all times.
*/
maybeToggleWifi: function wifi_maybeToggleWifi() {
// Keep CPU awake to ensure the flow is done.
var cpuLock = navigator.requestWakeLock('cpu');
var releaseCpuLock = function() {
if (cpuLock) {
cpuLock.unlock();
cpuLock = null;
}
if (timeoutId) {
window.clearTimeout(timeoutId);
timeoutId = 0;
}
};
// To prevent the CPU awake forever (if anything goes run)
var timeoutId = window.setTimeout(releaseCpuLock, 30000);
// Do nothing if we are being disabled.
if (!this.screenOffTimeout) {
releaseCpuLock();
return;
}
var battery = window.navigator.battery;
var wifiManager = window.navigator.mozWifiManager;
if (!battery || !wifiManager ||
// We don't need to do anything if wifi is not disabled by system app.
(!this.wifiEnabled && !this.wifiDisabledByWakelock)) {
releaseCpuLock();
return;
}
var lock = SettingsListener.getSettingsLock();
// Let's quietly turn off wifi if there is no wake lock and
// the screen is off and we are not on a power source.
if (!Service.query('screenEnabled') && !battery.charging &&
!this._wakeLockManager.isHeld) {
// We still need to turn of wifi even if there is no Alarm API
if (!navigator.mozAlarms) {
console.warn('Turning off wifi without sleep timer because' +
' Alarm API is not available');
this.sleep();
releaseCpuLock();
return;
}
// Set System Message Handler,
// so we will be notified when alarm goes off.
this.setSystemMessageHandler();
// When user wants to allow wifi off then start with a timer,
// only turn off wifi till timeout.
if (this.wifiSleepMode === true) {
// maybeToggleWifi state machine state #1
// We should not try to set more than one alarms.
if (this._hasAlarm) {
releaseCpuLock();
return;
}
this._hasAlarm = true;
var date = new Date(Date.now() + this.screenOffTimeout);
var req = navigator.mozAlarms.add(date, 'ignoreTimezone', 'wifi-off');
req.onsuccess = function wifi_offAlarmSet() {
this._alarmId = req.result;
releaseCpuLock();
}.bind(this);
req.onerror = function wifi_offAlarmSetFailed() {
console.warn('Fail to set wifi sleep timer on Alarm API. ' +
'Turn off wifi immediately.');
this._hasAlarm = false;
this.sleep();
releaseCpuLock();
}.bind(this);
} else {
releaseCpuLock();
return;
}
}
// ... and quietly turn it back on or cancel the timer otherwise
else {
// maybeToggleWifi state machine state #2 to #8
if (this._alarmId) {
navigator.mozAlarms.remove(this._alarmId);
this._hasAlarm = false;
this._alarmId = null;
}
// If wifi is enabled but disconnected.
// we would need to call getNetworks()
// so we could join known wifi network
if (this.wifiEnabled &&
wifiManager.connection.status == 'disconnected') {
wifiManager.getNetworks();
}
// We don't need to do anything if
// we didn't disable wifi at first place.
if (!this.wifiDisabledByWakelock) {
releaseCpuLock();
return;
}
this.wifiDisabledByWakelock = false;
if (this.wifiEnabled) {
releaseCpuLock();
return;
}
// turn wifi back on.
lock.set({ 'wifi.enabled': true });
lock.set({ 'wifi.disabled_by_wakelock': false });
window.addEventListener('wifi-enabled', function() {
releaseCpuLock();
});
}
},
// Quietly turn off wifi for real, set wifiDisabledByWakelock to true
// so we will turn it back on.
sleep: function wifi_sleep() {
// The |sleep| might be triggered when an alarm comes.
// If the CPU is in suspend mode at this moment, alarm servcie would wake
// up the CPU to run the handler and turn it back to suspend immediately
// |sleep| is finished.
// In this case, we acquire a CPU wake lock to prevent
// the CPU goes to suspend mode before the switching is done.
var wakeLockForWifi = navigator.requestWakeLock('cpu');
var releaseWakeLockForWifi = function() {
if (wakeLockForWifi) {
wakeLockForWifi.unlock();
wakeLockForWifi = null;
}
if (timeoutId) {
window.clearTimeout(timeoutId);
timeoutId = 0;
}
};
// To prevent the CPU awake forever (if wifi cannot be disabled)
var timeoutId = window.setTimeout(releaseWakeLockForWifi, 30000);
// Remember that it was turned off by us.
this.wifiDisabledByWakelock = true;
var request = null;
var lock = SettingsListener.getSettingsLock();
// Actually turn off the wifi
var wakeLockForSettings = navigator.requestWakeLock('cpu');
lock.set({ 'wifi.enabled': false });
window.addEventListener('wifi-disabled', releaseWakeLockForWifi);
// Keep this value in disk so if the phone reboots we'll
// be able to turn the wifi back on.
request = lock.set({ 'wifi.disabled_by_wakelock': true });
request.onsuccess = function() { wakeLockForSettings.unlock(); };
request.onerror = request.onsuccess;
},
// Register for handling system message,
// this cannot be done during |start()| because of bug 797803
setSystemMessageHandler: function wifi_setSystemMessageHandler() {
if (this._systemMessageHandlerRegistered) {
return;
}
this._systemMessageHandlerRegistered = true;
navigator.mozSetMessageHandler('alarm', function gotAlarm(message) {
if (message.data !== 'wifi-off') {
return;
}
this._hasAlarm = false;
this._alarmId = null;
// Wifi shall not sleep if the condition to turn this off has changed.
// (Alarm should have been removed when condition changes, but for
// reason unknown to us it still triggers.)
var battery = window.navigator.battery;
if (Service.query('screenEnabled') ||
battery.charging ||
this._wakeLockManager.isHeld) {
console.error(
'Wifi: mozAlarms should be cancelled but still triggers.');
return;
}
this.sleep();
}.bind(this));
}
};
exports.Wifi = Wifi;
}(window));