Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/apps/system/js
diff options
context:
space:
mode:
Diffstat (limited to 'apps/system/js')
-rw-r--r--apps/system/js/accessibility.js19
-rw-r--r--apps/system/js/activities.js86
-rw-r--r--apps/system/js/airplane_mode.js138
-rw-r--r--apps/system/js/app_install_manager.js443
-rw-r--r--apps/system/js/applications.js99
-rw-r--r--apps/system/js/attention_screen.js289
-rw-r--r--apps/system/js/authentication_dialog.js178
-rw-r--r--apps/system/js/background_service.js196
-rw-r--r--apps/system/js/battery_manager.js277
-rw-r--r--apps/system/js/bluetooth.js136
-rw-r--r--apps/system/js/bluetooth_transfer.js511
-rw-r--r--apps/system/js/bootstrap.js82
-rw-r--r--apps/system/js/call_forwarding.js45
-rw-r--r--apps/system/js/captive_portal.js73
-rw-r--r--apps/system/js/cards_view.js676
-rw-r--r--apps/system/js/context_menu.js24
-rw-r--r--apps/system/js/cost_control.js89
-rw-r--r--apps/system/js/crash_reporter.js140
-rw-r--r--apps/system/js/gridview.js40
-rw-r--r--apps/system/js/hardware_buttons.js318
-rw-r--r--apps/system/js/icc_cache.js86
-rw-r--r--apps/system/js/identity.js98
-rw-r--r--apps/system/js/keyboard_manager.js86
-rw-r--r--apps/system/js/list_menu.js180
-rw-r--r--apps/system/js/lockscreen.js1019
-rw-r--r--apps/system/js/modal_dialog.js443
-rw-r--r--apps/system/js/mouse2touch.js70
-rw-r--r--apps/system/js/notifications.js410
-rw-r--r--apps/system/js/operator_variant/operator_variant.js168
-rw-r--r--apps/system/js/payment.js151
-rw-r--r--apps/system/js/permission_manager.js203
-rw-r--r--apps/system/js/popup_manager.js313
-rw-r--r--apps/system/js/quick_settings.js279
-rw-r--r--apps/system/js/remote_debugger.js41
-rw-r--r--apps/system/js/screen_manager.js503
-rw-r--r--apps/system/js/screenshot.js115
-rw-r--r--apps/system/js/sim_lock.js110
-rw-r--r--apps/system/js/simcard_dialog.js355
-rw-r--r--apps/system/js/sleep_menu.js276
-rw-r--r--apps/system/js/sound_manager.js242
-rw-r--r--apps/system/js/source_view.js67
-rw-r--r--apps/system/js/statusbar.js618
-rw-r--r--apps/system/js/storage.js60
-rw-r--r--apps/system/js/system_banner.js33
-rw-r--r--apps/system/js/system_dialog.js113
-rw-r--r--apps/system/js/trusted_ui.js334
-rw-r--r--apps/system/js/ttlview.js50
-rw-r--r--apps/system/js/updatable.js269
-rw-r--r--apps/system/js/update_manager.js623
-rw-r--r--apps/system/js/utility_tray.js148
-rw-r--r--apps/system/js/value_selector/date_picker.js568
-rw-r--r--apps/system/js/value_selector/input_parser.js160
-rw-r--r--apps/system/js/value_selector/spin_date_picker.js341
-rw-r--r--apps/system/js/value_selector/value_picker.js222
-rw-r--r--apps/system/js/value_selector/value_selector.js526
-rw-r--r--apps/system/js/voicemail.js93
-rw-r--r--apps/system/js/wifi.js223
-rw-r--r--apps/system/js/window.js152
-rw-r--r--apps/system/js/window_manager.js2011
-rw-r--r--apps/system/js/wrapper.js198
60 files changed, 15816 insertions, 0 deletions
diff --git a/apps/system/js/accessibility.js b/apps/system/js/accessibility.js
new file mode 100644
index 0000000..c2fa111
--- /dev/null
+++ b/apps/system/js/accessibility.js
@@ -0,0 +1,19 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+SettingsListener.observe('accessibility.invert', false, function(value) {
+ var screen = document.getElementById('screen');
+ if (value)
+ screen.classList.add('accessibility-invert');
+ else
+ screen.classList.remove('accessibility-invert');
+});
+
+SettingsListener.observe('accessibility.screenreader', false, function(value) {
+ var event = document.createEvent('CustomEvent');
+ event.initCustomEvent('mozContentEvent', true, true,
+ {type: 'accessibility-screenreader', enabled: value});
+ window.dispatchEvent(event);
+});
diff --git a/apps/system/js/activities.js b/apps/system/js/activities.js
new file mode 100644
index 0000000..f4db861
--- /dev/null
+++ b/apps/system/js/activities.js
@@ -0,0 +1,86 @@
+/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var Activities = {
+ init: function act_init() {
+ window.addEventListener('mozChromeEvent', this);
+ },
+
+ handleEvent: function act_handleEvent(evt) {
+ switch (evt.type) {
+ case 'mozChromeEvent':
+ var detail = evt.detail;
+ switch (detail.type) {
+ case 'activity-choice':
+ this.chooseActivity(detail);
+ break;
+ }
+ break;
+ }
+ },
+
+ chooseActivity: function chooseActivity(detail) {
+ this._id = detail.id;
+
+ var choices = detail.choices;
+ if (choices.length === 1) {
+ this.choose('0');
+ } else {
+ // Since the mozChromeEvent could be triggered by a 'click', and gecko
+ // event are synchronous make sure to exit the event loop before
+ // showing the list.
+ setTimeout((function nextTick() {
+ var activityName = navigator.mozL10n.get('activity-' + detail.name);
+ ListMenu.request(this._listItems(choices), activityName,
+ this.choose.bind(this), this.cancel.bind(this));
+ }).bind(this));
+ }
+ },
+
+ choose: function act_choose(choice) {
+ var returnedChoice = {
+ id: this._id,
+ type: 'activity-choice',
+ value: choice
+ };
+
+ this._sendEvent(returnedChoice);
+ delete this._id;
+ },
+
+ cancel: function act_cancel(value) {
+ var returnedChoice = {
+ id: this._id,
+ type: 'activity-choice',
+ value: -1
+ };
+
+ this._sendEvent(returnedChoice);
+ delete this._id;
+ },
+
+ _sendEvent: function act_sendEvent(value) {
+ var event = document.createEvent('CustomEvent');
+ event.initCustomEvent('mozContentEvent', true, true, value);
+ window.dispatchEvent(event);
+ },
+
+ _listItems: function act_listItems(choices) {
+ var items = [];
+
+ choices.forEach(function(choice, index) {
+ var app = Applications.getByManifestURL(choice.manifest);
+ items.push({
+ label: new ManifestHelper(app.manifest).name,
+ icon: choice.icon,
+ value: index
+ });
+ });
+
+ return items;
+ }
+};
+
+Activities.init();
diff --git a/apps/system/js/airplane_mode.js b/apps/system/js/airplane_mode.js
new file mode 100644
index 0000000..781a53f
--- /dev/null
+++ b/apps/system/js/airplane_mode.js
@@ -0,0 +1,138 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var AirplaneMode = {
+ enabled: false,
+
+ init: function apm_init() {
+ if (!window.navigator.mozSettings)
+ return;
+
+ var mobileDataEnabled = false;
+ SettingsListener.observe('ril.data.enabled', false, function(value) {
+ mobileDataEnabled = value;
+ });
+
+ var bluetoothEnabled = false;
+ SettingsListener.observe('bluetooth.enabled', false, function(value) {
+ bluetoothEnabled = value;
+ });
+
+ var wifiEnabled = false;
+ SettingsListener.observe('wifi.enabled', false, function(value) {
+ wifiEnabled = value;
+ });
+
+ var geolocationEnabled = false;
+ SettingsListener.observe('geolocation.enabled', false, function(value) {
+ geolocationEnabled = value;
+ });
+
+ var bluetooth = window.navigator.mozBluetooth;
+ var wifiManager = window.navigator.mozWifiManager;
+ var mobileData = window.navigator.mozMobileConnection &&
+ window.navigator.mozMobileConnection.data;
+ var fmRadio = window.navigator.mozFMRadio;
+
+ var restoreMobileData = false;
+ var restoreBluetooth = false;
+ var restoreWifi = false;
+ var restoreGeolocation = false;
+ // Note that we don't restore Wifi tethering when leaving airplane mode
+ // because Wifi tethering can't be switched on before data connection is
+ // established.
+
+ var self = this;
+ SettingsListener.observe('ril.radio.disabled', false, function(value) {
+ if (value) {
+ // Entering airplane mode.
+ self.enabled = true;
+
+ // Turn off mobile data
+ // We toggle the mozSettings value here just for the sake of UI,
+ // platform ril disconnects mobile data when
+ // 'ril.radio.disabled' is true.
+ if (mobileData) {
+ restoreMobileData = mobileDataEnabled;
+ if (mobileDataEnabled) {
+ SettingsListener.getSettingsLock().set({
+ 'ril.data.enabled': false
+ });
+ }
+ }
+
+ // Turn off Bluetooth.
+ if (bluetooth) {
+ restoreBluetooth = bluetoothEnabled;
+ if (bluetoothEnabled) {
+ SettingsListener.getSettingsLock().set({
+ 'bluetooth.enabled': false
+ });
+ }
+ }
+
+ // Turn off Wifi.
+ if (wifiManager) {
+ restoreWifi = wifiEnabled;
+ if (wifiEnabled) {
+ SettingsListener.getSettingsLock().set({
+ 'wifi.enabled': false
+ });
+ }
+
+ // Turn off Wifi tethering.
+ SettingsListener.getSettingsLock().set({
+ 'tethering.wifi.enabled': false
+ });
+ }
+
+ // Turn off Geolocation.
+ restoreGeolocation = geolocationEnabled;
+ if (geolocationEnabled) {
+ SettingsListener.getSettingsLock().set({
+ 'geolocation.enabled': false
+ });
+ }
+
+ // Turn off FM Radio.
+ if (fmRadio && fmRadio.enabled)
+ fmRadio.disable();
+
+ } else {
+ self.enabled = false;
+ // Don't attempt to turn on mobile data if it's already on
+ if (mobileData && !mobileDataEnabled && restoreMobileData) {
+ SettingsListener.getSettingsLock().set({
+ 'ril.data.enabled': true
+ });
+ }
+
+ // Don't attempt to turn on Bluetooth if it's already on
+ if (bluetooth && !bluetooth.enabled && restoreBluetooth) {
+ SettingsListener.getSettingsLock().set({
+ 'bluetooth.enabled': true
+ });
+ }
+
+ // Don't attempt to turn on Wifi if it's already on
+ if (wifiManager && !wifiManager.enabled && restoreWifi) {
+ SettingsListener.getSettingsLock().set({
+ 'wifi.enabled': true
+ });
+ }
+
+ // Don't attempt to turn on Geolocation if it's already on
+ if (!geolocationEnabled && restoreGeolocation) {
+ SettingsListener.getSettingsLock().set({
+ 'geolocation.enabled': true
+ });
+ }
+ }
+ });
+ }
+};
+
+AirplaneMode.init();
+
diff --git a/apps/system/js/app_install_manager.js b/apps/system/js/app_install_manager.js
new file mode 100644
index 0000000..146a01b
--- /dev/null
+++ b/apps/system/js/app_install_manager.js
@@ -0,0 +1,443 @@
+'use strict';
+
+var AppInstallManager = {
+ mapDownloadErrorsToMessage: {
+ 'NETWORK_ERROR': 'download-failed',
+ 'DOWNLOAD_ERROR': 'download-failed',
+ 'MISSING_MANIFEST': 'install-failed',
+ 'INVALID_MANIFEST': 'install-failed',
+ 'INSTALL_FROM_DENIED': 'install-failed',
+ 'INVALID_SECURITY_LEVEL': 'install-failed',
+ 'INVALID_PACKAGE': 'install-failed',
+ 'APP_CACHE_DOWNLOAD_ERROR': 'download-failed'
+ },
+
+ init: function ai_init() {
+ this.dialog = document.getElementById('app-install-dialog');
+ this.msg = document.getElementById('app-install-message');
+ this.size = document.getElementById('app-install-size');
+ this.authorName = document.getElementById('app-install-author-name');
+ this.authorUrl = document.getElementById('app-install-author-url');
+ this.installButton = document.getElementById('app-install-install-button');
+ this.cancelButton = document.getElementById('app-install-cancel-button');
+ this.installCancelDialog =
+ document.getElementById('app-install-cancel-dialog');
+ this.downloadCancelDialog =
+ document.getElementById('app-download-cancel-dialog');
+ this.confirmCancelButton =
+ document.getElementById('app-install-confirm-cancel-button');
+ this.resumeButton = document.getElementById('app-install-resume-button');
+
+ this.notifContainer =
+ document.getElementById('install-manager-notification-container');
+ this.appInfos = {};
+
+ window.addEventListener('mozChromeEvent',
+ (function ai_handleChromeEvent(e) {
+ if (e.detail.type == 'webapps-ask-install') {
+ this.handleAppInstallPrompt(e.detail);
+ }
+ }).bind(this));
+
+ window.addEventListener('applicationinstall',
+ this.handleApplicationInstall.bind(this));
+
+
+ this.installButton.onclick = this.handleInstall.bind(this);
+ this.cancelButton.onclick = this.showInstallCancelDialog.bind(this);
+ this.confirmCancelButton.onclick = this.handleInstallCancel.bind(this);
+ this.resumeButton.onclick = this.hideInstallCancelDialog.bind(this);
+ this.notifContainer.onclick = this.showDownloadCancelDialog.bind(this);
+
+ this.downloadCancelDialog.querySelector('.confirm').onclick =
+ this.handleConfirmDownloadCancel.bind(this);
+ this.downloadCancelDialog.querySelector('.cancel').onclick =
+ this.handleCancelDownloadCancel.bind(this);
+
+ // bind these handlers so that we can have only one instance and check
+ // them later on
+ ['handleDownloadSuccess',
+ 'handleDownloadError',
+ 'handleProgress',
+ 'handleApplicationReady'
+ ].forEach(function(name) {
+ this[name] = this[name].bind(this);
+ }, this);
+
+ window.addEventListener('applicationready',
+ this.handleApplicationReady);
+ },
+
+ handleApplicationReady: function ai_handleApplicationReady(e) {
+ window.removeEventListener('applicationready',
+ this.handleApplicationReady);
+
+ var apps = e.detail.applications;
+
+ Object.keys(apps)
+ .filter(function(key) { return apps[key].installState === 'pending'; })
+ .map(function(key) { return apps[key]; })
+ .forEach(this.prepareForDownload, this);
+ },
+
+ handleApplicationInstall: function ai_handleApplicationInstallEvent(e) {
+ var app = e.detail.application;
+
+ if (app.installState === 'installed') {
+ this.showInstallSuccess(app);
+ return;
+ }
+
+ this.prepareForDownload(app);
+ },
+
+ handleAppInstallPrompt: function ai_handleInstallPrompt(detail) {
+ var _ = navigator.mozL10n.get;
+ var app = detail.app;
+ // updateManifest is used by packaged apps until they are installed
+ var manifest = app.manifest ? app.manifest : app.updateManifest;
+
+ if (!manifest)
+ return;
+
+ this.dialog.classList.add('visible');
+
+ var id = detail.id;
+
+ if (manifest.size) {
+ this.size.textContent = this.humanizeSize(manifest.size);
+ } else {
+ this.size.textContent = _('unknown');
+ }
+
+ // Wrap manifest to get localized properties
+ manifest = new ManifestHelper(manifest);
+ var msg = _('install-app', {'name': manifest.name});
+ this.msg.textContent = msg;
+
+ if (manifest.developer) {
+ this.authorName.textContent = manifest.developer.name || _('unknown');
+ this.authorUrl.textContent = manifest.developer.url || '';
+ } else {
+ this.authorName.textContent = _('unknown');
+ this.authorUrl.textContent = '';
+ }
+
+ this.installCallback = (function ai_installCallback() {
+ this.dispatchResponse(id, 'webapps-install-granted');
+ }).bind(this);
+
+ this.installCancelCallback = (function ai_cancelCallback() {
+ this.dispatchResponse(id, 'webapps-install-denied');
+ }).bind(this);
+
+ },
+
+ handleInstall: function ai_handleInstall(evt) {
+ if (evt)
+ evt.preventDefault();
+ if (this.installCallback)
+ this.installCallback();
+ this.installCallback = null;
+ this.dialog.classList.remove('visible');
+ },
+
+ prepareForDownload: function ai_prepareForDownload(app) {
+ var manifestURL = app.manifestURL;
+ this.appInfos[manifestURL] = {};
+
+ // these methods are already bound to |this|
+ app.ondownloadsuccess = this.handleDownloadSuccess;
+ app.ondownloaderror = this.handleDownloadError;
+ app.onprogress = this.handleProgress;
+ },
+
+ showInstallSuccess: function ai_showInstallSuccess(app) {
+ var manifest = app.manifest || app.updateManifest;
+ var name = new ManifestHelper(manifest).name;
+ var _ = navigator.mozL10n.get;
+ var msg = _('app-install-success', { appName: name });
+ SystemBanner.show(msg);
+ },
+
+ handleDownloadSuccess: function ai_handleDownloadSuccess(evt) {
+ var app = evt.application;
+ this.showInstallSuccess(app);
+ this.onDownloadStop(app);
+ this.onDownloadFinish(app);
+ },
+
+ handleDownloadError: function ai_handleDownloadError(evt) {
+ var app = evt.application;
+ var _ = navigator.mozL10n.get;
+ var manifest = app.manifest || app.updateManifest;
+ var name = new ManifestHelper(manifest).name;
+
+ var errorName = app.downloadError.name;
+
+ switch (errorName) {
+ case 'INSUFFICIENT_STORAGE':
+ var title = _('not-enough-space'),
+ buttonText = _('ok'),
+ message = _('not-enough-space-message');
+
+ ModalDialog.alert(title, message, {title: buttonText});
+ break;
+ default:
+ // showing the real error to a potential developer
+ console.info('downloadError event, error code is', errorName);
+
+ var key = this.mapDownloadErrorsToMessage[errorName] || 'generic-error';
+ var msg = _('app-install-' + key, { appName: name });
+ SystemBanner.show(msg);
+ }
+
+ this.onDownloadStop(app);
+ },
+
+ onDownloadStart: function ai_onDownloadStart(app) {
+ if (! this.hasNotification(app)) {
+ StatusBar.incSystemDownloads();
+ this.addNotification(app);
+ this.requestWifiLock(app);
+ }
+ },
+
+ onDownloadStop: function ai_onDownloadStop(app) {
+ if (this.hasNotification(app)) {
+ StatusBar.decSystemDownloads();
+ this.removeNotification(app);
+ this.releaseWifiLock(app);
+ }
+ },
+
+ hasNotification: function ai_hasNotification(app) {
+ var appInfo = this.appInfos[app.manifestURL];
+ return appInfo && !!appInfo.installNotification;
+ },
+
+ onDownloadFinish: function ai_onDownloadFinish(app) {
+ delete this.appInfos[app.manifestURL];
+
+ // check if these are our handlers before removing them
+ if (app.ondownloadsuccess === this.handleDownloadSuccess) {
+ app.ondownloadsuccess = null;
+ }
+
+ if (app.ondownloaderror === this.handleDownloadError) {
+ app.ondownloaderror = null;
+ }
+
+ if (app.onprogress === this.handleProgress) {
+ app.onprogress = null;
+ }
+ },
+
+ addNotification: function ai_addNotification(app) {
+ // should be unique (this is used already in applications.js)
+ var manifestURL = app.manifestURL,
+ manifest = app.manifest || app.updateManifest,
+ appInfo = this.appInfos[manifestURL];
+
+ if (appInfo.installNotification) {
+ return;
+ }
+
+ var newNotif =
+ '<div class="fake-notification">' +
+ '<div class="message"></div>' +
+ '<progress></progress>' +
+ '</div>';
+
+ this.notifContainer.insertAdjacentHTML('afterbegin', newNotif);
+
+ var newNode = this.notifContainer.firstElementChild;
+ newNode.dataset.manifest = manifestURL;
+
+ var _ = navigator.mozL10n.get;
+
+ var message = _('downloadingAppMessage', {
+ appName: new ManifestHelper(manifest).name
+ });
+
+ newNode.querySelector('.message').textContent = message;
+
+ var size = manifest.size,
+ progressNode = newNode.querySelector('progress');
+ if (size) {
+ progressNode.max = size;
+ appInfo.hasMax = true;
+ }
+
+ appInfo.installNotification = newNode;
+ NotificationScreen.incExternalNotifications();
+ },
+
+ getNotificationProgressNode: function ai_getNotificationProgressNode(app) {
+ var appInfo = this.appInfos[app.manifestURL];
+ var progressNode = appInfo &&
+ appInfo.installNotification &&
+ appInfo.installNotification.querySelector('progress');
+ return progressNode || null;
+ },
+
+ handleProgress: function ai_handleProgress(evt) {
+ var app = evt.application,
+ appInfo = this.appInfos[app.manifestURL];
+
+ this.onDownloadStart(app);
+
+
+ var progressNode = this.getNotificationProgressNode(app);
+ var message;
+ var _ = navigator.mozL10n.get;
+
+ if (isNaN(app.progress) || app.progress == null) {
+ // now we get NaN if there is no progress information but let's
+ // handle the null and undefined cases as well
+ message = _('downloadingAppProgressIndeterminate');
+ progressNode.value = undefined; // switch to indeterminate state
+ } else if (appInfo.hasMax) {
+ message = _('downloadingAppProgress',
+ {
+ progress: this.humanizeSize(app.progress),
+ max: this.humanizeSize(progressNode.max)
+ });
+ progressNode.value = app.progress;
+ } else {
+ message = _('downloadingAppProgressNoMax',
+ { progress: this.humanizeSize(app.progress) });
+ }
+ progressNode.textContent = message;
+ },
+
+ removeNotification: function ai_removeNotification(app) {
+ var manifestURL = app.manifestURL,
+ appInfo = this.appInfos[manifestURL],
+ node = appInfo.installNotification;
+
+ if (!node) {
+ return;
+ }
+
+ node.parentNode.removeChild(node);
+ delete appInfo.installNotification;
+ NotificationScreen.decExternalNotifications();
+ },
+
+ requestWifiLock: function ai_requestWifiLock(app) {
+ var appInfo = this.appInfos[app.manifestURL];
+ if (! appInfo.wifiLock) {
+ // we don't want 2 locks for the same app
+ appInfo.wifiLock = navigator.requestWakeLock('wifi');
+ }
+ },
+
+ releaseWifiLock: function ai_releaseWifiLock(app) {
+ var appInfo = this.appInfos[app.manifestURL];
+
+ if (appInfo.wifiLock) {
+ try {
+ appInfo.wifiLock.unlock();
+ } catch (e) {
+ // this can happen if the lock is already unlocked
+ console.error('error during unlock', e);
+ }
+
+ delete appInfo.wifiLock;
+ }
+ },
+
+ dispatchResponse: function ai_dispatchResponse(id, type) {
+ var event = document.createEvent('CustomEvent');
+
+ event.initCustomEvent('mozContentEvent', true, true, {
+ id: id,
+ type: type
+ });
+
+ window.dispatchEvent(event);
+ },
+
+ humanizeSize: function ai_humanizeSize(bytes) {
+ var _ = navigator.mozL10n.get;
+ var units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'];
+
+ if (!bytes)
+ return '0.00 ' + _(units[0]);
+
+ var e = Math.floor(Math.log(bytes) / Math.log(1024));
+ return (bytes / Math.pow(1024, Math.floor(e))).toFixed(2) + ' ' +
+ _(units[e]);
+ },
+
+ showInstallCancelDialog: function ai_showInstallCancelDialog(evt) {
+ if (evt)
+ evt.preventDefault();
+ this.installCancelDialog.classList.add('visible');
+ this.dialog.classList.remove('visible');
+ },
+
+ hideInstallCancelDialog: function ai_hideInstallCancelDialog(evt) {
+ if (evt)
+ evt.preventDefault();
+ this.dialog.classList.add('visible');
+ this.installCancelDialog.classList.remove('visible');
+ },
+
+ showDownloadCancelDialog: function ai_showDownloadCancelDialog(e) {
+ var currentNode = e.target;
+
+ if (!currentNode.classList.contains('fake-notification')) {
+ // tapped outside of a notification
+ return;
+ }
+
+ var manifestURL = currentNode.dataset.manifest,
+ app = Applications.getByManifestURL(manifestURL),
+ manifest = app.manifest || app.updateManifest,
+ dialog = this.downloadCancelDialog;
+
+ var title = dialog.querySelector('h1');
+
+ title.textContent = navigator.mozL10n.get('stopDownloading', {
+ app: new ManifestHelper(manifest).name
+ });
+
+ dialog.classList.add('visible');
+ dialog.dataset.manifest = manifestURL;
+ UtilityTray.hide();
+ },
+
+ handleInstallCancel: function ai_handleInstallCancel() {
+ if (this.installCancelCallback)
+ this.installCancelCallback();
+ this.installCancelCallback = null;
+ this.installCancelDialog.classList.remove('visible');
+ },
+
+ handleConfirmDownloadCancel: function ai_handleConfirmDownloadCancel(e) {
+ e && e.preventDefault();
+ var dialog = this.downloadCancelDialog,
+ manifestURL = dialog.dataset.manifest;
+ if (manifestURL) {
+ var app = Applications.getByManifestURL(manifestURL);
+ app && app.cancelDownload();
+ }
+
+ this.hideDownloadCancelDialog();
+ },
+
+ handleCancelDownloadCancel: function ai_handleCancelDownloadCancel(e) {
+ e && e.preventDefault();
+ this.hideDownloadCancelDialog();
+ },
+
+ hideDownloadCancelDialog: function() {
+ var dialog = this.downloadCancelDialog;
+ dialog.classList.remove('visible');
+ delete dialog.dataset.manifest;
+ }
+};
+
+AppInstallManager.init();
diff --git a/apps/system/js/applications.js b/apps/system/js/applications.js
new file mode 100644
index 0000000..f1e286f
--- /dev/null
+++ b/apps/system/js/applications.js
@@ -0,0 +1,99 @@
+/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+// Application module handles the information of apps on behalf of other
+// modules.
+
+var Applications = {
+ installedApps: {},
+ ready: false,
+ init: function a_init() {
+ var self = this;
+ var apps = navigator.mozApps;
+
+ var getAllApps = function getAllApps() {
+ navigator.mozApps.mgmt.getAll().onsuccess = function mozAppGotAll(evt) {
+ var apps = evt.target.result;
+ apps.forEach(function(app) {
+ self.installedApps[app.manifestURL] = app;
+ // TODO Followup for retrieving homescreen & comms app
+ });
+
+ self.ready = true;
+ self.fireApplicationReadyEvent();
+ };
+ };
+
+ // We need to wait for the chrome shell to let us know when it's ok to
+ // launch activities. This prevents race conditions.
+ // The event does not fire again when we reload System app in on
+ // B2G Desktop, so we save the information into sessionStorage.
+ if (window.sessionStorage.getItem('webapps-registry-ready')) {
+ getAllApps();
+ } else {
+ window.addEventListener('mozChromeEvent', function mozAppReady(event) {
+ if (event.detail.type != 'webapps-registry-ready')
+ return;
+
+ window.sessionStorage.setItem('webapps-registry-ready', 'yes');
+ window.removeEventListener('mozChromeEvent', mozAppReady);
+
+ getAllApps();
+ });
+ }
+
+ apps.mgmt.oninstall = function a_install(evt) {
+ var newapp = evt.application;
+ self.installedApps[newapp.manifestURL] = newapp;
+
+ self.fireApplicationInstallEvent(newapp);
+ };
+
+ apps.mgmt.onuninstall = function a_uninstall(evt) {
+ var deletedapp = evt.application;
+ delete self.installedApps[deletedapp.manifestURL];
+
+ self.fireApplicationUninstallEvent(deletedapp);
+ };
+ },
+
+ getByManifestURL: function a_getByManifestURL(manifestURL) {
+ if (manifestURL in this.installedApps) {
+ return this.installedApps[manifestURL];
+ }
+
+ return null;
+ },
+
+ fireApplicationReadyEvent: function a_fireAppReadyEvent() {
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('applicationready',
+ /* canBubble */ true, /* cancelable */ false,
+ { applications: this.installedApps });
+ window.dispatchEvent(evt);
+ },
+
+ // We need to dispatch the following events because
+ // mozApps is not doing so right now.
+ // ref: https://bugzilla.mozilla.org/show_bug.cgi?id=731746
+
+ fireApplicationInstallEvent: function a_fireApplicationInstallEvent(app) {
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('applicationinstall',
+ /* canBubble */ true, /* cancelable */ false,
+ { application: app });
+ window.dispatchEvent(evt);
+ },
+
+ fireApplicationUninstallEvent: function a_fireApplicationUninstallEvent(app) {
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('applicationuninstall',
+ /* canBubble */ true, /* cancelable */ false,
+ { application: app });
+ window.dispatchEvent(evt);
+ }
+};
+
+Applications.init();
diff --git a/apps/system/js/attention_screen.js b/apps/system/js/attention_screen.js
new file mode 100644
index 0000000..c14dbe8
--- /dev/null
+++ b/apps/system/js/attention_screen.js
@@ -0,0 +1,289 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+'use strict';
+
+var AttentionScreen = {
+ get mainScreen() {
+ delete this.mainScreen;
+ return this.mainScreen = document.getElementById('screen');
+ },
+
+ get attentionScreen() {
+ delete this.attentionScreen;
+ return this.attentionScreen = document.getElementById('attention-screen');
+ },
+
+ get bar() {
+ delete this.bar;
+ return this.bar = document.getElementById('attention-bar');
+ },
+
+ isVisible: function as_isVisible() {
+ return this.attentionScreen.classList.contains('displayed');
+ },
+
+ isFullyVisible: function as_isFullyVisible() {
+ return (this.isVisible() &&
+ !this.mainScreen.classList.contains('active-statusbar'));
+ },
+
+ init: function as_init() {
+ window.addEventListener('mozbrowseropenwindow', this.open.bind(this), true);
+ window.addEventListener('mozbrowserclose', this.close.bind(this), true);
+ window.addEventListener('mozbrowsererror', this.close.bind(this), true);
+ window.addEventListener('keyboardchange', this.resize.bind(this), true);
+ window.addEventListener('keyboardhide', this.resize.bind(this), true);
+
+ this.bar.addEventListener('click', this.show.bind(this));
+ window.addEventListener('home', this.hide.bind(this));
+ window.addEventListener('holdhome', this.hide.bind(this));
+ window.addEventListener('appwillopen', this.hide.bind(this));
+ },
+
+ resize: function as_resize(evt) {
+ if (evt.type == 'keyboardchange') {
+ if (!this.isFullyVisible())
+ return;
+
+ this.attentionScreen.style.height =
+ window.innerHeight - evt.detail.height + 'px';
+ } else if (evt.type == 'keyboardhide') {
+ // We still need to reset the height property even when the attention
+ // screen is not fully visible, or it will overrides the height
+ // we defined with #attention-screen.status-mode
+ this.attentionScreen.style.height = '';
+ }
+ },
+
+ // show the attention screen overlay with newly created frame
+ open: function as_open(evt) {
+ if (evt.detail.features != 'attention')
+ return;
+
+ // stopPropagation means we are not allowing
+ // Popup Manager to handle this event
+ evt.stopPropagation();
+
+ // Canceling any full screen web content
+ if (document.mozFullScreen)
+ document.mozCancelFullScreen();
+
+ // Check if the app has the permission to open attention screens
+ var manifestURL = evt.target.getAttribute('mozapp');
+ var app = Applications.getByManifestURL(manifestURL);
+
+ if (!app || !this._hasAttentionPermission(app))
+ return;
+
+ // Hide sleep menu/list menu if it is shown now
+ ListMenu.hide();
+ SleepMenu.hide();
+
+ // We want the user attention, so we need to turn the screen on
+ // if it's off. The lockscreen will grab the focus when we do that
+ // so we need to do it before adding the new iframe to the dom
+ if (!ScreenManager.screenEnabled)
+ ScreenManager.turnScreenOn();
+
+ var attentionFrame = evt.detail.frameElement;
+ attentionFrame.dataset.frameType = 'attention';
+ attentionFrame.dataset.frameName = evt.detail.name;
+ attentionFrame.dataset.frameOrigin = evt.target.dataset.frameOrigin;
+
+ // We would like to put the dialer call screen on top of all other
+ // attention screens by ensure it is the last iframe in the DOM tree
+ if (this._hasTelephonyPermission(app)) {
+ this.attentionScreen.appendChild(attentionFrame);
+ } else {
+ this.attentionScreen.insertBefore(attentionFrame,
+ this.bar.nextElementSibling);
+ }
+
+ this._updateAttentionFrameVisibility();
+
+ // Make the overlay visible if we are not displayed yet.
+ // alternatively, if the newly appended frame is the visible frame
+ // and we are in the status bar mode, expend to full screen mode.
+ if (!this.isVisible()) {
+ this.attentionScreen.classList.add('displayed');
+ this.mainScreen.classList.add('attention');
+ this.dispatchEvent('attentionscreenshow', {
+ origin: attentionFrame.dataset.frameOrigin
+ });
+ } else if (!this.isFullyVisible() &&
+ this.attentionScreen.lastElementChild === attentionFrame) {
+ this.show();
+ }
+ },
+
+ // Make sure visibililty state of all attention screens are set correctly
+ _updateAttentionFrameVisibility: function as_updateAtteFrameVisibility() {
+ var frames = this.attentionScreen.querySelectorAll('iframe');
+ var i = frames.length - 1;
+
+ // In case someone call this function w/o checking for frame first
+ if (i < 0)
+ return;
+
+ // set the last one in the DOM to visible
+ // The setTimeout() and the closure is used to workaround
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=810431
+ setTimeout(function(frame) {
+ frame.setVisible(true);
+ frame.focus();
+ }, 0, frames[i]);
+
+ while (i--) {
+ // The setTimeout() and the closure is used to workaround
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=810431
+ setTimeout(function(frame) {
+ frame.setVisible(false);
+ frame.blur();
+ }, 0, frames[i]);
+ }
+ },
+
+ // close the attention screen overlay
+ close: function as_close(evt) {
+ if (!'frameType' in evt.target.dataset ||
+ evt.target.dataset.frameType !== 'attention' ||
+ (evt.type === 'mozbrowsererror' && evt.detail.type !== 'fatal'))
+ return;
+
+ // Remove the frame
+ var origin = evt.target.dataset.frameOrigin;
+ this.attentionScreen.removeChild(evt.target);
+
+ // We've just removed the focused window leaving the system
+ // without any focused window, let's fix this.
+ window.focus();
+
+ // if there are other attention frames,
+ // we need to update the visibility and show() the overlay.
+ if (this.attentionScreen.querySelectorAll('iframe').length) {
+ this._updateAttentionFrameVisibility();
+
+ this.dispatchEvent('attentionscreenclose', { origin: origin });
+
+ if (!this.isFullyVisible())
+ this.show();
+
+ return;
+ }
+
+ // There is no iframes left;
+ // we should close the attention screen overlay.
+
+ // If the the attention screen is closed during active-statusbar
+ // mode, we would need to leave that mode.
+ if (!this.isFullyVisible()) {
+ this.mainScreen.classList.remove('active-statusbar');
+ this.attentionScreen.classList.remove('status-mode');
+ this.dispatchEvent('status-inactive',
+ { origin: this.attentionScreen.lastElementChild.dataset.frameOrigin });
+ }
+
+ this.attentionScreen.classList.remove('displayed');
+ this.mainScreen.classList.remove('attention');
+ this.dispatchEvent('attentionscreenhide', { origin: origin });
+ },
+
+ // expend the attention screen overlay to full screen
+ show: function as_show() {
+ // leaving "status-mode".
+ this.attentionScreen.classList.remove('status-mode');
+ // there shouldn't be a transition from "status-mode" to "active-statusbar"
+ this.attentionScreen.style.transition = 'none';
+
+ var self = this;
+ setTimeout(function nextTick() {
+ self.attentionScreen.style.transition = '';
+
+ // leaving "active-statusbar" mode,
+ // with a transform: translateY() slide down transition.
+ self.mainScreen.classList.remove('active-statusbar');
+ self.dispatchEvent('status-inactive', {
+ origin: self.attentionScreen.lastElementChild.dataset.frameOrigin
+ });
+ });
+ },
+
+ // shrink the attention screen overlay to status bar
+ // invoked when we get a "home" event
+ hide: function as_hide() {
+ if (!this.isFullyVisible())
+ return;
+
+ // entering "active-statusbar" mode,
+ // with a transform: translateY() slide up transition.
+ this.mainScreen.classList.add('active-statusbar');
+
+ // The only way to hide attention screen is the home/holdhome event.
+ // So we don't fire any origin information here.
+ // The expected behavior is restore homescreen visibility to 'true'
+ // in the Window Manager.
+ this.dispatchEvent('status-active');
+
+ var attentionScreen = this.attentionScreen;
+ attentionScreen.addEventListener('transitionend', function trWait() {
+ attentionScreen.removeEventListener('transitionend', trWait);
+
+ // transition completed, entering "status-mode" (40px height iframe)
+ attentionScreen.classList.add('status-mode');
+ });
+ },
+
+ dispatchEvent: function as_dispatchEvent(name, detail) {
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent(name, true, true, detail);
+ window.dispatchEvent(evt);
+ },
+
+ // If an app with an active attention screen is switched to,
+ // we would need to cover it with it's attention screen.
+ // Invoked when displayedApp in Window Manager changes
+ // XXX should be replaced with a call that listens to appwillopen
+ // TBD: display the attention screen underneath other attention screens.
+ showForOrigin: function as_showForOrigin(origin) {
+ if (!this.isVisible() || this.isFullyVisible())
+ return;
+
+ var attentionFrame = this.attentionScreen.lastElementChild;
+ var frameOrigin = attentionFrame.dataset.frameOrigin;
+ if (origin === frameOrigin) {
+ this.show();
+ }
+ },
+
+ getAttentionScreenOrigins: function as_getAttentionScreenOrigins() {
+ var attentionScreen = this.attentionScreen;
+ var frames = this.attentionScreen.querySelectorAll('iframe');
+ var attentiveApps = [];
+ Array.prototype.forEach.call(frames, function pushFrame(frame) {
+ attentiveApps.push(frame.dataset.frameOrigin);
+ });
+ return attentiveApps;
+ },
+
+ _hasAttentionPermission: function as_hasAttentionPermission(app) {
+ var mozPerms = navigator.mozPermissionSettings;
+ if (!mozPerms)
+ return false;
+
+ var value = mozPerms.get('attention', app.manifestURL, app.origin, false);
+
+ return (value === 'allow');
+ },
+
+ _hasTelephonyPermission: function as_hasAttentionPermission(app) {
+ var mozPerms = navigator.mozPermissionSettings;
+ if (!mozPerms)
+ return false;
+
+ var value = mozPerms.get('telephony', app.manifestURL, app.origin, false);
+
+ return (value === 'allow');
+ }
+};
+
+AttentionScreen.init();
diff --git a/apps/system/js/authentication_dialog.js b/apps/system/js/authentication_dialog.js
new file mode 100644
index 0000000..3a893ee
--- /dev/null
+++ b/apps/system/js/authentication_dialog.js
@@ -0,0 +1,178 @@
+/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+// This module listens to mozbrowserusernameandpasswordrequired event.
+// It's for http authentication only.
+// XXX: ftp authentication will be implemented here but not supported yet.
+
+var AuthenticationDialog = {
+ // Used for element id access.
+ // e.g., 'authentication-dialog-alert-ok'
+ prefix: 'authentication-dialog-',
+
+ // DOM
+ elements: {},
+
+ // Get all elements when inited.
+ getAllElements: function ad_getAllElements() {
+ var elementsID = [
+ 'http-authentication', 'http-username-input', 'http-password-input',
+ 'http-authentication-message', 'http-authentication-ok',
+ 'http-authentication-cancel', 'title'
+ ];
+
+ var toCamelCase = function toCamelCase(str) {
+ return str.replace(/\-(.)/g, function replacer(str, p1) {
+ return p1.toUpperCase();
+ });
+ };
+
+ elementsID.forEach(function createElementRef(name) {
+ this.elements[toCamelCase(name)] =
+ document.getElementById(this.prefix + name);
+ }, this);
+
+ this.screen = document.getElementById('screen');
+ this.overlay = document.getElementById('dialog-overlay');
+ },
+
+ // Save the events returned by
+ // mozbrowserusernameandpasswordrequired for later use.
+ // The events are stored according to webapp origin
+ // e.g., 'http://uitest.gaiamobile.org': evt
+ currentEvents: {},
+
+ init: function ad_init() {
+ // Get all elements initially.
+ this.getAllElements();
+ var elements = this.elements;
+
+ // Bind events
+ window.addEventListener('mozbrowserusernameandpasswordrequired', this);
+ window.addEventListener('appopen', this);
+ window.addEventListener('appwillclose', this);
+ window.addEventListener('appterminated', this);
+ window.addEventListener('resize', this);
+ window.addEventListener('keyboardchange', this);
+ window.addEventListener('keyboardhide', this);
+
+ for (var id in elements) {
+ if (elements[id].tagName.toLowerCase() == 'button') {
+ elements[id].addEventListener('click', this);
+ }
+ }
+ },
+
+ // Default event handler
+ handleEvent: function ad_handleEvent(evt) {
+ var elements = this.elements;
+ switch (evt.type) {
+ case 'mozbrowserusernameandpasswordrequired':
+ if (evt.target.dataset.frameType != 'window')
+ return;
+
+ evt.preventDefault();
+ var origin = evt.target.dataset.frameOrigin;
+ this.currentEvents[origin] = evt;
+
+ if (origin == WindowManager.getDisplayedApp())
+ this.show(origin);
+ break;
+
+ case 'click':
+ if (evt.currentTarget === elements.httpAuthenticationCancel) {
+ this.cancelHandler();
+ } else {
+ this.confirmHandler();
+ }
+ break;
+
+ case 'appopen':
+ if (this.currentEvents[evt.detail.origin])
+ this.show(evt.detail.origin);
+ break;
+
+ case 'appwillclose':
+ // Do nothing if the app is closed at background.
+ if (evt.detail.origin !== this.currentOrigin)
+ return;
+
+ // Reset currentOrigin
+ this.hide();
+ break;
+
+ case 'appterminated':
+ if (this.currentEvents[evt.detail.origin])
+ delete this.currentEvents[evt.detail.origin];
+
+ break;
+
+ case 'resize':
+ case 'keyboardhide':
+ if (!this.currentOrigin)
+ return;
+
+ this.setHeight(window.innerHeight - StatusBar.height);
+ break;
+
+ case 'keyboardchange':
+ this.setHeight(window.innerHeight -
+ evt.detail.height - StatusBar.height);
+ break;
+ }
+ },
+
+ setHeight: function ad_setHeight(height) {
+ if (this.isVisible())
+ this.overlay.style.height = height + 'px';
+ },
+
+ show: function ad_show(origin) {
+ this.currentOrigin = origin;
+ var evt = this.currentEvents[origin];
+ var elements = this.elements;
+ this.screen.classList.add('authentication-dialog');
+ elements.httpAuthentication.classList.add('visible');
+ elements.title.textContent = evt.detail.host;
+ elements.httpAuthenticationMessage.textContent = evt.detail.realm;
+ elements.httpUsernameInput.value = '';
+ elements.httpPasswordInput.value = '';
+
+ this.setHeight(window.innerHeight - StatusBar.height);
+ },
+
+ hide: function ad_hide() {
+ this.elements.httpUsernameInput.blur();
+ this.elements.httpPasswordInput.blur();
+ this.currentOrigin = null;
+ this.elements.httpAuthentication.classList.remove('visible');
+ this.screen.classList.remove('authentication-dialog');
+ },
+
+ confirmHandler: function ad_confirmHandler() {
+ var elements = this.elements;
+ var evt = this.currentEvents[this.currentOrigin];
+ evt.detail.authenticate(elements.httpUsernameInput.value,
+ elements.httpPasswordInput.value);
+ elements.httpAuthentication.classList.remove('visible');
+ delete this.currentEvents[this.currentOrigin];
+ this.screen.classList.remove('authentication-dialog');
+ },
+
+ cancelHandler: function ad_cancelHandler() {
+ var evt = this.currentEvents[this.currentOrigin];
+ var elements = this.elements;
+ evt.detail.cancel();
+ elements.httpAuthentication.classList.remove('visible');
+ delete this.currentEvents[this.currentOrigin];
+ this.screen.classList.remove('authentication-dialog');
+ },
+
+ isVisible: function ad_isVisible() {
+ return this.screen.classList.contains('authentication-dialog');
+ }
+};
+
+AuthenticationDialog.init();
diff --git a/apps/system/js/background_service.js b/apps/system/js/background_service.js
new file mode 100644
index 0000000..117b556
--- /dev/null
+++ b/apps/system/js/background_service.js
@@ -0,0 +1,196 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+/*
+ Allow web apps to inject a tiny persistent background iframe
+ as the phone starts.
+*/
+var BackgroundServiceManager = (function bsm() {
+ /* We keep the references to background page iframes here.
+ The iframes will be append to body */
+ var frames = {};
+
+ /* The name of the background window open by background_page in
+ manifest. */
+ var AUTO_OPEN_BG_PAGE_NAME = 'background';
+
+ /* Init */
+ var init = function bsm_init() {
+ var applications = Applications.installedApps;
+ Object.keys(applications).forEach(function bsm_each(manifestURL) {
+ var app = applications[manifestURL];
+ if (!app.manifest.background_page)
+ return;
+
+ // XXX: this work as if background_page is always a path not a full URL.
+ var url = app.origin + app.manifest.background_page;
+ open(manifestURL, AUTO_OPEN_BG_PAGE_NAME, url);
+ });
+ };
+
+ /* mozbrowseropenwindow */
+ window.addEventListener('mozbrowseropenwindow', function bsm_winopen(evt) {
+ if (evt.detail.features !== 'background')
+ return;
+
+ // stopPropagation means we are not allowing
+ // Popup Manager to handle this event
+ evt.stopPropagation();
+
+ var manifestURL = evt.target.getAttribute('mozapp');
+ var detail = evt.detail;
+
+ open(manifestURL, detail.name, detail.url, detail.frameElement);
+ }, true);
+
+ /* mozbrowserclose */
+ window.addEventListener('mozbrowserclose', function bsm_winclose(evt) {
+ if (!'frameType' in evt.target.dataset ||
+ evt.target.dataset.frameType !== 'background')
+ return;
+
+ var manifestURL = evt.target.getAttribute('mozapp');
+
+ close(manifestURL, evt.target.dataset.frameName);
+ }, true);
+
+ /* mozbrowsererror */
+ window.addEventListener('mozbrowsererror', function bsm_winclose(evt) {
+ if (!'frameType' in evt.target.dataset ||
+ evt.target.dataset.frameType !== 'background' ||
+ evt.detail.type !== 'fatal')
+ return;
+
+ var target = evt.target;
+ var manifestURL = target.getAttribute('mozapp');
+
+ // This bg service has just crashed, clean up the frame
+ var name = target.dataset.frameName;
+ close(manifestURL, name);
+ }, true);
+
+ /* OnInstall */
+ window.addEventListener('applicationinstall', function bsm_oninstall(evt) {
+ var app = evt.detail.application;
+ var origin = app.origin;
+ if (!app.manifest.background_page)
+ return;
+
+ // XXX: this work as if background_page is always a path not a full URL.
+ var url = origin + app.manifest.background_page;
+ open(manifestURL, AUTO_OPEN_BG_PAGE_NAME, url);
+ });
+
+ /* OnUninstall */
+ window.addEventListener('applicationuninstall', function bsm_oninstall(evt) {
+ var app = evt.detail.application;
+ close(app.manifestURL);
+ });
+
+ /* Check if the app has the permission to open a background page */
+ var hasBackgroundPermission = function bsm_checkPermssion(app) {
+ var mozPerms = navigator.mozPermissionSettings;
+ if (!mozPerms)
+ return false;
+
+ var value = mozPerms.get('backgroundservice', app.manifestURL,
+ app.origin, false);
+
+ return (value === 'allow');
+ };
+
+ /* The open function is responsible of containing the iframe */
+ var open = function bsm_open(manifestURL, name, url, frame) {
+ var app = Applications.getByManifestURL(manifestURL);
+ if (!app || !hasBackgroundPermission(app))
+ return false;
+
+ if (frames[manifestURL] && frames[manifestURL][name]) {
+ console.error('Window with the same name is there but Gecko ' +
+ ' failed to use it. See bug 766873. origin: "' + origin +
+ '", name: "' + name + '".');
+ return false;
+ }
+
+ if (!frame) {
+ frame = document.createElement('iframe');
+
+ // If we have a frame element, it's provided by mozbrowseropenwindow, and
+ // it has the mozbrowser, mozapp, and src attributes set already.
+ frame.setAttribute('mozbrowser', 'mozbrowser');
+ frame.setAttribute('mozapp', manifestURL);
+ frame.setAttribute('name', name);
+
+ var appName = app.manifest.name;
+ frame.setAttribute('remote', 'true');
+ console.info('%%%%% Launching', appName, 'bg service as remote (OOP)');
+ frame.src = url;
+ }
+ frame.className = 'backgroundWindow';
+ frame.dataset.frameType = 'background';
+ frame.dataset.frameName = name;
+
+ if (!frames[manifestURL])
+ frames[manifestURL] = {};
+ frames[manifestURL][name] = frame;
+
+ document.body.appendChild(frame);
+
+ // Background services should load in the background.
+ //
+ // (The funky setTimeout(0) is to work around
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=810431 .)
+ setTimeout(function() { frame.setVisible(false) }, 0);
+
+ return true;
+ };
+
+ /* The close function will remove the iframe from DOM and
+ delete the reference */
+ var close = function bsm_close(manifestURL, name) {
+ if (!frames[manifestURL])
+ return false;
+
+ if (typeof name == 'undefined') {
+ // Close all windows
+ Object.keys(frames[manifestURL]).forEach(function closeEach(name) {
+ document.body.removeChild(frames[manifestURL][name]);
+ frames[manifestURL][name] = null;
+ });
+ delete frames[manifestURL];
+ return true;
+ }
+
+ // Close one window
+ var frame = frames[manifestURL][name];
+ if (!frame)
+ return false;
+
+ document.body.removeChild(frame);
+ delete frames[manifestURL][name];
+
+ if (!Object.keys(frames[manifestURL]).length)
+ delete frames[manifestURL];
+ return true;
+ };
+
+ /* start initialization */
+ if (Applications.ready) {
+ init();
+ } else {
+ window.addEventListener('applicationready',
+ function bsm_appListReady(event) {
+ window.removeEventListener('applicationready', bsm_appListReady);
+ init();
+ });
+ }
+
+ /* Return the public APIs */
+ return {
+ 'open': open,
+ 'close': close
+ };
+}());
+
diff --git a/apps/system/js/battery_manager.js b/apps/system/js/battery_manager.js
new file mode 100644
index 0000000..d2eacf8
--- /dev/null
+++ b/apps/system/js/battery_manager.js
@@ -0,0 +1,277 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var BatteryManager = {
+ TOASTER_TIMEOUT: 5000,
+ TRANSITION_SPEED: 1.8,
+ TRANSITION_FRACTION: 0.30,
+
+ AUTO_SHUTDOWN_LEVEL: 0.02,
+ EMPTY_BATTERY_LEVEL: 0.1,
+
+ _battery: window.navigator.battery,
+ _notification: null,
+
+ getAllElements: function bm_getAllElements() {
+ this.screen = document.getElementById('screen');
+ this.overlay = document.getElementById('system-overlay');
+ this.notification = document.getElementById('battery');
+ },
+
+ checkBatteryDrainage: function bm_checkBatteryDrainage() {
+ var battery = this._battery;
+ if (!battery)
+ return;
+
+ if (battery.level <= this.AUTO_SHUTDOWN_LEVEL)
+ SleepMenu.startPowerOff(false);
+ },
+
+ init: function bm_init() {
+ this.getAllElements();
+ var battery = this._battery;
+ if (battery) {
+ // When the device is booted, check if the battery is drained.
+ // If so, SleepMenu.startPowerOff() would be called.
+ this.checkBatteryDrainage();
+
+ battery.addEventListener('levelchange', this);
+ battery.addEventListener('chargingchange', this);
+ }
+ window.addEventListener('screenchange', this);
+ this._toasterGD = new GestureDetector(this.notification);
+ ['mousedown', 'swipe'].forEach(function(evt) {
+ this.notification.addEventListener(evt, this);
+ }, this);
+
+ this._screenOn = true;
+ this._wasEmptyBatteryNotificationDisplayed = false;
+
+ this.displayIfNecessary();
+ },
+
+ handleEvent: function bm_handleEvent(evt) {
+ switch (evt.type) {
+ case 'screenchange':
+ this._screenOn = evt.detail.screenEnabled;
+ this.displayIfNecessary();
+ break;
+
+ case 'levelchange':
+ var battery = this._battery;
+ if (!battery)
+ return;
+
+ this.checkBatteryDrainage();
+ this.displayIfNecessary();
+
+ PowerSaveHandler.onBatteryChange();
+ break;
+ case 'chargingchange':
+ PowerSaveHandler.onBatteryChange();
+
+ var battery = this._battery;
+ // We turn the screen on if needed in order to let
+ // the user knows the device is charging
+
+ if (battery && battery.charging) {
+ this.hide();
+ this._wasEmptyBatteryNotificationDisplayed = false;
+
+ if (!this._screenOn) {
+ ScreenManager.turnScreenOn();
+ }
+ } else {
+ this.displayIfNecessary();
+ }
+ break;
+
+ case 'mousedown':
+ this.mousedown(evt);
+ break;
+ case 'swipe':
+ this.swipe(evt);
+ break;
+ }
+ },
+
+ _shouldWeDisplay: function bm_shouldWeDisplay() {
+ var battery = this._battery;
+ if (!battery) {
+ return false;
+ }
+
+ return (!this._wasEmptyBatteryNotificationDisplayed &&
+ !battery.charging &&
+ battery.level <= this.EMPTY_BATTERY_LEVEL &&
+ this._screenOn);
+ },
+
+ displayIfNecessary: function bm_display() {
+ if (! this._shouldWeDisplay()) {
+ return;
+ }
+
+ // we know it's here, it's checked in shouldWeDisplay()
+ var level = this._battery.level;
+
+ this.overlay.classList.add('battery');
+
+ this._toasterGD.startDetecting();
+ this._wasEmptyBatteryNotificationDisplayed = true;
+
+ if (this._toasterTimeout) {
+ clearTimeout(this._toasterTimeout);
+ }
+
+ this._toasterTimeout = setTimeout(this.hide.bind(this),
+ this.TOASTER_TIMEOUT);
+ },
+
+ hide: function bm_hide() {
+ var overlayCss = this.overlay.classList;
+ if (overlayCss.contains('battery')) {
+ this.overlay.classList.remove('battery');
+ this._toasterTimeout = null;
+ this._toasterGD.stopDetecting();
+ }
+ },
+
+ // Swipe handling
+ mousedown: function bm_mousedown(evt) {
+ evt.preventDefault();
+ this._containerWidth = this.overlay.clientWidth;
+ },
+
+ swipe: function bm_swipe(evt) {
+ var detail = evt.detail;
+ var distance = detail.start.screenX - detail.end.screenX;
+ var fastEnough = Math.abs(detail.vx) > this.TRANSITION_SPEED;
+ var farEnough = Math.abs(distance) >
+ this._containerWidth * this.TRANSITION_FRACTION;
+
+ // If the swipe distance is too short or swipe speed is too slow,
+ // do nothing.
+ if (!(farEnough || fastEnough))
+ return;
+
+ var self = this;
+ this.notification.addEventListener('animationend', function animationend() {
+ self.notification.removeEventListener('animationend', animationend);
+ self.notification.classList.remove('disappearing');
+ self.hide();
+ });
+ this.notification.classList.add('disappearing');
+ }
+};
+
+var PowerSaveHandler = (function PowerSaveHandler() {
+
+ var _powerSaveResume = {};
+ var _powerSaveEnabled = false;
+ var _states = {
+ 'wifi.enabled' : false,
+ 'ril.data.enabled' : false,
+ 'bluetooth.enabled' : false,
+ 'geolocation.enabled' : false
+ };
+
+ function init() {
+ SettingsListener.observe('powersave.enabled', false,
+ function sl_getPowerSave(value) {
+ var enabled = value;
+ if (enabled) {
+ enablePowerSave();
+ } else {
+ disablePowerSave();
+ }
+ _powerSaveEnabled = enabled;
+ });
+
+ // Monitor the states of various modules
+ for (var j in _states) {
+ SettingsListener.observe(j, true, function getState(state, value) {
+ _states[state] = value;
+ }.bind(null, j));
+ }
+ }
+
+ // XXX Break down obj keys in a for each loop because mozSettings
+ // does not currently supports multiple keys in one set()
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=779381
+ function setMozSettings(keypairs) {
+ var setlock = SettingsListener.getSettingsLock();
+ for (var key in keypairs) {
+ var obj = {};
+ obj[key] = keypairs[key];
+ setlock.set(obj);
+ }
+ }
+
+ function enablePowerSave() {
+ // Keep the original states of various modules
+ for (var j in _states) {
+ _powerSaveResume[j] = _states[j];
+ }
+
+ var settingsToSet = {
+ // Turn off Wifi
+ 'wifi.enabled' : false,
+ // Turn off Data
+ 'ril.data.enabled' : false,
+ // Turn off Bluetooth
+ 'bluetooth.enabled' : false,
+ // Turn off Geolocation
+ 'geolocation.enabled' : false
+ };
+
+ setMozSettings(settingsToSet);
+ }
+
+ function disablePowerSave() {
+
+ var settingsToSet = {};
+
+ for (var state in _powerSaveResume) {
+ if (_powerSaveResume[state] == true)
+ settingsToSet[state] = true;
+ }
+
+ setMozSettings(settingsToSet);
+ }
+
+ function onBatteryChange() {
+ var battery = BatteryManager._battery;
+
+ if (battery.charging) {
+ if (_powerSaveEnabled)
+ setMozSettings({'powersave.enabled' : false});
+
+ return;
+ }
+
+ SettingsListener.observe('powersave.threshold', 0,
+ function getThreshold(value) {
+ if (battery.level <= value && !_powerSaveEnabled) {
+ setMozSettings({'powersave.enabled' : true});
+ return;
+ }
+
+ if (value != 0 && battery.level > value && _powerSaveEnabled) {
+ setMozSettings({'powersave.enabled' : false});
+ return;
+ }
+ });
+ }
+
+ return {
+ init: init,
+ onBatteryChange: onBatteryChange
+ };
+})();
+
+// init PowerSaveHandler first, since it will be used by BatteryManager
+PowerSaveHandler.init();
+BatteryManager.init();
diff --git a/apps/system/js/bluetooth.js b/apps/system/js/bluetooth.js
new file mode 100644
index 0000000..3a77cc7
--- /dev/null
+++ b/apps/system/js/bluetooth.js
@@ -0,0 +1,136 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var Bluetooth = {
+
+ /* this property store a reference of the default adapter */
+ defaultAdapter: null,
+
+ /* keep a global connected property here */
+ connected: false,
+
+ init: function bt_init() {
+ if (!window.navigator.mozSettings)
+ return;
+
+ var bluetooth = window.navigator.mozBluetooth;
+
+ SettingsListener.observe('bluetooth.enabled', true, function(value) {
+ if (!bluetooth) {
+ // roll back the setting value to notify the UIs
+ // that Bluetooth interface is not available
+ if (value) {
+ SettingsListener.getSettingsLock().set({
+ 'bluetooth.enabled': false
+ });
+ }
+ return;
+ }
+ });
+
+ var self = this;
+ // when bluetooth adapter is ready, emit event to notify QuickSettings
+ // and try to get defaultAdapter at this moment
+ bluetooth.onadapteradded = function bt_onAdapterAdded() {
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('bluetooth-adapter-added',
+ /* canBubble */ true, /* cancelable */ false, null);
+ window.dispatchEvent(evt);
+ self.initDefaultAdapter();
+ };
+ // if bluetooth is enabled in booting time, try to get adapter now
+ this.initDefaultAdapter();
+
+ // when bluetooth is really disabled, emit event to notify QuickSettings
+ bluetooth.ondisabled = function bt_onDisabled() {
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('bluetooth-disabled',
+ /* canBubble */ true, /* cancelable */ false, null);
+ window.dispatchEvent(evt);
+ };
+
+ /* for v1, we only support two use cases for bluetooth connection:
+ * 1. connecting with a headset
+ * 2. transfering a file to/from another device
+ * So we need to monitor their event messages to know we are (aren't)
+ * connected, then summarize to an event and dispatch to StatusBar
+ */
+
+ // In headset connected case:
+ navigator.mozSetMessageHandler('bluetooth-hfp-status-changed',
+ this.updateConnected.bind(this)
+ );
+
+ /* In file transfering case:
+ * since System Message can't be listened in two js files within a app,
+ * so we listen here but dispatch events to bluetooth_transfer.js
+ * when getting bluetooth file transfer start/complete system messages
+ */
+ var self = this;
+ navigator.mozSetMessageHandler('bluetooth-opp-transfer-start',
+ function bt_fileTransferUpdate(transferInfo) {
+ self.updateConnected();
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('bluetooth-opp-transfer-start',
+ /* canBubble */ true, /* cancelable */ false,
+ {transferInfo: transferInfo});
+ window.dispatchEvent(evt);
+ }
+ );
+
+ navigator.mozSetMessageHandler('bluetooth-opp-transfer-complete',
+ function bt_fileTransferUpdate(transferInfo) {
+ self.updateConnected();
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('bluetooth-opp-transfer-complete',
+ /* canBubble */ true, /* cancelable */ false,
+ {transferInfo: transferInfo});
+ window.dispatchEvent(evt);
+ }
+ );
+
+ },
+
+ // Get adapter for BluetoothTransfer when everytime bluetooth is enabled
+ initDefaultAdapter: function bt_initDefaultAdapter() {
+ var bluetooth = window.navigator.mozBluetooth;
+ var self = this;
+
+ if (!bluetooth || !bluetooth.enabled ||
+ !('getDefaultAdapter' in bluetooth))
+ return;
+
+ var req = bluetooth.getDefaultAdapter();
+ req.onsuccess = function bt_gotDefaultAdapter(evt) {
+ self.defaultAdapter = req.result;
+ };
+ },
+
+ updateConnected: function bt_updateConnected() {
+ var bluetooth = window.navigator.mozBluetooth;
+
+ if (!bluetooth || !('isConnected' in bluetooth))
+ return;
+
+ var wasConnected = this.connected;
+ this.connected =
+ bluetooth.isConnected(0x111E) || bluetooth.isConnected(0x1105);
+
+ if (wasConnected !== this.connected) {
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('bluetoothconnectionchange',
+ /* canBubble */ true, /* cancelable */ false,
+ {deviceConnected: this.connected});
+ window.dispatchEvent(evt);
+ }
+ },
+
+ // This function is called by external (BluetoothTransfer) for re-use adapter
+ getAdapter: function bt_getAdapter() {
+ return this.defaultAdapter;
+ }
+};
+
+Bluetooth.init();
diff --git a/apps/system/js/bluetooth_transfer.js b/apps/system/js/bluetooth_transfer.js
new file mode 100644
index 0000000..47483f7
--- /dev/null
+++ b/apps/system/js/bluetooth_transfer.js
@@ -0,0 +1,511 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+'use strict';
+
+var BluetoothTransfer = {
+ bannerContainer: null,
+ pairList: {
+ index: []
+ },
+ _deviceStorage: navigator.getDeviceStorage('sdcard'),
+ _debug: false,
+
+ get transferStatusList() {
+ delete this.transferStatusList;
+ return this.transferStatusList =
+ document.getElementById('bluetooth-transfer-status-list');
+ },
+
+ get banner() {
+ delete this.banner;
+ return this.banner = document.getElementById('system-banner');
+ },
+
+ init: function bt_init() {
+ // Bind message handler for transferring file callback
+ navigator.mozSetMessageHandler('bluetooth-opp-receiving-file-confirmation',
+ this.onReceivingFileConfirmation.bind(this)
+ );
+
+ // Listen to 'bluetooth-opp-transfer-start' from bluetooth.js
+ window.addEventListener('bluetooth-opp-transfer-start',
+ this.onUpdateProgress.bind(this, 'start')
+ );
+
+ navigator.mozSetMessageHandler('bluetooth-opp-update-progress',
+ this.onUpdateProgress.bind(this, 'progress')
+ );
+
+ // Listen to 'bluetooth-opp-transfer-complete' from bluetooth.js
+ window.addEventListener('bluetooth-opp-transfer-complete',
+ this.onTransferComplete.bind(this)
+ );
+ this.bannerContainer = this.banner.firstElementChild;
+ },
+
+ getDeviceName: function bt_getDeviceName(address) {
+ var _ = navigator.mozL10n.get;
+ var length = this.pairList.index.length;
+ for (var i = 0; i < length; i++) {
+ if (this.pairList.index[i].address == address)
+ return this.pairList.index[i].name;
+ }
+ return _('unknown-device');
+ },
+
+ getPairedDevice: function bt_getPairedDevice(callback) {
+ var adapter = Bluetooth.getAdapter();
+ if (adapter == null) {
+ var msg = 'Cannot get Bluetooth adapter.';
+ this.debug(msg);
+ return;
+ }
+ var self = this;
+ var req = adapter.getPairedDevices();
+ req.onsuccess = function bt_getPairedSuccess() {
+ self.pairList.index = req.result;
+ var length = self.pairList.index.length;
+ if (length == 0) {
+ var msg =
+ 'There is no paired device! Please pair your bluetooth device first.';
+ self.debug(msg);
+ return;
+ }
+ if (callback) {
+ callback();
+ }
+ };
+ req.onerror = function() {
+ var msg = 'Can not get paired devices from adapter.';
+ self.debug(msg);
+ };
+ },
+
+ debug: function bt_debug(msg) {
+ if (!this._debug)
+ return;
+
+ console.log('[System Bluetooth Transfer]: ' + msg);
+ },
+
+ humanizeSize: function bt_humanizeSize(bytes) {
+ var _ = navigator.mozL10n.get;
+ var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
+ var size, e;
+ if (bytes) {
+ e = Math.floor(Math.log(bytes) / Math.log(1024));
+ size = (bytes / Math.pow(1024, e)).toFixed(2);
+ } else {
+ e = 0;
+ size = '0.00';
+ }
+ return _('fileSize', {
+ size: size,
+ unit: _('byteUnit-' + units[e])
+ });
+ },
+
+ onReceivingFileConfirmation: function bt_onReceivingFileConfirmation(evt) {
+ // Prompt appears when a transfer request from a paired device is received.
+ var _ = navigator.mozL10n.get;
+
+ var fileSize = evt.fileLength;
+ var self = this;
+ var icon = 'style/bluetooth_transfer/images/icon_bluetooth.png';
+
+ // Check storage is available or not before the prompt.
+ this.checkStorageSpace(fileSize,
+ function checkStorageSpaceComplete(isStorageAvailable, errorMessage) {
+ if (isStorageAvailable) {
+ NotificationHelper.send(_('notification-fileTransfer-title'),
+ _('notification-fileTransfer-description'),
+ icon,
+ function() {
+ UtilityTray.hide();
+ self.showReceivePrompt(evt);
+ });
+ } else {
+ self.showStorageUnavaliablePrompt(errorMessage);
+ }
+ });
+ },
+
+ showReceivePrompt: function bt_showReceivePrompt(evt) {
+ var _ = navigator.mozL10n.get;
+
+ var address = evt.address;
+ var fileName = evt.fileName;
+ var fileSize = this.humanizeSize(evt.fileLength);
+ var cancel = {
+ title: _('deny'),
+ callback: this.declineReceive.bind(this, address)
+ };
+
+ var confirm = {
+ title: _('transfer'),
+ callback: this.acceptReceive.bind(this, address)
+ };
+
+ var deviceName = '';
+ var self = this;
+ this.getPairedDevice(function getPairedDeviceComplete() {
+ deviceName = self.getDeviceName(address);
+ CustomDialog.show(_('acceptFileTransfer'),
+ _('wantToReceiveFile',
+ { deviceName: deviceName,
+ fileName: fileName,
+ fileSize: fileSize }),
+ cancel, confirm);
+ });
+ },
+
+ declineReceive: function bt_declineReceive(address) {
+ CustomDialog.hide();
+ var adapter = Bluetooth.getAdapter();
+ if (adapter != null) {
+ adapter.confirmReceivingFile(address, false);
+ } else {
+ var msg = 'Cannot get adapter from system Bluetooth monitor.';
+ this.debug(msg);
+ }
+ },
+
+ acceptReceive: function bt_acceptReceive(address, fileSize) {
+ CustomDialog.hide();
+ var adapter = Bluetooth.getAdapter();
+ if (adapter != null) {
+ adapter.confirmReceivingFile(address, true);
+ } else {
+ var msg = 'Cannot get adapter from system Bluetooth monitor.';
+ this.debug(msg);
+ }
+ },
+
+ showStorageUnavaliablePrompt: function bt_showStorageUnavaliablePrompt(msg) {
+ var _ = navigator.mozL10n.get;
+ var confirm = {
+ title: _('confirm'),
+ callback: function() {
+ CustomDialog.hide();
+ }
+ };
+
+ var body = msg;
+ CustomDialog.show(_('cannotReceiveFile'), body, confirm);
+ },
+
+ checkStorageSpace: function bt_checkStorageSpace(fileSize, callback) {
+ if (!callback)
+ return;
+
+ var _ = navigator.mozL10n.get;
+ var storage = this._deviceStorage;
+
+ var availreq = storage.available();
+ availreq.onsuccess = function(e) {
+ switch (availreq.result) {
+ case 'available':
+ // skip down to the code below
+ break;
+ case 'unavailable':
+ callback(false, _('sdcard-not-exist'));
+ return;
+ case 'shared':
+ callback(false, _('sdcard-in-use'));
+ return;
+ default:
+ callback(false, _('unknown-error'));
+ return;
+ }
+
+ // If we get here, then the sdcard is available, so we need to find out
+ // if there is enough free space on it
+ var freereq = storage.freeSpace();
+ freereq.onsuccess = function() {
+ if (freereq.result >= fileSize)
+ callback(true, '');
+ else
+ callback(false, _('sdcard-no-space2'));
+ };
+ freereq.onerror = function() {
+ callback(false, _('cannotGetStorageState'));
+ };
+ };
+
+ availreq.onerror = function(e) {
+ callback(false, _('cannotGetStorageState'));
+ };
+ },
+
+ onUpdateProgress: function bt_onUpdateProgress(mode, evt) {
+ switch (mode) {
+ case 'start':
+ var transferInfo = evt.detail.transferInfo;
+ this.initProgress(transferInfo);
+ break;
+
+ case 'progress':
+ var address = evt.address;
+ var processedLength = evt.processedLength;
+ var fileLength = evt.fileLength;
+ var progress = 0;
+ if (fileLength == 0) {
+ //XXX: May need to handle unknow progress
+ } else if (processedLength > fileLength) {
+ // According Bluetooth spec.,
+ // the processed length is a referenced value only.
+ // XXX: If processed length is bigger than file length,
+ // show an unknown progress
+ } else {
+ progress = processedLength / fileLength;
+ }
+ this.updateProgress(progress, evt);
+ break;
+ }
+ },
+
+ initProgress: function bt_initProgress(evt) {
+ var _ = navigator.mozL10n.get;
+ // Create progress dynamically in notification center
+ var address = evt.address;
+ var transferMode =
+ (evt.received == true) ?
+ _('bluetooth-receiving-progress') : _('bluetooth-sending-progress');
+ var content =
+ '<img src="style/bluetooth_transfer/images/transfer.png" />' +
+ '<div class="bluetooth-transfer-progress">' + transferMode + '</div>' +
+ // XXX: Bug 804533 - [Bluetooth]
+ // Need sending/receiving icon for Bluetooth file transfer
+ '<progress value="0" max="1"></progress>';
+
+ var transferTask = document.createElement('div');
+ transferTask.id = 'bluetooth-transfer-status';
+ transferTask.className = 'notification';
+ transferTask.setAttribute('data-id', address);
+ transferTask.innerHTML = content;
+ transferTask.addEventListener('click',
+ this.onCancelTransferTask.bind(this));
+ this.transferStatusList.appendChild(transferTask);
+ },
+
+ updateProgress: function bt_updateProgress(value, evt) {
+ var address = evt.address;
+ var id = 'div[data-id="' + address + '"] progress';
+ var progressEl = this.transferStatusList.querySelector(id);
+ progressEl.value = value;
+ },
+
+ removeProgress: function bt_removeProgress(evt) {
+ var address = evt.address;
+ var id = 'div[data-id="' + address + '"]';
+ var finishedTask = this.transferStatusList.querySelector(id);
+ finishedTask.removeEventListener('click',
+ this.onCancelTransferTask.bind(this));
+ this.transferStatusList.removeChild(finishedTask);
+ },
+
+ showBanner: function bt_showBanner(isComplete) {
+ var _ = navigator.mozL10n.get;
+ var status = (isComplete) ? 'complete' : 'failed';
+ this.banner.addEventListener('animationend', function animationend() {
+ this.banner.removeEventListener('animationend', animationend);
+ this.banner.classList.remove('visible');
+ }.bind(this));
+ this.bannerContainer.textContent = _('bluetooth-file-transfer-result',
+ { status: status });
+ this.banner.classList.add('visible');
+ },
+
+ onCancelTransferTask: function bt_onCancelTransferTask(evt) {
+ var id = evt.target.dataset.id;
+ // Show confirm dialog for user to cancel transferring task
+ UtilityTray.hide();
+ this.showCancelTransferPrompt(id);
+ },
+
+ showCancelTransferPrompt: function bt_showCancelTransferPrompt(address) {
+ var _ = navigator.mozL10n.get;
+
+ var cancel = {
+ title: _('continue'),
+ callback: this.continueTransfer.bind(this)
+ };
+
+ var confirm = {
+ title: _('cancel'),
+ callback: this.cancelTransfer.bind(this, address)
+ };
+
+ CustomDialog.show(_('cancelFileTransfer'), _('cancelFileTransfer'),
+ cancel, confirm);
+ },
+
+ continueTransfer: function bt_continueTransfer() {
+ CustomDialog.hide();
+ },
+
+ cancelTransfer: function bt_cancelTransfer(address) {
+ CustomDialog.hide();
+ var adapter = Bluetooth.getAdapter();
+ if (adapter != null) {
+ adapter.stopSendingFile(address);
+ } else {
+ var msg = 'Cannot get adapter from system Bluetooth monitor.';
+ this.debug(msg);
+ }
+ },
+
+ onTransferComplete: function bt_onTransferComplete(evt) {
+ var transferInfo = evt.detail.transferInfo;
+ var _ = navigator.mozL10n.get;
+ // Remove transferring progress
+ this.removeProgress(transferInfo);
+ var fileName =
+ (transferInfo.fileName) ? transferInfo.fileName : _('unknown-file');
+ var icon = 'style/bluetooth_transfer/images/icon_bluetooth.png';
+ // Show banner and notification
+ if (transferInfo.success == true) {
+ // Show completed message of transferred result on the banner
+ this.showBanner(true);
+ if (transferInfo.received) {
+ // Received file can be opened only
+ // TODO: Need to modify the icon after visual provide
+ NotificationHelper.send(_('transferFinished-receivedSuccessful-title'),
+ fileName,
+ icon,
+ this.openReceivedFile.bind(this, transferInfo));
+ } else {
+ NotificationHelper.send(_('transferFinished-sentSuccessful-title'),
+ fileName,
+ icon);
+ }
+ } else {
+ // Show failed message of transferred result on the banner
+ this.showBanner(false);
+ if (transferInfo.received) {
+ NotificationHelper.send(_('transferFinished-receivedFailed-title'),
+ fileName,
+ icon);
+ } else {
+ NotificationHelper.send(_('transferFinished-sentFailed-title'),
+ fileName,
+ icon);
+ }
+ }
+ },
+
+ openReceivedFile: function bt_openReceivedFile(evt) {
+ // Launch the gallery with an open activity to view this specific photo
+ // XXX: The prefix file path should be refined when API is ready to provide
+ var filePath = 'downloads/bluetooth/' + evt.fileName;
+ var contentType = evt.contentType;
+ var storageType = 'sdcard';
+ var self = this;
+ var storage = navigator.getDeviceStorage(storageType);
+ var getreq = storage.get(filePath);
+
+ getreq.onerror = function() {
+ var msg = 'failed to get file:' +
+ filePath + getreq.error.name +
+ a.error.name;
+ self.debug(msg);
+ };
+
+ getreq.onsuccess = function() {
+ var file = getreq.result;
+ // When we got the file by storage type of "sdcard"
+ // use the file.type to replace the empty fileType which is given by API
+ var fileType = '';
+ var fileName = file.name;
+ if (contentType != '' && contentType != 'image/*') {
+ fileType = contentType;
+ } else {
+ var fileNameExtension =
+ fileName.substring(fileName.lastIndexOf('.') + 1);
+ if (file.type != '') {
+ fileType = file.type;
+ // Refine the file type to "audio/ogg" when the file format is *.ogg
+ if (fileType == 'video/ogg' &&
+ (fileNameExtension.indexOf('ogg') != -1)) {
+ fileType == 'audio/ogg';
+ }
+ } else {
+ // Parse Filename Extension to find out MIMETYPE
+ // Following formats are supported by Gallery and Music APPs
+ var imageFormatList = ['jpg', 'jpeg', 'png'];
+ var audioFormatList = ['mp3', 'ogg', 'aac', 'mp4', 'm4a'];
+ var imageFormatIndex = imageFormatList.indexOf(fileNameExtension);
+ switch (imageFormatIndex) {
+ case 0:
+ case 1:
+ // The file type of format *.jpg, *.jpeg should be "image/jpeg"
+ fileType = 'image/jpeg';
+ break;
+ case 2:
+ // The file type of format *.png should be "image/png"
+ fileType = 'image/png';
+ break;
+ }
+
+ var audioFormatIndex = audioFormatList.indexOf(fileNameExtension);
+ switch (audioFormatIndex) {
+ case 0:
+ // The file type of format *.mp3 should be "audio/mpeg"
+ fileType = 'audio/mpeg';
+ break;
+ case 1:
+ // The file type of format *.ogg should be "audio/ogg"
+ fileType = 'audio/ogg';
+ break;
+ case 2:
+ case 3:
+ case 4:
+ // The file type of format *.acc, *.mp4, *.m4a
+ // should be "audio/mp4"
+ fileType = 'audio/mp4';
+ break;
+ }
+ }
+ }
+
+ var a = new MozActivity({
+ name: 'open',
+ data: {
+ type: fileType,
+ blob: file,
+ // XXX: https://bugzilla.mozilla.org/show_bug.cgi?id=812098
+ // Pass the file name for Music APP since it can not open blob
+ filename: file.name
+ }
+ });
+
+ a.onerror = function(e) {
+ var msg = 'open activity error:' + a.error.name;
+ self.debug(msg);
+ // Cannot identify MIMETYPE
+ // So, show cannot open file dialog with unknow media type
+ UtilityTray.hide();
+ self.showUnknownMediaPrompt(fileName);
+ };
+ a.onsuccess = function(e) {
+ var msg = 'open activity onsuccess';
+ self.debug(msg);
+ };
+ };
+ },
+
+ showUnknownMediaPrompt: function bt_showUnknownMediaPrompt(fileName) {
+ var _ = navigator.mozL10n.get;
+ var confirm = {
+ title: _('confirm'),
+ callback: function() {
+ CustomDialog.hide();
+ }
+ };
+
+ var body = _('unknownMediaTypeToOpen') + ' ' + fileName;
+ CustomDialog.show(_('cannotOpenFile'), body, confirm);
+ }
+};
+
+BluetoothTransfer.init();
diff --git a/apps/system/js/bootstrap.js b/apps/system/js/bootstrap.js
new file mode 100644
index 0000000..21e2238
--- /dev/null
+++ b/apps/system/js/bootstrap.js
@@ -0,0 +1,82 @@
+/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+window.addEventListener('load', function startup() {
+ function safelyLaunchFTU() {
+ WindowManager.retrieveHomescreen(WindowManager.retrieveFTU);
+ }
+
+ if (Applications.ready) {
+ safelyLaunchFTU();
+ } else {
+ window.addEventListener('applicationready', function appListReady(event) {
+ window.removeEventListener('applicationready', appListReady);
+ safelyLaunchFTU();
+ });
+ }
+
+ window.addEventListener('ftudone', function doneWithFTU() {
+ window.removeEventListener('ftudone', doneWithFTU);
+
+ var lock = window.navigator.mozSettings.createLock();
+ lock.set({
+ 'gaia.system.checkForUpdates': true
+ });
+ });
+
+ SourceView.init();
+ Shortcuts.init();
+ ScreenManager.turnScreenOn();
+
+ // We need to be sure to get the focus in order to wake up the screen
+ // if the phone goes to sleep before any user interaction.
+ // Apparently it works because no other window has the focus at this point.
+ window.focus();
+
+ // This is code copied from
+ // http://dl.dropbox.com/u/8727858/physical-events/index.html
+ // It appears to workaround the Nexus S bug where we're not
+ // getting orientation data. See:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=753245
+ // It seems it needs to be in both window_manager.js and bootstrap.js.
+ function dumbListener2(event) {}
+ window.addEventListener('devicemotion', dumbListener2);
+
+ window.setTimeout(function() {
+ window.removeEventListener('devicemotion', dumbListener2);
+ }, 2000);
+});
+
+/* === Shortcuts === */
+/* For hardware key handling that doesn't belong to anywhere */
+var Shortcuts = {
+ init: function rm_init() {
+ window.addEventListener('keyup', this);
+ },
+
+ handleEvent: function rm_handleEvent(evt) {
+ if (!ScreenManager.screenEnabled || evt.keyCode !== evt.DOM_VK_F6)
+ return;
+
+ document.location.reload();
+ }
+};
+
+/* === Localization === */
+/* set the 'lang' and 'dir' attributes to <html> when the page is translated */
+window.addEventListener('localized', function onlocalized() {
+ document.documentElement.lang = navigator.mozL10n.language.code;
+ document.documentElement.dir = navigator.mozL10n.language.direction;
+});
+
+// Define the default background to use for all homescreens
+SettingsListener.observe(
+ 'wallpaper.image',
+ 'resources/images/backgrounds/default.png',
+ function setWallpaper(value) {
+ document.getElementById('screen').style.backgroundImage =
+ 'url(' + value + ')';
+ }
+);
diff --git a/apps/system/js/call_forwarding.js b/apps/system/js/call_forwarding.js
new file mode 100644
index 0000000..ee46def
--- /dev/null
+++ b/apps/system/js/call_forwarding.js
@@ -0,0 +1,45 @@
+/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+(function() {
+
+ // Must be in sync with nsIDOMMozMobileCFInfo interface.
+ var _cfReason = {
+ CALL_FORWARD_REASON_UNCONDITIONAL: 0,
+ CALL_FORWARD_REASON_MOBILE_BUSY: 1,
+ CALL_FORWARD_REASON_NO_REPLY: 2,
+ CALL_FORWARD_REASON_NOT_REACHABLE: 3
+ };
+ var _cfAction = {
+ CALL_FORWARD_ACTION_DISABLE: 0,
+ CALL_FORWARD_ACTION_ENABLE: 1,
+ CALL_FORWARD_ACTION_QUERY_STATUS: 2,
+ CALL_FORWARD_ACTION_REGISTRATION: 3,
+ CALL_FORWARD_ACTION_ERASURE: 4
+ };
+
+ var settings = window.navigator.mozSettings;
+ if (!settings) {
+ return;
+ }
+ var mobileconnection = window.navigator.mozMobileConnection;
+ if (!mobileconnection) {
+ return;
+ }
+
+ mobileconnection.addEventListener('cfstatechange', function(event) {
+ if (event &&
+ event.reason == _cfReason.CALL_FORWARD_REASON_UNCONDITIONAL) {
+ var enabled = false;
+ if (event.success &&
+ (event.action == _cfAction.CALL_FORWARD_ACTION_REGISTRATION ||
+ event.action == _cfAction.CALL_FORWARD_ACTION_ENABLE)) {
+ enabled = true;
+ }
+ settings.createLock().set({'ril.cf.enabled': enabled});
+ }
+ });
+
+})();
diff --git a/apps/system/js/captive_portal.js b/apps/system/js/captive_portal.js
new file mode 100644
index 0000000..b23cb0d
--- /dev/null
+++ b/apps/system/js/captive_portal.js
@@ -0,0 +1,73 @@
+/* -*Mode: js; js-indent-level: 2; indent-tabs-mode: nil -**/
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+'use strict';
+
+var CaptivePortalLogin = (function() {
+ var eventId;
+ var isManualConnect = false;
+ var settings = window.navigator.mozSettings;
+ var notification = null;
+ var wifiManager = window.navigator.mozWifiManager;
+ var _ = window.navigator.mozL10n.get;
+ var captiveNotification_onTap = null;
+
+ function handleLogin(id, url) {
+ //captive portal login needed
+ eventId = id;
+ var currentNetwork = wifiManager.connection.network;
+ var networkName = (currentNetwork && currentNetwork.ssid) ?
+ currentNetwork.ssid : '';
+ var message = _('captive-wifi-available', { networkName: networkName});
+ if (!isManualConnect) {
+ notification = NotificationScreen.addNotification({
+ id: id, title: '', text: message, icon: null
+ });
+ captiveNotification_onTap = function() {
+ notification.removeEventListener('tap', captiveNotification_onTap);
+ captiveNotification_onTap = null;
+ NotificationScreen.removeNotification(id);
+ new MozActivity({
+ name: 'view',
+ data: { type: 'url', url: url }
+ });
+ };
+ notification.addEventListener('tap', captiveNotification_onTap);
+ } else {
+ settings.createLock().set({'wifi.connect_via_settings': false});
+ new MozActivity({
+ name: 'view',
+ data: { type: 'url', url: url }
+ });
+ }
+ }
+
+ function handleLoginAbort(id) {
+ if (id === eventId && notification) {
+ if (notification.parentNode) {
+ if (captiveNotification_onTap) {
+ notification.removeEventListener('tap', captiveNotification_onTap);
+ captiveNotification_onTap = null;
+ }
+ NotificationScreen.removeNotification(id);
+ notification = null;
+ }
+ }
+ }
+
+ window.addEventListener('mozChromeEvent', function handleChromeEvent(e) {
+ switch (e.detail.type) {
+ case 'captive-portal-login':
+ handleLogin(e.detail.id, e.detail.url);
+ break;
+ case 'captive-portal-login-abort':
+ handleLoginAbort(e.detail.id);
+ break;
+ }
+ });
+
+ // Using settings API to know whether user is manually selecting
+ // wifi AP from settings app.
+ SettingsListener.observe('wifi.connect_via_settings', true, function(value) {
+ isManualConnect = value;
+ });
+})();
diff --git a/apps/system/js/cards_view.js b/apps/system/js/cards_view.js
new file mode 100644
index 0000000..8bce02b
--- /dev/null
+++ b/apps/system/js/cards_view.js
@@ -0,0 +1,676 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+//
+// CardsView is responsible for managing opened apps
+//
+
+'use strict';
+
+var CardsView = (function() {
+
+ //display icon of an app on top of app's card
+ var DISPLAY_APP_ICON = false;
+ var USER_DEFINED_ORDERING = false;
+ // If 'true', scrolling moves the list one card
+ // at time, and snaps the list so the current card
+ // is centered in the view
+ // If 'false', use free, physics-based scrolling
+ // (Gaia default)
+ var SNAPPING_SCROLLING = true;
+ // if 'true' user can close the app
+ // by dragging it upwards
+ var MANUAL_CLOSING = true;
+
+ var cardsView = document.getElementById('cards-view');
+ var screenElement = document.getElementById('screen');
+ var cardsList = cardsView.firstElementChild;
+ var displayedApp;
+ var runningApps;
+ // Unkillable apps which have attention screen now
+ var attentionScreenApps = [];
+ // Card which we are re-ordering now
+ var reorderedCard = null;
+ var currentDisplayed = 0;
+ // Timer between scrolling CardList further,
+ // when reordering Cards
+ var scrollWhileSortingTimer;
+ // We don't allow user to scroll CardList
+ // before the timer ticks while in reordering
+ // mode
+ var allowScrollingWhileSorting = false;
+ // Initial margin of the reordered card
+ var dragMargin = 0;
+ // Are we reordering or removing the card now?
+ var draggingCardUp = false;
+ // Are we moving card left or right?
+ var sortingDirection;
+ // List of sorted apps
+ var userSortedApps = [];
+ var HVGA = document.documentElement.clientWidth < 480;
+ var cardsViewShown = false;
+
+ // init events
+ var gd = new GestureDetector(cardsView);
+ gd.startDetecting();
+
+ // A list of all the URLs we've created via URL.createObjectURL which we
+ // haven't yet revoked.
+ var screenshotObjectURLs = [];
+
+ /*
+ * Returns an icon URI
+ *
+ * @param{String} the app's origin
+ */
+ function getIconURI(origin) {
+ var icons = runningApps[origin].manifest.icons;
+ if (!icons) {
+ return null;
+ }
+
+ var sizes = Object.keys(icons).map(function parse(str) {
+ return parseInt(str, 10);
+ });
+
+ sizes.sort(function(x, y) { return y - x; });
+
+ var index = sizes[(HVGA) ? sizes.length - 1 : 0];
+ var iconPath = icons[index];
+
+ if (iconPath.indexOf('data:') !== 0) {
+ iconPath = origin + iconPath;
+ }
+
+ return iconPath;
+ }
+
+ // Build and display the card switcher overlay
+ // Note that we rebuild the switcher each time we need it rather
+ // than trying to keep it in sync with app launches. Performance is
+ // not an issue here given that the user has to hold the HOME button down
+ // for one second before the switcher will appear.
+ function showCardSwitcher() {
+ if (cardSwitcherIsShown())
+ return;
+
+ // events to handle
+ window.addEventListener('lock', CardsView);
+
+ // Close utility tray if it is opened.
+ UtilityTray.hide(true);
+
+ // Apps info from WindowManager
+ displayedApp = WindowManager.getDisplayedApp();
+ currentDisplayed = 0;
+ runningApps = WindowManager.getRunningApps();
+
+ // Switch to homescreen
+ WindowManager.launch(null);
+ cardsViewShown = true;
+
+ // If user is not able to sort apps manualy,
+ // display most recetly active apps on the far left
+ if (!USER_DEFINED_ORDERING) {
+ var sortable = [];
+ for (var origin in runningApps)
+ sortable.push({origin: origin, app: runningApps[origin]});
+
+ sortable.sort(function(a, b) {
+ return b.app.launchTime - a.app.launchTime;
+ });
+ runningApps = {};
+
+ // I assume that object properties are enumerated in
+ // the same order they were defined.
+ // There is nothing about that in spec, but I've never
+ // seen any unexpected behavior.
+ sortable.forEach(function(element) {
+ runningApps[element.origin] = element.app;
+ });
+
+ // First add an item to the cardsList for each running app
+ for (var origin in runningApps) {
+ addCard(origin, runningApps[origin], function showCards() {
+ screenElement.classList.add('cards-view');
+ cardsView.classList.add('active');
+ });
+ }
+
+ } else { // user ordering
+
+ // first run
+ if (userSortedApps.length === 0) {
+ for (var origin in runningApps) {
+ userSortedApps.push(origin);
+ }
+ } else {
+ for (var origin in runningApps) {
+ // if we have some new app opened
+ if (userSortedApps.indexOf(origin) === -1) {
+ userSortedApps.push(origin);
+ }
+ }
+ }
+
+ userSortedApps.forEach(function(origin) {
+ addCard(origin, runningApps[origin], function showCards() {
+ screenElement.classList.add('cards-view');
+ cardsView.classList.add('active');
+ });
+ });
+
+ cardsView.addEventListener('contextmenu', CardsView);
+
+ }
+
+ if (SNAPPING_SCROLLING) {
+ cardsView.style.overflow = 'hidden'; //disabling native scrolling
+ }
+
+ if (SNAPPING_SCROLLING || MANUAL_CLOSING) {
+ cardsView.addEventListener('mousedown', CardsView);
+ }
+
+ // Make sure we're in portrait mode
+ screen.mozLockOrientation('portrait-primary');
+
+ // If there is a displayed app, take keyboard focus away
+ if (displayedApp)
+ runningApps[displayedApp].frame.blur();
+
+ function addCard(origin, app, displayedAppCallback) {
+ // Display card switcher background first to make user focus on the
+ // frame closing animation without disturbing by homescreen display.
+ if (displayedApp == origin && displayedAppCallback) {
+ setTimeout(displayedAppCallback);
+ }
+ // Not showing homescreen
+ if (app.frame.classList.contains('homescreen')) {
+ return;
+ }
+
+ // Build a card representation of each window.
+ // And add it to the card switcher
+ var card = document.createElement('li');
+ card.classList.add('card');
+ card.dataset.origin = origin;
+
+ //display app icon on the tab
+ if (DISPLAY_APP_ICON) {
+ var iconURI = getIconURI(origin);
+ if (iconURI) {
+ var appIcon = document.createElement('img');
+ appIcon.classList.add('appIcon');
+ appIcon.src = iconURI;
+ card.appendChild(appIcon);
+ }
+ }
+
+ var title = document.createElement('h1');
+ title.textContent = app.name;
+ card.appendChild(title);
+
+ var frameForScreenshot = app.iframe;
+
+ if (PopupManager.getPopupFromOrigin(origin)) {
+ var popupFrame = PopupManager.getPopupFromOrigin(origin);
+ frameForScreenshot = popupFrame;
+
+ var subtitle = document.createElement('p');
+ subtitle.textContent =
+ PopupManager.getOpenedOriginFromOpener(origin);
+ card.appendChild(subtitle);
+ card.classList.add('popup');
+ } else if (getOffOrigin(app.frame.dataset.url ?
+ app.frame.dataset.url : app.frame.src, origin)) {
+ var subtitle = document.createElement('p');
+ subtitle.textContent = getOffOrigin(app.frame.dataset.url ?
+ app.frame.dataset.url : app.frame.src, origin);
+ card.appendChild(subtitle);
+ }
+
+ if (TrustedUIManager.hasTrustedUI(origin)) {
+ var popupFrame = TrustedUIManager.getDialogFromOrigin(origin);
+ frameForScreenshot = popupFrame.frame;
+ var header = document.createElement('section');
+ header.setAttribute('role', 'region');
+ header.classList.add('skin-organic');
+ header.innerHTML = '<header><button><span class="icon icon-close">';
+ header.innerHTML += '</span></button><h1>' + popupFrame.name;
+ header.innerHTML += '</h1></header>';
+ card.appendChild(header);
+ card.classList.add('trustedui');
+ } else if (attentionScreenApps.indexOf(origin) == -1) {
+ var closeButton = document.createElement('div');
+ closeButton.classList.add('close-card');
+ card.appendChild(closeButton);
+ }
+
+ cardsList.appendChild(card);
+ // rect is the final size (considering CSS transform) of the card.
+ var rect = card.getBoundingClientRect();
+
+ // And then switch it with screenshots when one will be ready
+ // (instead of -moz-element backgrounds)
+ frameForScreenshot.getScreenshot(rect.width, rect.height).onsuccess =
+ function gotScreenshot(screenshot) {
+ if (screenshot.target.result) {
+ var objectURL = URL.createObjectURL(screenshot.target.result);
+ screenshotObjectURLs.push(objectURL);
+ card.style.backgroundImage = 'url(' + objectURL + ')';
+ }
+ };
+
+ // Set up event handling
+ // A click elsewhere in the card switches to that task
+ card.addEventListener('tap', runApp);
+ }
+ }
+
+ function runApp(e) {
+ // Handle close events
+ if (e.target.classList.contains('close-card')) {
+ var element = e.target.parentNode;
+ cardsList.removeChild(element);
+ closeApp(element, true);
+ return;
+ }
+
+ var origin = this.dataset.origin;
+ alignCard(currentDisplayed, function cardAligned() {
+ WindowManager.launch(origin);
+ });
+ }
+
+ function closeApp(element, removeImmediately) {
+ // Stop the app itself
+ WindowManager.kill(element.dataset.origin);
+
+ // Fix for non selectable cards when we remove the last card
+ // Described in https://bugzilla.mozilla.org/show_bug.cgi?id=825293
+ if (cardsList.children.length === currentDisplayed) {
+ currentDisplayed--;
+ }
+
+ // If there are no cards left, then dismiss the task switcher.
+ if (!cardsList.children.length)
+ hideCardSwitcher(removeImmediately);
+ }
+
+ function getOriginObject(url) {
+ var parser = document.createElement('a');
+ parser.href = url;
+
+ return {
+ protocol: parser.protocol,
+ hostname: parser.hostname,
+ port: parser.port
+ };
+ }
+
+ function getOffOrigin(src, origin) {
+ // Use src and origin as cache key
+ var cacheKey = JSON.stringify(Array.prototype.slice.call(arguments));
+ if (!getOffOrigin.cache[cacheKey]) {
+ var native = getOriginObject(origin);
+ var current = getOriginObject(src);
+ if (current.protocol == 'http:') {
+ // Display http:// protocol anyway
+ getOffOrigin.cache[cacheKey] = current.protocol + '//' +
+ current.hostname;
+ } else if (native.protocol == current.protocol &&
+ native.hostname == current.hostname &&
+ native.port == current.port) {
+ // Same origin policy
+ getOffOrigin.cache[cacheKey] = '';
+ } else if (current.protocol == 'app:') {
+ // Avoid displaying app:// protocol
+ getOffOrigin.cache[cacheKey] = '';
+ } else {
+ getOffOrigin.cache[cacheKey] = current.protocol + '//' +
+ current.hostname;
+ }
+ }
+
+ return getOffOrigin.cache[cacheKey];
+ }
+
+ getOffOrigin.cache = {};
+
+ function hideCardSwitcher(removeImmediately) {
+ if (!cardSwitcherIsShown())
+ return;
+
+ // events to handle
+ window.removeEventListener('lock', CardsView);
+
+ // Make the cardsView overlay inactive
+ cardsView.classList.remove('active');
+ cardsViewShown = false;
+
+ // Release our screenshot blobs.
+ screenshotObjectURLs.forEach(function(url) {
+ URL.revokeObjectURL(url);
+ });
+ screenshotObjectURLs = [];
+
+ // And remove all the cards from the document after the transition
+ function removeCards() {
+ cardsView.removeEventListener('transitionend', removeCards);
+ screenElement.classList.remove('cards-view');
+
+ while (cardsList.firstElementChild) {
+ cardsList.removeChild(cardsList.firstElementChild);
+ }
+ }
+ if (removeImmediately) {
+ removeCards();
+ } else {
+ cardsView.addEventListener('transitionend', removeCards);
+ }
+ }
+
+ function cardSwitcherIsShown() {
+ return cardsViewShown;
+ }
+
+ //scrolling cards
+ var initialCardViewPosition;
+ var initialTouchPosition = {};
+ var threshold = window.innerWidth / 4;
+ // Distance after which dragged card starts moving
+ var moveCardThreshold = window.innerHeight / 6;
+ var removeCardThreshold = window.innerHeight / 4;
+
+ function alignCard(number, callback) {
+ if (!cardsList.children[number])
+ return;
+
+ var scrollLeft = cardsView.scrollLeft;
+ var targetScrollLeft = cardsList.children[number].offsetLeft;
+
+ if (Math.abs(scrollLeft - targetScrollLeft) < 4) {
+ cardsView.scrollLeft = cardsList.children[number].offsetLeft;
+ if (callback)
+ callback();
+ return;
+ }
+
+ cardsView.scrollLeft = scrollLeft + (targetScrollLeft - scrollLeft) / 2;
+
+ window.mozRequestAnimationFrame(function newFrameCallback() {
+ alignCard(number, callback);
+ });
+ }
+
+ function onStartEvent(evt) {
+ evt.stopPropagation();
+ evt.target.setCapture(true);
+ cardsView.addEventListener('mousemove', CardsView);
+ cardsView.addEventListener('swipe', CardsView);
+
+ initialCardViewPosition = cardsView.scrollLeft;
+ initialTouchPosition = {
+ x: evt.touches ? evt.touches[0].pageX : evt.pageX,
+ y: evt.touches ? evt.touches[0].pageY : evt.pageY
+ };
+ }
+
+ function onMoveEvent(evt) {
+ evt.stopPropagation();
+ var touchPosition = {
+ x: evt.touches ? evt.touches[0].pageX : evt.pageX,
+ y: evt.touches ? evt.touches[0].pageY : evt.pageY
+ };
+
+ if (evt.target.classList.contains('card') && MANUAL_CLOSING) {
+ var differenceY = initialTouchPosition.y - touchPosition.y;
+ if (differenceY > moveCardThreshold) {
+ // We don't want user to scroll the CardsView when one of the card is
+ // already dragger upwards
+ draggingCardUp = true;
+ evt.target.style.MozTransform = 'scale(0.6) translate(0, -' +
+ differenceY + 'px)';
+ }
+ }
+
+ // If we are not reordering or removing Cards now
+ // and Snapping Scrolling is enabled, we want to scroll
+ // the CardList
+ if (SNAPPING_SCROLLING && reorderedCard === null && !draggingCardUp) {
+ var differenceX = initialTouchPosition.x - touchPosition.x;
+ cardsView.scrollLeft = initialCardViewPosition + differenceX;
+ }
+
+ // If re are in reordering mode (there is a DOM element in)
+ // reorderedCard variable) we are able to put this element somewere
+ // among the others
+ if (USER_DEFINED_ORDERING && reorderedCard !== null) {
+ var differenceX = touchPosition.x - initialTouchPosition.x;
+ // Probably there is more clever solution for calculating
+ // position of transformed DOM element, but this was my
+ // first thought and it seems to work
+ var moveOffset = (cardsList.children[currentDisplayed].offsetLeft / 0.6) +
+ differenceX - (dragMargin / 0.6);
+
+ reorderedCard.style.MozTransform =
+ 'scale(0.6) translate(' + moveOffset + 'px, 0)';
+
+ if (Math.abs(differenceX) > threshold) {
+ // We don't want to jump to the next page immediately,
+ // We are waiting half a second for user to decide if
+ // he wants to leave the Card here or scroll further
+ if (allowScrollingWhileSorting) {
+ allowScrollingWhileSorting = false;
+
+ scrollWhileSortingTimer = setTimeout(function() {
+ allowScrollingWhileSorting = true;
+ }, 500);
+
+ if (differenceX > 0 &&
+ currentDisplayed <= cardsList.children.length) {
+ currentDisplayed++;
+ sortingDirection = 'right';
+ alignCard(currentDisplayed);
+ } else if (differenceX < 0 && currentDisplayed > 0) {
+ currentDisplayed--;
+ sortingDirection = 'left';
+ alignCard(currentDisplayed);
+ }
+ }
+ }
+ }
+ }
+
+ function onEndEvent(evt) {
+ evt.stopPropagation();
+ var element = evt.target;
+ var eventDetail = evt.detail;
+ var direction = eventDetail.direction;
+
+ document.releaseCapture();
+ cardsView.removeEventListener('mousemove', CardsView);
+ cardsView.removeEventListener('swipe', CardsView);
+
+ var touchPosition = {
+ x: eventDetail.end.pageX,
+ y: eventDetail.end.pageY
+ };
+
+ if (SNAPPING_SCROLLING && !draggingCardUp && reorderedCard === null) {
+ if (Math.abs(eventDetail.dx) > threshold) {
+ if (
+ direction === 'left' &&
+ currentDisplayed < cardsList.children.length - 1
+ ) {
+ currentDisplayed++;
+ alignCard(currentDisplayed);
+ } else if (direction === 'right' && currentDisplayed > 0) {
+ currentDisplayed--;
+ alignCard(currentDisplayed);
+ }
+ } else {
+ alignCard(currentDisplayed);
+ }
+ }
+
+ // if the element we start dragging on
+ // is a card and we are not in reordering mode
+ if (
+ element.classList.contains('card') &&
+ MANUAL_CLOSING &&
+ reorderedCard === null
+ ) {
+
+ draggingCardUp = false;
+ // Prevent user from closing the app with a attention screen
+ if (-eventDetail.dy > removeCardThreshold &&
+ attentionScreenApps.indexOf(element.dataset.origin) == -1
+ ) {
+
+ // remove the app also from the ordering list
+ if (
+ userSortedApps.indexOf(element.dataset.origin) !== -1 &&
+ USER_DEFINED_ORDERING
+ ) {
+ userSortedApps.splice(
+ userSortedApps.indexOf(element.dataset.origin),
+ 1
+ );
+ }
+
+ // Without removing the listener before closing card
+ // sometimes the 'click' event fires, even if 'mouseup'
+ // uses stopPropagation()
+ element.removeEventListener('tap', runApp);
+
+ // Remove the icon from the task list
+ cardsList.removeChild(element);
+
+ closeApp(element);
+
+ return;
+ } else {
+ element.style.MozTransform = '';
+ }
+ }
+
+ if (USER_DEFINED_ORDERING && reorderedCard !== null) {
+ // Position of the card depends on direction of scrolling
+ if (sortingDirection === 'right') {
+ if (currentDisplayed <= cardsList.children.length) {
+ cardsList.insertBefore(
+ reorderedCard,
+ cardsList.children[currentDisplayed + 1]
+ );
+ } else {
+ cardsList.appendChild(reorderedCard);
+ }
+ } else if (sortingDirection === 'left') {
+ cardsList.insertBefore(
+ reorderedCard,
+ cardsList.children[currentDisplayed]
+ );
+ }
+ reorderedCard.style.MozTransform = '';
+ reorderedCard.dataset['edit'] = 'false';
+ reorderedCard = null;
+
+ alignCard(currentDisplayed);
+
+ // remove the app origin from ordering array
+ userSortedApps.splice(
+ userSortedApps.indexOf(element.dataset.origin),
+ 1
+ );
+ // and put in on the new position
+ userSortedApps.splice(currentDisplayed, 0, element.dataset.origin);
+ }
+ }
+
+ function manualOrderStart(evt) {
+ evt.preventDefault();
+ reorderedCard = evt.target;
+ allowScrollingWhileSorting = true;
+ if (reorderedCard.classList.contains('card')) {
+ dragMargin = reorderedCard.offsetLeft;
+ reorderedCard.dataset['edit'] = true;
+ sortingDirection = 'left';
+ }
+ }
+
+ window.addEventListener('applicationuninstall',
+ function removeUninstaledApp(evt) {
+ var origin = evt.detail.application.origin;
+ if (userSortedApps.indexOf(origin) !== -1) {
+ userSortedApps.splice(userSortedApps.indexOf(origin), 1);
+ }
+ },
+ false);
+
+ function cv_handleEvent(evt) {
+ switch (evt.type) {
+ case 'mousedown':
+ onStartEvent(evt);
+ break;
+
+ case 'mousemove':
+ onMoveEvent(evt);
+ break;
+
+ case 'swipe':
+ onEndEvent(evt);
+ break;
+
+ case 'contextmenu':
+ manualOrderStart(evt);
+ break;
+
+ case 'home':
+ if (!cardSwitcherIsShown())
+ return;
+
+ evt.stopImmediatePropagation();
+ hideCardSwitcher();
+ break;
+
+ case 'lock':
+ case 'attentionscreenshow':
+ attentionScreenApps = AttentionScreen.getAttentionScreenOrigins();
+ hideCardSwitcher();
+ break;
+
+ case 'attentionscreenhide':
+ attentionScreenApps = AttentionScreen.getAttentionScreenOrigins();
+ break;
+
+ case 'holdhome':
+ if (LockScreen.locked)
+ return;
+
+ SleepMenu.hide();
+ showCardSwitcher();
+ break;
+
+ case 'appwillopen':
+ hideCardSwitcher();
+ break;
+ }
+ }
+
+ // Public API of CardsView
+ return {
+ showCardSwitcher: showCardSwitcher,
+ hideCardSwitcher: hideCardSwitcher,
+ cardSwitcherIsShown: cardSwitcherIsShown,
+ handleEvent: cv_handleEvent
+ };
+})();
+
+window.addEventListener('attentionscreenshow', CardsView);
+window.addEventListener('attentionscreenhide', CardsView);
+window.addEventListener('holdhome', CardsView);
+window.addEventListener('home', CardsView);
+window.addEventListener('appwillopen', CardsView);
+
diff --git a/apps/system/js/context_menu.js b/apps/system/js/context_menu.js
new file mode 100644
index 0000000..816ef71
--- /dev/null
+++ b/apps/system/js/context_menu.js
@@ -0,0 +1,24 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var ContextMenu = {
+ init: function cm_init() {
+ window.addEventListener('mozbrowsercontextmenu', this, true);
+ },
+
+ handleEvent: function cm_handleEvent(evt) {
+ var detail = evt.detail;
+ if (detail.contextmenu.items.length == 0)
+ return;
+
+ var onsuccess = function(action) {
+ detail.contextMenuItemSelected(action);
+ };
+
+ ListMenu.request(detail.contextmenu.items, '', onsuccess);
+ }
+};
+
+ContextMenu.init();
diff --git a/apps/system/js/cost_control.js b/apps/system/js/cost_control.js
new file mode 100644
index 0000000..0f07962
--- /dev/null
+++ b/apps/system/js/cost_control.js
@@ -0,0 +1,89 @@
+/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+(function() {
+
+ 'use strict';
+
+ var host = document.location.host;
+ var domain = host.replace(/(^[\w\d]+\.)?([\w\d]+\.[a-z]+)/, '$2');
+ var protocol = document.location.protocol + '//';
+ var origin = protocol + 'costcontrol.' + domain;
+
+ var widgetContainer = document.getElementById('cost-control-widget');
+
+ var widgetFrame;
+ function _ensureWidget() {
+ if (!Applications.ready)
+ return;
+
+ // Check widget is there
+ widgetFrame = widgetContainer.querySelector('iframe');
+ if (widgetFrame && !widgetFrame.dataset.killed)
+ return;
+
+ // Create the widget
+ if (!widgetFrame) {
+ widgetFrame = document.createElement('iframe');
+ widgetFrame.addEventListener('mozbrowsererror',
+ function ccdriver_onError(e) {
+ e.target.dataset.killed = true;
+ }
+ );
+ }
+
+ widgetFrame.dataset.frameType = 'widget';
+ widgetFrame.dataset.frameOrigin = origin;
+ delete widgetFrame.dataset.killed;
+
+ widgetFrame.setAttribute('mozbrowser', true);
+ widgetFrame.setAttribute('remote', 'true');
+ widgetFrame.setAttribute('mozapp', origin + '/manifest.webapp');
+
+ widgetFrame.src = origin + '/widget.html';
+ widgetContainer.appendChild(widgetFrame);
+
+ _adjustWidgetPosition();
+ }
+
+ function _showWidget() {
+ _ensureWidget();
+ widgetFrame.setVisible(true);
+ }
+
+ function _hideWidget() {
+ if (widgetFrame) {
+ widgetFrame.setVisible(false);
+ }
+ }
+
+ function _adjustWidgetPosition() {
+ // TODO: Remove this when weird bug #809031 (Bugzilla) is solved
+ // See cost_control.css as well to remove the last rule
+ var offsetY = document.getElementById('notification-bar').clientHeight;
+ offsetY +=
+ document.getElementById('notifications-container').clientHeight;
+ widgetFrame.style.transform = 'translate(0, ' + offsetY + 'px)';
+ }
+
+ // Listen to utilitytray show
+ window.addEventListener('utilitytrayshow', _showWidget);
+ window.addEventListener('utilitytrayhide', _hideWidget);
+
+ window.addEventListener('applicationready', function _onReady() {
+ asyncStorage.getItem('ftu.enabled', function _onValue(enabled) {
+ if (enabled !== false) {
+ window.addEventListener('ftudone', function ftudone(e) {
+ window.removeEventListener('ftudone', ftudone);
+ _ensureWidget();
+ widgetFrame.setVisible(false);
+ });
+ } else {
+ _ensureWidget();
+ widgetFrame.setVisible(false);
+ }
+ });
+ });
+
+ window.addEventListener('resize', _adjustWidgetPosition);
+}());
diff --git a/apps/system/js/crash_reporter.js b/apps/system/js/crash_reporter.js
new file mode 100644
index 0000000..4068291
--- /dev/null
+++ b/apps/system/js/crash_reporter.js
@@ -0,0 +1,140 @@
+/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+// This file calls getElementById without waiting for an onload event, so it
+// must have a defer attribute or be included at the end of the <body>.
+
+var CrashReporter = (function() {
+ var _ = navigator.mozL10n.get;
+ var settings = navigator.mozSettings;
+ var screen = document.getElementById('screen');
+
+ // The name of the app that just crashed.
+ var crashedAppName = '';
+
+ // Whether or not to show a "Report" button in the banner.
+ var showReportButton = false;
+
+ // Only show the "Report" button if the user hasn't set a preference to
+ // always/never report crashes.
+ SettingsListener.observe('app.reportCrashes', 'ask',
+ function handleCrashSetting(value) {
+ showReportButton = (value != 'always' && value != 'never');
+ });
+
+ // This function should only ever be called once.
+ function showDialog(crashID, isChrome) {
+ var title = isChrome ? _('crash-dialog-os2') :
+ _('crash-dialog-app', { name: crashedAppName });
+ document.getElementById('crash-dialog-title').textContent = title;
+
+ // "Don't Send Report" button in dialog
+ var noButton = document.getElementById('dont-send-report');
+ noButton.addEventListener('click', function onNoButtonClick() {
+ settings.createLock().set({'app.reportCrashes': 'never'});
+ removeDialog();
+ });
+
+ // "Send Report" button in dialog
+ var yesButton = document.getElementById('send-report');
+ yesButton.addEventListener('click', function onYesButtonClick() {
+ submitCrash(crashID);
+ if (checkbox.checked) {
+ settings.createLock().set({'app.reportCrashes': 'always'});
+ }
+ removeDialog();
+ });
+
+ var checkbox = document.getElementById('always-send');
+ checkbox.addEventListener('click', function onCheckboxClick() {
+ // Disable the "Don't Send Report" button if the "Always send..."
+ // checkbox is checked
+ noButton.disabled = this.checked;
+ });
+
+ // "What's in a crash report?" link
+ var crashInfoLink = document.getElementById('crash-info-link');
+ crashInfoLink.addEventListener('click', function onLearnMoreClick() {
+ var dialog = document.getElementById('crash-dialog');
+ document.getElementById('crash-reports-done').
+ addEventListener('click', function onDoneClick() {
+ this.removeEventListener('click', onDoneClick);
+ dialog.classList.remove('learn-more');
+ });
+ dialog.classList.add('learn-more');
+ });
+
+ screen.classList.add('crash-dialog');
+ }
+
+ // We can get rid of the dialog after it is shown once.
+ function removeDialog() {
+ screen.classList.remove('crash-dialog');
+ var dialog = document.getElementById('crash-dialog');
+ dialog.parentNode.removeChild(dialog);
+ }
+
+ function showBanner(crashID, isChrome) {
+ var message = isChrome ? _('crash-banner-os2') :
+ _('crash-banner-app', { name: crashedAppName });
+
+ var button = null;
+ if (showReportButton) {
+ button = {
+ label: _('crash-banner-report'),
+ callback: function reportCrash() {
+ submitCrash(crashID);
+ }
+ };
+ }
+
+ SystemBanner.show(message, button);
+ }
+
+ function submitCrash(crashID) {
+ var event = document.createEvent('CustomEvent');
+ event.initCustomEvent('mozContentEvent', true, true, {
+ type: 'submit-crash',
+ crashID: crashID
+ });
+ window.dispatchEvent(event);
+ }
+
+ // - Show a dialog only the first time there's a crash to report.
+ // - On subsequent crashes, show a banner letting the user know there was a
+ // crash.
+ // - If the user hasn't set a pref, add a "Report" button to the banner.
+ function handleCrash(crashID, isChrome) {
+ // Check to see if we should show a dialog.
+ var dialogReq = settings.createLock().get('crashReporter.dialogShown');
+ dialogReq.onsuccess = function dialogShownSuccess() {
+ var dialogShown = dialogReq.result['crashReporter.dialogShown'];
+ if (!dialogShown) {
+ settings.createLock().set({'crashReporter.dialogShown': true});
+ showDialog(crashID, isChrome);
+ } else {
+ showBanner(crashID, isChrome);
+ }
+ };
+ }
+
+ // We depend on window_manager.js calling this function before
+ // we get a 'handle-crash' event from shell.js
+ function setAppName(name) {
+ crashedAppName = name;
+ }
+
+ // We will be notified of system crashes from shell.js
+ window.addEventListener('mozChromeEvent', function handleChromeEvent(e) {
+ if (e.detail.type == 'handle-crash') {
+ handleCrash(e.detail.crashID, e.detail.chrome);
+ }
+ });
+
+ return {
+ setAppName: setAppName
+ };
+})();
+
diff --git a/apps/system/js/gridview.js b/apps/system/js/gridview.js
new file mode 100644
index 0000000..399bf3e
--- /dev/null
+++ b/apps/system/js/gridview.js
@@ -0,0 +1,40 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var GridView = {
+ grid: null,
+
+ get visible() {
+ return this.grid && this.grid.style.display === 'block';
+ },
+
+ hide: function gv_hide() {
+ if (this.grid)
+ this.grid.style.visibility = 'hidden';
+ },
+
+ show: function gv_show() {
+ var grid = this.grid;
+ if (!grid) {
+ grid = document.createElement('div');
+ grid.id = 'debug-grid';
+ grid.dataset.zIndexLevel = 'debug-grid';
+
+ this.grid = grid;
+ document.getElementById('screen').appendChild(grid);
+ }
+
+ grid.style.visibility = 'visible';
+ },
+
+ toggle: function gv_toggle() {
+ this.visible ? this.hide() : this.show();
+ }
+};
+
+SettingsListener.observe('debug.grid.enabled', false, function(value) {
+ !!value ? GridView.show() : GridView.hide();
+});
+
diff --git a/apps/system/js/hardware_buttons.js b/apps/system/js/hardware_buttons.js
new file mode 100644
index 0000000..3363463
--- /dev/null
+++ b/apps/system/js/hardware_buttons.js
@@ -0,0 +1,318 @@
+// hardware_buttons.js:
+//
+// Gecko code in b2g/chrome/content/shell.js sends mozChromeEvents
+// when the user presses or releases a hardware button such as Home, Sleep,
+// and Volume Up and Down.
+//
+// This module listens for those low-level mozChromeEvents, processes them
+// and generates higher-level events to handle autorepeat on the volume keys
+// long presses on Home and Sleep, and the Home+Sleep key combination.
+//
+// Other system app modules should listen for the high-level button events
+// generated by this module.
+//
+// The low-level input events processed by this module have type set
+// to "mozChromeEvent" and detail.type set to one of:
+//
+// home-button-press
+// home-button-release
+// sleep-button-press
+// sleep-button-release
+// volume-up-button-press
+// volume-up-button-release
+// volume-down-button-press
+// volume-down-button-release
+//
+// The high-level events generated by this module are simple Event objects
+// that are not cancelable and do not bubble. The are dispatched at the
+// window object. The type property is set to one of these:
+//
+// Event Type Meaning
+// --------------------------------------------------------------
+// home short press and release of home button
+// holdhome long press and hold of home button
+// sleep short press and release of sleep button
+// wake sleep or home pressed while sleeping
+// holdsleep long press and hold of sleep button
+// volumeup volume up pressed and released or autorepeated
+// volumedown volume down pressed and released or autorepeated
+// home+sleep home and sleep pressed at same time (used for screenshots)
+// home+volume home and either volume key at the same time (view source)
+//
+// Because these events are fired at the window object, they cannot be
+// captured. Many modules listen for the home event. Those that want
+// to respond to it and prevent others from responding should call
+// stopImmediatePropagation(). Overlays that want to prevent the window
+// manager from showing the homescreen on the home event should call that
+// method. Note, however, that this only works for scripts that run and
+// register their event handlers before window_manager.js does.
+//
+'use strict';
+
+(function() {
+ var HOLD_INTERVAL = 750; // How long for press and hold Home or Sleep
+ var REPEAT_DELAY = 700; // How long before volume autorepeat begins
+ var REPEAT_INTERVAL = 100; // How fast the autorepeat is.
+
+ // Dispatch a high-level event of the specified type
+ function fire(type) {
+ window.dispatchEvent(new Event(type));
+ }
+
+ // We process events with a finite state machine.
+ // Each state object has a process() method for handling events.
+ // And optionally has enter() and exit() methods called when the FSM
+ // enters and exits that state
+ var state;
+
+ // This function transitions to a new state
+ function setState(s, type) {
+ // Exit the current state()
+ if (state && state.exit)
+ state.exit(type);
+ state = s;
+ // Enter the new state
+ if (state && state.enter)
+ state.enter(type);
+ }
+
+ // This event handler listens for hardware button events and passes the
+ // event type to the process() method of the current state for processing
+ window.addEventListener('mozChromeEvent', function(e) {
+ var type = e.detail.type;
+ switch (type) {
+ case 'home-button-press':
+ case 'home-button-release':
+ case 'sleep-button-press':
+ case 'sleep-button-release':
+ case 'volume-up-button-press':
+ case 'volume-up-button-release':
+ case 'volume-down-button-press':
+ case 'volume-down-button-release':
+ state.process(type);
+ break;
+ }
+ });
+
+ // The base state is the default, when no hardware buttons are pressed
+ var baseState = {
+ process: function(type) {
+ switch (type) {
+ case 'home-button-press':
+ // If the phone is sleeping, then pressing Home wakes it
+ // (on press, not release)
+ if (!ScreenManager.screenEnabled) {
+ fire('wake');
+ setState(wakeState, type);
+ } else {
+ setState(homeState, type);
+ }
+ return;
+ case 'sleep-button-press':
+ // If the phone is sleeping, then pressing Sleep wakes it
+ // (on press, not release)
+ if (!ScreenManager.screenEnabled) {
+ fire('wake');
+ setState(wakeState, type);
+ } else {
+ setState(sleepState, type);
+ }
+ return;
+ case 'volume-up-button-press':
+ case 'volume-down-button-press':
+ setState(volumeState, type);
+ return;
+ case 'home-button-release':
+ case 'sleep-button-release':
+ case 'volume-up-button-release':
+ case 'volume-down-button-release':
+ // Ignore button releases that occur in this state.
+ // These can happen after home+sleep and home+volume.
+ return;
+ }
+ console.error('Unexpected hardware key: ', type);
+ }
+ };
+
+ // We enter the home state when the user presses the Home button
+ // We can fire home, holdhome, or homesleep events from this state
+ var homeState = {
+ timer: null,
+ enter: function() {
+ this.timer = setTimeout(function() {
+ fire('holdhome');
+ navigator.vibrate(50);
+ setState(baseState);
+ }, HOLD_INTERVAL);
+ },
+ exit: function() {
+ if (this.timer) {
+ clearTimeout(this.timer);
+ this.timer = null;
+ }
+ },
+ process: function(type) {
+ switch (type) {
+ case 'home-button-release':
+ fire('home');
+ navigator.vibrate(50);
+ setState(baseState, type);
+ return;
+ case 'sleep-button-press':
+ fire('home+sleep');
+ setState(baseState, type);
+ return;
+ case 'volume-up-button-press':
+ case 'volume-down-button-press':
+ fire('home+volume');
+ setState(baseState, type);
+ return;
+ }
+ console.error('Unexpected hardware key: ', type);
+ setState(baseState, type);
+ }
+ };
+
+ // We enter the sleep state when the user presses the Sleep button
+ // We can fire sleep, holdsleep, or homesleep events from this state
+ var sleepState = {
+ timer: null,
+ enter: function() {
+ this.timer = setTimeout(function() {
+ fire('holdsleep');
+ setState(baseState);
+ }, HOLD_INTERVAL);
+ },
+ exit: function() {
+ if (this.timer) {
+ clearTimeout(this.timer);
+ this.timer = null;
+ }
+ },
+ process: function(type) {
+ switch (type) {
+ case 'sleep-button-release':
+ fire('sleep');
+ setState(baseState, type);
+ return;
+ case 'home-button-press':
+ fire('home+sleep');
+ setState(baseState, type);
+ return;
+ case 'volume-up-button-press':
+ case 'volume-down-button-press':
+ setState(volumeState, type);
+ return;
+ }
+ console.error('Unexpected hardware key: ', type);
+ setState(baseState, type);
+ }
+ };
+
+ // We enter the volume state when the user presses the volume up or
+ // volume down buttons.
+ // We can fire volumeup and volumedown events from this state
+ var volumeState = {
+ direction: null,
+ timer: null,
+ repeating: false,
+ repeat: function() {
+ this.repeating = true;
+ if (this.direction === 'volume-up-button-press')
+ fire('volumeup');
+ else
+ fire('volumedown');
+ this.timer = setTimeout(this.repeat.bind(this), REPEAT_INTERVAL);
+ },
+ enter: function(type) {
+ var self = this;
+ this.direction = type; // Is volume going up or down?
+ this.repeating = false;
+ this.timer = setTimeout(this.repeat.bind(this), REPEAT_DELAY);
+ },
+ exit: function() {
+ if (this.timer) {
+ clearTimeout(this.timer);
+ this.timer = null;
+ }
+ },
+ process: function(type) {
+ switch (type) {
+ case 'home-button-press':
+ fire('home+volume');
+ setState(baseState, type);
+ return;
+ case 'sleep-button-press':
+ setState(sleepState, type);
+ return;
+ case 'volume-up-button-release':
+ if (this.direction === 'volume-up-button-press') {
+ if (!this.repeating)
+ fire('volumeup');
+ setState(baseState, type);
+ return;
+ }
+ break;
+ case 'volume-down-button-release':
+ if (this.direction === 'volume-down-button-press') {
+ if (!this.repeating)
+ fire('volumedown');
+ setState(baseState, type);
+ return;
+ }
+ break;
+ default:
+ // Ignore anything else (such as sleep button release)
+ return;
+ }
+ console.error('Unexpected hardware key: ', type);
+ setState(baseState, type);
+ }
+ };
+
+ // We enter this state when the user presses Home or Sleep on a sleeping
+ // phone. We give immediate feedback by waking the phone up on the press
+ // rather than waiting for the release, but this means we need a special
+ // state so that we don't actually send a home or sleep event on the
+ // key release. Note, however, that this state does set a timer so that
+ // it can send holdhome or holdsleep events. (This means that pressing and
+ // holding sleep will bring up the power menu, even on a sleeping phone.)
+ var wakeState = {
+ timer: null,
+ delegateState: null,
+ enter: function(type) {
+ if (type === 'home-button-press')
+ this.delegateState = homeState;
+ else
+ this.delegateState = sleepState;
+ this.timer = setTimeout(function() {
+ if (type === 'home-button-press') {
+ fire('holdhome');
+ } else if (type === 'sleep-button-press') {
+ fire('holdsleep');
+ }
+ setState(baseState, type);
+ }, HOLD_INTERVAL);
+ },
+ exit: function() {
+ if (this.timer) {
+ clearTimeout(this.timer);
+ this.timer = null;
+ }
+ },
+ process: function(type) {
+ switch (type) {
+ case 'home-button-release':
+ case 'sleep-button-release':
+ setState(baseState, type);
+ return;
+ default:
+ this.delegateState.process(type);
+ return;
+ }
+ }
+ };
+
+ // Kick off the FSM in the base state
+ setState(baseState);
+}());
diff --git a/apps/system/js/icc_cache.js b/apps/system/js/icc_cache.js
new file mode 100644
index 0000000..1f1d0df
--- /dev/null
+++ b/apps/system/js/icc_cache.js
@@ -0,0 +1,86 @@
+/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+(function() {
+ /**
+ * Constants
+ */
+ var DEBUG = false;
+
+ /**
+ * Debug method
+ */
+ function debug(msg, optObject) {
+ if (DEBUG) {
+ var output = '[DEBUG] STKCACHE: ' + msg;
+ if (optObject) {
+ output += JSON.stringify(optObject);
+ }
+ console.log(output);
+ }
+ }
+
+ if (!window.navigator.mozMobileConnection) {
+ return;
+ }
+
+ var icc = window.navigator.mozMobileConnection.icc;
+ // Remove previous menu
+ var resetApplications = window.navigator.mozSettings.createLock().set({
+ 'icc.applications': '{}'
+ });
+ resetApplications.onsuccess = function icc_resetApplications() {
+ debug('STK Cache Reseted');
+ // Register to receive STK commands
+ window.navigator.mozSetMessageHandler('icc-stkcommand',
+ function handleSTKCommand(command) {
+ debug('STK Proactive Command:', command);
+ if (command.typeOfCommand == icc.STK_CMD_SET_UP_MENU) {
+ debug('STK_CMD_SET_UP_MENU:', command.options);
+ var reqApplications = window.navigator.mozSettings.createLock().set({
+ 'icc.applications': JSON.stringify(command.options)
+ });
+ reqApplications.onsuccess = function icc_getApplications() {
+ debug('Cached');
+ icc.sendStkResponse(command, {
+ resultCode: icc.STK_RESULT_OK
+ });
+ }
+ } else {
+ // Unsolicited command? -> Open settings
+ debug('CMD: ', command);
+ var application = document.location.protocol + '//' +
+ document.location.host.replace('system', 'settings');
+ debug('application: ', application);
+ var reqIccData = window.navigator.mozSettings.createLock().set({
+ 'icc.data': JSON.stringify(command)
+ });
+ reqIccData.onsuccess = function icc_getIccData() {
+ if (WindowManager.getRunningApps()[application]) {
+ debug('Settings is running. Ignoring');
+ return; // If settings is opened, we don't manage it
+ }
+
+ debug('Locating settings . . .');
+ navigator.mozApps.mgmt.getAll().onsuccess = function gotApps(evt) {
+ var apps = evt.target.result;
+ apps.forEach(function appIterator(app) {
+ if (app.origin != application)
+ return;
+
+ var reqIccData = window.navigator.mozSettings.createLock().set({
+ 'icc.data': JSON.stringify(command)
+ });
+ reqIccData.onsuccess = function icc_getIccData() {
+ debug('Launching ', app.origin);
+ app.launch();
+ }
+ }, this);
+ }
+ }
+ }
+ });
+ }
+})();
diff --git a/apps/system/js/identity.js b/apps/system/js/identity.js
new file mode 100644
index 0000000..8030377
--- /dev/null
+++ b/apps/system/js/identity.js
@@ -0,0 +1,98 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+// When bug 794999 is resolved, switch to use the abstract Trusted UI Component
+
+'use strict';
+
+const kIdentityScreen = 'https://login.native-persona.org/sign_in#NATIVE';
+const kIdentityFrame =
+ 'https://login.native-persona.org/communication_iframe';
+
+var Identity = (function() {
+ var iframe;
+
+ return {
+ trustedUILayers: {},
+
+ init: function() {
+ window.addEventListener('mozChromeEvent', this);
+ },
+
+ handleEvent: function onMozChromeEvent(e) {
+ var chromeEventId = e.detail.id;
+ var requestId = e.detail.requestId;
+ switch (e.detail.type) {
+ // Chrome asks Gaia to show the identity dialog.
+ case 'open-id-dialog':
+ if (!chromeEventId)
+ return;
+
+ // When opening the dialog, we record the chrome event id, which
+ // we will need to send back to the TrustedUIManager when asking
+ // to close.
+ this.trustedUILayers[requestId] = chromeEventId;
+
+ if (!e.detail.showUI && iframe) {
+ this._dispatchEvent({
+ id: chromeEventId,
+ frame: iframe
+ });
+ return;
+ }
+ var frame = document.createElement('iframe');
+ frame.setAttribute('mozbrowser', 'true');
+ frame.setAttribute('remote', true);
+ frame.classList.add('screen');
+ frame.src = e.detail.showUI ? kIdentityScreen : kIdentityFrame;
+ frame.addEventListener('mozbrowserloadstart',
+ function loadStart(evt) {
+ // After creating the new frame containing the identity flow, we
+ // send it back to chrome so the identity callbacks can be injected.
+ this._dispatchEvent({
+ id: chromeEventId,
+ frame: evt.target
+ });
+ }.bind(this));
+
+
+ if (e.detail.showUI) {
+ // The identity flow is shown within the trusted UI.
+ TrustedUIManager.open(navigator.mozL10n.get('persona-signin'),
+ frame,
+ this.trustedUILayers[requestId]);
+ } else {
+ var container = document.getElementById('screen');
+ container.appendChild(frame);
+ frame.classList.add('communication-frame');
+ iframe = frame;
+ }
+ break;
+
+ case 'received-id-assertion':
+ if (e.detail.showUI) {
+ TrustedUIManager.close(this.trustedUILayers[requestId],
+ (function dialogClosed() {
+ delete this.trustedUILayers[requestId];
+ }).bind(this));
+ }
+ this._dispatchEvent({ id: chromeEventId });
+ break;
+ }
+ },
+ _dispatchEvent: function su_dispatchEvent(obj) {
+ var event = document.createEvent('CustomEvent');
+ event.initCustomEvent('mozContentEvent', true, true, obj);
+ window.dispatchEvent(event);
+ }
+ };
+})();
+
+// Make sure L10n is ready before init
+if (navigator.mozL10n.readyState == 'complete' ||
+ navigator.mozL10n.readyState == 'interactive') {
+ Identity.init();
+} else {
+ window.addEventListener('localized', Identity.init.bind(Identity));
+}
+
diff --git a/apps/system/js/keyboard_manager.js b/apps/system/js/keyboard_manager.js
new file mode 100644
index 0000000..62aeca7
--- /dev/null
+++ b/apps/system/js/keyboard_manager.js
@@ -0,0 +1,86 @@
+'use strict';
+
+var KeyboardManager = (function() {
+ function getKeyboardURL() {
+ // TODO: Retrieve it from Settings, allowing 3rd party keyboards
+ var host = document.location.host;
+ var domain = host.replace(/(^[\w\d]+\.)?([\w\d]+\.[a-z]+)/, '$2');
+ var protocol = document.location.protocol;
+
+ return protocol + '//keyboard.' + domain + '/';
+ }
+
+ function generateKeyboard(container, keyboardURL, manifestURL) {
+ var keyboard = document.createElement('iframe');
+ keyboard.src = keyboardURL;
+ keyboard.setAttribute('mozbrowser', 'true');
+ keyboard.setAttribute('mozpasspointerevents', 'true');
+ keyboard.setAttribute('mozapp', manifestURL);
+ //keyboard.setAttribute('remote', 'true');
+
+ container.appendChild(keyboard);
+ return keyboard;
+ }
+
+ // Generate a <iframe mozbrowser> containing the keyboard.
+ var container = document.getElementById('keyboard-frame');
+ var keyboardURL = getKeyboardURL() + 'index.html';
+ var manifestURL = getKeyboardURL() + 'manifest.webapp';
+ var keyboard = generateKeyboard(container, keyboardURL, manifestURL);
+
+ // Listen for mozbrowserlocationchange of keyboard iframe.
+ var previousHash = '';
+
+ var urlparser = document.createElement('a');
+ keyboard.addEventListener('mozbrowserlocationchange', function(e) {
+ urlparser.href = e.detail;
+ if (previousHash == urlparser.hash)
+ return;
+ previousHash = urlparser.hash;
+
+ var type = urlparser.hash.split('=');
+ switch (type[0]) {
+ case '#show':
+ var updateHeight = function updateHeight() {
+ container.removeEventListener('transitionend', updateHeight);
+ if (container.classList.contains('hide')) {
+ // The keyboard has been closed already, let's not resize the
+ // application and ends up with half apps.
+ return;
+ }
+
+ var detail = {
+ 'detail': {
+ 'height': parseInt(type[1])
+ }
+ };
+
+ dispatchEvent(new CustomEvent('keyboardchange', detail));
+ }
+
+ if (container.classList.contains('hide')) {
+ container.classList.remove('hide');
+ container.addEventListener('transitionend', updateHeight);
+ return;
+ }
+
+ updateHeight();
+ break;
+
+ case '#hide':
+ // inform window manager to resize app first or
+ // it may show the underlying homescreen
+ dispatchEvent(new CustomEvent('keyboardhide'));
+ container.classList.add('hide');
+ break;
+ }
+ });
+
+ // For Bug 812115: hide the keyboard when the app is closed here,
+ // since it would take a longer round-trip to receive focuschange
+ window.addEventListener('appwillclose', function closeKeyboard() {
+ dispatchEvent(new CustomEvent('keyboardhide'));
+ container.classList.add('hide');
+ });
+})();
+
diff --git a/apps/system/js/list_menu.js b/apps/system/js/list_menu.js
new file mode 100644
index 0000000..303fafe
--- /dev/null
+++ b/apps/system/js/list_menu.js
@@ -0,0 +1,180 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var ListMenu = {
+ get element() {
+ delete this.element;
+ return this.element = document.getElementById('listmenu');
+ },
+
+ get container() {
+ delete this.container;
+ return this.container = document.querySelector('#listmenu menu');
+ },
+
+ get visible() {
+ return this.element.classList.contains('visible');
+ },
+
+ // Listen to click event only
+ init: function lm_init() {
+ window.addEventListener('click', this, true);
+ window.addEventListener('screenchange', this, true);
+ window.addEventListener('home', this);
+ window.addEventListener('holdhome', this);
+ },
+
+ // Pass an array of list items and handler for clicking on the items
+ // Modified to fit contextmenu use case, loop into the menu items
+ request: function lm_request(listItems, title, successCb, errorCb) {
+ this.container.innerHTML = '';
+ this.currentLevel = 0;
+ this.internalList = [];
+ this.setTitle(title);
+ this.buildMenu(listItems);
+ this.internalList.forEach(function render_item(item) {
+ this.container.appendChild(item);
+ }, this);
+
+ this.onreturn = successCb || function() {};
+ this.oncancel = errorCb || function() {};
+
+ this.show();
+ },
+
+ buildMenu: function lm_buildMenu(items) {
+ var containerDiv = document.createElement('ul');
+ var _ = navigator.mozL10n.get;
+
+ if (this.currentLevel === 0) {
+ containerDiv.classList.add('list-menu-root');
+ containerDiv.id = 'list-menu-root';
+ } else {
+ containerDiv.id = 'list-menu-' + this.internalList.length;
+ }
+ this.internalList.push(containerDiv);
+
+ items.forEach(function traveseItems(item) {
+ var item_div = document.createElement('li');
+ var button = document.createElement('a');
+ button.setAttribute('role', 'button');
+ if (item.type && item.type == 'menu') {
+ // XXX: We disallow multi-level menu at this moment
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=824928
+ // for UX design and dev implementation tracking
+ return;
+ } else if (item.type && item.type == 'menuitem') {
+ button.dataset.value = item.id;
+ button.textContent = item.label;
+ } else {
+ button.dataset.value = item.value;
+ button.textContent = item.label;
+ }
+
+ item_div.appendChild(button);
+ if (item.icon) {
+ button.style.backgroundImage = 'url(' + item.icon + ')';
+ button.classList.add('icon');
+ }
+ containerDiv.appendChild(item_div);
+ }, this);
+
+ if (this.currentLevel > 0) {
+ var back = document.createElement('li');
+ var button = document.createElement('a');
+ button.setAttribute('role', 'button');
+ button.textContent = _('back');
+ button.href = '#' + this.currentParent;
+ back.classList.add('back');
+ back.appendChild(button);
+ containerDiv.appendChild(back);
+ } else {
+ var cancel = document.createElement('li');
+ var button = document.createElement('button');
+ button.textContent = _('cancel');
+ button.dataset.action = 'cancel';
+ cancel.appendChild(button);
+ containerDiv.appendChild(cancel);
+ }
+
+ containerDiv.dataset.level = this.currentLevel;
+ this.currentChild = containerDiv.id;
+ },
+
+ setTitle: function lm_setTitle(title) {
+ if (!title)
+ return;
+
+ var titleElement = document.createElement('h3');
+ titleElement.textContent = title;
+ this.container.appendChild(titleElement);
+ },
+
+ show: function lm_show() {
+ if (this.visible)
+ return;
+
+ this.container.classList.remove('slidedown');
+ this.element.classList.add('visible');
+ },
+
+ hide: function lm_hide() {
+ if (!this.visible)
+ return;
+
+ var self = this;
+ var container = this.container;
+ container.addEventListener('transitionend', function list_hide() {
+ container.removeEventListener('transitionend', list_hide);
+ self.element.classList.remove('visible');
+ });
+
+ setTimeout(function() {
+ container.classList.add('slidedown');
+ });
+ },
+
+ handleEvent: function lm_handleEvent(evt) {
+ switch (evt.type) {
+ case 'screenchange':
+ if (!evt.detail.screenEnabled) {
+ this.hide();
+ this.oncancel();
+ }
+ break;
+
+ case 'click':
+ if (!this.visible)
+ return;
+
+ var cancel = evt.target.dataset.action;
+ if (cancel && cancel == 'cancel') {
+ this.hide();
+ this.oncancel();
+ return;
+ }
+
+ var value = evt.target.dataset.value;
+ if (!value) {
+ return;
+ }
+
+ this.hide();
+ this.onreturn(value);
+ break;
+
+ case 'home':
+ case 'holdhome':
+ if (!this.visible)
+ return;
+
+ this.hide();
+ this.oncancel();
+ break;
+ }
+ }
+};
+
+ListMenu.init();
diff --git a/apps/system/js/lockscreen.js b/apps/system/js/lockscreen.js
new file mode 100644
index 0000000..f2275c9
--- /dev/null
+++ b/apps/system/js/lockscreen.js
@@ -0,0 +1,1019 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var LockScreen = {
+ /*
+ * Boolean return true when initialized.
+ */
+ ready: false,
+
+ /*
+ * Boolean return the status of the lock screen.
+ * Must not multate directly - use unlock()/lockIfEnabled()
+ * Listen to 'lock' and 'unlock' event to properly handle status changes
+ */
+ locked: true,
+
+ /*
+ * Boolean return whether if the lock screen is enabled or not.
+ * Must not multate directly - use setEnabled(val)
+ * Only Settings Listener should change this value to sync with data
+ * in Settings API.
+ */
+ enabled: true,
+
+ /*
+ * Boolean returns wether we want a sound effect when unlocking.
+ */
+ unlockSoundEnabled: true,
+
+ /*
+ * Boolean return whether if the lock screen is enabled or not.
+ * Must not multate directly - use setPassCodeEnabled(val)
+ * Only Settings Listener should change this value to sync with data
+ * in Settings API.
+ * Will be ignored if 'enabled' is set to false.
+ */
+ passCodeEnabled: false,
+
+ /*
+ * Four digit Passcode
+ * XXX: should come for Settings
+ */
+ passCode: '0000',
+
+ /*
+ * The time to request for passcode input since device is off.
+ */
+ passCodeRequestTimeout: 0,
+
+ /*
+ * Store the first time the screen went off since unlocking.
+ */
+ _screenOffTime: 0,
+
+ /*
+ * Check the timeout of passcode lock
+ */
+ _passCodeTimeoutCheck: false,
+
+ /*
+ * Current passcode entered by the user
+ */
+ passCodeEntered: '',
+
+ /**
+ * Are we currently switching panels ?
+ */
+ _switchingPanel: false,
+
+ /*
+ * Timeout after incorrect attempt
+ */
+ kPassCodeErrorTimeout: 500,
+
+ /*
+ * Airplane mode
+ */
+ airplaneMode: false,
+
+ /*
+ * Timeout ID for backing from triggered state to normal state
+ */
+ triggeredTimeoutId: 0,
+
+ /*
+ * Interval ID for elastic of curve and arrow
+ */
+ elasticIntervalId: 0,
+
+ /*
+ * elastic animation interval
+ */
+ ELASTIC_INTERVAL: 5000,
+
+ /*
+ * timeout for triggered state after swipe up
+ */
+ TRIGGERED_TIMEOUT: 7000,
+
+ /*
+ * Max value for handle swiper up
+ */
+ HANDLE_MAX: 70,
+
+ /* init */
+ init: function ls_init() {
+ if (this.ready) { // already initialized: just trigger a translation
+ this.updateTime();
+ this.updateConnState();
+ return;
+ }
+ this.ready = true;
+
+ this.getAllElements();
+
+ this.lockIfEnabled(true);
+ this.writeSetting(this.enabled);
+
+ /* Status changes */
+ window.addEventListener('volumechange', this);
+ window.addEventListener('screenchange', this);
+
+ /* Gesture */
+ this.area.addEventListener('mousedown', this);
+ this.areaCamera.addEventListener('click', this);
+ this.areaUnlock.addEventListener('click', this);
+ this.iconContainer.addEventListener('mousedown', this);
+
+ /* Unlock & camera panel clean up */
+ this.overlay.addEventListener('transitionend', this);
+
+ /* Passcode input pad*/
+ this.passcodePad.addEventListener('click', this);
+
+ /* switching panels */
+ window.addEventListener('home', this);
+
+ /* blocking holdhome and prevent Cards View from show up */
+ window.addEventListener('holdhome', this, true);
+
+ /* mobile connection state on lock screen */
+ var conn = window.navigator.mozMobileConnection;
+ if (conn && conn.voice) {
+ conn.addEventListener('voicechange', this);
+ conn.addEventListener('cardstatechange', this);
+ conn.addEventListener('iccinfochange', this);
+ this.updateConnState();
+ this.connstate.hidden = false;
+ }
+
+ var self = this;
+ if (navigator && navigator.mozCellBroadcast) {
+ navigator.mozCellBroadcast.onreceived = function onReceived(event) {
+ var msg = event.message;
+ if (conn &&
+ conn.voice.network.mcc === MobileOperator.BRAZIL_MCC &&
+ msg.messageId === MobileOperator.BRAZIL_CELLBROADCAST_CHANNEL) {
+ self.cellbroadcastLabel = msg.body;
+ self.updateConnState();
+ }
+ };
+ }
+
+ SettingsListener.observe('lockscreen.enabled', true, function(value) {
+ self.setEnabled(value);
+ });
+
+ SettingsListener.observe('ring.enabled', true, function(value) {
+ self.mute.hidden = value;
+ });
+
+ SettingsListener.observe('vibration.enabled', true, function(value) {
+ if (value) {
+ self.mute.classList.add('vibration');
+ } else {
+ self.mute.classList.remove('vibration');
+ }
+ });
+
+ SettingsListener.observe('ril.radio.disabled', false, function(value) {
+ self.airplaneMode = value;
+ self.updateConnState();
+ });
+
+ SettingsListener.observe('wallpaper.image',
+ 'resources/images/backgrounds/default.png',
+ function(value) {
+ self.updateBackground(value);
+ self.overlay.classList.remove('uninit');
+ });
+
+ SettingsListener.observe(
+ 'lockscreen.passcode-lock.code', '0000', function(value) {
+ self.passCode = value;
+ });
+
+ SettingsListener.observe(
+ 'lockscreen.passcode-lock.enabled', false, function(value) {
+ self.setPassCodeEnabled(value);
+ });
+
+ SettingsListener.observe('lockscreen.unlock-sound.enabled',
+ true, function(value) {
+ self.setUnlockSoundEnabled(value);
+ });
+
+ SettingsListener.observe('lockscreen.passcode-lock.timeout',
+ 0, function(value) {
+ self.passCodeRequestTimeout = value;
+ });
+ },
+
+ /*
+ * Set enabled state.
+ * If enabled state is somehow updated when the lock screen is enabled
+ * This function will unlock it.
+ */
+ setEnabled: function ls_setEnabled(val) {
+ if (typeof val === 'string') {
+ this.enabled = val == 'false' ? false : true;
+ } else {
+ this.enabled = val;
+ }
+
+ if (!this.enabled && this.locked) {
+ this.unlock();
+ }
+ },
+
+ setPassCodeEnabled: function ls_setPassCodeEnabled(val) {
+ if (typeof val === 'string') {
+ this.passCodeEnabled = val == 'false' ? false : true;
+ } else {
+ this.passCodeEnabled = val;
+ }
+ },
+
+ setUnlockSoundEnabled: function ls_setUnlockSoundEnabled(val) {
+ if (typeof val === 'string') {
+ this.unlockSoundEnabled = val == 'false' ? false : true;
+ } else {
+ this.unlockSoundEnabled = val;
+ }
+ },
+
+ handleEvent: function ls_handleEvent(evt) {
+ switch (evt.type) {
+ case 'screenchange':
+ // XXX: If the screen is not turned off by ScreenManager
+ // we would need to lock the screen again
+ // when it's being turned back on
+ if (!evt.detail.screenEnabled) {
+ // Don't update the time after we're already locked otherwise turning
+ // the screen off again will bypass the passcode before the timeout.
+ if (!this.locked) {
+ this._screenOffTime = new Date().getTime();
+ }
+
+ // Remove camera once screen turns off
+ if (this.camera.firstElementChild)
+ this.camera.removeChild(this.camera.firstElementChild);
+
+ } else {
+ var _screenOffInterval = new Date().getTime() - this._screenOffTime;
+ if (_screenOffInterval > this.passCodeRequestTimeout * 1000) {
+ this._passCodeTimeoutCheck = true;
+ } else {
+ this._passCodeTimeoutCheck = false;
+ }
+ }
+
+ this.lockIfEnabled(true);
+ break;
+ case 'voicechange':
+ case 'cardstatechange':
+ case 'iccinfochange':
+ this.updateConnState();
+
+ case 'click':
+ if (evt.target === this.areaUnlock || evt.target === this.areaCamera) {
+ this.handleIconClick(evt.target);
+ break;
+ }
+
+ if (!evt.target.dataset.key)
+ break;
+
+ // Cancel the default action of <a>
+ evt.preventDefault();
+ this.handlePassCodeInput(evt.target.dataset.key);
+ break;
+
+ case 'mousedown':
+ var leftTarget = this.areaCamera;
+ var rightTarget = this.areaUnlock;
+ var handle = this.areaHandle;
+ var overlay = this.overlay;
+ var target = evt.target;
+
+ // Reset timer when touch while overlay triggered
+ if (overlay.classList.contains('triggered')) {
+ clearTimeout(this.triggeredTimeoutId);
+ this.triggeredTimeoutId = setTimeout(this.unloadPanel.bind(this),
+ this.TRIGGERED_TIMEOUT);
+ break;
+ }
+
+ overlay.classList.remove('elastic');
+ this.setElasticEnabled(false);
+
+ this._touch = {
+ touched: false,
+ leftTarget: leftTarget,
+ rightTarget: rightTarget,
+ overlayWidth: this.overlay.offsetWidth,
+ handleWidth: this.areaHandle.offsetWidth,
+ maxHandleOffset: rightTarget.offsetLeft - handle.offsetLeft -
+ (handle.offsetWidth - rightTarget.offsetWidth) / 2
+ };
+ window.addEventListener('mouseup', this);
+ window.addEventListener('mousemove', this);
+
+ this._touch.touched = true;
+ this._touch.initX = evt.pageX;
+ this._touch.initY = evt.pageY;
+ overlay.classList.add('touched');
+ break;
+
+ case 'mousemove':
+ this.handleMove(evt.pageX, evt.pageY);
+ break;
+
+ case 'mouseup':
+ window.removeEventListener('mousemove', this);
+ window.removeEventListener('mouseup', this);
+
+ this.handleMove(evt.pageX, evt.pageY);
+ this.handleGesture();
+ delete this._touch;
+ this.overlay.classList.remove('touched');
+
+ break;
+
+ case 'transitionend':
+ if (evt.target !== this.overlay)
+ return;
+
+ if (this.overlay.dataset.panel !== 'camera' &&
+ this.camera.firstElementChild) {
+ this.camera.removeChild(this.camera.firstElementChild);
+ }
+
+ if (!this.locked)
+ this.switchPanel();
+ break;
+
+ case 'home':
+ if (this.locked) {
+ this.switchPanel();
+ evt.stopImmediatePropagation();
+ }
+ break;
+
+ case 'holdhome':
+ if (!this.locked)
+ return;
+
+ evt.stopImmediatePropagation();
+ evt.stopPropagation();
+ break;
+ }
+ },
+
+ handleMove: function ls_handleMove(pageX, pageY) {
+ var touch = this._touch;
+
+ if (!touch.touched) {
+ // Do nothing if the user have not move the finger to the handle yet
+ if (document.elementFromPoint(pageX, pageY) !== this.areaHandle)
+ return;
+
+ touch.touched = true;
+ touch.initX = pageX;
+ touch.initY = pageY;
+
+ var overlay = this.overlay;
+ overlay.classList.add('touched');
+ }
+
+ var dy = pageY - touch.initY;
+ var ty = Math.max(- this.HANDLE_MAX, dy);
+ var base = - ty / this.HANDLE_MAX;
+ // mapping position 20-100 to opacity 0.1-0.5
+ var opacity = base <= 0.2 ? 0.1 : base * 0.5;
+ touch.ty = ty;
+
+ this.iconContainer.style.transform = 'translateY(' + ty + 'px)';
+ this.areaCamera.style.opacity =
+ this.areaUnlock.style.opacity = opacity;
+ },
+
+ handleGesture: function ls_handleGesture() {
+ var touch = this._touch;
+ if (touch.ty < -50) {
+ this.areaHandle.style.transform =
+ this.areaHandle.style.opacity =
+ this.iconContainer.style.transform =
+ this.iconContainer.style.opacity =
+ this.areaCamera.style.transform =
+ this.areaCamera.style.opacity =
+ this.areaUnlock.style.transform =
+ this.areaUnlock.style.opacity = '';
+ this.overlay.classList.add('triggered');
+
+ this.triggeredTimeoutId =
+ setTimeout(this.unloadPanel.bind(this), this.TRIGGERED_TIMEOUT);
+ } else if (touch.ty > -10) {
+ touch.touched = false;
+ this.unloadPanel();
+ this.playElastic();
+
+ var self = this;
+ var container = this.iconContainer;
+ container.addEventListener('animationend', function prompt() {
+ container.removeEventListener('animationend', prompt);
+ self.overlay.classList.remove('elastic');
+ self.setElasticEnabled(true);
+ });
+ } else {
+ this.unloadPanel();
+ this.setElasticEnabled(true);
+ }
+ },
+
+ handleIconClick: function ls_handleIconClick(target) {
+ var self = this;
+ switch (target) {
+ case this.areaCamera:
+ var panelOrFullApp = function panelOrFullApp() {
+ if (self.passCodeEnabled) {
+ // Go to secure camera panel
+ self.switchPanel('camera');
+ return;
+ }
+
+ self.unlock();
+
+ var a = new MozActivity({
+ name: 'record',
+ data: {
+ type: 'photos'
+ }
+ });
+ a.onerror = function ls_activityError() {
+ console.log('MozActivity: camera launch error.');
+ };
+ };
+
+ panelOrFullApp();
+ break;
+
+ case this.areaUnlock:
+ var passcodeOrUnlock = function passcodeOrUnlock() {
+ if (!self.passCodeEnabled || !self._passCodeTimeoutCheck) {
+ self.unlock();
+ } else {
+ self.switchPanel('passcode');
+ }
+ };
+ passcodeOrUnlock();
+ break;
+ }
+ },
+
+ handlePassCodeInput: function ls_handlePassCodeInput(key) {
+ switch (key) {
+ case 'e': // Emergency Call
+ this.switchPanel('emergency-call');
+ break;
+
+ case 'c':
+ this.switchPanel();
+ break;
+
+ case 'b':
+ if (this.overlay.dataset.passcodeStatus)
+ return;
+
+ this.passCodeEntered =
+ this.passCodeEntered.substr(0, this.passCodeEntered.length - 1);
+ this.updatePassCodeUI();
+
+ break;
+ default:
+ if (this.overlay.dataset.passcodeStatus)
+ return;
+
+ this.passCodeEntered += key;
+ this.updatePassCodeUI();
+
+ if (this.passCodeEntered.length === 4)
+ this.checkPassCode();
+ break;
+ }
+ },
+
+ lockIfEnabled: function ls_lockIfEnabled(instant) {
+ if (this.enabled) {
+ this.lock(instant);
+ } else {
+ this.unlock(instant);
+ }
+ },
+
+ unlock: function ls_unlock(instant) {
+ var currentApp = WindowManager.getDisplayedApp();
+ WindowManager.setOrientationForApp(currentApp);
+
+ var currentFrame = WindowManager.getAppFrame(currentApp).firstChild;
+ var wasAlreadyUnlocked = !this.locked;
+ this.locked = false;
+ this.setElasticEnabled(false);
+ this.mainScreen.focus();
+
+ var repaintTimeout = 0;
+ var nextPaint = (function() {
+ clearTimeout(repaintTimeout);
+ currentFrame.removeNextPaintListener(nextPaint);
+
+ if (instant) {
+ this.overlay.classList.add('no-transition');
+ this.switchPanel();
+ } else {
+ this.overlay.classList.remove('no-transition');
+ }
+
+ this.mainScreen.classList.remove('locked');
+
+ if (!wasAlreadyUnlocked) {
+ // Any changes made to this,
+ // also need to be reflected in apps/system/js/storage.js
+ this.dispatchEvent('unlock');
+ this.writeSetting(false);
+
+ if (instant)
+ return;
+
+ if (this.unlockSoundEnabled) {
+ var unlockAudio = new Audio('./resources/sounds/unlock.ogg');
+ unlockAudio.play();
+ }
+ }
+ }).bind(this);
+
+ this.dispatchEvent('will-unlock');
+ currentFrame.addNextPaintListener(nextPaint);
+ repaintTimeout = setTimeout(function ensureUnlock() {
+ nextPaint();
+ }, 400);
+ },
+
+ lock: function ls_lock(instant) {
+ var wasAlreadyLocked = this.locked;
+ this.locked = true;
+
+ this.updateTime();
+
+ this.switchPanel();
+
+ this.setElasticEnabled(ScreenManager.screenEnabled);
+
+ this.overlay.focus();
+ if (instant)
+ this.overlay.classList.add('no-transition');
+ else
+ this.overlay.classList.remove('no-transition');
+
+ this.mainScreen.classList.add('locked');
+
+ screen.mozLockOrientation('portrait-primary');
+
+ if (!wasAlreadyLocked) {
+ if (document.mozFullScreen)
+ document.mozCancelFullScreen();
+
+ // Any changes made to this,
+ // also need to be reflected in apps/system/js/storage.js
+ this.dispatchEvent('lock');
+ this.writeSetting(true);
+ }
+ },
+
+ loadPanel: function ls_loadPanel(panel, callback) {
+ this._loadingPanel = true;
+ switch (panel) {
+ case 'passcode':
+ case 'main':
+ if (callback)
+ setTimeout(callback);
+ break;
+
+ case 'emergency-call':
+ // create the <iframe> and load the emergency call
+ var frame = document.createElement('iframe');
+
+ frame.src = './emergency-call/index.html';
+ frame.onload = function emergencyCallLoaded() {
+ if (callback)
+ callback();
+ };
+ this.panelEmergencyCall.appendChild(frame);
+
+ break;
+
+ case 'camera':
+ // create the <iframe> and load the camera
+ var frame = document.createElement('iframe');
+
+ frame.src = './camera/index.html';
+ var mainScreen = this.mainScreen;
+ frame.onload = function cameraLoaded() {
+ mainScreen.classList.add('lockscreen-camera');
+ if (callback)
+ callback();
+ };
+ this.overlay.classList.remove('no-transition');
+ this.camera.appendChild(frame);
+
+ break;
+ }
+ },
+
+ unloadPanel: function ls_unloadPanel(panel, toPanel, callback) {
+ switch (panel) {
+ case 'passcode':
+ // Reset passcode panel only if the status is not error
+ if (this.overlay.dataset.passcodeStatus == 'error')
+ break;
+
+ delete this.overlay.dataset.passcodeStatus;
+ this.passCodeEntered = '';
+ this.updatePassCodeUI();
+ break;
+
+ case 'camera':
+ this.mainScreen.classList.remove('lockscreen-camera');
+ break;
+
+ case 'emergency-call':
+ var ecPanel = this.panelEmergencyCall;
+ ecPanel.addEventListener('transitionend', function unloadPanel() {
+ ecPanel.removeEventListener('transitionend', unloadPanel);
+ ecPanel.removeChild(ecPanel.firstElementChild);
+ });
+ break;
+
+ case 'main':
+ default:
+ var self = this;
+ var unload = function unload() {
+ self.areaHandle.style.transform =
+ self.areaUnlock.style.transform =
+ self.areaCamera.style.transform =
+ self.iconContainer.style.transform =
+ self.iconContainer.style.opacity =
+ self.areaHandle.style.opacity =
+ self.areaUnlock.style.opacity =
+ self.areaCamera.style.opacity = '';
+ self.overlay.classList.remove('triggered');
+ self.areaHandle.classList.remove('triggered');
+ self.areaCamera.classList.remove('triggered');
+ self.areaUnlock.classList.remove('triggered');
+
+ clearTimeout(self.triggeredTimeoutId);
+ self.setElasticEnabled(false);
+ };
+
+ if (toPanel !== 'camera') {
+ unload();
+ break;
+ }
+
+ this.overlay.addEventListener('transitionend',
+ function ls_unloadDefaultPanel(evt) {
+ if (evt.target !== this)
+ return;
+
+ self.overlay.removeEventListener('transitionend',
+ ls_unloadDefaultPanel);
+ unload();
+ }
+ );
+
+ break;
+ }
+
+ if (callback)
+ setTimeout(callback);
+ },
+
+ switchPanel: function ls_switchPanel(panel) {
+ if (this._switchingPanel) {
+ return;
+ }
+
+ var overlay = this.overlay;
+ var self = this;
+ panel = panel || 'main';
+
+ this._switchingPanel = true;
+ this.loadPanel(panel, function panelLoaded() {
+ self.unloadPanel(overlay.dataset.panel, panel,
+ function panelUnloaded() {
+ if (overlay.dataset.panel !== panel)
+ self.dispatchEvent('lockpanelchange');
+
+ overlay.dataset.panel = panel;
+ self._switchingPanel = false;
+ });
+ });
+ },
+
+ updateTime: function ls_updateTime() {
+ if (!this.locked)
+ return;
+
+ var d = new Date();
+ var f = new navigator.mozL10n.DateTimeFormat();
+ var _ = navigator.mozL10n.get;
+
+ var timeFormat = _('shortTimeFormat') || '%H:%M';
+ var dateFormat = _('longDateFormat') || '%A %e %B';
+ var time = f.localeFormat(d, timeFormat);
+ this.clockNumbers.textContent = time.match(/([012]?\d).[0-5]\d/g);
+ this.clockMeridiem.textContent = (time.match(/AM|PM/i) || []).join('');
+ this.date.textContent = f.localeFormat(d, dateFormat);
+
+ var self = this;
+ window.setTimeout(function ls_clockTimeout() {
+ self.updateTime();
+ }, (59 - d.getSeconds()) * 1000);
+ },
+
+ updateConnState: function ls_updateConnState() {
+ var conn = window.navigator.mozMobileConnection;
+ if (!conn)
+ return;
+
+ var voice = conn.voice;
+ var iccInfo = conn.iccInfo;
+ var connstateLine1 = this.connstate.firstElementChild;
+ var connstateLine2 = this.connstate.lastElementChild;
+ var _ = navigator.mozL10n.get;
+
+ var updateConnstateLine1 = function updateConnstateLine1(l10nId) {
+ connstateLine1.dataset.l10nId = l10nId;
+ connstateLine1.textContent = _(l10nId) || '';
+ };
+
+ var self = this;
+ var updateConnstateLine2 = function updateConnstateLine2(l10nId) {
+ if (l10nId) {
+ self.connstate.classList.add('twolines');
+ connstateLine2.dataset.l10nId = l10nId;
+ connstateLine2.textContent = _(l10nId) || '';
+ } else {
+ self.connstate.classList.remove('twolines');
+ delete(connstateLine2.dataset.l10nId);
+ connstateLine2.textContent = '';
+ }
+ };
+
+ // Reset line 2
+ updateConnstateLine2();
+
+ if (this.airplaneMode) {
+ updateConnstateLine1('airplaneMode');
+ return;
+ }
+
+ // Possible value of voice.state are:
+ // 'notSearching', 'searching', 'denied', 'registered',
+ // where the latter three mean the phone is trying to grab the network.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=777057
+ if (voice.state == 'notSearching') {
+ updateConnstateLine1('noNetwork');
+ return;
+ }
+
+ if (!voice.connected && !voice.emergencyCallsOnly) {
+ // "Searching"
+ // voice.state can be any of the latter three values.
+ // (it's possible that the phone is briefly 'registered'
+ // but not yet connected.)
+ updateConnstateLine1('searching');
+ return;
+ }
+
+ if (voice.emergencyCallsOnly) {
+ updateConnstateLine1('emergencyCallsOnly');
+
+ switch (conn.cardState) {
+ case 'absent':
+ updateConnstateLine2('emergencyCallsOnly-noSIM');
+ break;
+
+ case 'pinRequired':
+ updateConnstateLine2('emergencyCallsOnly-pinRequired');
+ break;
+
+ case 'pukRequired':
+ updateConnstateLine2('emergencyCallsOnly-pukRequired');
+ break;
+
+ case 'networkLocked':
+ updateConnstateLine2('emergencyCallsOnly-networkLocked');
+ break;
+
+ default:
+ updateConnstateLine2();
+ break;
+ }
+ return;
+ }
+
+ var operatorInfos = MobileOperator.userFacingInfo(conn);
+ if (this.cellbroadcastLabel) {
+ connstateLine2.textContent = this.cellbroadcastLabel;
+ } else if (operatorInfos.carrier) {
+ connstateLine2.textContent = operatorInfos.carrier + ' ' +
+ operatorInfos.region;
+ }
+
+ var operator = operatorInfos.operator;
+
+ if (voice.roaming) {
+ var l10nArgs = { operator: operator };
+ connstateLine1.dataset.l10nId = 'roaming';
+ connstateLine1.dataset.l10nArgs = JSON.stringify(l10nArgs);
+ connstateLine1.textContent = _('roaming', l10nArgs);
+
+ return;
+ }
+
+ delete connstateLine1.dataset.l10nId;
+ connstateLine1.textContent = operator;
+ },
+
+ updatePassCodeUI: function lockscreen_updatePassCodeUI() {
+ var overlay = this.overlay;
+ if (overlay.dataset.passcodeStatus)
+ return;
+ if (this.passCodeEntered) {
+ overlay.classList.add('passcode-entered');
+ } else {
+ overlay.classList.remove('passcode-entered');
+ }
+ var i = 4;
+ while (i--) {
+ var span = this.passcodeCode.childNodes[i];
+ if (this.passCodeEntered.length > i) {
+ span.dataset.dot = true;
+ } else {
+ delete span.dataset.dot;
+ }
+ }
+ },
+
+ checkPassCode: function lockscreen_checkPassCode() {
+ if (this.passCodeEntered === this.passCode) {
+ var self = this;
+ this.overlay.dataset.passcodeStatus = 'success';
+ this.passCodeError = 0;
+
+ var transitionend = function() {
+ self.passcodeCode.removeEventListener('transitionend', transitionend);
+ self.unlock();
+ };
+ this.passcodeCode.addEventListener('transitionend', transitionend);
+ } else {
+ this.overlay.dataset.passcodeStatus = 'error';
+ if ('vibrate' in navigator)
+ navigator.vibrate([50, 50, 50]);
+
+ var self = this;
+ setTimeout(function error() {
+ delete self.overlay.dataset.passcodeStatus;
+ self.passCodeEntered = '';
+ self.updatePassCodeUI();
+ }, this.kPassCodeErrorTimeout);
+ }
+ },
+
+ updateBackground: function ls_updateBackground(background_datauri) {
+ this._imgPreload([background_datauri, 'style/lockscreen/images/mask.png'],
+ function(images) {
+
+ // Bug 829075 : We need a <canvas> in the DOM to prevent banding on
+ // Otoro-like devices
+ var canvas = document.createElement('canvas');
+ canvas.classList.add('lockscreen-wallpaper');
+ canvas.width = images[0].width;
+ canvas.height = images[0].height;
+
+ var ctx = canvas.getContext('2d');
+ ctx.drawImage(images[0], 0, 0);
+ ctx.drawImage(images[1], 0, 0);
+
+ var panels_selector = '.lockscreen-panel[data-wallpaper]';
+ var panels = document.querySelectorAll(panels_selector);
+ for (var i = 0, il = panels.length; i < il; i++) {
+ var copied_canvas;
+ var panel = panels[i];
+
+ // Remove previous <canvas> if they exist
+ var old_canvas = panel.querySelector('canvas');
+ if (old_canvas) {
+ old_canvas.parentNode.removeChild(old_canvas);
+ }
+
+ // For the first panel, we can use the existing <canvas>
+ if (!copied_canvas) {
+ copied_canvas = canvas;
+ } else {
+ // Otherwise, copy the node and content
+ copied_canvas = canvas.cloneNode();
+ copied_canvas.getContext('2d').drawImage(canvas, 0, 0);
+ }
+
+ panel.insertBefore(copied_canvas, panel.firstChild);
+ }
+ });
+ },
+
+ _imgPreload: function ls_imgPreload(img_paths, callback) {
+ var loaded = 0;
+ var images = [];
+ var il = img_paths.length;
+ var inc = function() {
+ loaded += 1;
+ if (loaded === il && callback) {
+ callback(images);
+ }
+ };
+ for (var i = 0; i < il; i++) {
+ images[i] = new Image();
+ images[i].onload = inc;
+ images[i].src = img_paths[i];
+ }
+ },
+
+ getAllElements: function ls_getAllElements() {
+ // ID of elements to create references
+ var elements = ['connstate', 'mute', 'clock-numbers', 'clock-meridiem',
+ 'date', 'area', 'area-unlock', 'area-camera', 'icon-container',
+ 'area-handle', 'passcode-code',
+ 'passcode-pad', 'camera', 'accessibility-camera',
+ 'accessibility-unlock', 'panel-emergency-call'];
+
+ var toCamelCase = function toCamelCase(str) {
+ return str.replace(/\-(.)/g, function replacer(str, p1) {
+ return p1.toUpperCase();
+ });
+ };
+
+ elements.forEach((function createElementRef(name) {
+ this[toCamelCase(name)] = document.getElementById('lockscreen-' + name);
+ }).bind(this));
+
+ this.overlay = document.getElementById('lockscreen');
+ this.mainScreen = document.getElementById('screen');
+ },
+
+ dispatchEvent: function ls_dispatchEvent(name) {
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent(name, true, true, null);
+ window.dispatchEvent(evt);
+ },
+
+ writeSetting: function ls_writeSetting(value) {
+ if (!window.navigator.mozSettings)
+ return;
+
+ SettingsListener.getSettingsLock().set({
+ 'lockscreen.locked': value
+ });
+ },
+
+ setElasticEnabled: function ls_setElasticEnabled(value) {
+ clearInterval(this.elasticIntervalId);
+ if (value) {
+ this.elasticIntervalId =
+ setInterval(this.playElastic.bind(this), this.ELASTIC_INTERVAL);
+ }
+ },
+
+ playElastic: function ls_playElastic() {
+ if (this._touch && this._touch.touched)
+ return;
+
+ var overlay = this.overlay;
+ var container = this.iconContainer;
+
+ overlay.classList.add('elastic');
+ container.addEventListener('animationend', function animationend(e) {
+ container.removeEventListener(e.type, animationend);
+ overlay.classList.remove('elastic');
+ });
+ }
+};
+
+// Bug 836195 - [Homescreen] Dock icons drop down in the UI
+// consistently when using a lockcode and visiting camera
+LockScreen.init();
+
+navigator.mozL10n.ready(LockScreen.init.bind(LockScreen));
+
diff --git a/apps/system/js/modal_dialog.js b/apps/system/js/modal_dialog.js
new file mode 100644
index 0000000..33f3201
--- /dev/null
+++ b/apps/system/js/modal_dialog.js
@@ -0,0 +1,443 @@
+/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+// The modal dialog listen to mozbrowsershowmodalprompt event.
+// Blocking the current app and then show cutom modal dialog
+// (alert/confirm/prompt)
+
+var ModalDialog = {
+ // Used for element id access.
+ // e.g., 'modal-dialog-alert-ok'
+ prefix: 'modal-dialog-',
+
+ // DOM
+ elements: {},
+
+ // Get all elements when inited.
+ getAllElements: function md_getAllElements() {
+ var elementsID = ['alert', 'alert-ok', 'alert-message',
+ 'prompt', 'prompt-ok', 'prompt-cancel', 'prompt-input', 'prompt-message',
+ 'confirm', 'confirm-ok', 'confirm-cancel', 'confirm-message',
+ 'select-one', 'select-one-cancel', 'select-one-menu', 'select-one-title',
+ 'alert-title', 'confirm-title', 'prompt-title'];
+
+ var toCamelCase = function toCamelCase(str) {
+ return str.replace(/\-(.)/g, function replacer(str, p1) {
+ return p1.toUpperCase();
+ });
+ };
+
+ // Loop and add element with camel style name to Modal Dialog attribute.
+ elementsID.forEach(function createElementRef(name) {
+ this.elements[toCamelCase(name)] =
+ document.getElementById(this.prefix + name);
+ }, this);
+
+ this.screen = document.getElementById('screen');
+ this.overlay = document.getElementById('dialog-overlay');
+ },
+
+ // Save the events returned by mozbrowsershowmodalprompt for later use.
+ // The events are stored according to webapp origin
+ // e.g., 'http://uitest.gaiamobile.org': evt
+ currentEvents: {},
+
+ init: function md_init() {
+ // Get all elements initially.
+ this.getAllElements();
+ var elements = this.elements;
+
+ // Bind events
+ window.addEventListener('mozbrowsershowmodalprompt', this);
+ window.addEventListener('appopen', this);
+ window.addEventListener('appwillclose', this);
+ window.addEventListener('appterminated', this);
+ window.addEventListener('resize', this);
+ window.addEventListener('keyboardchange', this);
+ window.addEventListener('keyboardhide', this);
+ window.addEventListener('home', this);
+ window.addEventListener('holdhome', this);
+
+ for (var id in elements) {
+ var tagName = elements[id].tagName.toLowerCase();
+ if (tagName == 'button' || tagName == 'ul') {
+ elements[id].addEventListener('click', this);
+ }
+ }
+ },
+
+ // Default event handler
+ handleEvent: function md_handleEvent(evt) {
+ var elements = this.elements;
+ switch (evt.type) {
+ case 'mozbrowsershowmodalprompt':
+ var frameType = evt.target.dataset.frameType;
+ if (frameType != 'window' && frameType != 'inline-activity')
+ return;
+
+ evt.preventDefault();
+ var origin = evt.target.dataset.frameOrigin;
+ this.currentEvents[origin] = evt;
+
+ // Show modal dialog only if
+ // the frame is currently displayed.
+ if (origin == WindowManager.getDisplayedApp() ||
+ frameType == 'inline-activity')
+ this.show(evt.target, origin);
+ break;
+
+ case 'click':
+ if (evt.currentTarget === elements.confirmCancel ||
+ evt.currentTarget === elements.promptCancel ||
+ evt.currentTarget === elements.selectOneCancel) {
+ this.cancelHandler();
+ } else if (evt.currentTarget === elements.selectOneMenu) {
+ this.selectOneHandler(evt.target);
+ } else {
+ this.confirmHandler();
+ }
+ break;
+
+ case 'appopen':
+ this.show(evt.target, evt.detail.origin);
+ break;
+
+ case 'home':
+ case 'holdhome':
+ // Inline activity, which origin is different from foreground app
+ if (this.isVisible() &&
+ this.currentOrigin != WindowManager.getDisplayedApp())
+ this.cancelHandler();
+ break;
+
+ case 'appwillclose':
+ // Do nothing if the app is closed at background.
+ if (evt.detail.origin !== this.currentOrigin)
+ return;
+
+ // Reset currentOrigin
+ this.hide();
+ break;
+
+ case 'appterminated':
+ if (this.currentEvents[evt.detail.origin])
+ delete this.currentEvents[evt.detail.origin];
+
+ break;
+
+ case 'resize':
+ case 'keyboardhide':
+ if (!this.currentOrigin)
+ return;
+
+ this.setHeight(window.innerHeight - StatusBar.height);
+ break;
+
+ case 'keyboardchange':
+ this.setHeight(window.innerHeight -
+ evt.detail.height - StatusBar.height);
+ break;
+ }
+ },
+
+ setHeight: function md_setHeight(height) {
+ if (this.isVisible())
+ this.overlay.style.height = height + 'px';
+ },
+
+ // Show relative dialog and set message/input value well
+ show: function md_show(target, origin) {
+ if (!(origin in this.currentEvents))
+ return;
+
+ var _ = navigator.mozL10n.get;
+ var evt = this.currentEvents[origin];
+ this.currentOrigin = origin;
+
+ var message = evt.detail.message || '';
+ var elements = this.elements;
+ this.screen.classList.add('modal-dialog');
+
+ function escapeHTML(str) {
+ var span = document.createElement('span');
+ span.textContent = str;
+ // Escape space for displaying multiple space in message.
+ span.innerHTML = span.innerHTML.replace(/\n/g, '<br/>');
+ return span.innerHTML;
+ }
+
+ var type = evt.detail.promptType || evt.detail.type;
+ if (type !== 'selectone') {
+ message = escapeHTML(message);
+ }
+
+ switch (type) {
+ case 'alert':
+ elements.alertMessage.innerHTML = message;
+ elements.alert.classList.add('visible');
+ this.setTitle('alert', '');
+ elements.alertOk.textContent = evt.yesText ? evt.yesText : _('ok');
+ break;
+
+ case 'prompt':
+ elements.prompt.classList.add('visible');
+ elements.promptInput.value = evt.detail.initialValue;
+ elements.promptMessage.innerHTML = message;
+ this.setTitle('prompt', '');
+ elements.promptOk.textContent = evt.yesText ? evt.yesText : _('ok');
+ elements.promptCancel.textContent = evt.noText ?
+ evt.noText : _('cancel');
+ break;
+
+ case 'confirm':
+ elements.confirm.classList.add('visible');
+ elements.confirmMessage.innerHTML = message;
+ this.setTitle('confirm', '');
+ elements.confirmOk.textContent = evt.yesText ? evt.yesText : _('ok');
+ elements.confirmCancel.textContent = evt.noText ?
+ evt.noText : _('cancel');
+ break;
+
+ case 'selectone':
+ this.buildSelectOneDialog(message);
+ elements.selectOne.classList.add('visible');
+ break;
+ }
+
+ this.setHeight(window.innerHeight - StatusBar.height);
+ },
+
+ hide: function md_hide() {
+ var evt = this.currentEvents[this.currentOrigin];
+ var type = evt.detail.promptType;
+ if (type == 'prompt') {
+ this.elements.promptInput.blur();
+ }
+ this.currentOrigin = null;
+ this.screen.classList.remove('modal-dialog');
+ this.elements[type].classList.remove('visible');
+ },
+
+ setTitle: function md_setTitle(type, title) {
+ this.elements[type + 'Title'].textContent = title;
+ },
+
+ // When user clicks OK button on alert/confirm/prompt
+ confirmHandler: function md_confirmHandler() {
+ this.screen.classList.remove('modal-dialog');
+ var elements = this.elements;
+
+ var evt = this.currentEvents[this.currentOrigin];
+
+ var type = evt.detail.promptType || evt.detail.type;
+ switch (type) {
+ case 'alert':
+ elements.alert.classList.remove('visible');
+ break;
+
+ case 'prompt':
+ evt.detail.returnValue = elements.promptInput.value;
+ elements.prompt.classList.remove('visible');
+ break;
+
+ case 'confirm':
+ evt.detail.returnValue = true;
+ elements.confirm.classList.remove('visible');
+ break;
+ }
+
+ if (evt.isPseudo && evt.callback) {
+ evt.callback(evt.detail.returnValue);
+ }
+
+ if (evt.detail.unblock)
+ evt.detail.unblock();
+
+ delete this.currentEvents[this.currentOrigin];
+ },
+
+ // When user clicks cancel button on confirm/prompt or
+ // when the user try to escape the dialog with the escape key
+ cancelHandler: function md_cancelHandler() {
+ var evt = this.currentEvents[this.currentOrigin];
+ this.screen.classList.remove('modal-dialog');
+ var elements = this.elements;
+
+ var type = evt.detail.promptType || evt.detail.type;
+ switch (type) {
+ case 'alert':
+ elements.alert.classList.remove('visible');
+ break;
+
+ case 'prompt':
+ /* return null when click cancel */
+ evt.detail.returnValue = null;
+ elements.prompt.classList.remove('visible');
+ break;
+
+ case 'confirm':
+ /* return false when click cancel */
+ evt.detail.returnValue = false;
+ elements.confirm.classList.remove('visible');
+ break;
+
+ case 'selectone':
+ /* return null when click cancel */
+ evt.detail.returnValue = null;
+ elements.selectOne.classList.remove('visible');
+ break;
+ }
+
+ if (evt.isPseudo && evt.cancelCallback) {
+ evt.cancelCallback(evt.detail.returnValue);
+ }
+
+ if (evt.detail.unblock)
+ evt.detail.unblock();
+
+ delete this.currentEvents[this.currentOrigin];
+ },
+
+ // When user selects an option on selectone dialog
+ selectOneHandler: function md_confirmHandler(target) {
+ this.screen.classList.remove('modal-dialog');
+ var elements = this.elements;
+
+ var evt = this.currentEvents[this.currentOrigin];
+
+ evt.detail.returnValue = target.id;
+ elements.selectOne.classList.remove('visible');
+
+ if (evt.isPseudo && evt.callback) {
+ evt.callback(evt.detail.returnValue);
+ }
+
+ if (evt.detail.unblock)
+ evt.detail.unblock();
+
+ delete this.currentEvents[this.currentOrigin];
+ },
+
+ buildSelectOneDialog: function md_buildSelectOneDialog(data) {
+ var elements = this.elements;
+ elements.selectOneTitle.textContent = data.title;
+ elements.selectOneMenu.innerHTML = '';
+
+ if (!data.options) {
+ return;
+ }
+
+ var itemsHTML = [];
+ for (var i = 0; i < data.options.length; i++) {
+ itemsHTML.push('<li><button id="');
+ itemsHTML.push(data.options[i].id);
+ itemsHTML.push('">');
+ itemsHTML.push(data.options[i].text);
+ itemsHTML.push('</button></li>');
+ }
+
+ elements.selectOneMenu.innerHTML = itemsHTML.join('');
+ },
+
+ /**
+ * Method about customized alert
+ * @param {String} title the title of the dialog. null or empty for
+ * no title.
+ * @param {String} text message for the dialog.
+ * @param {Object} confirm {title, callback} object when confirm.
+ */
+ alert: function md_alert(title, text, confirm) {
+ this.showWithPseudoEvent({
+ type: 'alert',
+ text: text,
+ callback: confirm.callback,
+ title: title,
+ yesText: confirm.title
+ });
+ },
+
+ /**
+ * Method about customized confirm
+ * @param {String} title the title of the dialog. null or empty for
+ * no title.
+ * @param {String} text message for the dialog.
+ * @param {Object} confirm {title, callback} object when confirm.
+ * @param {Object} cancel {title, callback} object when cancel.
+ */
+ confirm: function md_confirm(title, text, confirm, cancel) {
+ this.showWithPseudoEvent({
+ type: 'confirm',
+ text: text,
+ callback: confirm.callback,
+ cancel: cancel.callback,
+ title: title,
+ yesText: confirm.title,
+ noText: cancel.title
+ });
+ },
+
+ /**
+ * Method about customized prompt
+ * @param {String} title the title of the dialog. null or empty for
+ * no title.
+ * @param {String} text message for the dialog.
+ * @param {String} default_value message in the text field.
+ * @param {Object} confirm {title, callback} object when confirm.
+ * @param {Object} cancel {title, callback} object when cancel.
+ */
+ prompt: function md_prompt(title, text, default_value, confirm, cancel) {
+ this.showWithPseudoEvent({
+ type: 'prompt',
+ text: text,
+ initialValue: default_value,
+ callback: confirm.callback,
+ cancel: cancel.callback,
+ title: title,
+ yesText: confirm.title,
+ noText: cancel.title
+ });
+ },
+
+ selectOne: function md_selectOne(data, callback) {
+ this.showWithPseudoEvent({
+ type: 'selectone',
+ text: data,
+ callback: callback
+ });
+ },
+
+ showWithPseudoEvent: function md_showWithPseudoEvent(config) {
+ var pseudoEvt = {
+ isPseudo: true,
+ detail: {
+ unblock: null
+ }
+ };
+
+ pseudoEvt.detail.message = config.text;
+ pseudoEvt.callback = config.callback;
+ pseudoEvt.detail.promptType = config.type;
+ pseudoEvt.cancelCallback = config.cancel;
+ pseudoEvt.yesText = config.yesText;
+ pseudoEvt.noText = config.noText;
+ if (config.type == 'prompt') {
+ pseudoEvt.detail.initialValue = config.initialValue;
+ }
+
+ // Create a virtual mapping in this.currentEvents,
+ // since system-app uses the different way to call ModalDialog.
+ this.currentEvents['system'] = pseudoEvt;
+ this.show(null, 'system');
+ if (config.title)
+ this.setTitle(config.type, config.title);
+ },
+
+ isVisible: function md_isVisible() {
+ return this.screen.classList.contains('modal-dialog');
+ }
+};
+
+ModalDialog.init();
+
diff --git a/apps/system/js/mouse2touch.js b/apps/system/js/mouse2touch.js
new file mode 100644
index 0000000..b9d6a89
--- /dev/null
+++ b/apps/system/js/mouse2touch.js
@@ -0,0 +1,70 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+/*
+* Mouse2Touch is a shim that listens to MouseEvent but passes
+* fake TouchEvent object to touch event handler
+*
+* XXX: remove this if we are not going to support testing on
+* non-touch devices, e.g. B2G Desktop, Nightly, or,
+* make the creation of this object optional if we can reliably detect
+* touch support by evaluate (document instanceof DocumentTouch)
+*
+*/
+var Mouse2Touch = (function m2t() {
+ var Mouse2TouchEvent = {
+ 'mousedown': 'touchstart',
+ 'mousemove': 'touchmove',
+ 'mouseup': 'touchend'
+ };
+
+ var Touch2MouseEvent = {
+ 'touchstart': 'mousedown',
+ 'touchmove': 'mousemove',
+ 'touchend': 'mouseup'
+ };
+
+ var ForceOnWindow = {
+ 'touchmove': true,
+ 'touchend': true
+ };
+
+ var addEventHandler = function m2t_addEventHandlers(target,
+ name,
+ listener) {
+ target = ForceOnWindow[name] ? window : target;
+ name = Touch2MouseEvent[name] || name;
+ target.addEventListener(name, {
+ handleEvent: function m2t_handleEvent(evt) {
+ if (Mouse2TouchEvent[evt.type]) {
+ var original = evt;
+ evt = {
+ type: Mouse2TouchEvent[original.type],
+ target: original.target,
+ touches: [original],
+ preventDefault: function() {
+ original.preventDefault();
+ }
+ };
+ evt.changedTouches = evt.touches;
+ }
+ return listener.handleEvent(evt);
+ }
+ }, true);
+ };
+
+ var removeEventHandler = function m2t_removeEventHandler(target,
+ name,
+ listener) {
+ target = ForceOnWindow[name] ? window : target;
+ name = Touch2MouseEvent[name] || name;
+ target.removeEventListener(name, listener);
+ };
+
+ return {
+ addEventHandler: addEventHandler,
+ removeEventHandler: removeEventHandler
+ };
+})();
diff --git a/apps/system/js/notifications.js b/apps/system/js/notifications.js
new file mode 100644
index 0000000..dae4ab4
--- /dev/null
+++ b/apps/system/js/notifications.js
@@ -0,0 +1,410 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+(function appCacheIcons() {
+ // Caching the icon for notification if appCache is in effect
+ var appCache = window.applicationCache;
+ if (!appCache)
+ return;
+
+ var addIcons = function addIcons(app) {
+ var icons = app.manifest.icons;
+ if (icons) {
+ Object.keys(icons).forEach(function iconIterator(key) {
+ var url = app.origin + icons[key];
+ appCache.mozAdd(url);
+ });
+ }
+ };
+
+ var removeIcons = function removeIcons(app) {
+ var icons = app.manifest.icons;
+ if (icons) {
+ Object.keys(icons).forEach(function iconIterator(key) {
+ var url = app.origin + icons[key];
+ appCache.mozRemove(url);
+ });
+ }
+ };
+
+ window.addEventListener('applicationinstall', function bsm_oninstall(evt) {
+ addIcons(evt.detail.application);
+ });
+
+ window.addEventListener('applicationuninstall', function bsm_oninstall(evt) {
+ removeIcons(evt.detail.application);
+ });
+}());
+
+var NotificationScreen = {
+ TOASTER_TIMEOUT: 5000,
+ TRANSITION_SPEED: 1.8,
+ TRANSITION_FRACTION: 0.30,
+
+ _notification: null,
+ _containerWidth: null,
+ _toasterTimeout: null,
+ _toasterGD: null,
+
+ lockscreenPreview: true,
+ silent: false,
+ alerts: true,
+ vibrates: true,
+
+ init: function ns_init() {
+ window.addEventListener('mozChromeEvent', this);
+ this.container =
+ document.getElementById('desktop-notifications-container');
+ this.lockScreenContainer =
+ document.getElementById('notifications-lockscreen-container');
+ this.toaster = document.getElementById('notification-toaster');
+ this.toasterIcon = document.getElementById('toaster-icon');
+ this.toasterTitle = document.getElementById('toaster-title');
+ this.toasterDetail = document.getElementById('toaster-detail');
+ this.clearAllButton = document.getElementById('notification-clear');
+
+ this._toasterGD = new GestureDetector(this.toaster);
+ ['tap', 'mousedown', 'swipe'].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;
+
+ window.addEventListener('utilitytrayshow', this);
+ window.addEventListener('unlock', this.clearLockScreen.bind(this));
+ window.addEventListener('mozvisibilitychange', this);
+ window.addEventListener('appopen', this.handleAppopen.bind(this));
+
+ this._sound = 'style/notifications/ringtones/notifier_exclamation.ogg';
+
+ var self = this;
+ SettingsListener.observe('notification.ringtone', '', function(value) {
+ self._sound = value;
+ });
+ },
+
+ handleEvent: function ns_handleEvent(evt) {
+ switch (evt.type) {
+ case 'mozChromeEvent':
+ var detail = evt.detail;
+ if (detail.type !== 'desktop-notification')
+ return;
+
+ this.addNotification(detail);
+ break;
+ case 'tap':
+ var target = evt.target;
+ this.tap(target);
+ break;
+ case 'mousedown':
+ this.mousedown(evt);
+ break;
+ case 'swipe':
+ this.swipe(evt);
+ break;
+ case 'utilitytrayshow':
+ this.updateTimestamps();
+ StatusBar.updateNotificationUnread(false);
+ break;
+ case 'mozvisibilitychange':
+ //update timestamps in lockscreen notifications
+ if (!document.mozHidden) {
+ this.updateTimestamps();
+ }
+ break;
+ }
+ },
+
+ handleAppopen: function ns_handleAppopen(evt) {
+ var manifestURL = evt.detail.manifestURL,
+ selector = '[data-manifest-u-r-l="' + manifestURL + '"]';
+
+ var nodes = this.container.querySelectorAll(selector);
+
+ for (var i = nodes.length - 1; i >= 0; i--) {
+ this.closeNotification(nodes[i]);
+ }
+ },
+
+ // Swipe handling
+ mousedown: function ns_mousedown(evt) {
+ if (!evt.target.dataset.notificationID)
+ return;
+
+ evt.preventDefault();
+ this._notification = evt.target;
+ this._containerWidth = this.container.clientWidth;
+ },
+
+ swipe: function ns_swipe(evt) {
+ var detail = evt.detail;
+ var distance = detail.start.screenX - detail.end.screenX;
+ var fastEnough = Math.abs(detail.vx) > this.TRANSITION_SPEED;
+ var farEnough = Math.abs(distance) >
+ this._containerWidth * this.TRANSITION_FRACTION;
+
+ // We only remove the notification if the swipe was
+ // - left to right
+ // - far or fast enough
+ if ((distance > 0) ||
+ !(farEnough || fastEnough)) {
+ // Werent far or fast enough to delete, restore
+ delete this._notification;
+ return;
+ }
+
+ this._notification.classList.add('disappearing');
+
+ 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';
+ });
+ });
+ });
+ },
+
+ tap: function ns_tap(notificationNode) {
+ var notificationID = notificationNode.dataset.notificationID;
+
+ var event = document.createEvent('CustomEvent');
+ event.initCustomEvent('mozContentEvent', true, true, {
+ type: 'desktop-notification-click',
+ id: notificationID
+ });
+ window.dispatchEvent(event);
+
+ this.removeNotification(notificationNode.dataset.notificationID, false);
+
+ if (notificationNode == this.toaster) {
+ this.toaster.classList.remove('displayed');
+ } else {
+ UtilityTray.hide();
+ }
+ },
+
+ updateTimestamps: function ns_updateTimestamps() {
+ var timestamps = document.getElementsByClassName('timestamp');
+ for (var i = 0, l = timestamps.length; i < l; i++) {
+ timestamps[i].textContent =
+ this.prettyDate(new Date(timestamps[i].dataset.timestamp));
+ }
+ },
+
+ /**
+ * Display a human-readable relative timestamp.
+ */
+ prettyDate: function prettyDate(time) {
+ var date;
+ if (navigator.mozL10n) {
+ date = navigator.mozL10n.DateTimeFormat().fromNow(time, true);
+ } else {
+ date = time.toLocaleFormat();
+ }
+ return date;
+ },
+
+ addNotification: function ns_addNotification(detail) {
+ var notificationNode = document.createElement('div');
+ notificationNode.className = 'notification';
+
+ notificationNode.dataset.notificationID = detail.id;
+ notificationNode.dataset.manifestURL = detail.manifestURL;
+
+ if (detail.icon) {
+ var icon = document.createElement('img');
+ icon.src = detail.icon;
+ notificationNode.appendChild(icon);
+ this.toasterIcon.src = detail.icon;
+ this.toasterIcon.hidden = false;
+ } else {
+ this.toasterIcon.hidden = true
+ }
+
+ var time = document.createElement('span');
+ var timestamp = new Date();
+ time.classList.add('timestamp');
+ time.dataset.timestamp = timestamp;
+ time.textContent = this.prettyDate(timestamp);
+ notificationNode.appendChild(time);
+
+ var title = document.createElement('div');
+ title.textContent = detail.title;
+ notificationNode.appendChild(title);
+
+ this.toasterTitle.textContent = detail.title;
+
+ var message = document.createElement('div');
+ message.classList.add('detail');
+ message.textContent = detail.text;
+ notificationNode.appendChild(message);
+
+ this.toasterDetail.textContent = detail.text;
+
+ this.container.insertBefore(notificationNode,
+ this.container.firstElementChild);
+ new GestureDetector(notificationNode).startDetecting();
+
+ // We turn the screen on if needed in order to let
+ // the user see the notification toaster
+ if (typeof(ScreenManager) !== 'undefined' &&
+ !ScreenManager.screenEnabled) {
+ ScreenManager.turnScreenOn();
+ }
+
+ this.updateStatusBarIcon(true);
+
+ // Notification toaster
+ if (this.lockscreenPreview || !LockScreen.locked) {
+ this.toaster.dataset.notificationID = detail.id;
+
+ this.toaster.classList.add('displayed');
+ this._toasterGD.startDetecting();
+
+ if (this._toasterTimeout)
+ clearTimeout(this._toasterTimeout);
+
+ this._toasterTimeout = setTimeout((function() {
+ this.toaster.classList.remove('displayed');
+ this._toasterTimeout = null;
+ this._toasterGD.stopDetecting();
+ }).bind(this), this.TOASTER_TIMEOUT);
+ }
+
+ // Adding it to the lockscreen if locked and the privacy setting
+ // does not prevent it.
+ if (typeof(LockScreen) !== 'undefined' &&
+ LockScreen.locked && this.lockscreenPreview) {
+ var lockScreenNode = notificationNode.cloneNode(true);
+ this.lockScreenContainer.insertBefore(lockScreenNode,
+ this.lockScreenContainer.firstElementChild);
+ }
+
+ if (this.alerts && !this.silent) {
+ var ringtonePlayer = new Audio();
+ ringtonePlayer.src = this._sound;
+ ringtonePlayer.mozAudioChannelType = 'notification';
+ ringtonePlayer.play();
+ window.setTimeout(function smsRingtoneEnder() {
+ ringtonePlayer.pause();
+ ringtonePlayer.src = '';
+ }, 2000);
+ }
+
+ if (this.vibrates) {
+ if (document.mozHidden) {
+ window.addEventListener('mozvisibilitychange', function waitOn() {
+ window.removeEventListener('mozvisibilitychange', waitOn);
+ navigator.vibrate([200, 200, 200, 200]);
+ });
+ } else {
+ navigator.vibrate([200, 200, 200, 200]);
+ }
+ }
+
+ return notificationNode;
+ },
+
+ closeNotification: function ns_closeNotification(notificationNode) {
+ var notificationID = notificationNode.dataset.notificationID;
+
+ var event = document.createEvent('CustomEvent');
+ event.initCustomEvent('mozContentEvent', true, true, {
+ type: 'desktop-notification-close',
+ id: notificationID
+ });
+ window.dispatchEvent(event);
+
+ this.removeNotification(notificationNode.dataset.notificationID);
+ },
+
+ removeNotification: function ns_removeNotification(notificationID) {
+ var notifSelector = '[data-notification-i-d="' + notificationID + '"]';
+ var notificationNode = this.container.querySelector(notifSelector);
+
+ notificationNode.parentNode.removeChild(notificationNode);
+ this.updateStatusBarIcon();
+ },
+
+ clearAll: function ns_clearAll() {
+ while (this.container.firstElementChild) {
+ this.closeNotification(this.container.firstElementChild);
+ }
+ },
+
+ clearLockScreen: function ns_clearLockScreen() {
+ while (this.lockScreenContainer.firstElementChild) {
+ var element = this.lockScreenContainer.firstElementChild;
+ this.lockScreenContainer.removeChild(element);
+ }
+ },
+
+ updateStatusBarIcon: function ns_updateStatusBarIcon(unread) {
+ var nbTotalNotif = this.container.children.length +
+ this.externalNotificationsCount;
+ StatusBar.updateNotification(nbTotalNotif);
+
+ if (unread)
+ StatusBar.updateNotificationUnread(true);
+ },
+
+ incExternalNotifications: function ns_incExternalNotifications() {
+ this.externalNotificationsCount++;
+ this.updateStatusBarIcon(true);
+ },
+
+ decExternalNotifications: function ns_decExternalNotifications() {
+ this.externalNotificationsCount--;
+ if (this.externalNotificationsCount < 0) {
+ this.externalNotificationsCount = 0;
+ }
+ this.updateStatusBarIcon();
+ }
+
+};
+
+NotificationScreen.init();
+
+SettingsListener.observe(
+ 'lockscreen.notifications-preview.enabled', true, function(value) {
+
+ NotificationScreen.lockscreenPreview = value;
+});
+
+SettingsListener.observe('alert-sound.enabled', true, function(value) {
+ NotificationScreen.alerts = value;
+});
+
+SettingsListener.observe('ring.enabled', true, function(value) {
+ NotificationScreen.silent = !value;
+});
+
+SettingsListener.observe('alert-vibration.enabled', true, function(value) {
+ NotificationScreen.vibrates = value;
+});
diff --git a/apps/system/js/operator_variant/operator_variant.js b/apps/system/js/operator_variant/operator_variant.js
new file mode 100644
index 0000000..13ad257
--- /dev/null
+++ b/apps/system/js/operator_variant/operator_variant.js
@@ -0,0 +1,168 @@
+/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+(function OperatorVariant() {
+ /**
+ * Get the mcc/mnc info that has been stored in the settings.
+ */
+
+ var settings = window.navigator.mozSettings;
+ if (!settings)
+ return;
+
+ var iccSettings = { mcc: -1, mnc: -1 };
+
+ // Read the mcc/mnc settings, then trigger callback.
+ function getICCSettings(callback) {
+ var transaction = settings.createLock();
+ var mccKey = 'operatorvariant.mcc';
+ var mncKey = 'operatorvariant.mnc';
+
+ var mccRequest = transaction.get(mccKey);
+ mccRequest.onsuccess = function() {
+ iccSettings.mcc = parseInt(mccRequest.result[mccKey], 10) || 0;
+ var mncRequest = transaction.get(mncKey);
+ mncRequest.onsuccess = function() {
+ iccSettings.mnc = parseInt(mncRequest.result[mncKey], 10) || 0;
+ callback();
+ };
+ };
+ }
+
+
+ /**
+ * Compare the cached mcc/mnc info with the one in the SIM card,
+ * and retrieve/apply APN settings if they differ.
+ */
+
+ var mobileConnection = window.navigator.mozMobileConnection;
+ if (!mobileConnection)
+ return;
+
+ // Check the mcc/mnc information on the SIM card.
+ function checkICCInfo() {
+ if (!mobileConnection.iccInfo || mobileConnection.cardState !== 'ready')
+ return;
+
+ // ensure that the iccSettings have been retrieved
+ if ((iccSettings.mcc < 0) || (iccSettings.mnc < 0))
+ return;
+
+ // XXX sometimes we get 0/0 for mcc/mnc, even when cardState === 'ready'...
+ var mcc = parseInt(mobileConnection.iccInfo.mcc, 10) || 0;
+ var mnc = parseInt(mobileConnection.iccInfo.mnc, 10) || 0;
+ if (!mcc || !mnc)
+ return;
+
+ // same SIM card => do nothing
+ if ((mcc == iccSettings.mcc) && (mnc == iccSettings.mnc))
+ return;
+
+ // new SIM card => cache iccInfo, load and apply new APN settings
+ iccSettings.mcc = mcc;
+ iccSettings.mnc = mnc;
+ retrieveOperatorVariantSettings(applyOperatorVariantSettings);
+ };
+
+ // Load and query APN database, then trigger callback on results.
+ function retrieveOperatorVariantSettings(callback) {
+ var OPERATOR_VARIANT_FILE = 'shared/resources/apn.json';
+
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', OPERATOR_VARIANT_FILE, true);
+ xhr.responseType = 'json';
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState == 4 && (xhr.status == 200 || xhr.status === 0)) {
+ var apn = xhr.response;
+ var mcc = iccSettings.mcc;
+ var mnc = iccSettings.mnc;
+ // get a list of matching APNs
+ var compatibleAPN = apn[mcc] ? (apn[mcc][mnc] || []) : [];
+ callback(compatibleAPN);
+ }
+ };
+ xhr.send();
+ }
+
+ // Store APN settings for the first carrier matching the mcc/mnc info.
+ function applyOperatorVariantSettings(result) {
+ var apnPrefNames = {
+ 'default': {
+ 'ril.data.carrier': 'carrier',
+ 'ril.data.apn': 'apn',
+ 'ril.data.user': 'user',
+ 'ril.data.passwd': 'password',
+ 'ril.data.httpProxyHost': 'proxy',
+ 'ril.data.httpProxyPort': 'port'
+ },
+ 'supl': {
+ 'ril.supl.carrier': 'carrier',
+ 'ril.supl.apn': 'apn',
+ 'ril.supl.user': 'user',
+ 'ril.supl.passwd': 'password',
+ 'ril.supl.httpProxyHost': 'proxy',
+ 'ril.supl.httpProxyPort': 'port'
+ },
+ 'mms': {
+ 'ril.mms.carrier': 'carrier',
+ 'ril.mms.apn': 'apn',
+ 'ril.mms.user': 'user',
+ 'ril.mms.passwd': 'password',
+ 'ril.mms.httpProxyHost': 'proxy',
+ 'ril.mms.httpProxyPort': 'port',
+ 'ril.mms.mmsc': 'mmsc',
+ 'ril.mms.mmsproxy': 'mmsproxy',
+ 'ril.mms.mmsport': 'mmsport'
+ },
+ 'operatorvariant': {
+ 'ril.iccInfo.mbdn': 'voicemail',
+ 'ril.sms.strict7BitEncoding.enabled': 'enableStrict7BitEncodingForSms',
+ 'ril.cellbroadcast.searchlist': 'cellBroadcastSearchList'
+ }
+ };
+
+ var booleanPrefNames = [
+ 'ril.sms.strict7BitEncoding.enabled'
+ ];
+
+ // store relevant APN settings
+ var transaction = settings.createLock();
+ for (var type in apnPrefNames) {
+ var apn = {};
+ for (var i = 0; i < result.length; i++) {
+ if (result[i] && result[i].type.indexOf(type) != -1) {
+ apn = result[i];
+ break;
+ }
+ }
+ var prefNames = apnPrefNames[type];
+ for (var key in prefNames) {
+ var name = apnPrefNames[type][key];
+ var item = {};
+ if (booleanPrefNames.indexOf(key) != -1) {
+ item[key] = apn[name] || false;
+ } else {
+ item[key] = apn[name] || '';
+ }
+ transaction.set(item);
+ }
+ }
+
+ // store the current mcc/mnc info in the settings
+ transaction.set({
+ 'operatorvariant.mcc': iccSettings.mcc,
+ 'operatorvariant.mnc': iccSettings.mnc
+ });
+ }
+
+
+ /**
+ * Check the APN settings on startup and when the SIM card is changed.
+ */
+
+ getICCSettings(checkICCInfo);
+ mobileConnection.addEventListener('iccinfochange', checkICCInfo);
+})();
+
diff --git a/apps/system/js/payment.js b/apps/system/js/payment.js
new file mode 100644
index 0000000..a953f5c
--- /dev/null
+++ b/apps/system/js/payment.js
@@ -0,0 +1,151 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+// TODO: Blocked by [Payment] UX and visuals for the payment request
+// confirmation screen https://github.com/mozilla-b2g/gaia/issues/2692
+
+'use strict';
+
+const kPaymentConfirmationScreen = '../payment.html';
+
+var Payment = {
+ chromeEventId: null,
+ trustedUILayers: {},
+
+ init: function init() {
+ window.addEventListener('mozChromeEvent', this);
+ },
+
+ handleEvent: function onMozChromeEvent(e) {
+ // We save the mozChromeEvent identifiers to send replies back from content
+ // with this exact value.
+ this.chromeEventId = e.detail.id;
+ if (!this.chromeEventId)
+ return;
+
+ var requestId = e.detail.requestId;
+
+ switch (e.detail.type) {
+ // Chrome asks Gaia to show the payment request confirmation dialog.
+ case 'open-payment-confirmation-dialog':
+ var requests = e.detail.paymentRequests;
+ if (!requests)
+ return;
+
+ var returnSelection = (function returnSelection(selection) {
+ if (!selection)
+ return;
+
+ this._dispatchEvent({
+ id: this.chromeEventId,
+ userSelection: selection
+ });
+ }).bind(this);
+
+ // If there is only one request, we skip the confirmation dialog and
+ // send the request type back to the chrome as a user selection, so
+ // the payment flow can continue.
+ if (requests.length == 1) {
+ returnSelection(requests[0].type);
+ return;
+ }
+
+ var frame = document.createElement('iframe');
+ frame.setAttribute('mozbrowser', 'true');
+ frame.setAttribute('remote', true);
+ frame.classList.add('screen');
+ frame.src = kPaymentConfirmationScreen;
+ frame.addEventListener('mozbrowserloadend', function addReqs(evt) {
+ var frame = evt.target;
+ if (!frame || !requests)
+ return;
+
+ // TODO: Temp layout until issue #2692 is solved.
+ var frameDocument = frame.contentWindow.document;
+ var requestsList = frameDocument.getElementById('requests')
+ .getElementsByTagName('ul')[0];
+ for (var i in requests) {
+ var requestElement = frameDocument.createElement('li');
+ var button = frameDocument.createElement('button');
+ button.setAttribute('value', requests[i].type);
+ var requestText = 'Pay with ' + requests[i].providerName + '\n' +
+ requests[i].productName + '\n' +
+ requests[i].productDescription + '\n' +
+ requests[i].productPrice[0].amount + ' ' +
+ requests[i].productPrice[0].currency;
+ button.appendChild(frameDocument.createTextNode(requestText));
+ button.onclick = function selectRequest() {
+ // We send the selected request back to Chrome so it can start
+ // the appropriate payment flow.
+ returnSelection(this.getAttribute('value'));
+ };
+ requestElement.appendChild(button);
+ requestsList.appendChild(requestElement);
+ }
+ });
+
+ this._openTrustedUI(frame);
+ break;
+
+ // Chrome asks Gaia to show the payment flow according to the
+ // payment request selected by the user.
+ case 'open-payment-flow-dialog':
+ if (!e.detail.uri)
+ return;
+
+ // TODO: For now, known payment providers (BlueVia and Mozilla Market)
+ // only accepts the JWT by GET, so we just add it to the URI.
+ e.detail.uri += e.detail.jwt;
+
+ this.trustedUILayers[requestId] = this.chromeEventId;
+
+ var frame = document.createElement('iframe');
+ frame.setAttribute('mozbrowser', 'true');
+ frame.classList.add('screen');
+ frame.src = e.detail.uri;
+ frame.addEventListener('mozbrowserloadstart', (function loadStart(evt) {
+ // After creating the new frame containing the payment provider buy
+ // flow, we send it back to chrome so the payment callbacks can be
+ // injected.
+ this._dispatchEvent({
+ id: this.chromeEventId,
+ frame: evt.target
+ });
+ }).bind(this));
+
+ // The payment flow is shown within the trusted UI
+ this._openTrustedUI(frame);
+ break;
+
+ case 'close-payment-flow-dialog':
+ TrustedUIManager.close(this.trustedUILayers[requestId],
+ (function dialogClosed() {
+ this._dispatchEvent({ id: this.chromeEventId });
+ delete this.trustedUILayers[requestId];
+ }).bind(this));
+ break;
+ }
+ },
+
+ _openTrustedUI: function _openTrustedUI(frame) {
+ // The payment flow is shown within the trusted UI with the name of
+ // the mozPay caller application as title.
+ var title = WindowManager.getCurrentDisplayedApp().name;
+ title = title ? title : navigator.mozL10n.get('payment-flow');
+ TrustedUIManager.open(title, frame, this.chromeEventId);
+ },
+
+ _dispatchEvent: function _dispatchEvent(obj) {
+ var event = document.createEvent('CustomEvent');
+ event.initCustomEvent('mozContentEvent', true, true, obj);
+ window.dispatchEvent(event);
+ }
+};
+
+// Make sure L10n is ready before init
+if (navigator.mozL10n.readyState == 'complete' ||
+ navigator.mozL10n.readyState == 'interactive') {
+ Payment.init();
+} else {
+ window.addEventListener('localized', Payment.init.bind(Payment));
+}
diff --git a/apps/system/js/permission_manager.js b/apps/system/js/permission_manager.js
new file mode 100644
index 0000000..f3c013c
--- /dev/null
+++ b/apps/system/js/permission_manager.js
@@ -0,0 +1,203 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var PermissionManager = (function() {
+ var _ = navigator.mozL10n.get;
+
+ window.addEventListener('mozChromeEvent', function pm_chromeEventHandler(e) {
+ var detail = e.detail;
+ switch (detail.type) {
+ case 'permission-prompt':
+ overlay.dataset.type = detail.permission;
+ handlePermissionPrompt(detail);
+ break;
+ case 'fullscreenoriginchange':
+ delete overlay.dataset.type;
+ handleFullscreenOriginChange(detail);
+ break;
+ }
+ });
+
+ var fullscreenRequest = undefined;
+
+ var handleFullscreenOriginChange = function(detail) {
+ // If there's already a fullscreen request visible, cancel it,
+ // we'll show the request for the new domain.
+ if (fullscreenRequest != undefined) {
+ cancelRequest(fullscreenRequest);
+ fullscreenRequest = undefined;
+ }
+ if (detail.fullscreenorigin != WindowManager.getDisplayedApp()) {
+ // The message to be displayed on the approval UI.
+ var message = _('fullscreen-request', { 'origin': detail.fullscreenorigin });
+ fullscreenRequest = requestPermission(message,
+ /* yesCallback */ null,
+ /* noCallback */ function() {
+ document.mozCancelFullScreen();
+ });
+ }
+ };
+
+ var handlePermissionPrompt = function pm_handlePermissionPrompt(detail) {
+ remember.checked = detail.remember ? true : false;
+ var str = '';
+
+ var permissionID = 'perm-' + detail.permission.replace(':', '-');
+ if (detail.isApp) { // App
+ str = _(permissionID + '-appRequest', { 'app': detail.appName });
+ } else { // Web content
+ str = _(permissionID + '-webRequest', { 'site': detail.origin });
+ }
+
+ requestPermission(str, function pm_permYesCB() {
+ dispatchResponse(detail.id, 'permission-allow', remember.checked);
+ }, function pm_permNoCB() {
+ dispatchResponse(detail.id, 'permission-deny', remember.checked);
+ });
+ };
+
+ var dispatchResponse = function pm_dispatchResponse(id, type, remember) {
+ var event = document.createEvent('CustomEvent');
+ remember = remember ? true : false;
+
+ event.initCustomEvent('mozContentEvent', true, true, {
+ id: id,
+ type: type,
+ remember: remember
+ });
+ window.dispatchEvent(event);
+ };
+
+ // A queue of pending requests. Callers of requestPermission() must be
+ // careful not to create an infinite loop!
+ var pending = [];
+
+ // Div over in which the permission UI resides.
+ var overlay = document.getElementById('permission-screen');
+ var dialog = document.getElementById('permission-dialog');
+ var message = document.getElementById('permission-message');
+
+ // "Yes"/"No" buttons on the permission UI.
+ var yes = document.getElementById('permission-yes');
+ var no = document.getElementById('permission-no');
+
+ // Remember the choice checkbox
+ var remember = document.getElementById('permission-remember-checkbox');
+ var rememberSection = document.getElementById('permission-remember-section');
+
+ // The ID of the next permission request. This is incremented by one
+ // on every request, modulo some large number to prevent overflow problems.
+ var nextRequestID = 0;
+
+ // The ID of the request currently visible on the screen. This has the value
+ // "undefined" when there is no request visible on the screen.
+ var currentRequestId = undefined;
+
+ var hidePermissionPrompt = function() {
+ overlay.classList.remove('visible');
+ currentRequestId = undefined;
+ // Cleanup the event handlers.
+ yes.removeEventListener('click', clickHandler);
+ yes.callback = null;
+ no.removeEventListener('click', clickHandler);
+ no.callback = null;
+ };
+
+ // Show the next request, if we have one.
+ var showNextPendingRequest = function() {
+ if (pending.length == 0)
+ return;
+ var request = pending.shift();
+ showPermissionPrompt(request.id,
+ request.message,
+ request.yescallback,
+ request.nocallback);
+ };
+
+ // This is the event listener function for the yes/no buttons.
+ var clickHandler = function(evt) {
+ var callback = null;
+ if (evt.target === yes && yes.callback) {
+ callback = yes.callback;
+ } else if (evt.target === no && no.callback) {
+ callback = no.callback;
+ }
+ hidePermissionPrompt();
+
+ // Call the appropriate callback, if it is defined.
+ if (callback)
+ window.setTimeout(callback, 0);
+
+ showNextPendingRequest();
+ };
+
+ var requestPermission = function(msg,
+ yescallback, nocallback) {
+ var id = nextRequestID;
+ nextRequestID = (nextRequestID + 1) % 1000000;
+
+ if (currentRequestId != undefined) {
+ // There is already a permission request being shown, queue this one.
+ pending.push({
+ id: id,
+ message: msg,
+ yescallback: yescallback,
+ nocallback: nocallback
+ });
+ return id;
+ }
+
+ showPermissionPrompt(id, msg, yescallback, nocallback);
+
+ return id;
+ };
+
+ var showPermissionPrompt = function(id, msg,
+ yescallback, nocallback) {
+ // Put the message in the dialog.
+ // Note plain text since this may include text from
+ // untrusted app manifests, for example.
+ message.textContent = msg;
+
+ currentRequestId = id;
+
+ // Make the screen visible
+ overlay.classList.add('visible');
+
+ // Set event listeners for the yes and no buttons
+ yes.addEventListener('click', clickHandler);
+ yes.callback = yescallback;
+
+ no.addEventListener('click', clickHandler);
+ no.callback = nocallback;
+ };
+
+ // Cancels a request with a specfied id. Request can either be
+ // currently showing, or pending. If there are further pending requests,
+ // the next is shown.
+ var cancelRequest = function(id) {
+ if (currentRequestId === id) {
+ // Request is currently being displayed. Hide the permission prompt,
+ // and show the next request, if we have any.
+ hidePermissionPrompt();
+ showNextPendingRequest();
+ } else {
+ // The request is currently not being displayed. Search through the
+ // list of pending requests, and remove it from the list if present.
+ for (var i = 0; i < pending.length; i++) {
+ if (pending[i].id === id) {
+ pending.splice(i, 1);
+ break;
+ }
+ }
+ }
+ };
+
+ rememberSection.addEventListener('click', function onLabelClick() {
+ remember.checked = !remember.checked;
+ });
+
+}());
+
diff --git a/apps/system/js/popup_manager.js b/apps/system/js/popup_manager.js
new file mode 100644
index 0000000..583c9c8
--- /dev/null
+++ b/apps/system/js/popup_manager.js
@@ -0,0 +1,313 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+'use strict';
+
+var PopupManager = {
+ _currentPopup: {},
+ _currentOrigin: '',
+ _endTimes: 0,
+ _startTimes: 0,
+
+ throbber: document.getElementById('popup-throbber'),
+
+ overlay: document.getElementById('dialog-overlay'),
+
+ popupContainer: document.getElementById('popup-container'),
+
+ container: document.getElementById('frame-container'),
+
+ screen: document.getElementById('screen'),
+
+ closeButton: document.getElementById('popup-close'),
+
+ errorTitle: document.getElementById('popup-error-title'),
+
+ errorMessage: document.getElementById('popup-error-message'),
+
+ errorReload: document.getElementById('popup-error-reload'),
+
+ errorBack: document.getElementById('popup-error-back'),
+
+ init: function pm_init() {
+ this.title = document.getElementById('popup-title');
+ window.addEventListener('mozbrowseropenwindow', this);
+ window.addEventListener('mozbrowserclose', this);
+ window.addEventListener('appwillclose', this);
+ window.addEventListener('appopen', this);
+ window.addEventListener('appterminated', this);
+ window.addEventListener('home', this);
+ window.addEventListener('keyboardhide', this);
+ window.addEventListener('keyboardchange', this);
+ this.closeButton.addEventListener('click', this);
+ this.errorReload.addEventListener('click', this);
+ this.errorBack.addEventListener('click', this);
+ },
+
+ open: function pm_open(frame, origin) {
+ // Only one popup per origin at a time.
+ // If the popup is being shown, we swap frames.
+ if (this._currentPopup[origin]) {
+ this.container.removeChild(this._currentPopup[origin]);
+ delete this._currentPopup[origin];
+ }
+
+ this.title.textContent = this.getTitleFromUrl(frame.dataset.url);
+
+ // Reset overlay height
+ this.setHeight(window.innerHeight - StatusBar.height);
+
+ this._currentPopup[origin] = frame;
+
+ var popup = this._currentPopup[origin];
+ var dataset = popup.dataset;
+ dataset.frameType = 'popup';
+ dataset.frameName = name;
+ dataset.frameOrigin = origin;
+
+ // this seems needed, or an override to origin in close()
+ this._currentOrigin = origin;
+
+ this.container.appendChild(popup);
+
+ this.screen.classList.add('popup');
+
+ popup.addEventListener('mozbrowsererror', this);
+ popup.addEventListener('mozbrowserloadend', this);
+ popup.addEventListener('mozbrowserloadstart', this);
+ popup.addEventListener('mozbrowserlocationchange', this);
+ },
+
+ close: function pm_close(evt) {
+ if (evt && (!'frameType' in evt.target.dataset ||
+ evt.target.dataset.frameType !== 'popup'))
+ return;
+
+ var self = this;
+ this.popupContainer.addEventListener('transitionend', function wait(event) {
+ self.popupContainer.removeEventListener('transitionend', wait);
+ self.screen.classList.remove('popup');
+ self.popupContainer.classList.remove('disappearing');
+ self.container.removeChild(self._currentPopup[self._currentOrigin]);
+ delete self._currentPopup[self._currentOrigin];
+ });
+
+ this.popupContainer.classList.add('disappearing');
+
+ // We just removed the focused window leaving the system
+ // without any focused window, let's fix this.
+ window.focus();
+ },
+
+ backHandling: function pm_backHandling() {
+ if (!this._currentPopup[this._currentOrigin])
+ return;
+
+ this.close();
+ },
+
+ isVisible: function pm_isVisible() {
+ return (this._currentPopup[this._currentOrigin] != null);
+ },
+
+ setHeight: function pm_setHeight(height) {
+ if (this.isVisible())
+ this.overlay.style.height = height + 'px';
+ },
+
+ handleEvent: function pm_handleEvent(evt) {
+ switch (evt.type) {
+ case 'click':
+ switch (evt.target) {
+ case this.closeButton:
+ this.backHandling();
+ break;
+
+ case this.errorBack:
+ this.backHandling();
+ break;
+
+ case this.errorReload:
+ this.container.classList.remove('error');
+ delete this._currentPopup[this._currentOrigin].dataset.error;
+ this._currentPopup[this._currentOrigin].reload(true);
+ break;
+ }
+ break;
+
+ case 'mozbrowserloadstart':
+ this.throbber.classList.add('loading');
+ break;
+
+ case 'mozbrowserloadend':
+ this.throbber.classList.remove('loading');
+ break;
+
+ case 'mozbrowserlocationchange':
+ evt.target.dataset.url = evt.detail;
+ this.show();
+ break;
+
+ case 'mozbrowsererror':
+ this._currentPopup[evt.target.dataset.frameOrigin].dataset.error = true;
+ this.showError();
+ break;
+
+ case 'mozbrowseropenwindow':
+ var detail = evt.detail;
+ var openerType = evt.target.dataset.frameType;
+ var openerOrigin = evt.target.dataset.frameOrigin;
+
+ // Only app frame is allowed to launch popup
+ if (openerType !== 'window')
+ return;
+
+ // <a href="" target="_blank"> links should opened outside the app
+ // itself and fire an activity to be opened into a new browser window.
+ if (detail.name === '_blank') {
+ new MozActivity({ name: 'view',
+ data: { type: 'url', url: detail.url }});
+ return;
+ }
+
+ this.throbber.classList.remove('loading');
+
+ var frame = detail.frameElement;
+ frame.dataset.url = detail.url;
+
+ this.container.classList.remove('error');
+ this.open(frame, openerOrigin);
+
+ break;
+
+ case 'mozbrowserclose':
+ this.close(evt);
+ break;
+
+ case 'home':
+ // Reset overlay height before hiding
+ this.setHeight(window.innerHeight - StatusBar.height);
+ this.hide(this._currentOrigin);
+ break;
+
+ case 'appwillclose':
+ if (!this._currentPopup[evt.detail.origin])
+ return;
+
+ this.hide(evt.detail.origin);
+ break;
+
+ case 'appopen':
+ this._currentOrigin = evt.detail.origin;
+ this.show();
+ break;
+
+ case 'appterminated':
+ if (!this._currentPopup[evt.detail.origin])
+ return;
+ this.close(evt.detail.origin);
+ break;
+
+ case 'keyboardchange':
+ this.setHeight(window.innerHeight -
+ StatusBar.height - evt.detail.height);
+ break;
+
+ case 'keyboardhide':
+ this.setHeight(window.innerHeight - StatusBar.height);
+ break;
+ }
+ },
+
+ showError: function pm_showError() {
+ if (!('error' in this._currentPopup[this._currentOrigin].dataset)) {
+ this.container.classList.remove('error');
+ return;
+ }
+
+ var contentOrigin =
+ this.getTitleFromUrl(this._currentPopup[this._currentOrigin].dataset.url);
+ var _ = navigator.mozL10n.get;
+
+ if (AirplaneMode.enabled) {
+ this.errorTitle.textContent = _('airplane-is-on');
+ this.errorMessage.textContent = _('airplane-is-turned-on',
+ {name: contentOrigin});
+ } else if (!navigator.onLine) {
+ this.errorTitle.textContent = _('network-connection-unavailable');
+ this.errorMessage.textContent = _('network-error', {name: contentOrigin});
+ } else {
+ this.errorTitle.textContent = _('error-title', {name: contentOrigin});
+ this.errorMessage.textContent = _('error-message', {name: contentOrigin});
+ }
+ this.container.classList.add('error');
+ },
+
+ // This is for card view to request
+ // Return nothing if the content is the same origin as opener
+ // Return URL if the content is off-origin
+ getOpenedOriginFromOpener: function pm_getOpenedOriginOpener(origin) {
+ var opened = this._getOriginObject(this._currentPopup[origin].dataset.url);
+ var opener = this._getOriginObject(origin);
+ // Same origin means: Protocol, Domain, Port
+ if (opened.protocol == opener.protocol &&
+ opened.hostname == opener.hostname &&
+ opened.port == opener.port) {
+ return '';
+ } else {
+ return opened.protocol + '//' + opened.hostname;
+ }
+ },
+
+ getTitleFromUrl: function pm_getTitleFromUrl(url) {
+ var app = WindowManager.getCurrentDisplayedApp();
+ var opened = this._getOriginObject(url);
+ var opener = this._getOriginObject(app.frame.dataset.frameOrigin);
+ // Same origin means: Protocol, Domain, Port
+ if (opened.protocol == opener.protocol &&
+ opened.hostname == opener.hostname &&
+ opened.port == opener.port) {
+ return app.name;
+ } else {
+ return opened.protocol + '//' + opened.hostname;
+ }
+ },
+
+ _getOriginObject: function pm__getOriginObject(url) {
+ var parser = document.createElement('a');
+ parser.href = url;
+
+ return {
+ protocol: parser.protocol,
+ hostname: parser.hostname,
+ port: parser.port
+ };
+ },
+
+ getPopupFromOrigin: function pm_getPopupFromOrigin(origin) {
+ return this._currentPopup[origin];
+ },
+
+ show: function pm_show() {
+ if (!this._currentPopup[this._currentOrigin])
+ return;
+
+
+ this.showError();
+ this.screen.classList.add('popup');
+
+ var popup = this._currentPopup[this._currentOrigin];
+ this.title.textContent = this.getTitleFromUrl(popup.dataset.url);
+ popup.hidden = false;
+ },
+
+ hide: function pm_hide(origin) {
+ if (!this._currentPopup[origin])
+ return;
+
+ this.screen.classList.remove('popup');
+ this._currentPopup[origin].hidden = true;
+ }
+};
+
+PopupManager.init();
+
diff --git a/apps/system/js/quick_settings.js b/apps/system/js/quick_settings.js
new file mode 100644
index 0000000..328bb35
--- /dev/null
+++ b/apps/system/js/quick_settings.js
@@ -0,0 +1,279 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var QuickSettings = {
+ // Indicate setting status of geolocation.enabled
+ geolocationEnabled: false,
+ WIFI_STATUSCHANGE_TIMEOUT: 2000,
+
+ init: function qs_init() {
+ var settings = window.navigator.mozSettings;
+ var conn = window.navigator.mozMobileConnection;
+ if (!settings || !conn)
+ return;
+
+ this.getAllElements();
+
+ this.overlay.addEventListener('click', this);
+ window.addEventListener('utilitytrayshow', this);
+
+ var self = this;
+
+ /*
+ * Monitor data network icon
+ */
+ conn.addEventListener('datachange', function qs_onDataChange() {
+ var label = {
+ 'lte': '4G', // 4G LTE
+ 'ehrpd': '4G', // 4G CDMA
+ 'hspa+': 'H+', // 3.5G HSPA+
+ 'hsdpa': 'H', 'hsupa': 'H', 'hspa': 'H', // 3.5G HSDPA
+ 'evdo0': '3G', 'evdoa': '3G', 'evdob': '3G', '1xrtt': '3G', // 3G CDMA
+ 'umts': '3G', // 3G
+ 'edge': 'E', // EDGE
+ 'is95a': '2G', 'is95b': '2G', // 2G CDMA
+ 'gprs': '2G'
+ };
+ self.data.dataset.network = label[conn.data.type];
+ });
+
+ /* monitor data setting
+ * TODO prevent quickly tapping on it
+ */
+ SettingsListener.observe('ril.data.enabled', true, function(value) {
+ if (value) {
+ self.data.dataset.enabled = 'true';
+ } else {
+ delete self.data.dataset.enabled;
+ }
+ });
+
+ /* monitor bluetooth setting and initialization/disable ready event
+ * - when settings changed, update UI and lock toogle to prevent quickly
+ * tapping on it.
+ * - when got bluetooth initialization/disable ready, active toogle, so
+ * return the control to user.
+ */
+ var btFirstSet = true;
+ SettingsListener.observe('bluetooth.enabled', true, function(value) {
+ // check self.bluetooth.dataset.enabled and value are identical
+ if ((self.bluetooth.dataset.enabled && value) ||
+ (self.bluetooth.dataset.enabled === undefined && !value))
+ return;
+
+ if (value) {
+ self.bluetooth.dataset.enabled = 'true';
+ } else {
+ delete self.bluetooth.dataset.enabled;
+ }
+
+ // Set to the initializing state to block user interaction until the
+ // operation completes. (unless we are being called for the first time,
+ // where Bluetooth is already initialize
+ if (!btFirstSet)
+ self.bluetooth.dataset.initializing = 'true';
+ btFirstSet = false;
+ });
+ window.addEventListener('bluetooth-adapter-added', this);
+ window.addEventListener('bluetooth-disabled', this);
+
+
+ /* monitor wifi setting and initialization/disable ready event
+ * - when settings changed, update UI and lock toogle to prevent quickly
+ * tapping on it.
+ * - when got bluetooth initialization/disable ready, active toogle, so
+ * return the control to user.
+ */
+ var wifiFirstSet = true;
+ SettingsListener.observe('wifi.enabled', true, function(value) {
+ // check self.wifi.dataset.enabled and value are identical
+ if ((self.wifi.dataset.enabled && value) ||
+ (self.wifi.dataset.enabled === undefined && !value))
+ return;
+
+ if (value) {
+ self.wifi.dataset.enabled = 'true';
+ } else {
+ delete self.wifi.dataset.enabled;
+ }
+ // Set to the initializing state to block user interaction until the
+ // operation completes. (unless we are being called for the first time,
+ // where Wifi is already initialize
+ if (!wifiFirstSet)
+ self.wifi.dataset.initializing = 'true';
+ wifiFirstSet = false;
+ });
+ window.addEventListener('wifi-enabled', this);
+ window.addEventListener('wifi-disabled', this);
+ window.addEventListener('wifi-statuschange', this);
+
+ /* monitor geolocation setting
+ * TODO prevent quickly tapping on it
+ */
+ SettingsListener.observe('geolocation.enabled', true, function(value) {
+ self.geolocationEnabled = value;
+ });
+
+ // monitor airplane mode
+ SettingsListener.observe('ril.radio.disabled', false, function(value) {
+ self.data.dataset.airplaneMode = value;
+ if (value) {
+ self.data.classList.add('quick-settings-airplane-mode');
+ self.airplaneMode.dataset.enabled = 'true';
+ } else {
+ self.data.classList.remove('quick-settings-airplane-mode');
+ delete self.airplaneMode.dataset.enabled;
+ }
+ });
+ },
+
+ handleEvent: function qs_handleEvent(evt) {
+ evt.preventDefault();
+ switch (evt.type) {
+ case 'click':
+ switch (evt.target) {
+ case this.wifi:
+ // do nothing if wifi isn't ready
+ if (this.wifi.dataset.initializing)
+ return;
+ var enabled = !!this.wifi.dataset.enabled;
+ SettingsListener.getSettingsLock().set({
+ 'wifi.enabled': !enabled
+ });
+ if (!enabled)
+ this.toggleAutoConfigWifi = true;
+ break;
+
+ case this.data:
+ if (this.data.dataset.airplaneMode !== 'true') {
+ // TODO should ignore the action if data initialization isn't done
+ var enabled = !!this.data.dataset.enabled;
+
+ SettingsListener.getSettingsLock().set({
+ 'ril.data.enabled': !enabled
+ });
+ }
+
+ break;
+
+ case this.bluetooth:
+ // do nothing if bluetooth isn't ready
+ if (this.bluetooth.dataset.initializing)
+ return;
+
+ var enabled = !!this.bluetooth.dataset.enabled;
+ SettingsListener.getSettingsLock().set({
+ 'bluetooth.enabled': !enabled
+ });
+ break;
+
+ case this.airplaneMode:
+ var enabled = !!this.airplaneMode.dataset.enabled;
+ SettingsListener.getSettingsLock().set({
+ 'ril.radio.disabled': !enabled
+ });
+ break;
+
+ case this.fullApp:
+ // XXX: This should be replaced probably by Web Activities
+ var host = document.location.host;
+ var domain = host.replace(/(^[\w\d]+\.)?([\w\d]+\.[a-z]+)/, '$2');
+ var protocol = document.location.protocol + '//';
+ Applications.getByManifestURL(protocol + 'settings.' +
+ domain + '/manifest.webapp').launch();
+
+ UtilityTray.hide();
+ break;
+ }
+ break;
+
+ case 'utilitytrayshow':
+ break;
+
+ // unlock bluetooth toggle
+ case 'bluetooth-adapter-added':
+ case 'bluetooth-disabled':
+ delete this.bluetooth.dataset.initializing;
+ break;
+ // unlock wifi toggle
+ case 'wifi-enabled':
+ delete this.wifi.dataset.initializing;
+ if (this.toggleAutoConfigWifi) {
+ // Check whether it found a wifi to connect after a timeout.
+ this.wifiStatusTimer = setTimeout(this.autoConfigWifi.bind(this),
+ this.WIFI_STATUSCHANGE_TIMEOUT);
+ }
+ break;
+ case 'wifi-disabled':
+ delete this.wifi.dataset.initializing;
+ if (this.toggleAutoConfigWifi) {
+ clearTimeout(this.wifiStatusTimer);
+ this.wifiStatusTimer = null;
+ this.toggleAutoConfigWifi = false;
+ }
+ break;
+
+ case 'wifi-statuschange':
+ if (this.toggleAutoConfigWifi && !this.wifi.dataset.initializing)
+ this.autoConfigWifi();
+ break;
+ }
+ },
+
+ getAllElements: function qs_getAllElements() {
+ // ID of elements to create references
+ var elements = ['wifi', 'data', 'bluetooth', 'airplane-mode', 'full-app'];
+
+ var toCamelCase = function toCamelCase(str) {
+ return str.replace(/\-(.)/g, function replacer(str, p1) {
+ return p1.toUpperCase();
+ });
+ };
+
+ elements.forEach(function createElementRef(name) {
+ this[toCamelCase(name)] =
+ document.getElementById('quick-settings-' + name);
+ }, this);
+
+ this.overlay = document.getElementById('quick-settings');
+ },
+
+ // XXX Break down obj keys in a for each loop because mozSettings
+ // does not currently supports multiple keys in one set()
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=779381
+ setMozSettings: function qs_setter(keypairs) {
+ var setlock = SettingsListener.getSettingsLock();
+ for (var key in keypairs) {
+ var obj = {};
+ obj[key] = keypairs[key];
+ setlock.set(obj);
+ }
+ },
+
+ /* Auto-config wifi if user enabled wifi from quick settings bar.
+ * If there are no known networks around, wifi settings page
+ * will be opened. Otherwise nothing will be done.
+ */
+ autoConfigWifi: function qs_autoConfigWifi() {
+ clearTimeout(this.wifiStatusTimer);
+ this.wifiStatusTimer = null;
+ this.toggleAutoConfigWifi = false;
+
+ var wifiManager = window.navigator.mozWifiManager;
+ var status = wifiManager.connection.status;
+
+ if (status == 'disconnected') {
+ var activity = new MozActivity({
+ name: 'configure',
+ data: {
+ target: 'device',
+ section: 'wifi'
+ }
+ });
+ }
+ }
+};
+
+QuickSettings.init();
diff --git a/apps/system/js/remote_debugger.js b/apps/system/js/remote_debugger.js
new file mode 100644
index 0000000..51544bb
--- /dev/null
+++ b/apps/system/js/remote_debugger.js
@@ -0,0 +1,41 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var RemoteDebugger = (function() {
+
+ return {
+ init: function() {
+ window.addEventListener('mozChromeEvent', this);
+ },
+
+ handleEvent: function onMozChromeEvent(e) {
+ if (e.detail.type !== 'remote-debugger-prompt') {
+ return;
+ }
+
+ // Reusing the ModalDialog infrastructure.
+ ModalDialog.showWithPseudoEvent({
+ text: navigator.mozL10n.get('remoteDebuggerMessage'),
+ type: 'confirm',
+ callback: function() {
+ RemoteDebugger._dispatchEvent(true);
+ },
+ cancel: function() {
+ RemoteDebugger._dispatchEvent(false);
+ }
+ });
+ },
+
+ _dispatchEvent: function su_dispatchEvent(value) {
+ var event = document.createEvent('CustomEvent');
+ event.initCustomEvent('mozContentEvent', true, true,
+ { type: 'remote-debugger-prompt',
+ value: value });
+ window.dispatchEvent(event);
+ }
+ };
+})();
+
+RemoteDebugger.init();
diff --git a/apps/system/js/screen_manager.js b/apps/system/js/screen_manager.js
new file mode 100644
index 0000000..9a69e8c
--- /dev/null
+++ b/apps/system/js/screen_manager.js
@@ -0,0 +1,503 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var 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,
+
+ /*
+ * 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 wake lock is enabled or not
+ */
+ _screenWakeLocked: 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,
+
+ /*
+ * The auto-brightness algorithm will never set the screen brightness
+ * to a value smaller than this. 0.1 seems like a good screen brightness
+ * in a completely dark room on a Unagi.
+ */
+ AUTO_BRIGHTNESS_MINIMUM: 0.1,
+
+ /*
+ * This constant is used in the auto brightness algorithm. We take
+ * the base 10 logarithm of the incoming lux value from the light
+ * sensor and multiplied it by this constant. That value is used to
+ * compute a weighted average with the current brightness and
+ * finally that average brightess is and then clamped to the range
+ * [AUTO_BRIGHTNESS_MINIMUM, 1.0].
+ *
+ * Making this value larger will increase the brightness for a given
+ * ambient light level. At a value of about .25, the screen will be
+ * at full brightness in sunlight or in a well-lighted work area.
+ * At a value of about .3, the screen will typically be at maximum
+ * brightness in outdoor daylight conditions, even when overcast.
+ */
+ AUTO_BRIGHTNESS_CONSTANT: .27,
+
+ /*
+ * When we change brightness we animate it smoothly.
+ * This constant is the number of milliseconds between adjustments
+ */
+ BRIGHTNESS_ADJUST_INTERVAL: 20,
+
+ /*
+ * When brightening or dimming the screen, this is how much we adjust
+ * the brightness value at a time.
+ */
+ BRIGHTNESS_ADJUST_STEP: 0.04,
+
+ /*
+ * 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,
+
+ /*
+ * If the screen off is triggered by promixity during phon call then
+ * we need wake it up while phone is ended.
+ */
+ _screenOffByProximity: false,
+
+ /*
+ * Request wakelock during in_call state.
+ * To ensure turnScreenOff by proximity event is protected by wakelock for
+ * early suspend only.
+ */
+ _cpuWakeLock: null,
+
+ init: function scm_init() {
+ window.addEventListener('sleep', this);
+ window.addEventListener('wake', this);
+
+ this.screen = document.getElementById('screen');
+
+ var self = this;
+ var power = navigator.mozPower;
+
+ if (power) {
+ power.addWakeLockListener(function scm_handleWakeLock(topic, state) {
+ switch (topic) {
+ case 'screen':
+ self._screenWakeLocked = (state == 'locked-foreground');
+
+ if (self._screenWakeLocked)
+ // Turn screen on if wake lock is acquire
+ self.turnScreenOn();
+ self._reconfigScreenTimeout();
+ break;
+
+ case 'cpu':
+ power.cpuSleepAllowed = (state != 'locked-foreground' &&
+ state != 'locked-background');
+ break;
+ }
+ });
+ }
+
+ 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) {
+ self._userBrightness = value;
+ self.setScreenBrightness(value, false);
+ });
+
+ var telephony = window.navigator.mozTelephony;
+ if (telephony) {
+ telephony.addEventListener('callschanged', this);
+ }
+ },
+
+ //
+ // Automatically adjust the screen brightness based on the ambient
+ // light (in lux) measured by the device light sensor
+ //
+ autoAdjustBrightness: function scm_adjustBrightness(lux) {
+ var currentBrightness = this._targetBrightness;
+
+ if (lux < 1) // Can't take the log of 0 or negative numbers
+ lux = 1;
+
+ var computedBrightness =
+ Math.log(lux) / Math.LN10 * this.AUTO_BRIGHTNESS_CONSTANT;
+
+ var clampedBrightness = Math.max(this.AUTO_BRIGHTNESS_MINIMUM,
+ Math.min(1.0, computedBrightness));
+
+ // If nothing changed, we're done.
+ if (clampedBrightness === currentBrightness)
+ return;
+
+ this.setScreenBrightness(clampedBrightness, false);
+ },
+
+ handleEvent: function scm_handleEvent(evt) {
+ switch (evt.type) {
+ case 'devicelight':
+ if (!this._deviceLightEnabled || !this.screenEnabled ||
+ this._inTransition)
+ return;
+ this.autoAdjustBrightness(evt.value);
+ break;
+
+ case 'sleep':
+ this.turnScreenOff(true);
+ break;
+
+ case 'wake':
+ this.turnScreenOn();
+ break;
+
+ case 'userproximity':
+ this._screenOffByProximity = evt.near;
+ if (evt.near) {
+ this.turnScreenOff(true);
+ } else {
+ this.turnScreenOn();
+ }
+ break;
+
+ case 'callschanged':
+ var telephony = window.navigator.mozTelephony;
+ if (!telephony.calls.length) {
+ if (this._screenOffByProximity) {
+ this.turnScreenOn();
+ }
+
+ window.removeEventListener('userproximity', this);
+ this._screenOffByProximity = false;
+
+ if (this._cpuWakeLock) {
+ this._cpuWakeLock.unlock();
+ this._cpuWakeLock = null;
+ }
+ break;
+ }
+
+ // If the _cpuWakeLock is already set we are in a multiple
+ // call setup, turning the screen on to let user see the
+ // notification.
+ if (this._cpuWakeLock) {
+ this.turnScreenOn();
+
+ break;
+ }
+
+ // Enable the user proximity sensor once the call is connected.
+ var call = telephony.calls[0];
+ call.addEventListener('statechange', this);
+
+ break;
+
+ case 'statechange':
+ var call = evt.target;
+ if (call.state !== 'connected') {
+ break;
+ }
+
+ // The call is connected. Remove the statechange listener
+ // and enable the user proximity sensor.
+ call.removeEventListener('statechange', this);
+
+ this._cpuWakeLock = navigator.requestWakeLock('cpu');
+ window.addEventListener('userproximity', this);
+ break;
+ }
+ },
+
+ toggleScreen: function scm_toggleScreen() {
+ if (this.screenEnabled) {
+ this.turnScreenOff();
+ } else {
+ this.turnScreenOn();
+ }
+ },
+
+ turnScreenOff: function scm_turnScreenOff(instant) {
+ if (!this.screenEnabled)
+ return false;
+
+ var self = this;
+
+ // Remember the current screen brightness. We will restore it when
+ // we turn the screen back on.
+ self._savedBrightness = navigator.mozPower.screenBrightness;
+
+ // Remove the cpuWakeLock if screen is not turned off by
+ // userproximity event.
+ if (!this._screenOffByProximity && this._cpuWakeLock) {
+ window.removeEventListener('userproximity', this);
+ this._cpuWakeLock.unlock();
+ this._cpuWakeLock = null;
+ }
+
+ var screenOff = function scm_screenOff() {
+ self._setIdleTimeout(0);
+
+ window.removeEventListener('devicelight', self);
+
+ self.screenEnabled = false;
+ self._inTransition = false;
+ self.screen.classList.add('screenoff');
+ 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;
+ }, 20);
+
+ self.fireScreenChangeEvent();
+ };
+
+ if (instant) {
+ if (!WindowManager.isFtuRunning()) {
+ 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) {
+ if (this.screenEnabled) {
+ if (this._inTransition) {
+ // Cancel the dim out
+ this._inTransition = false;
+ this.setScreenBrightness(this._savedBrightness, true);
+ this._reconfigScreenTimeout();
+ }
+ return false;
+ }
+
+ // Set the brightness before the screen is on.
+ this.setScreenBrightness(this._savedBrightness, instant);
+
+ // If we are in a call and there is no cpuWakeLock,
+ // we would have to get one here.
+ var telephony = window.navigator.mozTelephony;
+ if (!this._cpuWakeLock && telephony && telephony.calls.length) {
+ telephony.calls.some(function checkCallConnection(call) {
+ if (call.state == 'connected') {
+ this._cpuWakeLock = navigator.requestWakeLock('cpu');
+ window.addEventListener('userproximity', this);
+ return true;
+ }
+ return false;
+ }, this);
+ }
+
+ // Actually turn the screen on.
+ var power = navigator.mozPower;
+ if (power)
+ power.screenEnabled = true;
+ this.screenEnabled = true;
+ this.screen.classList.remove('screenoff');
+
+ // 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.
+ if (this._screenWakeLocked) {
+ this._setIdleTimeout(0);
+ // The screen should be turn off with shorter timeout if
+ // it was never unlocked.
+ } else if (LockScreen.locked) {
+ this._setIdleTimeout(10, true);
+ var self = this;
+ var stopShortIdleTimeout = function scm_stopShortIdleTimeout() {
+ window.removeEventListener('unlock', stopShortIdleTimeout);
+ window.removeEventListener('lockpanelchange', stopShortIdleTimeout);
+ self._setIdleTimeout(self._idleTimeout, false);
+ };
+
+ window.addEventListener('unlock', stopShortIdleTimeout);
+ window.addEventListener('lockpanelchange', stopShortIdleTimeout);
+ } else {
+ this._setIdleTimeout(this._idleTimeout, false);
+ }
+ },
+
+ setScreenBrightness: function scm_setScreenBrightness(brightness, instant) {
+ this._targetBrightness = brightness;
+ var power = navigator.mozPower;
+ if (!power)
+ return;
+
+ // Make sure we don't have another brightness change scheduled
+ if (this._transitionBrightnessTimer) {
+ clearTimeout(this._transitionBrightnessTimer);
+ this._transitionBrightnessTimer = null;
+ }
+
+ if (typeof instant !== 'boolean')
+ instant = true;
+
+ if (instant) {
+ power.screenBrightness = brightness;
+ return;
+ }
+
+ // transitionBrightness() is a looping function that will
+ // gracefully tune the brightness to _targetBrightness for us.
+ this.transitionBrightness();
+ },
+
+ transitionBrightness: function scm_transitionBrightness() {
+ var self = this;
+ var power = navigator.mozPower;
+ var screenBrightness = power.screenBrightness;
+ var delta = this.BRIGHTNESS_ADJUST_STEP;
+
+ // Is this the last time adjustment we need to make?
+ if (Math.abs(this._targetBrightness - screenBrightness) <= delta) {
+ power.screenBrightness = this._targetBrightness;
+ this._transitionBrightnessTimer = null;
+ return;
+ }
+
+ if (screenBrightness > this._targetBrightness)
+ delta *= -1;
+
+ screenBrightness += delta;
+ power.screenBrightness = screenBrightness;
+
+ this._transitionBrightnessTimer =
+ setTimeout(function transitionBrightnessTimeout() {
+ self.transitionBrightness();
+ }, this.BRIGHTNESS_ADJUST_INTERVAL);
+ },
+
+ 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;
+
+ 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);
+ };
+ var activeCallback = function active_proxy() {
+ self.turnScreenOn(true);
+ };
+
+ this._idleTimerId = window.setIdleTimeout(idleCallback,
+ activeCallback, time * 1000);
+ },
+
+ fireScreenChangeEvent: function scm_fireScreenChangeEvent() {
+ var evt = new CustomEvent('screenchange',
+ { bubbles: true, cancelable: false,
+ detail: { screenEnabled: this.screenEnabled } });
+ window.dispatchEvent(evt);
+ }
+};
+
+ScreenManager.init();
diff --git a/apps/system/js/screenshot.js b/apps/system/js/screenshot.js
new file mode 100644
index 0000000..9e8e7f6
--- /dev/null
+++ b/apps/system/js/screenshot.js
@@ -0,0 +1,115 @@
+// screenshot.js: system screenshot module
+//
+// This system module takes a screenshot of the currently running app
+// or homescreen and stores it with DeviceStorage when the user
+// presses the home and sleep buttons at the same time. It communicates
+// with gecko code running in b2g/chrome/content/shell.js using a private
+// event-based API. It is the gecko code that creates the screenshot.
+//
+// This script must be used with the defer attribute.
+//
+//
+(function() {
+ window.addEventListener('home+sleep', takeScreenshot);
+
+ // Assume that the maximum screenshot size is 4 bytes per pixel
+ // plus a bit extra. In practice, with compression, our PNG files will be
+ // much smaller than this.
+ var MAX_SCREENSHOT_SIZE = window.innerWidth * window.innerHeight * 4 + 4096;
+
+ function takeScreenshot() {
+ // Give feedback that the screenshot request was received
+ navigator.vibrate(100);
+
+ // We don't need device storage here, but check to see that
+ // it is available before sending the screenshot request to chrome.
+ // If device storage is available, the callback will be called.
+ // Otherwise, an error message notification will be displayed.
+ getDeviceStorage(function() {
+ // Let chrome know we'd like a screenshot.
+ // This is a completely non-standard undocumented API
+ // for communicating with our chrome code.
+ var screenshotProps = {
+ detail: {
+ type: 'take-screenshot'
+ }
+ };
+ window.dispatchEvent(new CustomEvent('mozContentEvent', screenshotProps));
+ });
+ }
+
+ // Display a screenshot success or failure notification.
+ // Localize the first argument, and localize the third if the second is null
+ function notify(titleid, body, bodyid) {
+ var title = navigator.mozL10n.get(titleid) || titleid;
+ body = body || navigator.mozL10n.get(bodyid);
+ navigator.mozNotification.createNotification(title, body).show();
+ }
+
+ // Get a DeviceStorage object and pass it to the callback.
+ // Or, if device storage is not available, display a notification.
+ function getDeviceStorage(callback) {
+ var storage = navigator.getDeviceStorage('pictures');
+ var availreq = storage.available();
+ availreq.onsuccess = function() {
+ var state = availreq.result;
+ if (state === 'unavailable') {
+ notify('screenshotFailed', null, 'screenshotNoSDCard');
+ }
+ else if (state === 'shared') {
+ notify('screenshotFailed', null, 'screenshotSDCardInUse');
+ }
+ else if (state === 'available') {
+ var freereq = storage.freeSpace();
+ freereq.onsuccess = function() {
+ if (freereq.result < MAX_SCREENSHOT_SIZE) {
+ notify('screenshotFailed', null, 'screenshotSDCardLow');
+ }
+ else {
+ callback(storage);
+ }
+ };
+ freereq.onerror = function() {
+ notify('screenshotFailed', freereq.error && freereq.error.name);
+ };
+ }
+ }
+ availreq.onerror = function() {
+ notify('screenshotFailed', availreq.error && availreq.error.name);
+ }
+ }
+
+ // Handle the event we get from chrome with the screenshot
+ window.addEventListener('mozChromeEvent', function ss_onMozChromeEvent(e) {
+ try {
+ if (e.detail.type === 'take-screenshot-success') {
+ getDeviceStorage(function(storage) {
+ var filename = 'screenshots/' +
+ new Date().toISOString().slice(0, -5).replace(/[:T]/g, '-') +
+ '.png';
+
+ var saveRequest = storage.addNamed(e.detail.file, filename);
+
+ saveRequest.onsuccess = function ss_onsuccess() {
+ // Vibrate again when the screenshot is saved
+ navigator.vibrate(100);
+
+ // Display filename in a notification
+ notify('screenshotSaved', filename);
+ };
+
+ saveRequest.onerror = function ss_onerror() {
+ notify('screenshotFailed', saveRequest.error.name);
+ };
+ });
+ }
+ else if (e.detail.type === 'take-screenshot-error') {
+ notify('screenshotFailed', e.detail.error);
+ }
+ }
+ catch (e) {
+ console.log('exception in screenshot handler', e);
+ notify('screenshotFailed', e.toString());
+ }
+ });
+}());
diff --git a/apps/system/js/sim_lock.js b/apps/system/js/sim_lock.js
new file mode 100644
index 0000000..3a3d3e0
--- /dev/null
+++ b/apps/system/js/sim_lock.js
@@ -0,0 +1,110 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var SimLock = {
+ init: function sl_init() {
+ // Do not do anything if we can't have access to MobileConnection API
+ var conn = window.navigator.mozMobileConnection;
+ if (!conn)
+ return;
+
+ this.onClose = this.onClose.bind(this);
+
+ // Watch for apps that need a mobile connection
+ window.addEventListener('appwillopen', this);
+
+ // Display the dialog only after lockscreen is unlocked
+ // To prevent keyboard being displayed behind it.
+ window.addEventListener('unlock', this);
+
+ // always monitor card state change
+ conn.addEventListener('cardstatechange', this.showIfLocked.bind(this));
+ },
+
+ handleEvent: function sl_handleEvent(evt) {
+ switch (evt.type) {
+ case 'unlock':
+ this.showIfLocked();
+ break;
+ case 'appwillopen':
+ // if an app needs telephony or sms permission,
+ // we will launch the unlock screen if needed.
+
+ var app = Applications.getByManifestURL(
+ evt.target.getAttribute('mozapp'));
+
+ if (!app || !app.manifest.permissions)
+ return;
+
+ // Ignore first time usage app which already ask for SIM code
+ if (evt.target.classList.contains('ftu'))
+ return;
+
+ if (!('telephony' in app.manifest.permissions ||
+ 'sms' in app.manifest.permissions))
+ return;
+
+ // Ignore second `appwillopen` event when showIfLocked ends up
+ // eventually opening the app on valid pin code
+ var origin = evt.target.dataset.frameOrigin;
+ if (origin == this._lastOrigin) {
+ delete this._lastOrigin;
+ return;
+ }
+ this._lastOrigin = origin;
+
+ // if sim is locked, cancel app opening in order to display
+ // it after PIN dialog
+ if (this.showIfLocked())
+ evt.preventDefault();
+
+ break;
+ }
+ },
+
+ showIfLocked: function sl_showIfLocked() {
+ var conn = window.navigator.mozMobileConnection;
+ if (!conn)
+ return false;
+
+ if (LockScreen.locked)
+ return false;
+
+ switch (conn.cardState) {
+ // do nothing in absent and null card states
+ case null:
+ case 'absent':
+ break;
+ case 'pukRequired':
+ case 'pinRequired':
+ SimPinDialog.show('unlock', this.onClose);
+ return true;
+ case 'networkLocked':
+ // XXXX: After unlocking the SIM the cardState is
+ // 'networkLocked' but it changes inmediately to 'ready'
+ // if the phone is not SIM-locked. If the cardState
+ // is still 'networkLocked' after two seconds we unlock
+ // the network control key lock (network personalization).
+ setTimeout(function checkState() {
+ if (conn.cardState == 'networkLocked') {
+ SimPinDialog.show('unlock', SimLock.onClose);
+ }
+ }, 5000);
+ break;
+ }
+ return false;
+ },
+
+ onClose: function sl_onClose(reason) {
+ // Display the app only when PIN code is valid and when we click
+ // on `X` button
+ if (this._lastOrigin && (reason == 'success' || reason == 'skip'))
+ WindowManager.setDisplayedApp(this._lastOrigin);
+ delete this._lastOrigin;
+ }
+
+};
+
+SimLock.init();
diff --git a/apps/system/js/simcard_dialog.js b/apps/system/js/simcard_dialog.js
new file mode 100644
index 0000000..4176cbf
--- /dev/null
+++ b/apps/system/js/simcard_dialog.js
@@ -0,0 +1,355 @@
+/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var SimPinDialog = {
+ dialogTitle: document.querySelector('#simpin-dialog header h1'),
+ dialogDone: document.querySelector('#simpin-dialog button[type="submit"]'),
+ dialogClose: document.querySelector('#simpin-dialog button[type="reset"]'),
+
+ pinArea: document.getElementById('pinArea'),
+ pukArea: document.getElementById('pukArea'),
+ nckArea: document.getElementById('nckArea'),
+ newPinArea: document.getElementById('newPinArea'),
+ confirmPinArea: document.getElementById('confirmPinArea'),
+
+ pinInput: null,
+ pukInput: null,
+ nckInput: null,
+ newPinInput: null,
+ confirmPinInput: null,
+
+ errorMsg: document.getElementById('errorMsg'),
+ errorMsgHeader: document.getElementById('messageHeader'),
+ errorMsgBody: document.getElementById('messageBody'),
+
+ mobileConnection: null,
+
+ lockType: 'pin',
+ action: 'unlock',
+
+ // Now we don't have a number-password type for input field
+ // mimic one by binding one number input and one text input
+ getNumberPasswordInputField: function spl_wrapNumberInput(name) {
+ var valueEntered = '';
+ var inputField = document.querySelector('input[name="' + name + '"]');
+ var displayField = document.querySelector('input[name="' + name + 'Vis"]');
+ var codeMaxLength = parseInt(inputField.getAttribute('maxlength'), 10);
+ var self = this;
+
+ inputField.addEventListener('keypress', function(evt) {
+ if (evt.target !== inputField)
+ return;
+ evt.preventDefault();
+
+ var code = evt.charCode;
+ if (code !== 0 && (code < 0x30 || code > 0x39))
+ return;
+
+ if (code === 0) { // backspace
+ valueEntered = valueEntered.substr(0, valueEntered.length - 1);
+ } else {
+ if (valueEntered.length >= codeMaxLength)
+ return;
+ valueEntered += String.fromCharCode(code);
+ }
+ displayField.value = encryption(valueEntered);
+ if (displayField.value.length >= 4)
+ self.dialogDone.disabled = false;
+ else
+ self.dialogDone.disabled = true;
+ });
+
+ function encryption(str) {
+ return (new Array(str.length + 1)).join('*');
+ }
+
+ function setValue(value) {
+ valueEntered = value;
+ inputField.value = value;
+ displayField.value = encryption(valueEntered);
+ }
+
+ function setFocus() {
+ inputField.focus();
+ }
+
+ function blur() {
+ inputField.blur();
+ }
+
+ return {
+ get value() { return valueEntered; },
+ set value(value) { setValue(value) },
+ focus: setFocus,
+ blur: blur
+ };
+ },
+
+ handleCardState: function spl_handleCardState() {
+ var _ = navigator.mozL10n.get;
+
+ var cardState = this.mobileConnection.cardState;
+ switch (cardState) {
+ case 'pinRequired':
+ this.lockType = 'pin';
+ this.errorMsg.hidden = true;
+ this.inputFieldControl(true, false, false, false);
+ this.pinInput.focus();
+ break;
+ case 'pukRequired':
+ this.lockType = 'puk';
+ this.errorMsgHeader.textContent = _('simCardLockedMsg') || '';
+ this.errorMsgHeader.dataset.l10nId = 'simCardLockedMsg';
+ this.errorMsgBody.textContent = _('enterPukMsg') || '';
+ this.errorMsgBody.dataset.l10nId = 'enterPukMsg';
+ this.errorMsg.hidden = false;
+ this.inputFieldControl(false, true, false, true);
+ this.pukInput.focus();
+ break;
+ case 'networkLocked':
+ this.lockType = 'nck';
+ this.errorMsg.hidden = true;
+ this.inputFieldControl(false, false, true, false);
+ this.nckInput.focus();
+ break;
+ default:
+ this.skip();
+ break;
+ }
+ this.dialogTitle.textContent = _(this.lockType + 'Title') || '';
+ this.dialogTitle.dataset.l10nId = this.lockType + 'Title';
+ },
+
+ handleError: function spl_handleLockError(evt) {
+ var retry = (evt.retryCount) ? evt.retryCount : -1;
+ this.showErrorMsg(retry, evt.lockType);
+ if (retry === -1) {
+ this.skip();
+ return;
+ }
+ if (evt.lockType === 'pin') {
+ this.pinInput.focus();
+ } else if (evt.lockType === 'puk') {
+ this.pukInput.focus();
+ } else {
+ this.nckInput.focus();
+ }
+ },
+
+ showErrorMsg: function spl_showErrorMsg(retry, type) {
+ var _ = navigator.mozL10n.get;
+
+ this.errorMsgHeader.textContent = _(type + 'ErrorMsg');
+ this.errorMsgHeader.dataset.l10nId = type + 'ErrorMsg';
+
+ if (retry !== 1) {
+ var l10nArgs = { n: retry };
+ this.errorMsgBody.dataset.l10nId = type + 'AttemptMsg';
+ this.errorMsgBody.dataset.l10nArgs = JSON.stringify(l10nArgs);
+ this.errorMsgBody.textContent = _(type + 'AttemptMsg', l10nArgs);
+ } else {
+ this.errorMsgBody.dataset.l10nId = type + 'LastChanceMsg';
+ this.errorMsgBody.textContent = _(type + 'LastChanceMsg');
+ }
+
+ this.errorMsg.hidden = false;
+ },
+
+ unlockPin: function spl_unlockPin() {
+ var pin = this.pinInput.value;
+ if (pin === '')
+ return;
+
+ var options = {lockType: 'pin', pin: pin };
+ this.unlockCardLock(options);
+ this.clear();
+ },
+
+ unlockPuk: function spl_unlockPuk() {
+ var _ = navigator.mozL10n.get;
+
+ var puk = this.pukInput.value;
+ var newPin = this.newPinInput.value;
+ var confirmPin = this.confirmPinInput.value;
+ if (puk === '' || newPin === '' || confirmPin === '')
+ return;
+
+ if (newPin !== confirmPin) {
+ this.errorMsgHeader.textContent = _('newPinErrorMsg');
+ this.errorMsgHeader.dataset.l10nId = 'newPinErrorMsg';
+ this.errorMsgBody.textContent = '';
+ this.errorMsg.hidden = false;
+ return;
+ }
+ var options = {lockType: 'puk', puk: puk, newPin: newPin };
+ this.unlockCardLock(options);
+ this.clear();
+ },
+
+ unlockNck: function spl_unlockNck() {
+ var nck = this.nckInput.value;
+ if (nck === '')
+ return;
+
+ var options = {lockType: 'nck', pin: nck };
+ this.unlockCardLock(options);
+ this.clear();
+ },
+
+ unlockCardLock: function spl_unlockCardLock(options) {
+ var req = this.mobileConnection.unlockCardLock(options);
+ req.onsuccess = this.close.bind(this, 'success');
+ },
+
+ enableLock: function spl_enableLock() {
+ var pin = this.pinInput.value;
+ if (pin === '')
+ return;
+
+ var enabled = SimPinLock.simPinCheckBox.checked;
+ var options = {lockType: 'pin', pin: pin, enabled: enabled};
+ this.setCardLock(options);
+ this.clear();
+ },
+
+ changePin: function spl_changePin() {
+ var _ = navigator.mozL10n.get;
+
+ var pin = this.pinInput.value;
+ var newPin = this.newPinInput.value;
+ var confirmPin = this.confirmPinInput.value;
+ if (pin === '' || newPin === '' || confirmPin === '')
+ return;
+
+ if (newPin !== confirmPin) {
+ this.errorMsgHeader.textContent = _('newPinErrorMsg');
+ this.errorMsgHeader.dataset.l10nId = 'newPinErrorMsg';
+ this.errorMsgBody.textContent = '';
+ this.errorMsg.hidden = false;
+ return;
+ }
+ var options = {lockType: 'pin', pin: pin, newPin: newPin};
+ this.setCardLock(options);
+ this.clear();
+ },
+
+ setCardLock: function spl_setCardLock(options) {
+ var req = this.mobileConnection.setCardLock(options);
+ req.onsuccess = this.close.bind(this, 'success');
+ },
+ inputFieldControl: function spl_inputField(isPin, isPuk, isNck, isNewPin) {
+ this.pinArea.hidden = !isPin;
+ this.pukArea.hidden = !isPuk;
+ this.nckArea.hidden = !isNck;
+ this.newPinArea.hidden = !isNewPin;
+ this.confirmPinArea.hidden = !isNewPin;
+ },
+
+ verify: function spl_verify() {
+ switch (this.action) {
+ case 'unlock':
+ if (this.lockType === 'pin')
+ this.unlockPin();
+ else if (this.lockType === 'puk') {
+ this.unlockPuk();
+ } else {
+ this.unlockNck();
+ }
+ break;
+ case 'enable':
+ this.enableLock();
+ break;
+ case 'changePin':
+ this.changePin();
+ break;
+ }
+ return false;
+ },
+
+ onHide: function spl_onHide(reason) {
+ this.clear();
+ if (this.onclose)
+ this.onclose(reason);
+ },
+
+ clear: function spl_clear() {
+ this.errorMsg.hidden = true;
+ this.pinInput.value = '';
+ this.pinInput.blur();
+ this.pukInput.value = '';
+ this.pukInput.blur();
+ this.newPinInput.value = '';
+ this.confirmPinInput.value = '';
+ },
+
+ onclose: null,
+ /**
+ * Show the SIM pin dialog
+ * @param {String} action Name of the action to execute,
+ * either: unlock, enable or changePin.
+ * @param {Function} title Optional function called when dialog is closed.
+ * Receive a single argument being the reason of
+ * dialog closing: success, skip, home or holdhome.
+ */
+ show: function spl_show(action, onclose) {
+ var _ = navigator.mozL10n.get;
+
+ this.systemDialog.show();
+ this.dialogDone.disabled = true;
+ this.action = action;
+ this.lockType = 'pin';
+ switch (action) {
+ case 'unlock':
+ this.handleCardState();
+ break;
+ case 'enable':
+ this.inputFieldControl(true, false, false, false);
+ this.dialogTitle.textContent = _('pinTitle') || '';
+ this.dialogTitle.dataset.l10nId = 'pinTitle';
+ break;
+ case 'changePin':
+ this.inputFieldControl(true, false, false, true);
+ this.dialogTitle.textContent = _('newpinTitle') || '';
+ this.dialogTitle.dataset.l10nId = 'newpinTitle';
+ break;
+ }
+
+ if (onclose && typeof onclose === 'function')
+ this.onclose = onclose;
+ },
+
+ close: function spl_close(reason) {
+ this.systemDialog.hide(reason);
+ },
+
+ skip: function spl_skip() {
+ this.close('skip');
+ return false;
+ },
+
+ init: function spl_init() {
+ this.systemDialog = SystemDialog('simpin-dialog', {
+ onHide: this.onHide.bind(this)
+ });
+
+ this.mobileConnection = window.navigator.mozMobileConnection;
+ if (!this.mobileConnection)
+ return;
+
+ this.mobileConnection.addEventListener('icccardlockerror',
+ this.handleError.bind(this));
+
+ this.dialogDone.onclick = this.verify.bind(this);
+ this.dialogClose.onclick = this.skip.bind(this);
+ this.pinInput = this.getNumberPasswordInputField('simpin');
+ this.pukInput = this.getNumberPasswordInputField('simpuk');
+ this.nckInput = this.getNumberPasswordInputField('nckpin');
+ this.newPinInput = this.getNumberPasswordInputField('newSimpin');
+ this.confirmPinInput = this.getNumberPasswordInputField('confirmNewSimpin');
+ }
+};
+
+SimPinDialog.init();
+
diff --git a/apps/system/js/sleep_menu.js b/apps/system/js/sleep_menu.js
new file mode 100644
index 0000000..a97b772
--- /dev/null
+++ b/apps/system/js/sleep_menu.js
@@ -0,0 +1,276 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var SleepMenu = {
+ // Indicate setting status of ril.radio.disabled
+ isFlightModeEnabled: false,
+
+ // Indicate setting status of volume
+ isSilentModeEnabled: false,
+
+ elements: {},
+
+ get visible() {
+ return this.elements.overlay.classList.contains('visible');
+ },
+
+ getAllElements: function sm_getAllElements() {
+ this.elements.overlay = document.getElementById('sleep-menu');
+ this.elements.container =
+ document.querySelector('#sleep-menu-container ul');
+ this.elements.cancel = document.querySelector('#sleep-menu button');
+ },
+
+ init: function sm_init() {
+ this.getAllElements();
+ window.addEventListener('holdsleep', this.show.bind(this));
+ window.addEventListener('click', this, true);
+ window.addEventListener('screenchange', this, true);
+ window.addEventListener('home', this);
+ this.elements.cancel.addEventListener('click', this);
+
+ var self = this;
+ SettingsListener.observe('ril.radio.disabled', false, function(value) {
+ self.isFlightModeEnabled = value;
+ });
+
+ var settings = navigator.mozSettings;
+ SettingsListener.observe('audio.volume.notification', 7, function(value) {
+ settings.createLock().set({'ring.enabled': (value != 0)});
+ });
+
+ SettingsListener.observe('ring.enabled', true, function(value) {
+ self.isSilentModeEnabled = !value;
+ });
+ },
+
+ // Generate items
+ generateItems: function sm_generateItems() {
+ var items = [];
+ var _ = navigator.mozL10n.get;
+ var options = {
+ airplane: {
+ label: _('airplane'),
+ value: 'airplane',
+ icon: '/style/sleep_menu/images/airplane.png'
+ },
+ airplaneOff: {
+ label: _('airplaneOff'),
+ value: 'airplane'
+ },
+ silent: {
+ label: _('silent'),
+ value: 'silent',
+ icon: '/style/sleep_menu/images/vibration.png'
+ },
+ silentOff: {
+ label: _('normal'),
+ value: 'silentOff'
+ },
+ restart: {
+ label: _('restart'),
+ value: 'restart',
+ icon: '/style/sleep_menu/images/restart.png'
+ },
+ power: {
+ label: _('power'),
+ value: 'power',
+ icon: '/style/sleep_menu/images/power-off.png'
+ }
+ };
+
+ if (this.isFlightModeEnabled) {
+ items.push(options.airplaneOff);
+ } else {
+ items.push(options.airplane);
+ }
+
+ if (!this.isSilentModeEnabled) {
+ items.push(options.silent);
+ } else {
+ items.push(options.silentOff);
+ }
+
+ items.push(options.restart);
+ items.push(options.power);
+
+ return items;
+ },
+
+ show: function sm_show() {
+ this.elements.container.innerHTML = '';
+ this.buildMenu(this.generateItems());
+ this.elements.overlay.classList.add('visible');
+ },
+
+ buildMenu: function sm_buildMenu(items) {
+ items.forEach(function traveseItems(item) {
+ var item_li = document.createElement('li');
+ item_li.dataset.value = item.value;
+ item_li.textContent = item.label;
+ this.elements.container.appendChild(item_li);
+ }, this);
+ },
+
+ hide: function lm_hide() {
+ this.elements.overlay.classList.remove('visible');
+ },
+
+ handleEvent: function sm_handleEvent(evt) {
+ switch (evt.type) {
+ case 'screenchange':
+ if (!evt.detail.screenEnabled)
+ this.hide();
+ break;
+
+ case 'click':
+ if (!this.visible)
+ return;
+
+ if (evt.currentTarget === this.elements.cancel) {
+ this.hide();
+ return;
+ }
+
+ var action = evt.target.dataset.value;
+ if (!action) {
+ return;
+ }
+ this.hide();
+ this.handler(action);
+ break;
+
+ case 'home':
+ if (this.visible) {
+ this.hide();
+ }
+ break;
+ }
+ },
+
+ handler: function sm_handler(action) {
+ switch (action) {
+ case 'airplane':
+ // Airplane mode should turn off
+ //
+ // Radio ('ril.radio.disabled'`)
+ // Data ('ril.data.enabled'`)
+ // Wifi
+ // Bluetooth
+ // Geolocation
+ //
+ // It should also save the status of the latter 4 items
+ // so when leaving the airplane mode we could know which one to turn on.
+
+ if (!window.navigator.mozSettings)
+ return;
+
+ SettingsListener.getSettingsLock().set({
+ 'ril.radio.disabled': !this.isFlightModeEnabled
+ });
+
+ break;
+
+ // About silent and silentOff
+ // * Turn on silent mode will cause:
+ // * Turn off ringtone no matter if ring is on or off
+ // * for sms and incoming calls.
+ // * Turn off silent mode will cause:
+ // * Turn on ringtone no matter if ring is on or off
+ // * for sms and incoming calls.
+ case 'silent':
+ if (!window.navigator.mozSettings)
+ return;
+
+ SettingsListener.getSettingsLock().set({
+ 'ring.enabled': false
+ });
+ this.isSilentModeEnabled = true;
+
+ break;
+
+ case 'silentOff':
+ if (!window.navigator.mozSettings)
+ return;
+
+ SettingsListener.getSettingsLock().set({
+ 'ring.enabled': true
+ });
+ this.isSilentModeEnabled = false;
+
+ break;
+
+ case 'restart':
+ this.startPowerOff(true);
+
+ break;
+
+ case 'power':
+ this.startPowerOff(false);
+
+ break;
+ }
+ },
+
+ startPowerOff: function sm_startPowerOff(reboot) {
+ var power = navigator.mozPower;
+ if (!power)
+ return;
+
+ // Early return if we are already shutting down.
+ if (document.getElementById('poweroff-splash'))
+ return;
+
+ // Show shutdown animation before actually performing shutdown.
+ // * step1: fade-in poweroff-splash.
+ // * step2: The 3-rings animation is performed on the screen.
+ var div = document.createElement('div');
+ div.dataset.zIndexLevel = 'poweroff-splash';
+ div.id = 'poweroff-splash';
+
+ // The overall animation ends when the inner span of the bottom ring
+ // is animated, so we store it for detecting.
+ var inner;
+
+ for (var i = 1; i <= 3; i++) {
+ var outer = document.createElement('span');
+ outer.className = 'poweroff-ring';
+ outer.id = 'poweroff-ring-' + i;
+ div.appendChild(outer);
+
+ inner = document.createElement('span');
+ outer.appendChild(inner);
+ }
+
+ div.className = 'step1';
+
+ var nextAnimation = function nextAnimation(e) {
+ // Switch to next class
+ if (e.target == div)
+ div.className = 'step2';
+
+ if (e.target != inner)
+ return;
+
+ // Actual poweroff/reboot
+ setTimeout(function powerOffAnimated() {
+ if (reboot) {
+ power.reboot();
+ } else {
+ power.powerOff();
+ }
+ });
+
+ // Paint screen to black before reboot/poweroff
+ ScreenManager.turnScreenOff(true);
+ };
+
+ div.addEventListener('animationend', nextAnimation);
+
+ document.getElementById('screen').appendChild(div);
+ }
+};
+
+SleepMenu.init();
diff --git a/apps/system/js/sound_manager.js b/apps/system/js/sound_manager.js
new file mode 100644
index 0000000..52dfc06
--- /dev/null
+++ b/apps/system/js/sound_manager.js
@@ -0,0 +1,242 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+(function() {
+ window.addEventListener('volumeup', function() {
+ if (onBTEarphoneConnected() && onCall()) {
+ changeVolume(1, 'bt_sco');
+ } else {
+ changeVolume(1);
+ }
+ });
+ window.addEventListener('volumedown', function() {
+ if (onBTEarphoneConnected() && onCall()) {
+ changeVolume(-1, 'bt_sco');
+ } else {
+ changeVolume(-1);
+ }
+ });
+
+ // Store the current active channel;
+ // change with 'audio-channel-changed' mozChromeEvent
+ var currentChannel = 'notification';
+
+ var vibrationEnabled = true;
+
+ // This event is generated in shell.js in response to bluetooth headset.
+ // Bluetooth headset always assign audio volume to a specific value when
+ // pressing its volume-up/volume-down buttons.
+ window.addEventListener('mozChromeEvent', function(e) {
+ var type = e.detail.type;
+ if (type == 'bluetooth-volumeset') {
+ changeVolume(e.detail.value - currentVolume['bt_sco'], 'bt_sco');
+ } else if (type == 'audio-channel-changed') {
+ currentChannel = e.detail.channel;
+ }
+ });
+
+ function onCall() {
+ if (currentChannel == 'telephony')
+ return true;
+
+ // XXX: This work should be removed
+ // once we could get telephony channel change event
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=819858
+ var telephony = window.navigator.mozTelephony;
+ if (!telephony)
+ return false;
+
+ return telephony.calls.some(function callIterator(call) {
+ return (call.state == 'connected');
+ });
+ }
+
+ function onBTEarphoneConnected() {
+ var bluetooth = navigator.mozBluetooth;
+ if (!bluetooth)
+ return false;
+
+ // 0x111E is for querying earphone type.
+ return navigator.mozBluetooth.isConnected(0x111E);
+ };
+
+ // Platform doesn't provide the maximum value of each channel
+ // therefore, hard code here.
+ var MAX_VOLUME = {
+ 'alarm': 15,
+ 'notification': 15,
+ 'telephony': 5,
+ 'content': 15,
+ 'bt_sco': 15
+ };
+
+ // Please refer https://wiki.mozilla.org/WebAPI/AudioChannels > Settings
+ var currentVolume = {
+ 'alarm': 15,
+ 'notification': 15,
+ 'telephony': 5,
+ 'content': 15,
+ 'bt_sco': 15
+ };
+ var pendingRequestCount = 0;
+
+ // We have three virtual states here:
+ // OFF -> VIBRATION -> MUTE
+ var muteState = 'OFF';
+
+ for (var channel in currentVolume) {
+ (function(channel) {
+ var setting = 'audio.volume.' + channel;
+ SettingsListener.observe(setting, 5, function onSettingsChange(volume) {
+ if (pendingRequestCount)
+ return;
+
+ var max = MAX_VOLUME[channel];
+ currentVolume[channel] =
+ parseInt(Math.max(0, Math.min(max, volume)), 10);
+ });
+ })(channel);
+ }
+
+ SettingsListener.observe('vibration.enabled', true, function(vibration) {
+ if (pendingRequestCount)
+ return;
+
+ vibrationEnabled = vibration;
+ });
+
+ var activeTimeout = 0;
+
+ // When hardware volume key is pressed, we need to decide which channel we
+ // should toggle.
+ // This method returns the string for setting key 'audio.volume.*' represents
+ // that.
+ // Note: this string does not always equal to currentChannel since some
+ // different channels are grouped together to listen to the same setting.
+ function getChannel() {
+ if (onCall())
+ return 'telephony';
+
+ switch (currentChannel) {
+ case 'normal':
+ case 'content':
+ return 'content';
+ case 'telephony':
+ return 'telephony';
+ case 'alarm':
+ return 'alarm';
+ case 'notification':
+ case 'ringer':
+ default:
+ return 'notification';
+ }
+ }
+
+ function getVolumeState(currentVolume, delta, channel) {
+ if (channel == 'notification') {
+ if (currentVolume + delta <= 0) {
+ if (currentVolume == 0 && vibrationEnabled) {
+ vibrationEnabled = false;
+ } else if (currentVolume > 0 && !vibrationEnabled) {
+ vibrationEnabled = true;
+ }
+ return 'MUTE';
+ } else {
+ return 'OFF';
+ }
+ } else {
+ if (currentVolume + delta <= 0) {
+ return 'MUTE';
+ } else {
+ return 'OFF';
+ }
+ }
+ }
+
+ function changeVolume(delta, channel) {
+ channel = channel ? channel : getChannel();
+
+ muteState = getVolumeState(currentVolume[channel], delta, channel);
+
+ var volume = currentVolume[channel] + delta;
+
+ currentVolume[channel] = volume =
+ Math.max(0, Math.min(MAX_VOLUME[channel], volume));
+
+ var overlay = document.getElementById('system-overlay');
+ var notification = document.getElementById('volume');
+ var overlayClasses = overlay.classList;
+ var classes = notification.classList;
+
+ switch (muteState) {
+ case 'OFF':
+ classes.remove('mute');
+ if (vibrationEnabled) {
+ classes.add('vibration');
+ } else {
+ classes.remove('vibration');
+ }
+ break;
+ case 'MUTE':
+ classes.add('mute');
+ if (channel == 'notification') {
+ if (vibrationEnabled) {
+ classes.add('vibration');
+ SettingsListener.getSettingsLock().set({
+ 'vibration.enabled': true
+ });
+ } else {
+ classes.remove('vibration');
+ SettingsListener.getSettingsLock().set({
+ 'vibration.enabled': false
+ });
+ }
+ }
+ break;
+ }
+
+ var steps =
+ Array.prototype.slice.call(notification.querySelectorAll('div'), 0);
+
+ for (var i = 0; i < steps.length; i++) {
+ var step = steps[i];
+ if (i < volume) {
+ step.classList.add('active');
+ } else {
+ step.classList.remove('active');
+ }
+ }
+
+ overlayClasses.add('volume');
+ classes.add('visible');
+ window.clearTimeout(activeTimeout);
+ activeTimeout = window.setTimeout(function hideSound() {
+ overlayClasses.remove('volume');
+ classes.remove('visible');
+ }, 1500);
+
+ if (!window.navigator.mozSettings)
+ return;
+
+ pendingRequestCount++;
+ var req;
+
+ notification.dataset.channel = channel;
+
+ var settingObject = {};
+ settingObject['audio.volume.' + channel] = volume;
+
+ req = SettingsListener.getSettingsLock().set(settingObject);
+
+ req.onsuccess = function onSuccess() {
+ pendingRequestCount--;
+ };
+
+ req.onerror = function onError() {
+ pendingRequestCount--;
+ };
+ }
+})();
+
diff --git a/apps/system/js/source_view.js b/apps/system/js/source_view.js
new file mode 100644
index 0000000..dc6b8e4
--- /dev/null
+++ b/apps/system/js/source_view.js
@@ -0,0 +1,67 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var SourceView = {
+ get viewer() {
+ return document.getElementById('appViewsource');
+ },
+
+ get active() {
+ return !this.viewer ? false : this.viewer.style.visibility === 'visible';
+ },
+
+ init: function sv_init() {
+ window.addEventListener('home+volume', function() {
+ if (ScreenManager.screenEnabled)
+ SourceView.toggle();
+ });
+ window.addEventListener('locked', function() {
+ SourceView.hide();
+ });
+ },
+
+ show: function sv_show() {
+ var viewsource = this.viewer;
+ if (!viewsource) {
+ var style = '#appViewsource { ' +
+ ' position: absolute;' +
+ ' top: -moz-calc(10%);' +
+ ' left: -moz-calc(10%);' +
+ ' width: -moz-calc(80% - 2 * 15px);' +
+ ' height: -moz-calc(80% - 2 * 15px);' +
+ ' visibility: hidden;' +
+ ' margin: 15px;' +
+ ' background-color: white;' +
+ ' opacity: 0.92;' +
+ ' color: black;' +
+ ' z-index: 9999;' +
+ '}';
+ document.styleSheets[0].insertRule(style, 0);
+
+ viewsource = document.createElement('iframe');
+ viewsource.id = 'appViewsource';
+ document.body.appendChild(viewsource);
+ }
+
+ var url = WindowManager.getDisplayedApp();
+ if (!url)
+ // Assume the home screen is the visible app.
+ url = window.location.toString();
+ viewsource.src = 'view-source: ' + url;
+ viewsource.style.visibility = 'visible';
+ },
+
+ hide: function sv_hide() {
+ var viewsource = this.viewer;
+ if (viewsource) {
+ viewsource.style.visibility = 'hidden';
+ viewsource.src = 'about:blank';
+ }
+ },
+
+ toggle: function sv_toggle() {
+ this.active ? this.hide() : this.show();
+ }
+};
diff --git a/apps/system/js/statusbar.js b/apps/system/js/statusbar.js
new file mode 100644
index 0000000..1d95d99
--- /dev/null
+++ b/apps/system/js/statusbar.js
@@ -0,0 +1,618 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var StatusBar = {
+ /* all elements that are children nodes of the status bar */
+ ELEMENTS: ['notification', 'time',
+ 'battery', 'wifi', 'data', 'flight-mode', 'signal', 'network-activity',
+ 'tethering', 'alarm', 'bluetooth', 'mute', 'headphones',
+ 'recording', 'sms', 'geolocation', 'usb', 'label', 'system-downloads',
+ 'call-forwarding'],
+
+ /* Timeout for 'recently active' indicators */
+ kActiveIndicatorTimeout: 60 * 1000,
+
+ /* Whether or not status bar is actively updating or not */
+ active: true,
+
+ /* Some values that sync from mozSettings */
+ settingValues: {},
+
+ /* Keep the DOM element references here */
+ icons: {},
+
+ /* A mapping table between technology names
+ we would get from API v.s. the icon we want to show. */
+ mobileDataIconTypes: {
+ 'lte': '4G', // 4G LTE
+ 'ehrpd': '4G', // 4G CDMA
+ 'hspa+': 'H+', // 3.5G HSPA+
+ 'hsdpa': 'H', 'hsupa': 'H', 'hspa': 'H', // 3.5G HSDPA
+ 'evdo0': '3G', 'evdoa': '3G', 'evdob': '3G', '1xrtt': '3G', // 3G CDMA
+ 'umts': '3G', // 3G
+ 'edge': 'E', // EDGE
+ 'is95a': '2G', 'is95b': '2G', // 2G CDMA
+ 'gprs': '2G'
+ },
+
+ geolocationActive: false,
+ geolocationTimer: null,
+
+ recordingActive: false,
+ recordingTimer: null,
+
+ umsActive: false,
+
+ headphonesActive: false,
+
+ /**
+ * this keeps how many current installs/updates we do
+ * it triggers the icon "systemDownloads"
+ */
+ systemDownloadsCount: 0,
+
+ /* For other modules to acquire */
+ get height() {
+ if (this.screen.classList.contains('fullscreen-app') ||
+ document.mozFullScreen) {
+ return 0;
+ } else if (this.screen.classList.contains('active-statusbar')) {
+ return this.attentionBar.offsetHeight;
+ } else {
+ return this._cacheHeight ||
+ (this._cacheHeight = this.element.getBoundingClientRect().height);
+ }
+ },
+
+ init: function sb_init() {
+ this.getAllElements();
+
+ var settings = {
+ 'ril.radio.disabled': ['signal', 'data'],
+ 'ril.data.enabled': ['data'],
+ 'wifi.enabled': ['wifi'],
+ 'bluetooth.enabled': ['bluetooth'],
+ 'tethering.usb.enabled': ['tethering'],
+ 'tethering.wifi.enabled': ['tethering'],
+ 'tethering.wifi.connectedClients': ['tethering'],
+ 'tethering.usb.connectedClients': ['tethering'],
+ 'ring.enabled': ['mute'],
+ 'alarm.enabled': ['alarm'],
+ 'vibration.enabled': ['vibration'],
+ 'ril.cf.enabled': ['callForwarding']
+ };
+
+ var self = this;
+ for (var settingKey in settings) {
+ (function sb_setSettingsListener(settingKey) {
+ SettingsListener.observe(settingKey, false,
+ function sb_settingUpdate(value) {
+ self.settingValues[settingKey] = value;
+ settings[settingKey].forEach(
+ function sb_callUpdate(name) {
+ self.update[name].call(self);
+ }
+ );
+ }
+ );
+ self.settingValues[settingKey] = false;
+ })(settingKey);
+ }
+
+ // Listen to 'screenchange' from screen_manager.js
+ window.addEventListener('screenchange', this);
+
+ // Listen to 'geolocation-status' and 'recording-status' mozChromeEvent
+ window.addEventListener('mozChromeEvent', this);
+
+ // Listen to 'bluetoothconnectionchange' from bluetooth.js
+ window.addEventListener('bluetoothconnectionchange', this);
+
+ // Listen to 'moztimechange'
+ window.addEventListener('moztimechange', this);
+
+ this.systemDownloadsCount = 0;
+ this.setActive(true);
+ },
+
+ handleEvent: function sb_handleEvent(evt) {
+ switch (evt.type) {
+ case 'screenchange':
+ this.setActive(evt.detail.screenEnabled);
+ break;
+
+ case 'chargingchange':
+ case 'levelchange':
+ case 'statuschange':
+ this.update.battery.call(this);
+ break;
+
+ case 'voicechange':
+ this.update.signal.call(this);
+ this.update.label.call(this);
+ break;
+
+ case 'cardstatechange':
+ this.update.signal.call(this);
+ this.update.label.call(this);
+ this.update.data.call(this);
+ break;
+
+ case 'callschanged':
+ this.update.signal.call(this);
+ break;
+
+ case 'iccinfochange':
+ this.update.label.call(this);
+ break;
+
+ case 'datachange':
+ this.update.data.call(this);
+ break;
+
+ case 'bluetoothconnectionchange':
+ this.update.bluetooth.call(this);
+ break;
+
+ case 'moztimechange':
+ this.update.time.call(this);
+ break;
+
+ case 'mozChromeEvent':
+ switch (evt.detail.type) {
+ case 'geolocation-status':
+ this.geolocationActive = evt.detail.active;
+ this.update.geolocation.call(this);
+ break;
+
+ case 'recording-status':
+ this.recordingActive = evt.detail.active;
+ this.update.recording.call(this);
+ break;
+
+ case 'volume-state-changed':
+ this.umsActive = evt.detail.active;
+ this.update.usb.call(this);
+ break;
+
+ case 'headphones-status-changed':
+ this.headphonesActive = (evt.detail.state != 'off');
+ this.update.headphones.call(this);
+ break;
+ }
+
+ break;
+
+ case 'moznetworkupload':
+ case 'moznetworkdownload':
+ this.update.networkActivity.call(this);
+ break;
+ }
+ },
+
+ setActive: function sb_setActive(active) {
+ this.active = active;
+ if (active) {
+ this.update.time.call(this);
+
+ var battery = window.navigator.battery;
+ if (battery) {
+ battery.addEventListener('chargingchange', this);
+ battery.addEventListener('levelchange', this);
+ battery.addEventListener('statuschange', this);
+ this.update.battery.call(this);
+ }
+
+ var conn = window.navigator.mozMobileConnection;
+ if (conn) {
+ conn.addEventListener('voicechange', this);
+ conn.addEventListener('iccinfochange', this);
+ conn.addEventListener('datachange', this);
+ this.update.signal.call(this);
+ this.update.data.call(this);
+ }
+
+ window.addEventListener('wifi-statuschange',
+ this.update.wifi.bind(this));
+ this.update.wifi.call(this);
+
+ window.addEventListener('moznetworkupload', this);
+ window.addEventListener('moznetworkdownload', this);
+ } else {
+ clearTimeout(this._clockTimer);
+
+ var battery = window.navigator.battery;
+ if (battery) {
+ battery.removeEventListener('chargingchange', this);
+ battery.removeEventListener('levelchange', this);
+ battery.removeEventListener('statuschange', this);
+ }
+
+ var conn = window.navigator.mozMobileConnection;
+ if (conn) {
+ conn.removeEventListener('voicechange', this);
+ conn.removeEventListener('iccinfochange', this);
+ conn.removeEventListener('datachange', this);
+ }
+
+ window.removeEventListener('moznetworkupload', this);
+ window.removeEventListener('moznetworkdownload', this);
+ }
+ },
+
+ update: {
+ label: function sb_updateLabel() {
+ var conn = window.navigator.mozMobileConnection;
+ var label = this.icons.label;
+ var l10nArgs = JSON.parse(label.dataset.l10nArgs || '{}');
+
+ if (!conn || !conn.voice || !conn.voice.connected ||
+ conn.voice.emergencyCallsOnly) {
+ delete l10nArgs.operator;
+ label.dataset.l10nArgs = JSON.stringify(l10nArgs);
+
+ label.dataset.l10nId = '';
+ label.textContent = l10nArgs.date;
+
+ return;
+ }
+
+ var operatorInfos = MobileOperator.userFacingInfo(conn);
+ l10nArgs.operator = operatorInfos.operator;
+
+ if (operatorInfos.region) {
+ l10nArgs.operator += ' ' + operatorInfos.region;
+ }
+
+ label.dataset.l10nArgs = JSON.stringify(l10nArgs);
+
+ label.dataset.l10nId = 'statusbarLabel';
+ label.textContent = navigator.mozL10n.get('statusbarLabel', l10nArgs);
+ },
+
+ time: function sb_updateTime() {
+ // Schedule another clock update when a new minute rolls around
+ var _ = navigator.mozL10n.get;
+ var f = new navigator.mozL10n.DateTimeFormat();
+ var now = new Date();
+ var sec = now.getSeconds();
+ if (this._clockTimer)
+ window.clearTimeout(this._clockTimer);
+ this._clockTimer =
+ window.setTimeout((this.update.time).bind(this), (59 - sec) * 1000);
+
+ var formated = f.localeFormat(now, _('shortTimeFormat'));
+ formated = formated.replace(/\s?(AM|PM)\s?/i, '<span>$1</span>');
+ this.icons.time.innerHTML = formated;
+
+ var label = this.icons.label;
+ var l10nArgs = JSON.parse(label.dataset.l10nArgs || '{}');
+ l10nArgs.date = f.localeFormat(now, _('statusbarDateFormat'));
+ label.dataset.l10nArgs = JSON.stringify(l10nArgs);
+ this.update.label.call(this);
+ },
+
+ battery: function sb_updateBattery() {
+ var battery = window.navigator.battery;
+ if (!battery)
+ return;
+
+ var icon = this.icons.battery;
+
+ icon.hidden = false;
+ icon.dataset.charging = battery.charging;
+ icon.dataset.level = Math.floor(battery.level * 10) * 10;
+ },
+
+ networkActivity: function sb_updateNetworkActivity() {
+ // Each time we receive an update, make network activity indicator
+ // show up for 500ms.
+
+ var icon = this.icons.networkActivity;
+
+ clearTimeout(this._networkActivityTimer);
+ icon.hidden = false;
+
+ this._networkActivityTimer = setTimeout(function hideNetActivityIcon() {
+ icon.hidden = true;
+ }, 500);
+ },
+
+ signal: function sb_updateSignal() {
+ var conn = window.navigator.mozMobileConnection;
+ if (!conn || !conn.voice)
+ return;
+
+ var voice = conn.voice;
+ var icon = this.icons.signal;
+ var flightModeIcon = this.icons.flightMode;
+ var _ = navigator.mozL10n.get;
+
+ if (this.settingValues['ril.radio.disabled']) {
+ // "Airplane Mode"
+ icon.hidden = true;
+ flightModeIcon.hidden = false;
+ return;
+ }
+
+ flightModeIcon.hidden = true;
+ icon.hidden = false;
+
+ if (conn.cardState === 'absent') {
+ // no SIM
+ delete icon.dataset.level;
+ delete icon.dataset.emergency;
+ delete icon.dataset.searching;
+ delete icon.dataset.roaming;
+ } else if (voice.connected || this.hasActiveCall()) {
+ // "Carrier" / "Carrier (Roaming)"
+ icon.dataset.level = Math.ceil(voice.relSignalStrength / 20); // 0-5
+ icon.dataset.roaming = voice.roaming;
+
+ delete icon.dataset.emergency;
+ delete icon.dataset.searching;
+ } else {
+ // "No Network" / "Emergency Calls Only (REASON)" / trying to connect
+ icon.dataset.level = -1;
+ // logically, we should have "&& !voice.connected" as well but we
+ // already know this.
+ icon.dataset.searching = (!voice.emergencyCallsOnly &&
+ voice.state !== 'notSearching');
+ icon.dataset.emergency = (voice.emergencyCallsOnly);
+ delete icon.dataset.roaming;
+ }
+
+ if (voice.emergencyCallsOnly) {
+ this.addCallListener();
+ } else {
+ this.removeCallListener();
+ }
+
+ },
+
+ data: function sb_updateSignal() {
+ var conn = window.navigator.mozMobileConnection;
+ if (!conn || !conn.data)
+ return;
+
+ var data = conn.data;
+ var icon = this.icons.data;
+
+ if (this.settingValues['ril.radio.disabled'] ||
+ !this.settingValues['ril.data.enabled'] ||
+ !this.icons.wifi.hidden || !data.connected) {
+ icon.hidden = true;
+
+ return;
+ }
+
+ icon.hidden = false;
+ icon.dataset.type =
+ this.mobileDataIconTypes[data.type] || 'circle';
+ },
+
+
+ wifi: function sb_updateWifi() {
+ var wifiManager = window.navigator.mozWifiManager;
+ if (!wifiManager)
+ return;
+
+ var icon = this.icons.wifi;
+ var wasHidden = icon.hidden;
+
+ if (!this.settingValues['wifi.enabled']) {
+ icon.hidden = true;
+ if (!wasHidden)
+ this.update.data.call(this);
+
+ return;
+ }
+
+ switch (wifiManager.connection.status) {
+ case 'disconnected':
+ icon.hidden = true;
+
+ break;
+
+ case 'connecting':
+ case 'associated':
+ icon.hidden = false;
+ icon.dataset.connecting = true;
+ icon.dataset.level = 0;
+
+ break;
+
+ case 'connected':
+ icon.hidden = false;
+
+ var relSignalStrength =
+ wifiManager.connectionInformation.relSignalStrength;
+ icon.dataset.level = Math.floor(relSignalStrength / 25);
+
+ break;
+ }
+
+ if (icon.hidden !== wasHidden)
+ this.update.data.call(this);
+ },
+
+ tethering: function sb_updateTethering() {
+ var icon = this.icons.tethering;
+ icon.hidden = !(this.settingValues['tethering.usb.enabled'] ||
+ this.settingValues['tethering.wifi.enabled']);
+
+ icon.dataset.active =
+ (this.settingValues['tethering.wifi.connectedClients'] !== 0) ||
+ (this.settingValues['tethering.usb.connectedClients'] !== 0);
+ },
+
+ bluetooth: function sb_updateBluetooth() {
+ var icon = this.icons.bluetooth;
+
+ icon.hidden = !this.settingValues['bluetooth.enabled'];
+ icon.dataset.active = Bluetooth.connected;
+ },
+
+ alarm: function sb_updateAlarm() {
+ this.icons.alarm.hidden = !this.settingValues['alarm.enabled'];
+ },
+
+ mute: function sb_updateMute() {
+ this.icons.mute.hidden =
+ (this.settingValues['ring.enabled'] == true);
+ },
+
+ vibration: function sb_vibration() {
+ var vibrate = (this.settingValues['vibration.enabled'] == true);
+ if (vibrate) {
+ this.icons.mute.classList.add('vibration');
+ } else {
+ this.icons.mute.classList.remove('vibration');
+ }
+ },
+
+ recording: function sb_updateRecording() {
+ window.clearTimeout(this.recordingTimer);
+
+ var icon = this.icons.recording;
+ icon.dataset.active = this.recordingActive;
+
+ if (this.recordingActive) {
+ // Geolocation is currently active, show the active icon.
+ icon.hidden = false;
+ return;
+ }
+
+ // Geolocation is currently inactive.
+ // Show the inactive icon and hide it after kActiveIndicatorTimeout
+ this.recordingTimer = window.setTimeout(function hideGeoIcon() {
+ icon.hidden = true;
+ }, this.kActiveIndicatorTimeout);
+ },
+
+ sms: function sb_updateSms() {
+ // We are not going to show this for v1
+
+ // this.icon.sms.hidden = ?
+ // this.icon.sms.dataset.num = ?;
+ },
+
+ geolocation: function sb_updateGeolocation() {
+ window.clearTimeout(this.geolocationTimer);
+
+ var icon = this.icons.geolocation;
+ icon.dataset.active = this.geolocationActive;
+
+ if (this.geolocationActive) {
+ // Geolocation is currently active, show the active icon.
+ icon.hidden = false;
+ return;
+ }
+
+ // Geolocation is currently inactive.
+ // Show the inactive icon and hide it after kActiveIndicatorTimeout
+ this.geolocationTimer = window.setTimeout(function hideGeoIcon() {
+ icon.hidden = true;
+ }, this.kActiveIndicatorTimeout);
+ },
+
+ usb: function sb_updateUsb() {
+ var icon = this.icons.usb;
+ icon.hidden = !this.umsActive;
+ },
+
+ headphones: function sb_updateHeadphones() {
+ var icon = this.icons.headphones;
+ icon.hidden = !this.headphonesActive;
+ },
+
+ systemDownloads: function sb_updatesystemDownloads() {
+ var icon = this.icons.systemDownloads;
+ icon.hidden = (this.systemDownloadsCount === 0);
+ },
+
+ callForwarding: function sb_updateCallForwarding() {
+ var icon = this.icons.callForwarding;
+ icon.hidden = !this.settingValues['ril.cf.enabled'];
+ }
+ },
+
+ hasActiveCall: function sb_hasActiveCall() {
+ var telephony = navigator.mozTelephony;
+
+ // will return true as soon as we begin dialing
+ return !!(telephony && telephony.active);
+ },
+
+ addCallListener: function sb_addCallListener() {
+ var telephony = navigator.mozTelephony;
+ if (telephony) {
+ telephony.addEventListener('callschanged', this);
+ }
+ },
+
+ removeCallListener: function sb_addCallListener() {
+ var telephony = navigator.mozTelephony;
+ if (telephony) {
+ telephony.removeEventListener('callschanged', this);
+ }
+ },
+
+ updateNotification: function sb_updateNotification(count) {
+ var icon = this.icons.notification;
+ if (!count) {
+ icon.hidden = true;
+ return;
+ }
+
+ icon.hidden = false;
+ icon.dataset.num = count;
+ },
+
+ updateNotificationUnread: function sb_updateNotificationUnread(unread) {
+ this.icons.notification.dataset.unread = unread;
+ },
+
+ incSystemDownloads: function sb_incSystemDownloads() {
+ this.systemDownloadsCount++;
+ this.update.systemDownloads.call(this);
+ },
+
+ decSystemDownloads: function sb_decSystemDownloads() {
+ if (--this.systemDownloadsCount < 0) {
+ this.systemDownloadsCount = 0;
+ }
+
+ this.update.systemDownloads.call(this);
+ },
+
+ getAllElements: function sb_getAllElements() {
+ // ID of elements to create references
+
+ var toCamelCase = function toCamelCase(str) {
+ return str.replace(/\-(.)/g, function replacer(str, p1) {
+ return p1.toUpperCase();
+ });
+ };
+
+ this.ELEMENTS.forEach((function createElementRef(name) {
+ this.icons[toCamelCase(name)] =
+ document.getElementById('statusbar-' + name);
+ }).bind(this));
+
+ this.element = document.getElementById('statusbar');
+ this.screen = document.getElementById('screen');
+ this.attentionBar = document.getElementById('attention-bar');
+ }
+};
+
+if (navigator.mozL10n.readyState == 'complete' ||
+ navigator.mozL10n.readyState == 'interactive') {
+ StatusBar.init();
+} else {
+ window.addEventListener('localized', StatusBar.init.bind(StatusBar));
+}
+
+
diff --git a/apps/system/js/storage.js b/apps/system/js/storage.js
new file mode 100644
index 0000000..ff7b10e
--- /dev/null
+++ b/apps/system/js/storage.js
@@ -0,0 +1,60 @@
+var Storage = {
+
+ automounterDisable: 0,
+ automounterEnable: 1,
+ automounterDisableWhenUnplugged: 2,
+
+ umsEnabled: 'ums.enabled',
+ umsMode: 'ums.mode',
+
+ init: function storageInit() {
+ this.setMode(this.automounterDisable, 'init');
+ window.addEventListener('lock', this);
+ window.addEventListener('unlock', this);
+
+ SettingsListener.observe(this.umsEnabled, false, function umsChanged(val) {
+ if (LockScreen.locked) {
+ // covers startup
+ Storage.setMode(Storage.automounterDisable, 'screen locked');
+ } else {
+ Storage.setMode(Storage.modeFromBool(val), 'change in ums.enabled');
+ }
+ });
+ },
+
+ modeFromBool: function storageModeFromBool(val) {
+ return val ? this.automounterEnable : this.automounterDisable;
+ },
+
+ setMode: function storageSetMode(val, reason) {
+ if (!window.navigator.mozSettings)
+ return;
+
+ //console.info('Setting', this.umsMode, 'to', val, 'due to', reason);
+ var param = {};
+ param[this.umsMode] = val;
+ SettingsListener.getSettingsLock().set(param);
+ },
+
+ handleEvent: function storageHandleEvent(e) {
+ switch (e.type) {
+ case 'lock':
+ this.setMode(this.automounterDisableWhenUnplugged, 'screen locked');
+ break;
+ case 'unlock':
+ if (!window.navigator.mozSettings)
+ return;
+
+ var req = SettingsListener.getSettingsLock().get(this.umsEnabled);
+ req.onsuccess = function umsEnabledFetched() {
+ var mode = Storage.modeFromBool(req.result[Storage.umsEnabled]);
+ Storage.setMode(mode, 'screen unlocked');
+ };
+ break;
+ default:
+ return;
+ }
+ }
+};
+
+Storage.init();
diff --git a/apps/system/js/system_banner.js b/apps/system/js/system_banner.js
new file mode 100644
index 0000000..89028e2
--- /dev/null
+++ b/apps/system/js/system_banner.js
@@ -0,0 +1,33 @@
+'use strict';
+
+var SystemBanner = {
+ banner: document.getElementById('system-banner'),
+
+ // Shows a banner with a given message.
+ // Optionally shows a button with a given label/callback.
+ // buttonParams = { label: ..., callback: ... }
+ show: function sb_show(message, buttonParams) {
+ var banner = this.banner;
+ banner.firstElementChild.textContent = message;
+
+ var button = banner.querySelector('button');
+ if (buttonParams) {
+ banner.dataset.button = true;
+ button.textContent = buttonParams.label;
+ button.addEventListener('click', buttonParams.callback);
+ }
+
+ banner.addEventListener('animationend', function animationend() {
+ banner.removeEventListener('animationend', animationend);
+ banner.classList.remove('visible');
+
+ if (buttonParams) {
+ banner.dataset.button = false;
+ button.removeEventListener('click', buttonParams.callback);
+ button.classList.remove('visible');
+ }
+ });
+
+ banner.classList.add('visible');
+ }
+};
diff --git a/apps/system/js/system_dialog.js b/apps/system/js/system_dialog.js
new file mode 100644
index 0000000..b0c32d8
--- /dev/null
+++ b/apps/system/js/system_dialog.js
@@ -0,0 +1,113 @@
+/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+'use strict';
+
+/**
+ * System app is made of a top-level `<div ="screen"></div>` DOM element
+ * which contain all possible screens displayed by the app.
+ * Multiple screens can be displayed at a time. We store the list of currently
+ * visible screens into this DOM element class attribute.
+ */
+var SystemScreen = {
+ screen: document.getElementById('screen'),
+
+ show: function ss_show(screenName) {
+ this.screen.classList.add(screenName);
+ },
+
+ hide: function ss_show(screenName) {
+ this.screen.classList.remove(screenName);
+ },
+
+ isVisible: function ss_isVisible(screenName) {
+ return this.screen.classList.contains(screenName);
+ }
+};
+
+/**
+ * System app displays various kind of dialogs.
+ * A dialog is a system app 'screen' that has a high z-index and is used to be
+ * displayed on top of other apps. But it doesn't display over the status bar,
+ * nor the eventually displayed keyboard.
+ *
+ * `SystemDialog` except the dialog DOM Element `id`.
+ * This DOM Element has to have a DOM attribute 'role' set to 'dialog'.
+ *
+ * It also supports a second `options` object with following attributes:
+ * `onHide`: function called when dialog is hidden, either when `hide()`
+ * method is called, or when dialog is automatically hidden on
+ * home button press
+ */
+function SystemDialog(id, options) {
+ var overlay = document.getElementById('dialog-overlay');
+ var dialog = document.getElementById(id);
+ var screenName = 'dialog';
+
+ // Listen to keyboard visibility changes and window resizing
+ // in order to resize the dialog accordingly
+ function updateHeight(keyboardHeight) {
+ if (SystemScreen.isVisible(screenName)) {
+ var height = window.innerHeight -
+ (keyboardHeight ? keyboardHeight : 0) -
+ StatusBar.height;
+ overlay.style.height = height + 'px';
+ }
+ };
+ function handleEvent(evt) {
+ switch (evt.type) {
+ case 'resize':
+ case 'keyboardhide':
+ updateHeight();
+ break;
+ case 'keyboardchange':
+ updateHeight(evt.detail.height);
+ break;
+ case 'home':
+ case 'holdhome':
+ // Automatically hide the dialog on home button press
+ if (SystemScreen.isVisible(screenName)) {
+ hide(evt.type);
+ // Prevent WindowManager to shift homescreen to the first page
+ // when the dialog is on top of the homescreen
+ var displayedApp = WindowManager.getDisplayedApp();
+ var displayedAppFrame = WindowManager.getAppFrame(displayedApp);
+ if (evt.type == 'home' &&
+ displayedAppFrame.classList.contains('homescreen'))
+ evt.stopImmediatePropagation();
+ }
+ break;
+ }
+ };
+ window.addEventListener('resize', handleEvent);
+ window.addEventListener('keyboardchange', handleEvent);
+ window.addEventListener('keyboardhide', handleEvent);
+ window.addEventListener('home', handleEvent);
+ window.addEventListener('holdhome', handleEvent);
+
+ function show() {
+ dialog.hidden = false;
+ dialog.classList.add(id);
+ SystemScreen.show(screenName);
+ updateHeight();
+ }
+
+ function hide(reason) {
+ dialog.hidden = true;
+ dialog.classList.remove(id);
+ SystemScreen.hide(screenName);
+ if (typeof(options.onHide) == 'function')
+ options.onHide(reason);
+ }
+
+ function isVisible() {
+ return SystemScreen.isVisible(screenName) &&
+ overlay.classList.contains(id);
+ }
+
+ return {
+ show: show,
+ hide: hide,
+ isVisible: isVisible
+ };
+}
+
diff --git a/apps/system/js/trusted_ui.js b/apps/system/js/trusted_ui.js
new file mode 100644
index 0000000..43bea21
--- /dev/null
+++ b/apps/system/js/trusted_ui.js
@@ -0,0 +1,334 @@
+/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil -*- */
+/* vim: set ft=javascript sw=2 ts=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var TrustedUIManager = {
+
+ get currentStack() {
+ if (!this._dialogStacks[this._lastDisplayedApp]) {
+ this._dialogStacks[this._lastDisplayedApp] = [];
+ }
+ return this._dialogStacks[this._lastDisplayedApp];
+ },
+
+ _dialogStacks: {},
+ _lastDisplayedApp: null,
+
+ overlay: document.getElementById('dialog-overlay'),
+
+ popupContainer: document.getElementById('trustedui-container'),
+
+ popupContainerInner: document.getElementById('trustedui-inner'),
+
+ container: document.getElementById('trustedui-frame-container'),
+
+ dialogTitle: document.getElementById('trustedui-title'),
+
+ screen: document.getElementById('screen'),
+
+ loadingIcon: document.getElementById('statusbar-loading'),
+
+ throbber: document.getElementById('trustedui-throbber'),
+
+ closeButton: document.getElementById('trustedui-close'),
+
+ hasTrustedUI: function trui_hasTrustedUI(origin) {
+ return (this._dialogStacks[origin] && this._dialogStacks[origin].length);
+ },
+
+ getDialogFromOrigin: function trui_getDialogFromOrigin(origin) {
+ if (!origin || !this.hasTrustedUI(origin))
+ return false;
+ var stack = this._dialogStacks[origin];
+ return stack[stack.length - 1];
+ },
+
+ init: function trui_init() {
+ window.addEventListener('home', this);
+ window.addEventListener('holdhome', this);
+ window.addEventListener('appwillopen', this);
+ window.addEventListener('appopen', this);
+ window.addEventListener('appwillclose', this);
+ window.addEventListener('appterminated', this);
+ window.addEventListener('keyboardhide', this);
+ window.addEventListener('keyboardchange', this);
+ window.addEventListener('mozbrowserloadstart', this);
+ window.addEventListener('mozbrowserloadend', this);
+ this.closeButton.addEventListener('click', this);
+ },
+
+ hideTrustedApp: function trui_hideTrustedApp() {
+ var self = this;
+ this.popupContainer.classList.add('closing');
+ this.popupContainer.addEventListener('transitionend', function hide() {
+ this.removeEventListener('transitionend', hide);
+ self.hide();
+ });
+ },
+
+ reopenTrustedApp: function trui_reopenTrustedApp() {
+ this._hideAllFrames();
+ var dialog = this._getTopDialog();
+ this._makeDialogVisible(dialog);
+ this.popupContainer.classList.add('closing');
+ this.show();
+ this.popupContainer.classList.remove('closing');
+ },
+
+ open: function trui_open(name, frame, chromeEventId, onCancelCB) {
+ screen.mozLockOrientation('portrait');
+ this._hideAllFrames();
+ if (this.currentStack.length) {
+ this._makeDialogHidden(this._getTopDialog());
+ this._pushNewDialog(name, frame, chromeEventId, onCancelCB);
+ } else {
+ // first time, spin back to home screen first
+ this.popupContainer.classList.add('up');
+ this.popupContainer.classList.remove('closing');
+ WindowManager.hideCurrentApp(function openTrustedUI() {
+ this.popupContainer.classList.remove('up');
+ this._pushNewDialog(name, frame, chromeEventId, onCancelCB);
+ }.bind(this));
+ }
+ },
+
+ close: function trui_close(chromeEventId, callback) {
+ var stackSize = this.currentStack.length;
+
+ this._restoreOrientation();
+
+ if (callback)
+ callback();
+
+ if (stackSize === 0) {
+ // nothing to close. what are you doing?
+ return;
+ } else if (stackSize === 1) {
+ // only one dialog, so transition back to main app
+ var self = this;
+ var container = this.popupContainer;
+ if (!CardsView.cardSwitcherIsShown()) {
+ WindowManager.restoreCurrentApp();
+ container.addEventListener('transitionend', function wait(event) {
+ this.removeEventListener('transitionend', wait);
+ self._closeDialog(chromeEventId);
+ });
+ } else {
+ WindowManager.restoreCurrentApp(this._lastDisplayedApp);
+ this._closeDialog(chromeEventId);
+ }
+
+ // The css transition caused by the removal of the trustedui
+ // class by the hide() method will trigger a 'transitionend'
+ // event ultimately to be fired.
+ this.hide();
+
+ window.focus();
+ } else {
+ this._closeDialog(chromeEventId);
+ }
+ },
+
+ _dispatchCloseEvent: function dispatchCloseEvent(eventId) {
+ var _ = navigator.mozL10n.get;
+ if (!eventId)
+ return;
+ var event = document.createEvent('customEvent');
+ var details = {
+ id: eventId,
+ type: 'cancel',
+ errorMsg: _('dialog-closed')
+ };
+ event.initCustomEvent('mozContentEvent', true, true, details);
+ window.dispatchEvent(event);
+ },
+
+ _getTopDialog: function trui_getTopDialog() {
+ // get the topmost dialog for the _lastDisplayedApp or null
+ return this.currentStack[this.currentStack.length - 1];
+ },
+
+ _pushNewDialog: function trui_PushNewDialog(name, frame, chromeEventId,
+ onCancelCB) {
+ // add some data attributes to the frame
+ var dataset = frame.dataset;
+ dataset.frameType = 'popup';
+ dataset.frameName = frame.name;
+ dataset.frameOrigin = this._lastDisplayedApp;
+
+ // make a shiny new dialog object
+ var dialog = {
+ name: name,
+ frame: frame,
+ chromeEventId: chromeEventId,
+ onCancelCB: onCancelCB
+ };
+
+ // push and show
+ this.currentStack.push(dialog);
+ this.dialogTitle.textContent = dialog.name;
+ this.container.appendChild(dialog.frame);
+ this._makeDialogVisible(dialog);
+ },
+
+ _makeDialogVisible: function trui_makeDialogVisible(dialog) {
+ // make sure the trusty ui is visible
+ this.popupContainer.classList.remove('closing');
+ this.show();
+
+ // ensure the frame is visible and the dialog title is correct.
+ dialog.frame.classList.add('selected');
+ this.dialogTitle.textContent = dialog.name;
+ },
+
+ _makeDialogHidden: function trui_makeDialogHidden(dialog) {
+ if (!dialog)
+ return;
+ this._restoreOrientation();
+ dialog.frame.classList.remove('selected');
+ },
+
+ _restoreOrientation: function trui_restoreOrientation() {
+ var app = WindowManager.getDisplayedApp();
+ WindowManager.setOrientationForApp(app);
+ },
+
+ /**
+ * close the dialog identified by the chromeEventId
+ */
+ _closeDialog: function trui_closeDialog(chromeEventId) {
+ if (this.currentStack.length === 0)
+ return;
+
+ var found = false;
+ for (var i = 0; i < this.currentStack.length; i++) {
+ if (this.currentStack[i].chromeEventId === chromeEventId) {
+ var dialog = this.currentStack.splice(i, 1)[0];
+ this.container.removeChild(dialog.frame);
+ found = true;
+ break;
+ }
+ }
+
+ if (found && this.currentStack.length) {
+ this._makeDialogVisible(this._getTopDialog());
+ }
+ },
+
+ hide: function trui_hide() {
+ this.screen.classList.remove('trustedui');
+ },
+
+ show: function trui_show() {
+ this.screen.classList.add('trustedui');
+ },
+
+ isVisible: function trui_show() {
+ return this.screen.classList.contains('trustedui');
+ },
+
+ setHeight: function trui_setHeight(height) {
+ this.overlay.style.height = height + 'px';
+ },
+
+ /*
+ * _destroyDialog: internal method called when the dialog is closed
+ * by user action (canceled), or when 'appterminated' is received.
+ * In either case, notify the caller.
+ */
+ _destroyDialog: function trui_destroyDialog(origin) {
+ var stack = this.currentStack;
+ if (origin)
+ stack = this._dialogStacks[origin];
+
+ if (stack.length === 0)
+ return;
+
+ // If the user closed a trusty UI dialog, they probably meant
+ // to close every dialog.
+ for (var i = 0, toClose = stack.length; i < toClose; i++) {
+ var dialog = this._getTopDialog();
+
+ // First, send a chrome event saying we've been canceled
+ this._dispatchCloseEvent(dialog.chromeEventId);
+
+ // Now close and fire the cancel callback, if it exists
+ this.close(dialog.chromeEventId, dialog.onCancelCB);
+ }
+ this.hide();
+ this.popupContainer.classList.remove('closing');
+ },
+
+ _hideAllFrames: function trui_hideAllFrames() {
+ var selectedFrames = this.container.querySelectorAll('iframe.selected');
+ for (var i = 0; i < selectedFrames.length; i++) {
+ selectedFrames[i].classList.remove('selected');
+ }
+ },
+
+ handleEvent: function trui_handleEvent(evt) {
+ switch (evt.type) {
+ case 'home':
+ case 'holdhome':
+ if (!this.isVisible())
+ return;
+
+ this.hideTrustedApp();
+ break;
+ case 'click':
+ // Close-button clicked
+ this._destroyDialog();
+ break;
+ case 'appterminated':
+ this._destroyDialog(evt.detail.origin);
+ break;
+ case 'appwillopen':
+ // Hiding trustedUI when coming from Activity
+ if (this.isVisible())
+ this.hideTrustedApp();
+
+ // Ignore homescreen
+ if (evt.target.classList.contains('homescreen'))
+ return;
+ this._lastDisplayedApp = evt.detail.origin;
+ if (this.currentStack.length) {
+ // Reopening an app with trustedUI
+ this.popupContainer.classList.remove('up');
+ this._makeDialogVisible(this._getTopDialog());
+ WindowManager.hideCurrentApp();
+ this.reopenTrustedApp();
+ }
+ break;
+ case 'appopen':
+ if (this.currentStack.length) {
+ screen.mozLockOrientation('portrait');
+ }
+ break;
+ case 'appwillclose':
+ if (this.isVisible())
+ return;
+ var dialog = this._getTopDialog();
+ this._makeDialogHidden(dialog);
+ this.hide();
+ break;
+ case 'keyboardchange':
+ this.setHeight(window.innerHeight -
+ StatusBar.height - evt.detail.height);
+ break;
+ case 'keyboardhide':
+ this.setHeight(window.innerHeight - StatusBar.height);
+ break;
+ case 'mozbrowserloadstart':
+ this.throbber.classList.add('loading');
+ break;
+ case 'mozbrowserloadend':
+ this.throbber.classList.remove('loading');
+ break;
+ }
+ }
+
+};
+
+TrustedUIManager.init();
+
diff --git a/apps/system/js/ttlview.js b/apps/system/js/ttlview.js
new file mode 100644
index 0000000..81503af
--- /dev/null
+++ b/apps/system/js/ttlview.js
@@ -0,0 +1,50 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var TTLView = {
+ element: null,
+
+ get visible() {
+ return this.element && this.element.style.display === 'block';
+ },
+
+ hide: function tv_hide() {
+ if (this.element)
+ this.element.style.visibility = 'hidden';
+ },
+
+ show: function tv_show() {
+ var element = this.element;
+ if (!element) {
+ element = document.createElement('div');
+ element.id = 'debug-ttl';
+ element.innerHTML = '00000';
+ element.dataset.zIndexLevel = 'debug-ttl';
+
+ this.element = element;
+ document.getElementById('screen').appendChild(element);
+
+ // this is fired when the app launching is initialized
+ window.addEventListener('appwillopen', function willopen(e) {
+ element.innerHTML = '00000';
+ });
+
+ window.addEventListener('apploadtime', function apploadtime(e) {
+ element.innerHTML = e.detail.time + ' [' + e.detail.type + ']';
+ });
+ }
+
+ element.style.visibility = 'visible';
+ },
+
+ toggle: function tv_toggle() {
+ this.visible ? this.hide() : this.show();
+ }
+};
+
+SettingsListener.observe('debug.ttl.enabled', false, function(value) {
+ !!value ? TTLView.show() : TTLView.hide();
+});
+
diff --git a/apps/system/js/updatable.js b/apps/system/js/updatable.js
new file mode 100644
index 0000000..57ce737
--- /dev/null
+++ b/apps/system/js/updatable.js
@@ -0,0 +1,269 @@
+'use strict';
+
+/*
+ * An Updatable object represents an application *or* system update.
+ * It takes care of the interaction with the UpdateManager and observes
+ * the update itself to handle success/error cases.
+ *
+ * - name of the update
+ * - size of the update
+ * - download() to start the download
+ * - cancelDownload() to cancel it
+ */
+
+/* === App Updates === */
+function AppUpdatable(app) {
+ this._mgmt = navigator.mozApps.mgmt;
+ this.app = app;
+
+ var manifest = app.manifest ? app.manifest : app.updateManifest;
+ this.name = new ManifestHelper(manifest).name;
+
+ this.size = app.downloadSize;
+ this.progress = null;
+
+ UpdateManager.addToUpdatableApps(this);
+ app.ondownloadavailable = this.availableCallBack.bind(this);
+ if (app.downloadAvailable) {
+ this.availableCallBack();
+ }
+ if (app.readyToApplyDownload) {
+ this.applyUpdate();
+ }
+}
+
+AppUpdatable.prototype.download = function() {
+ UpdateManager.addToDownloadsQueue(this);
+ this.progress = 0;
+
+ this.app.download();
+};
+
+AppUpdatable.prototype.cancelDownload = function() {
+ this.app.cancelDownload();
+};
+
+AppUpdatable.prototype.uninit = function() {
+ this.app.ondownloadavailable = null;
+ this.clean();
+};
+
+AppUpdatable.prototype.clean = function() {
+ this.app.ondownloaderror = null;
+ this.app.ondownloadsuccess = null;
+ this.app.ondownloadapplied = null;
+ this.app.onprogress = null;
+
+ this.progress = null;
+};
+
+AppUpdatable.prototype.availableCallBack = function() {
+ this.size = this.app.downloadSize;
+
+ if (this.app.installState === 'installed') {
+ UpdateManager.addToUpdatesQueue(this);
+
+ // we add these callbacks only now to prevent interfering
+ // with other modules (especially the AppInstallManager)
+ this.app.ondownloaderror = this.errorCallBack.bind(this);
+ this.app.ondownloadsuccess = this.successCallBack.bind(this);
+ this.app.ondownloadapplied = this.appliedCallBack.bind(this);
+ this.app.onprogress = this.progressCallBack.bind(this);
+ }
+};
+
+AppUpdatable.prototype.successCallBack = function() {
+ var app = this.app;
+ if (WindowManager.getDisplayedApp() !== app.origin) {
+ this.applyUpdate();
+ } else {
+ var self = this;
+ window.addEventListener('appwillclose', function waitClose() {
+ window.removeEventListener('appwillclose', waitClose);
+ self.applyUpdate();
+ });
+ }
+
+ UpdateManager.removeFromDownloadsQueue(this);
+ UpdateManager.removeFromUpdatesQueue(this);
+};
+
+AppUpdatable.prototype.applyUpdate = function() {
+ WindowManager.kill(this.app.origin);
+ this._mgmt.applyDownload(this.app);
+};
+
+AppUpdatable.prototype.appliedCallBack = function() {
+ this.clean();
+};
+
+AppUpdatable.prototype.errorCallBack = function(e) {
+ var errorName = e.application.downloadError.name;
+ console.info('downloadError event, error code is', errorName);
+ UpdateManager.requestErrorBanner();
+ UpdateManager.removeFromDownloadsQueue(this);
+ this.progress = null;
+};
+
+AppUpdatable.prototype.progressCallBack = function() {
+ if (this.progress === null) {
+ // this is the first progress
+ UpdateManager.addToDownloadsQueue(this);
+ this.progress = 0;
+ }
+
+ var delta = this.app.progress - this.progress;
+
+ this.progress = this.app.progress;
+ UpdateManager.downloadProgressed(delta);
+};
+
+/*
+ * System Updates
+ * Will be instanciated only once by the UpdateManager
+ *
+ */
+function SystemUpdatable() {
+ var _ = navigator.mozL10n.get;
+ this.name = _('systemUpdate');
+ this.size = 0;
+ this.downloading = false;
+ this.paused = false;
+
+ // XXX: this state should be kept on the platform side
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=827090
+ this.checkKnownUpdate(UpdateManager.checkForUpdates.bind(UpdateManager));
+
+ window.addEventListener('mozChromeEvent', this);
+}
+
+SystemUpdatable.KNOWN_UPDATE_FLAG = 'known-sysupdate';
+
+SystemUpdatable.prototype.download = function() {
+ if (this.downloading) {
+ return;
+ }
+
+ this.downloading = true;
+ this.paused = false;
+ this._dispatchEvent('update-available-result', 'download');
+ UpdateManager.addToDownloadsQueue(this);
+ this.progress = 0;
+};
+
+SystemUpdatable.prototype.cancelDownload = function() {
+ this._dispatchEvent('update-download-cancel');
+ this.downloading = false;
+ this.paused = false;
+};
+
+SystemUpdatable.prototype.uninit = function() {
+ window.removeEventListener('mozChromeEvent', this);
+};
+
+SystemUpdatable.prototype.handleEvent = function(evt) {
+ if (evt.type !== 'mozChromeEvent')
+ return;
+
+ var detail = evt.detail;
+ if (!detail.type)
+ return;
+
+ switch (detail.type) {
+ case 'update-error':
+ this.errorCallBack();
+ break;
+ case 'update-download-started':
+ // TODO UpdateManager glue
+ this.paused = false;
+ break;
+ case 'update-download-progress':
+ var delta = detail.progress - this.progress;
+ this.progress = detail.progress;
+
+ UpdateManager.downloadProgressed(delta);
+ break;
+ case 'update-download-stopped':
+ // TODO UpdateManager glue
+ this.paused = detail.paused;
+ if (!this.paused) {
+ UpdateManager.startedUncompressing();
+ }
+ break;
+ case 'update-downloaded':
+ this.downloading = false;
+ this.showApplyPrompt();
+ break;
+ case 'update-prompt-apply':
+ this.showApplyPrompt();
+ break;
+ }
+};
+
+SystemUpdatable.prototype.errorCallBack = function() {
+ UpdateManager.requestErrorBanner();
+ UpdateManager.removeFromDownloadsQueue(this);
+ this.downloading = false;
+};
+
+SystemUpdatable.prototype.showApplyPrompt = function() {
+ var _ = navigator.mozL10n.get;
+
+ // Update will be completed after restart
+ this.forgetKnownUpdate();
+
+ var cancel = {
+ title: _('later'),
+ callback: this.declineInstall.bind(this)
+ };
+
+ var confirm = {
+ title: _('installNow'),
+ callback: this.acceptInstall.bind(this)
+ };
+
+ UtilityTray.hide();
+ CustomDialog.show(_('systemUpdateReady'), _('wantToInstall'),
+ cancel, confirm);
+};
+
+SystemUpdatable.prototype.declineInstall = function() {
+ CustomDialog.hide();
+ this._dispatchEvent('update-prompt-apply-result', 'wait');
+
+ UpdateManager.removeFromDownloadsQueue(this);
+};
+
+SystemUpdatable.prototype.acceptInstall = function() {
+ CustomDialog.hide();
+ this._dispatchEvent('update-prompt-apply-result', 'restart');
+};
+
+SystemUpdatable.prototype.rememberKnownUpdate = function() {
+ asyncStorage.setItem(SystemUpdatable.KNOWN_UPDATE_FLAG, true);
+};
+
+SystemUpdatable.prototype.checkKnownUpdate = function(callback) {
+ if (typeof callback !== 'function') {
+ return;
+ }
+
+ asyncStorage.getItem(SystemUpdatable.KNOWN_UPDATE_FLAG, function(value) {
+ callback(!!value);
+ });
+};
+
+SystemUpdatable.prototype.forgetKnownUpdate = function() {
+ asyncStorage.removeItem(SystemUpdatable.KNOWN_UPDATE_FLAG);
+};
+
+SystemUpdatable.prototype._dispatchEvent = function(type, result) {
+ var event = document.createEvent('CustomEvent');
+ var data = { type: type };
+ if (result) {
+ data.result = result;
+ }
+
+ event.initCustomEvent('mozContentEvent', true, true, data);
+ window.dispatchEvent(event);
+};
diff --git a/apps/system/js/update_manager.js b/apps/system/js/update_manager.js
new file mode 100644
index 0000000..8e37733
--- /dev/null
+++ b/apps/system/js/update_manager.js
@@ -0,0 +1,623 @@
+'use strict';
+
+/*
+ * The UpdateManager is a central component for apps *and* system updates.
+ * The user can start or cancel all downloads at once.
+ * This component also makes sure of bothering the user to a minimum by
+ * showing active notifications for new updates/errors only once.
+ *
+ * It maintains 2 queues of Updatable objects.
+ * - updatesQueue for available updates
+ * - downloadsQueue for active downloads
+ */
+
+var UpdateManager = {
+ _mgmt: null,
+ _downloading: false,
+ _uncompressing: false,
+ _downloadedBytes: 0,
+ _errorTimeout: null,
+ _wifiLock: null,
+ _systemUpdateDisplayed: false,
+ _isDataConnectionWarningDialogEnabled: true,
+ _settings: null,
+ _conn: null,
+ NOTIFICATION_BUFFERING_TIMEOUT: 30 * 1000,
+ TOASTER_TIMEOUT: 1200,
+
+ container: null,
+ message: null,
+ toaster: null,
+ toasterMessage: null,
+ laterButton: null,
+ notnowButton: null,
+ downloadButton: null,
+ downloadViaDataConnectionButton: null,
+ downloadDialog: null,
+ downloadViaDataConnectionDialog: null,
+ downloadDialogTitle: null,
+ downloadDialogList: null,
+ lastUpdatesAvailable: 0,
+ _notificationTimeout: null,
+
+ updatableApps: [],
+ systemUpdatable: null,
+ updatesQueue: [],
+ downloadsQueue: [],
+
+ init: function um_init() {
+ if (!this._mgmt) {
+ this._mgmt = navigator.mozApps.mgmt;
+ }
+
+ this._mgmt.getAll().onsuccess = (function gotAll(evt) {
+ var apps = evt.target.result;
+ apps.forEach(function appIterator(app) {
+ new AppUpdatable(app);
+ });
+ }).bind(this);
+
+ this._settings = navigator.mozSettings;
+
+ this.systemUpdatable = new SystemUpdatable();
+
+ this.container = document.getElementById('update-manager-container');
+ this.message = this.container.querySelector('.message');
+
+ this.toaster = document.getElementById('update-manager-toaster');
+ this.toasterMessage = this.toaster.querySelector('.message');
+
+ this.laterButton = document.getElementById('updates-later-button');
+ this.notnowButton =
+ document.getElementById('updates-viaDataConnection-notnow-button');
+ this.downloadButton = document.getElementById('updates-download-button');
+ this.downloadViaDataConnectionButton =
+ document.getElementById('updates-viaDataConnection-download-button');
+ this.downloadDialog = document.getElementById('updates-download-dialog');
+ this.downloadDialogTitle = this.downloadDialog.querySelector('h1');
+ this.downloadDialogList = this.downloadDialog.querySelector('ul');
+ this.downloadViaDataConnectionDialog =
+ document.getElementById('updates-viaDataConnection-dialog');
+
+ this.container.onclick = this.containerClicked.bind(this);
+ this.laterButton.onclick = this.cancelPrompt.bind(this);
+ this.downloadButton.onclick = this.requestDownloads.bind(this);
+ this.downloadDialogList.onchange = this.updateDownloadButton.bind(this);
+ this.notnowButton.onclick =
+ this.cancelDataConnectionUpdatesPrompt.bind(this);
+ this.downloadViaDataConnectionButton.onclick =
+ this.requestDownloads.bind(this);
+
+ window.addEventListener('mozChromeEvent', this);
+ window.addEventListener('applicationinstall', this);
+ window.addEventListener('applicationuninstall', this);
+ window.addEventListener('online', this);
+ window.addEventListener('offline', this);
+
+ SettingsListener.observe('gaia.system.checkForUpdates', false,
+ this.checkForUpdates.bind(this));
+
+ // We maintain the the edge and nowifi data attributes to show
+ // a warning on the download dialog
+ window.addEventListener('wifi-statuschange', this);
+ this.updateWifiStatus();
+ this.updateOnlineStatus();
+
+ this._conn = window.navigator.mozMobileConnection;
+ if (this._conn) {
+ this._conn.addEventListener('datachange', this);
+ this.updateDataStatus();
+ }
+
+ window.asyncStorage.
+ getItem('gaia.system.isDataConnectionWarningDialogEnabled',
+ (function(value) {
+ value = value || true;
+ this._isDataConnectionWarningDialogEnabled = true;
+ this.downloadDialog.dataset.dataConnectionInlineWarning = !value;
+ }).bind(this));
+ },
+
+ requestDownloads: function um_requestDownloads(evt) {
+ evt.preventDefault();
+
+ if (evt.target == this.downloadViaDataConnectionButton) {
+ window.asyncStorage.
+ setItem('gaia.system.isDataConnectionWarningDialogEnabled', false);
+ this._isDataConnectionWarningDialogEnabled = false;
+ this.downloadDialog.dataset.dataConnectionInlineWarning = true;
+ this.startDownloads();
+ } else {
+ if (this._isDataConnectionWarningDialogEnabled &&
+ this.downloadDialog.dataset.nowifi) {
+ this.downloadViaDataConnectionDialog.classList.add('visible');
+ } else {
+ this.startDownloads();
+ }
+ }
+ },
+
+ startDownloads: function um_startDownloads() {
+ this.downloadDialog.classList.remove('visible');
+ this.downloadViaDataConnectionDialog.classList.remove('visible');
+
+ UtilityTray.show();
+
+ var checkValues = {};
+ var dialog = this.downloadDialogList;
+ var checkboxes = dialog.querySelectorAll('input[type="checkbox"]');
+ for (var i = 0; i < checkboxes.length; i++) {
+ var checkbox = checkboxes[i];
+ checkValues[checkbox.dataset.position] = checkbox.checked;
+ }
+
+ this.updatesQueue.forEach(function(updatable, index) {
+ // The user opted out of the download
+ if (updatable.app && !checkValues[index]) {
+ return;
+ }
+
+ updatable.download();
+ });
+
+ this._downloadedBytes = 0;
+ this.render();
+ },
+
+ cancelAllDownloads: function um_cancelAllDownloads() {
+ CustomDialog.hide();
+
+ // We're emptying the array while iterating
+ while (this.downloadsQueue.length) {
+ var updatable = this.downloadsQueue[0];
+ updatable.cancelDownload();
+ this.removeFromDownloadsQueue(updatable);
+ }
+ },
+
+ requestErrorBanner: function um_requestErrorBanner() {
+ if (this._errorTimeout)
+ return;
+
+ var _ = navigator.mozL10n.get;
+ var self = this;
+ this._errorTimeout = setTimeout(function waitForMore() {
+ SystemBanner.show(_('downloadError'));
+ self._errorTimeout = null;
+ }, this.NOTIFICATION_BUFFERING_TIMEOUT);
+ },
+
+ containerClicked: function um_containerClicker() {
+ var _ = navigator.mozL10n.get;
+
+ if (this._downloading) {
+ var cancel = {
+ title: _('no'),
+ callback: this.cancelPrompt.bind(this)
+ };
+
+ var confirm = {
+ title: _('yes'),
+ callback: this.cancelAllDownloads.bind(this)
+ };
+
+ CustomDialog.show(_('cancelAllDownloads'), _('wantToCancelAll'),
+ cancel, confirm);
+ } else {
+ this.showDownloadPrompt();
+ }
+
+ UtilityTray.hide();
+ },
+
+ showDownloadPrompt: function um_showDownloadPrompt() {
+ var _ = navigator.mozL10n.get;
+
+ this._systemUpdateDisplayed = false;
+ this.downloadDialogTitle.textContent = _('numberOfUpdates', {
+ n: this.updatesQueue.length
+ });
+
+ var updateList = '';
+
+ // System update should always be on top
+ this.updatesQueue.sort(function sortUpdates(updatable, otherUpdatable) {
+ if (!updatable.app)
+ return -1;
+ if (!otherUpdatable.app)
+ return 1;
+
+ if (updatable.name < otherUpdatable.name)
+ return -1;
+ if (updatable.name > otherUpdatable.name)
+ return 1;
+ return 0;
+ });
+
+ this.downloadDialogList.innerHTML = '';
+ this.updatesQueue.forEach(function updatableIterator(updatable, index) {
+ var listItem = document.createElement('li');
+
+ // The user can choose not to update an app
+ var checkContainer = document.createElement('label');
+ if (updatable instanceof SystemUpdatable) {
+ checkContainer.textContent = _('required');
+ checkContainer.classList.add('required');
+ this._systemUpdateDisplayed = true;
+ } else {
+ var checkbox = document.createElement('input');
+ checkbox.type = 'checkbox';
+ checkbox.dataset.position = index;
+ checkbox.checked = true;
+
+ var span = document.createElement('span');
+
+ checkContainer.appendChild(checkbox);
+ checkContainer.appendChild(span);
+ }
+ listItem.appendChild(checkContainer);
+
+ var name = document.createElement('div');
+ name.classList.add('name');
+ name.textContent = updatable.name;
+ listItem.appendChild(name);
+
+ if (updatable.size) {
+ var sizeItem = document.createElement('div');
+ sizeItem.textContent = this._humanizeSize(updatable.size);
+ listItem.appendChild(sizeItem);
+ } else {
+ listItem.classList.add('nosize');
+ }
+
+ this.downloadDialogList.appendChild(listItem);
+ }, this);
+
+ this.downloadDialog.classList.add('visible');
+ },
+
+ updateDownloadButton: function() {
+ if (this._systemUpdateDisplayed) {
+ this.downloadButton.disabled = false;
+ return;
+ }
+
+ var disabled = true;
+
+ var dialog = this.downloadDialogList;
+ var checkboxes = dialog.querySelectorAll('input[type="checkbox"]');
+ for (var i = 0; i < checkboxes.length; i++) {
+ if (checkboxes[i].checked) {
+ disabled = false;
+ break;
+ }
+ }
+
+ this.downloadButton.disabled = disabled;
+ },
+
+ cancelPrompt: function um_cancelPrompt() {
+ CustomDialog.hide();
+ this.downloadDialog.classList.remove('visible');
+ },
+
+ cancelDataConnectionUpdatesPrompt: function um_cancelDCUpdatesPrompt() {
+ CustomDialog.hide();
+ this.downloadViaDataConnectionDialog.classList.remove('visible');
+ this.downloadDialog.classList.remove('visible');
+ },
+
+ downloadProgressed: function um_downloadProgress(bytes) {
+ if (bytes > 0) {
+ this._downloadedBytes += bytes;
+ this.render();
+ }
+ },
+
+ startedUncompressing: function um_startedUncompressing() {
+ this._uncompressing = true;
+ this.render();
+ },
+
+ render: function um_render() {
+ var _ = navigator.mozL10n.get;
+
+ this.toasterMessage.innerHTML =
+ _('updateAvailableInfo', {
+ n: this.updatesQueue.length - this.lastUpdatesAvailable
+ });
+
+ var message = '';
+ if (this._downloading) {
+ if (this._uncompressing && this.downloadsQueue.length === 1) {
+ message = _('uncompressingMessage');
+ } else {
+ var humanProgress = this._humanizeSize(this._downloadedBytes);
+ message = _('downloadingUpdateMessage', {
+ progress: humanProgress
+ });
+ }
+ } else {
+ message = _('updateAvailableInfo', {
+ n: this.updatesQueue.length
+ });
+ }
+
+ this.message.innerHTML = message;
+ var css = this.container.classList;
+ this._downloading ? css.add('downloading') : css.remove('downloading');
+ },
+
+ addToUpdatableApps: function um_addtoUpdatableapps(updatableApp) {
+ this.updatableApps.push(updatableApp);
+ },
+
+ removeFromAll: function um_removeFromAll(updatableApp) {
+ var removeIndex = this.updatableApps.indexOf(updatableApp);
+ if (removeIndex === -1)
+ return;
+
+ var removedApp = this.updatableApps[removeIndex];
+ this.removeFromUpdatesQueue(removedApp);
+
+ removedApp.uninit();
+ this.updatableApps.splice(removeIndex, 1);
+ },
+
+ addToUpdatesQueue: function um_addToUpdatesQueue(updatable) {
+ if (this._downloading) {
+ return;
+ }
+
+ if (updatable.app &&
+ updatable.app.installState !== 'installed') {
+ return;
+ }
+
+ if (updatable.app &&
+ this.updatableApps.indexOf(updatable) === -1) {
+ return;
+ }
+
+ var alreadyThere = this.updatesQueue.some(function lookup(u) {
+ return (u.app === updatable.app);
+ });
+ if (alreadyThere) {
+ return;
+ }
+
+ this.updatesQueue.push(updatable);
+
+ if (this._notificationTimeout === null) {
+ this._notificationTimeout = setTimeout(this.displayNotificationAndToaster.bind(this),
+ this.NOTIFICATION_BUFFERING_TIMEOUT);
+ }
+ this.render();
+ },
+
+ displayNotificationAndToaster: function um_displayNotificationAndToaster() {
+ this._notificationTimeout = null;
+ if (this.updatesQueue.length && !this._downloading) {
+ this.lastUpdatesAvailable = this.updatesQueue.length;
+ StatusBar.updateNotificationUnread(true);
+ this.displayNotificationIfHidden();
+ this.toaster.classList.add('displayed');
+ var self = this;
+ setTimeout(function waitToHide() {
+ self.toaster.classList.remove('displayed');
+ }, this.TOASTER_TIMEOUT);
+ }
+ },
+
+ removeFromUpdatesQueue: function um_removeFromUpdatesQueue(updatable) {
+ var removeIndex = this.updatesQueue.indexOf(updatable);
+ if (removeIndex === -1)
+ return;
+
+ this.updatesQueue.splice(removeIndex, 1);
+ this.lastUpdatesAvailable = this.updatesQueue.length;
+
+ if (this.updatesQueue.length === 0) {
+ this.hideNotificationIfDisplayed();
+ }
+
+ this.render();
+ },
+
+ addToDownloadsQueue: function um_addToDownloadsQueue(updatable) {
+ if (updatable.app &&
+ this.updatableApps.indexOf(updatable) === -1) {
+ return;
+ }
+
+ var alreadyThere = this.downloadsQueue.some(function lookup(u) {
+ return (u.app === updatable.app);
+ });
+ if (alreadyThere) {
+ return;
+ }
+
+ this.downloadsQueue.push(updatable);
+
+ if (this.downloadsQueue.length === 1) {
+ this._downloading = true;
+ StatusBar.incSystemDownloads();
+ this._wifiLock = navigator.requestWakeLock('wifi');
+
+ this.displayNotificationIfHidden();
+ this.render();
+ }
+ },
+
+ removeFromDownloadsQueue: function um_removeFromDownloadsQueue(updatable) {
+ var removeIndex = this.downloadsQueue.indexOf(updatable);
+ if (removeIndex === -1)
+ return;
+
+ this.downloadsQueue.splice(removeIndex, 1);
+
+ if (this.downloadsQueue.length === 0) {
+ this._downloading = false;
+ StatusBar.decSystemDownloads();
+ this._downloadedBytes = 0;
+ this.checkStatuses();
+
+ if (this._wifiLock) {
+ try {
+ this._wifiLock.unlock();
+ } catch (e) {
+ // this can happen if the lock is already unlocked
+ console.error('error during unlock', e);
+ }
+
+ this._wifiLock = null;
+ }
+
+ this.render();
+ }
+ },
+
+ hideNotificationIfDisplayed: function() {
+ if (this.container.classList.contains('displayed')) {
+ this.container.classList.remove('displayed');
+ NotificationScreen.decExternalNotifications();
+ }
+ },
+
+ displayNotificationIfHidden: function() {
+ if (!this.container.classList.contains('displayed')) {
+ this.container.classList.add('displayed');
+ NotificationScreen.incExternalNotifications();
+ }
+ },
+
+ checkStatuses: function um_checkStatuses() {
+ this.updatableApps.forEach(function(updatableApp) {
+ var app = updatableApp.app;
+ if (app.downloadAvailable) {
+ this.addToUpdatesQueue(updatableApp);
+ }
+ }, this);
+ },
+
+ oninstall: function um_oninstall(evt) {
+ var app = evt.application;
+ var updatableApp = new AppUpdatable(app);
+ },
+
+ onuninstall: function um_onuninstall(evt) {
+ this.updatableApps.some(function appIterator(updatableApp, index) {
+ // The application object we get from the event
+ // has only origin and manifestURL properties
+ if (updatableApp.app.manifestURL === evt.application.manifestURL) {
+ this.removeFromAll(updatableApp);
+ return true;
+ }
+ return false;
+ }, this);
+ },
+
+ handleEvent: function um_handleEvent(evt) {
+ if (!evt.type)
+ return;
+
+ switch (evt.type) {
+ case 'applicationinstall':
+ this.oninstall(evt.detail);
+ break;
+ case 'applicationuninstall':
+ this.onuninstall(evt.detail);
+ break;
+ case 'datachange':
+ this.updateDataStatus();
+ break;
+ case 'offline':
+ this.updateOnlineStatus();
+ break;
+ case 'online':
+ this.updateOnlineStatus();
+ break;
+ case 'wifi-statuschange':
+ this.updateWifiStatus();
+ break;
+ }
+
+ if (evt.type !== 'mozChromeEvent')
+ return;
+
+ var detail = evt.detail;
+
+ if (detail.type && detail.type === 'update-available') {
+ this.systemUpdatable.size = detail.size;
+ this.systemUpdatable.rememberKnownUpdate();
+ this.addToUpdatesQueue(this.systemUpdatable);
+ }
+ },
+
+ updateOnlineStatus: function su_updateOnlineStatus() {
+ var online = (navigator && 'onLine' in navigator) ? navigator.onLine : true;
+ this.downloadDialog.dataset.online = online;
+
+ if (online) {
+ this.laterButton.classList.remove('full');
+ } else {
+ this.laterButton.classList.add('full');
+ }
+ },
+
+ updateWifiStatus: function su_updateWifiStatus() {
+ var wifiManager = window.navigator.mozWifiManager;
+ if (!wifiManager)
+ return;
+
+ this.downloadDialog.dataset.nowifi =
+ (wifiManager.connection.status != 'connected');
+ },
+
+ checkForUpdates: function su_checkForUpdates(shouldCheck) {
+ if (!shouldCheck) {
+ return;
+ }
+
+ this._dispatchEvent('force-update-check');
+
+ if (!this._settings) {
+ return;
+ }
+
+ var lock = this._settings.createLock();
+ lock.set({
+ 'gaia.system.checkForUpdates': false
+ });
+ },
+
+ _dispatchEvent: function um_dispatchEvent(type, result) {
+ var event = document.createEvent('CustomEvent');
+ var data = { type: type };
+ if (result) {
+ data.result = result;
+ }
+
+ event.initCustomEvent('mozContentEvent', true, true, data);
+ window.dispatchEvent(event);
+ },
+
+ // This is going to be part of l10n.js
+ _humanizeSize: function um_humanizeSize(bytes) {
+ var _ = navigator.mozL10n.get;
+ var units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'];
+
+ if (!bytes)
+ return '0.00 ' + _(units[0]);
+
+ var e = Math.floor(Math.log(bytes) / Math.log(1024));
+ return (bytes / Math.pow(1024, Math.floor(e))).toFixed(2) + ' ' +
+ _(units[e]);
+ }
+};
+
+window.addEventListener('localized', function startup(evt) {
+ window.removeEventListener('localized', startup);
+
+ UpdateManager.init();
+});
diff --git a/apps/system/js/utility_tray.js b/apps/system/js/utility_tray.js
new file mode 100644
index 0000000..13f124d
--- /dev/null
+++ b/apps/system/js/utility_tray.js
@@ -0,0 +1,148 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var UtilityTray = {
+ shown: false,
+
+ active: false,
+
+ overlay: document.getElementById('utility-tray'),
+
+ statusbar: document.getElementById('statusbar'),
+
+ screen: document.getElementById('screen'),
+
+ init: function ut_init() {
+ var touchEvents = ['touchstart', 'touchmove', 'touchend'];
+
+ // XXX: Always use Mouse2Touch here.
+ // We cannot reliably detect touch support normally
+ // by evaluate (document instanceof DocumentTouch) on Desktop B2G.
+ touchEvents.forEach(function bindEvents(name) {
+ // window.addEventListener(name, this);
+ Mouse2Touch.addEventHandler(window, name, this);
+ }, this);
+
+ window.addEventListener('screenchange', this);
+ window.addEventListener('home', this);
+
+ this.overlay.addEventListener('transitionend', this);
+ },
+
+ handleEvent: function ut_handleEvent(evt) {
+ switch (evt.type) {
+ case 'home':
+ if (this.shown) {
+ this.hide();
+ }
+ break;
+
+ case 'screenchange':
+ if (this.shown && !evt.detail.screenEnabled)
+ this.hide(true);
+
+ break;
+
+ case 'touchstart':
+ if (LockScreen.locked)
+ return;
+ if (evt.target !== this.overlay &&
+ evt.target !== this.statusbar)
+ return;
+
+ this.active = true;
+ // XXX: required for Mouse2Touch fake events to function
+ evt.target.setCapture(true);
+
+ this.onTouchStart(evt.touches[0]);
+ break;
+
+ case 'touchmove':
+ if (!this.active)
+ return;
+
+ this.onTouchMove(evt.touches[0]);
+ break;
+
+ case 'touchend':
+ if (!this.active)
+ return;
+
+ this.active = false;
+ // XXX: required for Mouse2Touch fake events to function
+ document.releaseCapture();
+
+ this.onTouchEnd(evt.changedTouches[0]);
+ break;
+
+ case 'transitionend':
+ if (!this.shown)
+ this.screen.classList.remove('utility-tray');
+ break;
+ }
+ },
+
+ onTouchStart: function ut_onTouchStart(touch) {
+ this.startX = touch.pageX;
+ this.startY = touch.pageY;
+ this.screen.classList.add('utility-tray');
+ this.onTouchMove({ pageY: touch.pageY + this.statusbar.offsetHeight });
+ },
+
+ onTouchMove: function ut_onTouchMove(touch) {
+ var screenHeight = this.overlay.getBoundingClientRect().height;
+ var y = touch.pageY;
+ if (y > this.lastY)
+ this.opening = true;
+ else if (y < this.lastY)
+ this.opening = false;
+ this.lastY = y;
+ var dy = -(this.startY - y);
+ if (this.shown)
+ dy += screenHeight;
+ dy = Math.min(screenHeight, dy);
+
+ var style = this.overlay.style;
+ style.MozTransition = '';
+ style.MozTransform = 'translateY(' + dy + 'px)';
+ },
+
+ onTouchEnd: function ut_onTouchEnd(touch) {
+ this.opening ? this.show() : this.hide();
+ },
+
+ hide: function ut_hide(instant) {
+ var alreadyHidden = !this.shown;
+ var style = this.overlay.style;
+ style.MozTransition = instant ? '' : '-moz-transform 0.2s linear';
+ style.MozTransform = 'translateY(0)';
+ this.shown = false;
+ if (instant)
+ this.screen.classList.remove('utility-tray');
+
+ if (!alreadyHidden) {
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('utilitytrayhide', true, true, null);
+ window.dispatchEvent(evt);
+ }
+ },
+
+ show: function ut_show(dy) {
+ var alreadyShown = this.shown;
+ var style = this.overlay.style;
+ style.MozTransition = '-moz-transform 0.2s linear';
+ style.MozTransform = 'translateY(100%)';
+ this.shown = true;
+ this.screen.classList.add('utility-tray');
+
+ if (!alreadyShown) {
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('utilitytrayshow', true, true, null);
+ window.dispatchEvent(evt);
+ }
+ }
+};
+
+UtilityTray.init();
diff --git a/apps/system/js/value_selector/date_picker.js b/apps/system/js/value_selector/date_picker.js
new file mode 100644
index 0000000..9cdd484
--- /dev/null
+++ b/apps/system/js/value_selector/date_picker.js
@@ -0,0 +1,568 @@
+/**
+ * DatePicker is a html/js "widget" which will display
+ * all the days of a given month and allow selection of
+ * one specific day. It also implements controls to travel
+ * between months and jump into arbitrary time.
+ *
+ * The DatePicker itself contains no UI for the controls.
+ *
+ * Example usage:
+ *
+ * // the container will have elements for the month
+ * // added and removed from it.
+ * var picker = new DatePicker(container);
+ *
+ * // EVENTS:
+ *
+ * // called when the user clicks a day in the calendar.
+ * picker.onvaluechange = function(date) {}
+ *
+ * // called when the month of the calendar changes.
+ * // NOTE: at this time this can only happen programmatically
+ * // so there is only for control flow.
+ * picker.onmonthchange = function(date) {}
+ *
+ * // display a given year/month/date on the calendar the month
+ * // is zero based just like the JS date constructor.
+ * picker.display(2012, 0, 2);
+ *
+ * // move to the next month.
+ * picker.next();
+ *
+ * // move to the previous month
+ * picker.previous();
+ *
+ */
+var DatePicker = (function() {
+ 'use strict';
+
+ const SELECTED = 'selected';
+
+ var Calc = {
+
+ NEXT_MONTH: 'next-month',
+
+ OTHER_MONTH: 'other-month',
+
+ PRESENT: 'present',
+
+ FUTURE: 'future',
+
+ PAST: 'past',
+
+ get today() {
+ return new Date();
+ },
+
+ daysInWeek: function() {
+ //XXX: We need to localize this...
+ return 7;
+ },
+
+ /**
+ * Checks is given date is today.
+ *
+ * @param {Date} date compare.
+ * @return {Boolean} true when today.
+ */
+ isToday: function(date) {
+ return Calc.isSameDate(date, Calc.today);
+ },
+
+ /**
+ * Checks if two date objects occur
+ * on the same date (in the same month, year, day).
+ * Disregards time.
+ *
+ * @param {Date} first date.
+ * @param {Date} second date.
+ * @return {Boolean} true when they are the same date.
+ */
+ isSameDate: function(first, second) {
+ return first.getMonth() == second.getMonth() &&
+ first.getDate() == second.getDate() &&
+ first.getFullYear() == second.getFullYear();
+ },
+
+ /**
+ * Returns an identifier for a specific
+ * date in time for a given date
+ *
+ * @param {Date} date to get id for.
+ * @return {String} identifier.
+ */
+ getDayId: function(date) {
+ return [
+ date.getFullYear(),
+ date.getMonth(),
+ date.getDate()
+ ].join('-');
+ },
+
+ /**
+ * Returns a date object from
+ * a string id for a date.
+ *
+ * @param {String} id identifier for date.
+ * @return {Date} date output.
+ */
+ dateFromId: function(id) {
+ var parts = id.split('-');
+ return new Date(parts[0], parts[1], parts[2]);
+ },
+
+ createDay: function(date, day, month, year) {
+ return new Date(
+ typeof year !== 'undefined' ? year : date.getFullYear(),
+ typeof month !== 'undefined' ? month : date.getMonth(),
+ typeof day !== 'undefined' ? day : date.getDate()
+ );
+ },
+
+ /**
+ * Finds localized week start date of given date.
+ *
+ * @param {Date} date any day the week.
+ * @return {Date} first date in the week of given date.
+ */
+ getWeekStartDate: function(date) {
+ var currentDay = date.getDay();
+ var startDay = date.getDate() - currentDay;
+
+ return Calc.createDay(date, startDay);
+ },
+
+ getWeekEndDate: function(date) {
+ // TODO: There are localization problems
+ // with this approach as we assume a 7 day week.
+ var start = Calc.getWeekStartDate(date);
+ start.setDate(start.getDate() + 7);
+ start.setMilliseconds(-1);
+
+ return start;
+ },
+
+ /**
+ * Returns an array of dates objects.
+ * Inclusive. First and last are
+ * the given instances.
+ *
+ * @param {Date} start starting day.
+ * @param {Date} end ending day.
+ * @param {Boolean} includeTime include times start/end ?
+ */
+ daysBetween: function(start, end, includeTime) {
+ if (!(start instanceof Date)) {
+ throw new Error('start date must be an instanceof Date');
+ }
+
+ if (!(end instanceof Date)) {
+ throw new Error('end date must be an instanceof Date');
+ }
+
+ if (start > end) {
+ var tmp = end;
+ end = start;
+ start = tmp;
+ tmp = null;
+ }
+
+ var list = [];
+ var last = start.getDate();
+ var cur;
+
+ // for infinite loop protection.
+ var max = 500;
+ var macInc = 0;
+
+ while (macInc++ < max) {
+ var next = new Date(
+ start.getFullYear(),
+ start.getMonth(),
+ ++last
+ );
+
+ if (next > end) {
+ throw new Error(
+ 'sanity fails next is greater then end'
+ );
+ }
+
+ if (!Calc.isSameDate(next, end)) {
+ list.push(next);
+ continue;
+ }
+
+ break;
+ }
+
+ if (includeTime) {
+ list.unshift(start);
+ list.push(end);
+ } else {
+ list.unshift(this.createDay(start));
+ list.push(this.createDay(end));
+ }
+
+ return list;
+ },
+
+ /**
+ * Checks if date is in the past
+ *
+ * @param {Date} date to check.
+ * @return {Boolean} true when date is in the past.
+ */
+ isPast: function(date) {
+ return (date.valueOf() < Calc.today.valueOf());
+ },
+
+ /**
+ * Checks if date is in the future
+ *
+ * @param {Date} date to check.
+ * @return {Boolean} true when date is in the future.
+ */
+ isFuture: function(date) {
+ return !Calc.isPast(date);
+ },
+
+ /**
+ * Based on the input date
+ * will return one of the following states
+ *
+ * past, present, future
+ *
+ * @param {Date} day for compare.
+ * @param {Date} month comparison month.
+ * @return {String} state.
+ */
+ relativeState: function(day, month) {
+ var states;
+ //var today = Calc.today;
+
+ // 1. the date is today (real time)
+ if (Calc.isToday(day)) {
+ return Calc.PRESENT;
+ }
+
+ // 2. the date is in the past (real time)
+ if (Calc.isPast(day)) {
+ states = Calc.PAST;
+ // 3. the date is in the future (real time)
+ } else {
+ states = Calc.FUTURE;
+ }
+
+ // 4. the date is not in the current month (relative time)
+ if (day.getMonth() !== month.getMonth()) {
+ states += ' ' + Calc.OTHER_MONTH;
+ }
+
+ return states;
+ }
+
+ };
+
+ /* expose calc */
+ DatePicker.Calc = Calc;
+
+ /**
+ * Initialize a date picker widget.
+ *
+ * @param {HTMLELement} element target of widget creation.
+ */
+ function DatePicker(element) {
+ this.element = element;
+ // default time is set so next/previous work
+ // but we do not render the initial display here.
+ this._position = new Date();
+
+ // register events
+ element.addEventListener('click', this);
+
+ //XXX: When the document is localized again
+ // we must also re-render the month because
+ // the week days may have changed?
+ // This will only happen when we change timezones
+ // unless we add this information to the locales.
+ }
+
+ DatePicker.prototype = {
+
+ /**
+ * Internal value not exposed so we can fire events
+ * when the getter/setter's are used.
+ *
+ * @type Date
+ */
+ _value: null,
+
+ SELECTED: 'selected',
+
+ /**
+ * Gets current value
+ *
+ * @return {Null|Date} date or null.
+ */
+ get value() {
+ return this._value;
+ },
+
+ /**
+ * Sets the current value of the date picker.
+ * When value differs from the currently set the
+ * `onvaluechange` event will be fired with the new/old value.
+ */
+ set value(value) {
+ var old = this._value;
+ if (old !== value) {
+ this._value = value;
+ this._clearSelectedDay(value);
+ this.onvaluechange(value, old);
+ }
+ },
+
+ /**
+ * Clears the currently selected date of its 'selected' class.
+ * @private
+ */
+ _clearSelectedDay: function(value) {
+ var target = this.element.querySelector('.' + SELECTED);
+ if (target) {
+ target.classList.remove(SELECTED);
+ }
+ },
+
+ handleEvent: function(e) {
+ switch (e.type) {
+ case 'click':
+ var target = e.target;
+ //XXX: if the html of the date elements changes
+ // this may also need to be altered as it
+ // assumes that there is no nesting of elements.
+ if (target.dataset.date) {
+ var date = Calc.dateFromId(target.dataset.date);
+ // order here is important as setting value will
+ // clear all the past selected dates...
+ this.value = date;
+ this._position = date;
+ // must come after setting selected date
+ target.classList.add(SELECTED);
+ }
+ break;
+ }
+ },
+
+ /**
+ * Getter is used for date normalization.
+ */
+ get year() {
+ return this._position.getFullYear();
+ },
+
+ /**
+ * Getter is used for date normalization.
+ */
+ get month() {
+ return this._position.getMonth();
+ },
+
+ get date() {
+ return this._position.getDate();
+ },
+
+ /**
+ * Find the number of days in the given month/year.
+ * Month is zero based like the JS date constructor.
+ *
+ * @param {Numeric} year year value.
+ * @param {Numeric} month month value.
+ * @return {Numeric} number of days in month.
+ */
+ _daysInMonth: function(year, month) {
+ var end = new Date(year, month + 1);
+ end.setMilliseconds(-1);
+ return end.getDate();
+ },
+
+ /**
+ * Build the container for a day element.
+ * Each element has classes added to it based
+ * on what date it is created for.
+ *
+ * _today_ is based on today's actual date.
+ * Each date element also contains a data-date attribute
+ * with its current date as a string represented in
+ * the following format: "yyyy-mm-dd".
+ *
+ * Possible classes:
+ * - past
+ * - present (today)
+ * - future
+ * - other-month (day of another month but falls within same week)
+ *
+ * @param {Date} date date desired.
+ * @return {HTMLElement} dom element for day.
+ */
+ _renderDay: function(date) {
+ var dayContainer = document.createElement('li');
+ var dayEl = document.createElement('span');
+
+ dayContainer.className = Calc.relativeState(
+ date,
+ this._position
+ );
+
+ dayEl.dataset.date = Calc.getDayId(date);
+ dayEl.textContent = date.getDate();
+
+ dayContainer.appendChild(dayEl);
+
+ return dayContainer;
+ },
+
+ /**
+ * Renders a set of dates and returns an ol element
+ * containing each date.
+ *
+ * @private
+ * @param {Array[Date]} dates array of dates.
+ * @return {HTMLELement} container for week.
+ */
+ _renderWeek: function(dates) {
+ var container = document.createElement('ol');
+ var i = 0;
+ var len = dates.length;
+
+ for (; i < len; i++) {
+ container.appendChild(
+ this._renderDay(dates[i])
+ );
+ }
+
+ return container;
+ },
+
+ /**
+ * Finds all dates in a given month by week.
+ * Includes leading and trailing days that occur
+ * outside the given year/month combination.
+ *
+ * @private
+ * @param {Numeric} year target year.
+ * @param {Numeric} month target month.
+ * @return {Array[Date]} array of dates.
+ */
+ _getMonthDays: function(year, month) {
+ var date = new Date(year, month);
+ var dateEnd = new Date(year, month + 1);
+ dateEnd.setMilliseconds(-1);
+
+ var start = Calc.getWeekStartDate(date);
+ var end = Calc.getWeekEndDate(dateEnd);
+ return Calc.daysBetween(start, end);
+ },
+
+ /**
+ * Returns a section element with all
+ * the days of the given month/year pair.
+ *
+ * Each month has a class for the number of weeks
+ * it contains.
+ *
+ * Possible values:
+ * - weeks-4
+ * - weeks-5
+ * - weeks-6
+ *
+ * @private
+ */
+ _renderMonth: function(year, month) {
+ var container = document.createElement('section');
+ var days = this._getMonthDays(year, month);
+ var daysInWeek = Calc.daysInWeek();
+ var weeks = days.length / daysInWeek;
+ var i = 0;
+
+ container.classList.add('weeks-' + weeks);
+
+ for (; i < weeks; i++) {
+ container.appendChild(this._renderWeek(
+ days.splice(0, daysInWeek)
+ ));
+ }
+
+ return container;
+ },
+
+ /**
+ * Moves calendar one month into the future.
+ */
+ next: function() {
+ this.display(this.year, this.month + 1, this.date);
+ },
+
+ /**
+ * Moves calendar one month into the past.
+ */
+ previous: function() {
+ this.display(this.year, this.month - 1, this.date);
+ },
+
+ /**
+ * Primary method to display given month.
+ * Will remove the current display and replace
+ * it with the given month.
+ *
+ * @param {Numeric} year year to display.
+ * @param {Numeric} month month to display.
+ * @param {Numeric} date date to display.
+ */
+ display: function(year, month, date) {
+
+ // reset the date to the last date if overflow
+ var lastDate = new Date(year, month + 1, 0).getDate();
+ if (lastDate < date)
+ date = lastDate;
+
+ // Should come before render month
+ this._position = new Date(year, month, date);
+
+ var element = this._renderMonth(year, month);
+
+ if (this.monthDisplay) {
+ this.monthDisplay.parentNode.removeChild(
+ this.monthDisplay
+ );
+ }
+
+ this.monthDisplay = element;
+ this.element.appendChild(this.monthDisplay);
+
+ this.onmonthchange(this._position);
+
+ // Set the date as selected if presented
+ this._clearSelectedDay();
+ if (date) {
+ var dayId = Calc.getDayId(this._position);
+ this.value = this._position;
+ var selector = '[data-date="' + dayId + '"]';
+ var dateElement = document.querySelector(selector);
+ dateElement.classList.add(SELECTED);
+ }
+ },
+
+ /**
+ * Called when the month is changed.
+ */
+ onmonthchange: function(month, year) {},
+
+ /**
+ * Called when the selected day changes.
+ */
+ onvaluechange: function(date) {}
+ };
+
+ return DatePicker;
+}());
diff --git a/apps/system/js/value_selector/input_parser.js b/apps/system/js/value_selector/input_parser.js
new file mode 100644
index 0000000..5eef320
--- /dev/null
+++ b/apps/system/js/value_selector/input_parser.js
@@ -0,0 +1,160 @@
+/**
+ * Stateless object for input parser functions..
+ * The intent is the methods here will only relate to the parsing
+ * of input[type="date|time"]
+ */
+
+ValueSelector.InputParser = (function() {
+
+ var InputParser = {
+ _dateParts: ['year', 'month', 'date'],
+ _timeParts: ['hours', 'minutes', 'seconds'],
+
+ /**
+ * Import HTML5 input[type="time"] string value
+ *
+ * @param {String} value 23:20:50.52, 17:39:57.
+ * @return {Object} { hours: 23, minutes: 20, seconds: 50 }.
+ */
+ importTime: function(value) {
+ var result = {
+ hours: 0,
+ minutes: 0,
+ seconds: 0
+ };
+
+ var parts = value.split(':');
+ var part;
+ var partName;
+
+ var i = 0;
+ var len = InputParser._timeParts.length;
+
+ for (; i < len; i++) {
+ partName = InputParser._timeParts[i];
+ part = parts[i];
+ if (part) {
+ result[partName] = parseInt(part.slice(0, 2), 10) || 0;
+ }
+ }
+
+ return result;
+ },
+
+ /**
+ * Export date to HTML5 input[type="time"]
+ *
+ * @param {Date} value export value.
+ * @return {String} 17:39:57.
+ */
+ exportTime: function(value) {
+ var hour = value.getHours();
+ var minute = value.getMinutes();
+ var second = value.getSeconds();
+
+ var result = '';
+
+ result += InputParser.padNumber(hour) + ':';
+ result += InputParser.padNumber(minute) + ':';
+ result += InputParser.padNumber(second);
+
+ return result;
+ },
+
+ /**
+ * Import HTML5 input[type="time"] to object.
+ *
+ * @param {String} value 1997-12-19.
+ * @return {Object} { year: 1997, month: 12, date: 19 }.
+ */
+ importDate: function(value) {
+ var result = {
+ year: 0,
+ month: 0,
+ date: 0
+ };
+
+ var parts = value.split('-');
+ var part;
+ var partName;
+
+ var i = 0;
+ var len = InputParser._dateParts.length;
+
+ for (; i < len; i++) {
+ partName = InputParser._dateParts[i];
+ part = parts[i];
+ if (part) {
+ result[partName] = parseInt(part, 10);
+ }
+ }
+
+ if (result.month > 0) {
+ result.month = result.month - 1;
+ }
+
+ result.date = result.date || 1;
+
+ return result;
+ },
+
+ /**
+ * Export js date to HTML5 input[type="date"]
+ *
+ * @param {Date} value export value.
+ * @return {String} date string (1997-12-19).
+ */
+ exportDate: function(value) {
+ var year = value.getFullYear();
+ var month = value.getMonth() + 1;
+ var date = value.getDate();
+
+ var result = '';
+
+ result += InputParser.padNumber(year) + '-';
+ result += InputParser.padNumber(month) + '-';
+ result += InputParser.padNumber(date);
+
+ return result;
+ },
+
+ /**
+ * Designed to take a date & time value from
+ * html5 input types and returns a JS Date.
+ *
+ * @param {String} date input date.
+ * @param {String} time input time.
+ *
+ * @return {Date} full date object from date/time.
+ */
+ formatInputDate: function(date, time) {
+ time = InputParser.importTime(time);
+ date = InputParser.importDate(date);
+
+ return new Date(
+ date.year,
+ date.month,
+ date.date,
+ time.hours,
+ time.minutes,
+ time.seconds
+ );
+ },
+
+ /**
+ * @param {Numeric} numeric value.
+ * @return {String} Pad the numeric with a leading zero if < 10.
+ */
+ padNumber: function(numeric) {
+ var value = String(numeric);
+ if (numeric < 10) {
+ return '0' + value;
+ }
+
+ return value;
+ }
+ };
+
+ return InputParser;
+
+}());
diff --git a/apps/system/js/value_selector/spin_date_picker.js b/apps/system/js/value_selector/spin_date_picker.js
new file mode 100644
index 0000000..feb0602
--- /dev/null
+++ b/apps/system/js/value_selector/spin_date_picker.js
@@ -0,0 +1,341 @@
+/**
+ * SpinDatePicker is a html/js "widget" which enables users
+ * pick a specific date. It display the date in the way based
+ * on the language setting.
+ *
+ * The SpinDatePicker itself contains no UI for the controls.
+ *
+ * Example usage:
+ *
+ * // All necessary UI elements are contained in the root element.
+ * var picker = new SpinDatePicker(root);
+ * picker.value = new Date();
+ * // after users pick a date
+ * var newDate = picker.value;
+ */
+var SpinDatePicker = (function SpinDatePicker() {
+ 'use strict';
+
+ var FIRST_YEAR = 1900;
+ var LAST_YEAR = 2099;
+
+ function getYearText() {
+ var yearText = [];
+ var dateTimeFormat = navigator.mozL10n.DateTimeFormat();
+
+ for (var i = FIRST_YEAR; i <= LAST_YEAR; i++) {
+ var date = new Date(i, 0, 1);
+ yearText.push(dateTimeFormat.localeFormat(date, '%Y'));
+ }
+
+ return yearText;
+ }
+
+ function getMonthText() {
+ var monthText = [];
+ var date = new Date(0);
+ var dateTimeFormat = navigator.mozL10n.DateTimeFormat();
+
+ for (var i = 0; i < 12; i++) {
+ date.setMonth(i);
+ monthText.push(dateTimeFormat.localeFormat(date, '%B'));
+ }
+
+ return monthText;
+ }
+
+ function getDateText(days) {
+ var dateText = [];
+ var date = new Date(0);
+ var dateTimeFormat = navigator.mozL10n.DateTimeFormat();
+
+ for (var i = 1; i <= days; i++) {
+ date.setDate(i);
+ dateText.push(dateTimeFormat.localeFormat(date, '%d'));
+ }
+
+ return dateText;
+ }
+
+ function getDaysInMonth(year, month) {
+ var date = new Date(year, month + 1, 0);
+ return date.getDate();
+ }
+
+ /**
+ * Get the order of date components.
+ *
+ * @param {String} date format.
+ */
+ function getDateComponentOrder(format) {
+ var format = navigator.mozL10n.get('dateTimeFormat_%x');
+ var order = '';
+ var tokens = format.match(/(%E.|%O.|%.)/g);
+
+ if (tokens) {
+ tokens.forEach(function(token) {
+ switch (token) {
+ case '%Y':
+ case '%y':
+ case '%Oy':
+ case 'Ey':
+ case 'EY':
+ order += 'Y';
+ break;
+ case '%B':
+ case '%b':
+ case '%m':
+ case '%Om':
+ order += 'M';
+ break;
+ case '%d':
+ case '%e':
+ case '%Od':
+ case '%Oe':
+ order += 'D';
+ break;
+ }
+ });
+ }
+
+ if (order.length != 3)
+ order = 'DMY';
+
+ return order;
+ }
+
+ /**
+ * Initialize a date picker widget.
+ *
+ * @param {HTMLELement} element target of widget creation.
+ */
+ function SpinDatePicker(element) {
+ this.element = element;
+
+ this.yearPicker = null;
+ this.monthPicker = null;
+ this.datePickers = {
+ '28': null,
+ '29': null,
+ '30': null,
+ '31': null
+ };
+
+ //XXX: When the document is localized again
+ // we must also re-render the month because
+ // the week days may have changed?
+ // This will only happen when we change timezones
+ // unless we add this information to the locales.
+
+ var pickerContainer =
+ element.querySelector('.picker-container');
+ var yearPickerContainer =
+ element.querySelector('.value-picker-year');
+ var monthPickerContainer =
+ element.querySelector('.value-picker-month');
+ var tmpDatePickerContainers =
+ element.querySelectorAll('.value-picker-date');
+ var datePickerContainers = {
+ '28': tmpDatePickerContainers[0],
+ '29': tmpDatePickerContainers[1],
+ '30': tmpDatePickerContainers[2],
+ '31': tmpDatePickerContainers[3]
+ };
+
+ var updateCurrentValue = function spd_updateCurrentValue() {
+ var selectedYear = this.yearPicker.getSelectedIndex() + FIRST_YEAR;
+ var selectedMonth = this.monthPicker.getSelectedIndex();
+ var days = getDaysInMonth(selectedYear, selectedMonth);
+ var datePicker = this.datePickers[days];
+ var selectedDate = datePicker.getSelectedIndex() + 1;
+
+ this._value = new Date(selectedYear, selectedMonth, selectedDate);
+ };
+
+ var updateDatePickerVisibility =
+ function spd_updateDatePickerVisibility() {
+ var days = getDaysInMonth(this.yearPicker.getSelectedIndex() +
+ FIRST_YEAR, this.monthPicker.getSelectedIndex());
+ for (var i = 28; i <= 31; i++) {
+ datePickerContainers[i].hidden = true;
+ this.datePickers[i].setSelectedIndex(this._currentSelectedDateIndex);
+ }
+ datePickerContainers[days].hidden = false;
+ };
+
+ var onvaluechangeInternal =
+ function spd_onvaluechangeInternal(newDateValue) {
+ this.yearPicker.setSelectedIndex(newDateValue.getFullYear() - FIRST_YEAR);
+ this.monthPicker.setSelectedIndex(newDateValue.getMonth());
+ for (var i = 28; i <= 31; i++) {
+ this.datePickers[i].setSelectedIndex(newDateValue.getDate() - 1);
+ }
+ updateDatePickerVisibility.apply(this);
+ updateCurrentValue.apply(this);
+ };
+
+ var onSelectedYearChanged =
+ function spd_onSelectedYearChanged(selectedYear) {
+ updateDatePickerVisibility.apply(this);
+ updateCurrentValue.apply(this);
+ };
+
+ var onSelectedMonthChanged =
+ function spd_onSelectedMonthChanged(selectedMonth) {
+ updateDatePickerVisibility.apply(this);
+ updateCurrentValue.apply(this);
+ };
+
+ var onSelectedDateChanged =
+ function spd_onSelectedDateChanged(selectedDate) {
+ this._currentSelectedDateIndex = selectedDate;
+ updateCurrentValue.apply(this);
+ };
+
+ var unitClassName = 'picker-unit';
+
+ // year value picker
+ var yearUnitStyle = {
+ valueDisplayedText: getYearText(),
+ className: unitClassName
+ };
+ if (this.yearPicker)
+ this.yearPicker.uninit();
+ this.yearPicker = new ValuePicker(yearPickerContainer, yearUnitStyle);
+ this.yearPicker.onselectedindexchange = onSelectedYearChanged.bind(this);
+
+ // month value picker
+ var monthUnitStyle = {
+ valueDisplayedText: getMonthText(),
+ className: unitClassName
+ };
+ if (this.monthPicker)
+ this.monthPicker.uninit();
+ this.monthPicker =
+ new ValuePicker(monthPickerContainer, monthUnitStyle);
+ this.monthPicker.onselectedindexchange = onSelectedMonthChanged.bind(this);
+
+ // date value picker
+ for (var i = 28; i <= 31; i++) {
+ var datePickerContainer = datePickerContainers[i];
+ var dateUnitStyle = {
+ valueDisplayedText: getDateText(i),
+ className: unitClassName
+ };
+ var datePicker = this.datePickers[i];
+
+ if (datePicker)
+ datePicker.uninit();
+ datePickerContainer.hidden = false;
+ this.datePickers[i] = new ValuePicker(datePickerContainer, dateUnitStyle);
+ this.datePickers[i].onselectedindexchange =
+ onSelectedDateChanged.bind(this);
+ }
+
+ // set component order
+ var dateComponentOrder = getDateComponentOrder();
+ var pickerClassList = pickerContainer.classList;
+ pickerClassList.remove('YMD');
+ pickerClassList.remove('DMY');
+ pickerClassList.remove('MDY');
+ pickerClassList.add(dateComponentOrder);
+
+ // Prevent focus being taken away by us for time picker.
+ // The event listener on outer box will not be triggered cause
+ // there is a evt.stopPropagation() in value_picker.js
+ this.pickerElements = [monthPickerContainer, yearPickerContainer];
+ for (var i = 28; i <= 31; i++) {
+ this.pickerElements.push(datePickerContainers[i]);
+ }
+
+ this.pickerElements.forEach((function pickerElements_forEach(picker) {
+ picker.addEventListener('mousedown', this);
+ }).bind(this));
+
+ this.onvaluechangeInternal = onvaluechangeInternal.bind(this);
+ }
+
+ SpinDatePicker.prototype = {
+
+ /**
+ * Internal value not exposed so we can fire events
+ * when the getter/setter's are used.
+ *
+ * @type Date
+ */
+ _value: null,
+
+ /**
+ * Gets current value
+ *
+ * @return {Null|Date} date or null.
+ */
+ get value() {
+ return this._value;
+ },
+
+ /**
+ * Sets the current value of the date picker.
+ * When value differs from the currently set the
+ * `onvaluechange` event will be fired with the new/old value.
+ */
+ set value(value) {
+ var old = this._value;
+ if (old !== value) {
+ this._value = value;
+ this.onvaluechangeInternal(value);
+ }
+ },
+
+ /**
+ * Getter is used for date normalization.
+ */
+ get year() {
+ return this._value.getFullYear();
+ },
+
+ /**
+ * Getter is used for date normalization.
+ */
+ get month() {
+ return this._value.getMonth();
+ },
+
+ get date() {
+ return this._value.getDate();
+ },
+
+ handleEvent: function vs_handleEvent(evt) {
+ switch (evt.type) {
+ case 'mousedown':
+ // Prevent focus being taken away by us.
+ evt.preventDefault();
+ break;
+ }
+ },
+
+ uninit: function() {
+ if (this.yearPicker)
+ this.yearPicker.uninit();
+ if (this.monthPicker)
+ this.monthPicker.uninit();
+ if (this.datePickers) {
+ for (var i = 28; i <= 31; i++) {
+ var datePicker = this.datePickers[i];
+ datePicker.uninit();
+ }
+ }
+
+ this.pickerElements.forEach((function pickerElements_forEach(picker) {
+ picker.removeEventListener('mousedown', this);
+ }).bind(this));
+ },
+
+ /**
+ * Called when the selected date changes.
+ */
+ onvaluechangeInternal: function(date) {}
+ };
+
+ return SpinDatePicker;
+}());
diff --git a/apps/system/js/value_selector/value_picker.js b/apps/system/js/value_selector/value_picker.js
new file mode 100644
index 0000000..34e686f
--- /dev/null
+++ b/apps/system/js/value_selector/value_picker.js
@@ -0,0 +1,222 @@
+var ValuePicker = (function() {
+ //
+ // Constructor
+ //
+ function VP(e, unitStyle) {
+ this.element = e;
+ this._valueDisplayedText = unitStyle.valueDisplayedText;
+ this._unitClassName = unitStyle.className;
+ this._lower = 0;
+ this._upper = unitStyle.valueDisplayedText.length - 1;
+ this._range = unitStyle.valueDisplayedText.length;
+ this._currentIndex = 0;
+ this.init();
+ }
+
+ //
+ // Public methods
+ //
+ VP.prototype.getSelectedIndex = function() {
+ var selectedIndex = this._currentIndex;
+ return selectedIndex;
+ };
+
+ VP.prototype.getSelectedDisplayedText = function() {
+ var displayedText = this._valueDisplayedText[this._currentIndex];
+ return displayedText;
+ };
+
+ VP.prototype.setSelectedIndex = function(tunedIndex, ignorePicker) {
+ if ((tunedIndex % 1) > 0.5) {
+ tunedIndex = Math.floor(tunedIndex) + 1;
+ } else {
+ tunedIndex = Math.floor(tunedIndex);
+ }
+
+ if (tunedIndex < this._lower) {
+ tunedIndex = this._lower;
+ }
+
+ if (tunedIndex > this._upper) {
+ tunedIndex = this._upper;
+ }
+
+ if (this._currentIndex != tunedIndex) {
+ this._currentIndex = tunedIndex;
+ this.onselectedindexchange(this._currentIndex);
+ }
+ this.updateUI(tunedIndex, ignorePicker);
+
+ return tunedIndex;
+ };
+
+ VP.prototype.setSelectedIndexByDisplayedText = function(displayedText) {
+ var newIndex = this._valueDisplayedText.indexOf(displayedText);
+ if (newIndex != -1) {
+ if (this._currentIndex != newIndex) {
+ this._currentIndex = newIndex;
+ this.onselectedindexchange(this._currentIndex);
+ }
+ this.updateUI(newIndex);
+ }
+ };
+
+ //
+ // Internal methods
+ //
+ VP.prototype.init = function() {
+ this.initUI();
+ this.setSelectedIndex(0); // Default Index is zero
+ this.mousedonwHandler = vp_mousedown.bind(this);
+ this.mousemoveHandler = vp_mousemove.bind(this);
+ this.mouseupHandler = vp_mouseup.bind(this);
+ this.addEventListeners();
+ };
+
+ VP.prototype.initUI = function() {
+ var lower = this._lower;
+ var upper = this._upper;
+ var unitCount = this._valueDisplayedText.length;
+ for (var i = 0; i < unitCount; ++i) {
+ this.addPickerUnit(i);
+ }
+ // cache the size of picker
+ this._pickerUnits = this.element.children;
+ this._pickerUnitsHeight = this._pickerUnits[0].clientHeight;
+ this._pickerHeight = this._pickerUnits[0].clientHeight *
+ this._pickerUnits.length;
+ this._space = this._pickerHeight / this._range;
+ };
+
+ VP.prototype.addPickerUnit = function(index) {
+ var html = this._valueDisplayedText[index];
+ var unit = document.createElement('div');
+ unit.className = this._unitClassName;
+ unit.innerHTML = html;
+ this.element.appendChild(unit);
+ };
+
+ VP.prototype.updateUI = function(index, ignorePicker) {
+ if (true !== ignorePicker) {
+ this.element.style.top =
+ (this._lower - index) * this._space + 'px';
+ }
+ };
+
+ VP.prototype.addEventListeners = function() {
+ this.element.addEventListener('mousedown', this.mousedonwHandler, false);
+ };
+
+ VP.prototype.removeEventListeners = function() {
+ this.element.removeEventListener('mouseup', this.mouseupHandler, false);
+ this.element.removeEventListener('mousemove', this.mousemoveHandler, false);
+ };
+
+ VP.prototype.uninit = function() {
+ this.element.removeEventListener('mousedown', this.mousedonwHandler, false);
+ this.element.removeEventListener('mouseup', this.mouseupHandler, false);
+ this.element.removeEventListener('mousemove', this.mousemoveHandler, false);
+ this.element.style.top = '0px';
+ this.onselectedindexchange = null;
+ empty(this.element);
+ };
+
+ VP.prototype.onselectedindexchange = function(index) {};
+
+ function cloneEvent(evt) {
+ if ('touches' in evt) {
+ evt = evt.touches[0];
+ }
+ return { x: evt.pageX, y: evt.pageY, timestamp: evt.timeStamp };
+ }
+
+ function empty(element) {
+ while (element.hasChildNodes())
+ element.removeChild(element.lastChild);
+ element.innerHTML = '';
+ }
+
+ //
+ // Tuneable parameters
+ //
+ var SPEED_THRESHOLD = 0.1;
+ var currentEvent, startEvent, currentSpeed;
+ var tunedIndex = 0;
+
+ function toFixed(value) {
+ return parseFloat(value.toFixed(1));
+ }
+
+ function calcSpeed() {
+ var movingSpace = startEvent.y - currentEvent.y;
+ var deltaTime = currentEvent.timestamp - startEvent.timestamp;
+ var speed = movingSpace / deltaTime;
+ currentSpeed = parseFloat(speed.toFixed(2));
+ }
+
+ function calcTargetIndex(space) {
+ return tunedIndex - getMovingSpace() / space;
+ }
+
+ // If the user swap really slow, narrow down the moving space
+ // So the user can fine tune value.
+ function getMovingSpace() {
+ var movingSpace = currentEvent.y - startEvent.y;
+ var reValue = Math.abs(currentSpeed) > SPEED_THRESHOLD ?
+ movingSpace : movingSpace / 4;
+ return reValue;
+ }
+
+ function vp_mousemove(event) {
+ event.stopPropagation();
+ event.target.setCapture(true);
+ currentEvent = cloneEvent(event);
+
+ calcSpeed();
+
+ // move selected index
+ this.element.style.top = parseFloat(this.element.style.top) +
+ getMovingSpace() + 'px';
+
+ tunedIndex = calcTargetIndex(this._space);
+ var roundedIndex = Math.round(tunedIndex * 10) / 10;
+
+ if (roundedIndex != this._currentIndex) {
+ this.setSelectedIndex(toFixed(roundedIndex), true);
+ }
+
+ startEvent = currentEvent;
+ }
+
+ function vp_mouseup(event) {
+ event.stopPropagation();
+ this.removeEventListeners();
+
+ // Add animation back
+ this.element.classList.add('animation-on');
+
+ // Add momentum if speed is higher than a given threshold.
+ if (Math.abs(currentSpeed) > SPEED_THRESHOLD) {
+ var direction = currentSpeed > 0 ? 1 : -1;
+ tunedIndex += Math.min(Math.abs(currentSpeed) * 5, 5) * direction;
+ }
+ tunedIndex = this.setSelectedIndex(toFixed(tunedIndex));
+ currentSpeed = 0;
+ }
+
+ function vp_mousedown(event) {
+ event.stopPropagation();
+
+ // Stop animation
+ this.element.classList.remove('animation-on');
+
+ startEvent = currentEvent = cloneEvent(event);
+ tunedIndex = this._currentIndex;
+
+ this.removeEventListeners();
+ this.element.addEventListener('mousemove', this.mousemoveHandler, false);
+ this.element.addEventListener('mouseup', this.mouseupHandler, false);
+ }
+
+ return VP;
+}());
diff --git a/apps/system/js/value_selector/value_selector.js b/apps/system/js/value_selector/value_selector.js
new file mode 100644
index 0000000..b3381b3
--- /dev/null
+++ b/apps/system/js/value_selector/value_selector.js
@@ -0,0 +1,526 @@
+/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var ValueSelector = {
+
+ _containers: {},
+ _popups: {},
+ _buttons: {},
+ _datePicker: null,
+
+ debug: function(msg) {
+ var debugFlag = false;
+ if (debugFlag) {
+ console.log('[ValueSelector] ', msg);
+ }
+ },
+
+ init: function vs_init() {
+
+ var self = this;
+
+ window.navigator.mozKeyboard.onfocuschange = function onfocuschange(evt) {
+ var typeToHandle = ['select-one', 'select-multiple', 'date',
+ 'time', 'datetime', 'datetime-local', 'blur'];
+
+ var type = evt.detail.type;
+ // handle the <select> element and inputs with type of date/time
+ // in system app for now
+ if (typeToHandle.indexOf(type) == -1)
+ return;
+
+ var currentValue = evt.detail.value;
+
+ switch (evt.detail.type) {
+ case 'select-one':
+ case 'select-multiple':
+ self.debug('select triggered' + JSON.stringify(evt.detail));
+ self._currentPickerType = evt.detail.type;
+ self.showOptions(evt.detail);
+ break;
+
+ case 'date':
+ self.showDatePicker(currentValue);
+ break;
+
+ case 'time':
+ self.showTimePicker(currentValue);
+ break;
+
+ case 'datetime':
+ case 'datetime-local':
+ // TODO
+ break;
+ case 'blur':
+ self.hide();
+ break;
+ }
+ };
+
+ this._element = document.getElementById('value-selector');
+ this._element.addEventListener('mousedown', this);
+ this._containers['select'] =
+ document.getElementById('value-selector-container');
+ this._containers['select'].addEventListener('click', this);
+ ActiveEffectHelper.enableActive(this._containers['select']);
+
+ this._popups['select'] =
+ document.getElementById('select-option-popup');
+ this._popups['select'].addEventListener('submit', this);
+ this._popups['time'] =
+ document.getElementById('time-picker-popup');
+ this._popups['date'] =
+ document.getElementById('spin-date-picker-popup');
+
+ this._buttons['select'] = document.getElementById('select-options-buttons');
+ this._buttons['select'].addEventListener('click', this);
+
+ this._buttons['time'] = document.getElementById('time-picker-buttons');
+ this._buttons['time'].addEventListener('click', this);
+ this._buttons['date'] = document.getElementById('spin-date-picker-buttons');
+
+ this._buttons['date'].addEventListener('click', this);
+
+ this._containers['time'] = document.getElementById('picker-bar');
+ this._containers['date'] = document.getElementById('spin-date-picker');
+
+ ActiveEffectHelper.enableActive(this._buttons['select']);
+ ActiveEffectHelper.enableActive(this._buttons['time']);
+ ActiveEffectHelper.enableActive(this._buttons['date']);
+
+ // Prevent focus being taken away by us for time picker.
+ // The event listener on outer box will not be triggered cause
+ // there is a evt.stopPropagation() in value_picker.js
+ var pickerElements = ['value-picker-hours', 'value-picker-minutes',
+ 'value-picker-hour24-state'];
+
+ pickerElements.forEach((function pickerElements_forEach(id) {
+ var element = document.getElementById(id);
+ element.addEventListener('mousedown', this);
+ }).bind(this));
+
+ window.addEventListener('appopen', this);
+ window.addEventListener('appwillclose', this);
+
+ // invalidate the current spin date picker when language setting changes
+ navigator.mozSettings.addObserver('language.current',
+ (function language_change(e) {
+ if (this._datePicker) {
+ this._datePicker.uninit();
+ this._datePicker = null;
+ }}).bind(this));
+ },
+
+ handleEvent: function vs_handleEvent(evt) {
+ switch (evt.type) {
+ case 'appopen':
+ case 'appwillclose':
+ this.hide();
+ break;
+
+ case 'click':
+ var currentTarget = evt.currentTarget;
+ switch (currentTarget) {
+ case this._buttons['select']:
+ case this._buttons['time']:
+ case this._buttons['date']:
+ var target = evt.target;
+ if (target.dataset.type == 'cancel') {
+ this.cancel();
+ } else if (target.dataset.type == 'ok') {
+ this.confirm();
+ }
+ break;
+
+ case this._containers['select']:
+ this.handleSelect(evt.target);
+ break;
+ }
+ break;
+
+ case 'submit':
+ // Prevent the form from submit.
+ case 'mousedown':
+ // Prevent focus being taken away by us.
+ evt.preventDefault();
+ break;
+
+ default:
+ this.debug('no event handler defined for' + evt.type);
+ break;
+ }
+ },
+
+ handleSelect: function vs_handleSelect(target) {
+
+ if (target.dataset === undefined ||
+ (target.dataset.optionIndex === undefined &&
+ target.dataset.optionValue === undefined))
+ return;
+
+ if (this._currentPickerType === 'select-one') {
+ var selectee = this._containers['select'].
+ querySelectorAll('[aria-checked="true"]');
+ for (var i = 0; i < selectee.length; i++) {
+ selectee[i].removeAttribute('aria-checked');
+ }
+
+ target.setAttribute('aria-checked', 'true');
+ } else if (target.getAttribute('aria-checked') === 'true') {
+ target.removeAttribute('aria-checked');
+ } else {
+ target.setAttribute('aria-checked', 'true');
+ }
+
+ // setValue here to trigger change event
+ var singleOptionIndex;
+ var optionIndices = [];
+
+ var selectee = this._containers['select'].
+ querySelectorAll('[aria-checked="true"]');
+
+ if (this._currentPickerType === 'select-one') {
+
+ if (selectee.length > 0)
+ singleOptionIndex = selectee[0].dataset.optionIndex;
+
+ window.navigator.mozKeyboard.setSelectedOption(singleOptionIndex);
+
+ } else if (this._currentPickerType === 'select-multiple') {
+ // Multiple select case
+ for (var i = 0; i < selectee.length; i++) {
+
+ var index = parseInt(selectee[i].dataset.optionIndex);
+ optionIndices.push(index);
+ }
+
+ window.navigator.mozKeyboard.setSelectedOptions(optionIndices);
+ }
+
+ },
+
+ show: function vs_show(detail) {
+ this._element.hidden = false;
+ },
+
+ showPanel: function vs_showPanel(type) {
+ for (var p in this._containers) {
+ if (p === type) {
+ this._popups[p].hidden = false;
+ } else {
+ this._popups[p].hidden = true;
+ }
+ }
+ },
+
+ hide: function vs_hide() {
+ this._element.hidden = true;
+ },
+
+ cancel: function vs_cancel() {
+ this.debug('cancel invoked');
+ window.navigator.mozKeyboard.removeFocus();
+ this.hide();
+ },
+
+ confirm: function vs_confirm() {
+
+ if (this._currentPickerType === 'time') {
+
+ var timeValue = TimePicker.getTimeValue();
+ this.debug('output value: ' + timeValue);
+
+ window.navigator.mozKeyboard.setValue(timeValue);
+ } else if (this._currentPickerType === 'date') {
+ var dateValue = this._datePicker.value;
+ // The format should be 2012-09-19
+ dateValue = dateValue.toLocaleFormat('%Y-%m-%d');
+ this.debug('output value: ' + dateValue);
+ window.navigator.mozKeyboard.setValue(dateValue);
+ }
+
+ window.navigator.mozKeyboard.removeFocus();
+ this.hide();
+ },
+
+ showOptions: function vs_showOptions(detail) {
+
+ var options = null;
+ if (detail.choices && detail.choices.choices)
+ options = detail.choices.choices;
+
+ if (options)
+ this.buildOptions(options);
+
+ this.show();
+ this.showPanel('select');
+ },
+
+ buildOptions: function(options) {
+
+ var optionHTML = '';
+
+ function escapeHTML(str) {
+ var span = document.createElement('span');
+ span.textContent = str;
+ return span.innerHTML;
+ }
+
+ for (var i = 0, n = options.length; i < n; i++) {
+
+ var checked = options[i].selected ? ' aria-checked="true"' : '';
+
+ // This for attribute is created only to avoid applying
+ // a general rule in building block
+ var forAttribute = ' for="gaia-option-' + options[i].optionIndex + '"';
+
+ optionHTML += '<li data-option-index="' + options[i].optionIndex + '"' +
+ checked + '>' +
+ '<label' + forAttribute + '> <span>' +
+ escapeHTML(options[i].text) +
+ '</span></label>' +
+ '</li>';
+ }
+
+ var optionsContainer = document.querySelector(
+ '#value-selector-container ol');
+ if (!optionsContainer)
+ return;
+
+ optionsContainer.innerHTML = optionHTML;
+
+
+ // Apply different style when the options are more than 1 page
+ if (options.length > 5) {
+ this._containers['select'].classList.add('scrollable');
+ } else {
+ this._containers['select'].classList.remove('scrollable');
+ }
+
+ // Change the title for multiple select
+ var titleL10nId = 'choose-options';
+ if (this._currentPickerType === 'select-one')
+ titleL10nId = 'choose-option';
+
+ var optionsTitle = document.querySelector(
+ '#value-selector-container h1');
+
+ if (optionsTitle) {
+ optionsTitle.dataset.l10nId = titleL10nId;
+ optionsTitle.textContent = navigator.mozL10n.get(titleL10nId);
+ }
+ },
+
+ showTimePicker: function vs_showTimePicker(currentValue) {
+ this._currentPickerType = 'time';
+ this.show();
+ this.showPanel('time');
+
+ if (!this._timePickerInitialized) {
+ TimePicker.initTimePicker();
+ this._timePickerInitialized = true;
+ }
+
+ var time;
+ if (!currentValue) {
+ var now = new Date();
+ time = {
+ hours: now.getHours(),
+ minutes: now.getMinutes()
+ };
+ } else {
+ var inputParser = ValueSelector.InputParser;
+ if (!inputParser)
+ console.error('Cannot get input parser for value selector');
+
+ time = inputParser.importTime(currentValue);
+ }
+
+ var timePicker = TimePicker.timePicker;
+ // Set the value of time picker according to the current value
+ if (timePicker.is12hFormat) {
+ var hour = (time.hours % 12);
+ hour = (hour == 0) ? 12 : hour;
+ // 24-hour state value selector: AM = 0, PM = 1
+ var hour24State = (time.hours >= 12) ? 1 : 0;
+ timePicker.hour.setSelectedIndexByDisplayedText(hour);
+ timePicker.hour24State.setSelectedIndex(hour24State);
+ } else {
+ timePicker.hour.setSelectedIndex(time.hours);
+ }
+
+ timePicker.minute.setSelectedIndex(time.minutes);
+ },
+
+ showDatePicker: function vs_showDatePicker(currentValue) {
+ this._currentPickerType = 'date';
+ this.show();
+ this.showPanel('date');
+
+ if (!this._datePicker) {
+ this._datePicker = new SpinDatePicker(this._containers['date']);
+ }
+
+ // Show current date as default value
+ var date = new Date();
+ if (currentValue) {
+ var inputParser = ValueSelector.InputParser;
+ if (!inputParser)
+ console.error('Cannot get input parser for value selector');
+
+ date = inputParser.formatInputDate(currentValue, '');
+ }
+ this._datePicker.value = date;
+ }
+
+};
+
+var TimePicker = {
+ timePicker: {
+ hour: null,
+ minute: null,
+ hour24State: null,
+ is12hFormat: false
+ },
+
+ get hourSelector() {
+ delete this.hourSelector;
+ return this.hourSelector =
+ document.getElementById('value-picker-hours');
+ },
+
+ get minuteSelector() {
+ delete this.minuteSelector;
+ return this.minuteSelector =
+ document.getElementById('value-picker-minutes');
+ },
+
+ get hour24StateSelector() {
+ delete this.hour24StateSelector;
+ return this.hour24StateSelector =
+ document.getElementById('value-picker-hour24-state');
+ },
+
+ initTimePicker: function tp_initTimePicker() {
+ var localeTimeFormat = navigator.mozL10n.get('dateTimeFormat_%X');
+ var is12hFormat = (localeTimeFormat.indexOf('%p') >= 0);
+ this.timePicker.is12hFormat = is12hFormat;
+ this.setTimePickerStyle();
+ var startHour = is12hFormat ? 1 : 0;
+ var endHour = is12hFormat ? (startHour + 12) : (startHour + 12 * 2);
+ var unitClassName = 'picker-unit';
+ var hourDisplayedText = [];
+ for (var i = startHour; i < endHour; i++) {
+ var value = i;
+ hourDisplayedText.push(value);
+ }
+ var hourUnitStyle = {
+ valueDisplayedText: hourDisplayedText,
+ className: unitClassName
+ };
+ this.timePicker.hour = new ValuePicker(this.hourSelector, hourUnitStyle);
+
+ var minuteDisplayedText = [];
+ for (var i = 0; i < 60; i++) {
+ var value = (i < 10) ? '0' + i : i;
+ minuteDisplayedText.push(value);
+ }
+ var minuteUnitStyle = {
+ valueDisplayedText: minuteDisplayedText,
+ className: unitClassName
+ };
+ this.timePicker.minute =
+ new ValuePicker(this.minuteSelector, minuteUnitStyle);
+
+ if (is12hFormat) {
+ var hour24StateUnitStyle = {
+ valueDisplayedText: ['AM', 'PM'],
+ className: unitClassName
+ };
+ this.timePicker.hour24State =
+ new ValuePicker(this.hour24StateSelector, hour24StateUnitStyle);
+ }
+ },
+
+ setTimePickerStyle: function tp_setTimePickerStyle() {
+ var style = (this.timePicker.is12hFormat) ? 'format12h' : 'format24h';
+ document.getElementById('picker-bar').classList.add(style);
+ },
+
+ // return a string for the time value, format: "16:37"
+ getTimeValue: function tp_getTimeValue() {
+ var hour = 0;
+ if (this.timePicker.is12hFormat) {
+ var hour24Offset = 12 * this.timePicker.hour24State.getSelectedIndex();
+ hour = this.timePicker.hour.getSelectedDisplayedText();
+ hour = (hour == 12) ? 0 : hour;
+ hour = hour + hour24Offset;
+ } else {
+ hour = this.timePicker.hour.getSelectedIndex();
+ }
+ var minute = this.timePicker.minute.getSelectedDisplayedText();
+
+ return hour + ':' + minute;
+ }
+};
+
+var ActiveEffectHelper = (function() {
+
+ var lastActiveElement = null;
+
+ function _setActive(element, isActive) {
+ if (isActive) {
+ element.classList.add('active');
+ lastActiveElement = element;
+ } else {
+ element.classList.remove('active');
+ if (lastActiveElement) {
+ lastActiveElement.classList.remove('active');
+ lastActiveElement = null;
+ }
+ }
+ }
+
+ function _onMouseDown(evt) {
+ var target = evt.target;
+
+ _setActive(target, true);
+ target.addEventListener('mouseleave', _onMouseLeave);
+ }
+
+ function _onMouseUp(evt) {
+ var target = evt.target;
+
+ _setActive(target, false);
+ target.removeEventListener('mouseleave', _onMouseLeave);
+ }
+
+ function _onMouseLeave(evt) {
+ var target = evt.target;
+ _setActive(target, false);
+ target.removeEventListener('mouseleave', _onMouseLeave);
+ }
+
+ var _events = {
+ 'mousedown': _onMouseDown,
+ 'mouseup': _onMouseUp
+ };
+
+ function _enableActive(element) {
+ // Attach event listeners
+ for (var event in _events) {
+ var callback = _events[event] || null;
+ if (callback)
+ element.addEventListener(event, callback);
+ }
+ }
+
+ return {
+ enableActive: _enableActive
+ };
+
+})();
+
+ValueSelector.init();
diff --git a/apps/system/js/voicemail.js b/apps/system/js/voicemail.js
new file mode 100644
index 0000000..dea5116
--- /dev/null
+++ b/apps/system/js/voicemail.js
@@ -0,0 +1,93 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+// Custom voicemail notification -- This can be removed once DesktopNotification
+// supports removing notifications via API
+var Voicemail = {
+
+ icon: null,
+ notification: null,
+ // A random starting point that is unlikely to be used by other notifications
+ notificationId: 3000 + Math.floor(Math.random() * 999),
+
+ init: function vm_init() {
+ var voicemail = window.navigator.mozVoicemail;
+ if (!voicemail)
+ return;
+
+ voicemail.addEventListener('statuschanged', this);
+
+ this.icon = window.location.protocol + '//' +
+ window.location.hostname + '/style/icons/voicemail.png';
+ },
+
+ handleEvent: function vm_handleEvent(evt) {
+ var voicemail = window.navigator.mozVoicemail;
+ if (!voicemail.status)
+ return;
+
+ this.updateNotification(voicemail.status);
+ },
+
+ updateNotification: function vm_updateNotification(status) {
+ var _ = window.navigator.mozL10n.get;
+ var title = status.returnMessage;
+ var showCount = status.hasMessages && status.messageCount > 0;
+
+ if (!title) {
+ title = showCount ? _('newVoicemails', { n: status.messageCount }) :
+ _('newVoicemailsUnknown');
+ }
+
+ var text = title;
+ var voicemailNumber = navigator.mozVoicemail.number;
+ if (voicemailNumber) {
+ text = _('dialNumber', { number: voicemailNumber });
+ }
+
+ this.hideNotification();
+ if (status.hasMessages) {
+ this.showNotification(title, text, voicemailNumber);
+ }
+ },
+
+ showNotification: function vm_showNotification(title, text, voicemailNumber) {
+ this.notificationId++;
+ this.notification = NotificationScreen.addNotification({
+ id: this.notificationId, title: title, text: text, icon: this.icon
+ });
+
+ if (!voicemailNumber) {
+ return;
+ }
+
+ var self = this;
+ function vmNotification_onTap(event) {
+ self.notification.removeEventListener('tap', vmNotification_onTap);
+
+ var telephony = window.navigator.mozTelephony;
+ if (!telephony) {
+ return;
+ }
+
+ telephony.dial(voicemailNumber);
+ }
+
+ this.notification.addEventListener('tap', vmNotification_onTap);
+ },
+
+ hideNotification: function vm_hideNotification() {
+ if (!this.notification) {
+ return;
+ }
+
+ if (this.notification.parentNode) {
+ NotificationScreen.removeNotification(this.notificationId);
+ }
+
+ this.notification = null;
+ this.notificationId = 0;
+ }
+};
+
+Voicemail.init();
diff --git a/apps/system/js/wifi.js b/apps/system/js/wifi.js
new file mode 100644
index 0000000..3456fdf
--- /dev/null
+++ b/apps/system/js/wifi.js
@@ -0,0 +1,223 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var Wifi = {
+ wifiWakeLocked: false,
+
+ wifiEnabled: true,
+
+ wifiDisabledByWakelock: false,
+
+ // Without wake lock, wait for kOffTime milliseconds and turn wifi off
+ // after the conditions are met.
+ kOffTime: 60 * 1000,
+
+ // if Wifi is enabled but disconnected, try to scan for networks every
+ // kScanInterval ms.
+ kScanInterval: 20 * 1000,
+
+ _scanTimer: null,
+
+ init: function wf_init() {
+ window.addEventListener('screenchange', this);
+
+ var battery = window.navigator.battery;
+ battery.addEventListener('chargingchange', this);
+
+ if (!window.navigator.mozSettings)
+ return;
+
+ // 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 });
+ };
+
+ var self = this;
+ var wifiManager = window.navigator.mozWifiManager;
+ // when wifi is really enabled, emit event to notify QuickSettings
+ wifiManager.onenabled = function onWifiEnabled() {
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('wifi-enabled',
+ /* canBubble */ true, /* cancelable */ false, null);
+ window.dispatchEvent(evt);
+ };
+
+ // when wifi is really disabled, emit event to notify QuickSettings
+ wifiManager.ondisabled = function onWifiDisabled() {
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('wifi-disabled',
+ /* canBubble */ true, /* cancelable */ false, null);
+ window.dispatchEvent(evt);
+ };
+
+ // 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);
+ };
+
+ // 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;
+
+ 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();
+ });
+ });
+
+ var power = navigator.mozPower;
+ power.addWakeLockListener(function wifi_handleWakeLock(topic, state) {
+ if (topic !== 'wifi')
+ return;
+
+ self.wifiWakeLocked = (state == 'locked-foreground' ||
+ state == 'locked-background');
+
+ self.maybeToggleWifi();
+ });
+ },
+
+ handleEvent: function wifi_handleEvent(evt) {
+ this.maybeToggleWifi();
+ },
+
+ // Check the status of screen, wifi wake lock and power source
+ // and turn on/off wifi accordingly
+ maybeToggleWifi: function wifi_maybeToggleWifi() {
+ var battery = window.navigator.battery;
+ var wifiManager = window.navigator.mozWifiManager;
+ if (!battery || !wifiManager ||
+ (!this.wifiEnabled && !this.wifiDisabledByWakelock))
+ return;
+
+
+ // 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 (!ScreenManager.screenEnabled &&
+ !this.wifiWakeLocked && !battery.charging) {
+ // We don't need to do anything if wifi is not enabled currently
+ if (!this.wifiEnabled)
+ return;
+
+ // 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();
+
+ return;
+ }
+
+ // Set System Message Handler, so we will be notified when alarm goes off.
+ this.setSystemMessageHandler();
+
+ // Start with a timer, only turn off wifi till timeout.
+ var date = new Date(Date.now() + this.kOffTime);
+ var self = this;
+ var req = navigator.mozAlarms.add(date, 'ignoreTimezone', 'wifi-off');
+ req.onsuccess = function wifi_offAlarmSet() {
+ self._alarmId = req.result;
+ };
+ req.onerror = function wifi_offAlarmSetFailed() {
+ console.warn('Fail to set wifi sleep timer on Alarm API. ' +
+ 'Turn off wifi immediately.');
+ self.sleep();
+ };
+ }
+ // ... and quietly turn it back on or cancel the timer otherwise
+ else {
+ if (this._alarmId) {
+ navigator.mozAlarms.remove(this._alarmId);
+ 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)
+ return;
+
+ var lock = SettingsListener.getSettingsLock();
+ // turn wifi back on.
+ lock.set({ 'wifi.enabled': true });
+
+ this.wifiDisabledByWakelock = false;
+ lock.set({ 'wifi.disabled_by_wakelock': false });
+ }
+ },
+
+ // Quietly turn off wifi for real, set wifiDisabledByWakelock to true
+ // so we will turn it back on.
+ sleep: function wifi_sleep() {
+ var lock = SettingsListener.getSettingsLock();
+ // Actually turn off the wifi
+ lock.set({ 'wifi.enabled': false });
+
+ // Remember that it was turned off by us.
+ this.wifiDisabledByWakelock = true;
+
+ // Keep this value in disk so if the phone reboots we'll
+ // be able to turn the wifi back on.
+ lock.set({ 'wifi.disabled_by_wakelock': true });
+ },
+
+ // Register for handling system message,
+ // this cannot be done during |init()| because of bug 797803
+ setSystemMessageHandler: function wifi_setSystemMessageHandler() {
+ if (this._systemMessageHandlerRegistered)
+ return;
+
+ this._systemMessageHandlerRegistered = true;
+ var self = this;
+ navigator.mozSetMessageHandler('alarm', function gotAlarm(message) {
+ if (message.data !== 'wifi-off')
+ return;
+
+ self.sleep();
+ });
+ }
+};
+
+Wifi.init();
diff --git a/apps/system/js/window.js b/apps/system/js/window.js
new file mode 100644
index 0000000..a9109dd
--- /dev/null
+++ b/apps/system/js/window.js
@@ -0,0 +1,152 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+(function(window){
+
+ var _ = navigator.mozL10n.get;
+
+ var ENABLE_LOG = false;
+
+ // Use mutation observer to monitor appWindow status change
+ window.AppLog = function AppLog(app) {
+ // select the target node
+ var target = app.frame;
+
+ // create an observer instance
+ var observer = new MutationObserver(function(mutations) {
+ mutations.forEach(function(mutation) {
+ console.log(mutation.target.id,
+ mutation.target.className,
+ mutation.attributeName);
+ });
+ });
+
+ // configuration of the observer:
+ var config = { attributes: true };
+
+ // pass in the target node, as well as the observer options
+ observer.observe(target, config);
+ }
+
+ window.AppError = function AppError(app) {
+ var self = this;
+ this.app = app;
+ this.app.frame.addEventListener('mozbrowsererror', function (evt) {
+ if (evt.detail.type != 'other')
+ return;
+
+ console.warn('app of [' + self.app.origin + '] got a mozbrowsererror event.');
+
+ if (self.injected) {
+ self.update();
+ } else {
+ self.render();
+ }
+ self.show();
+ self.injected = true;
+ });
+ return this;
+ };
+
+ AppError.className = 'appError';
+
+ AppError.prototype.hide = function() {
+ this.element.classList.remove('visible');
+ }
+
+ AppError.prototype.show = function() {
+ this.element.classList.add('visible');
+ }
+
+ AppError.prototype.render = function() {
+ this.app.frame.insertAdjacentHTML('beforeend', this.view());
+ this.closeButton = this.app.frame.querySelector('.' + AppError.className + ' .close');
+ this.reloadButton = this.app.frame.querySelector('.' + AppError.className + ' .reload');
+ this.titleElement = this.app.frame.querySelector('.' + AppError.className + ' .title');
+ this.messageElement = this.app.frame.querySelector('.' + AppError.className + ' .message');
+ this.element = this.app.frame.querySelector('.' + AppError.className);
+ var self = this;
+ this.closeButton.onclick = function() {
+ self.app.kill();
+ }
+
+ this.reloadButton.onclick = function() {
+ self.hide();
+ self.app.reload();
+ }
+ }
+
+ AppError.prototype.update = function() {
+ this.titleElement.textContent = this.getTitle();
+ this.messageElement.textContent = this.getMessage();
+ }
+
+ AppError.prototype.id = function() {
+ return AppError.className + '-' + this.app.frame.id;
+ }
+
+ AppError.prototype.getTitle = function() {
+ if (AirplaneMode.enabled) {
+ return _('airplane-is-on');
+ } else if (!navigator.onLine) {
+ return _('network-connection-unavailable');
+ } else {
+ return _('error-title', { name: this.app.name });
+ }
+ }
+
+ AppError.prototype.getMessage = function() {
+ if (AirplaneMode.enabled) {
+ return _('airplane-is-turned-on', { name: this.app.name });
+ } else if (!navigator.onLine) {
+ return _('network-error', { name: this.app.name });
+ } else {
+ return _('error-message', { name: this.app.name });
+ }
+ }
+
+ AppError.prototype.view = function() {
+ return '<div id="' + this.id() + '" class="' + AppError.className + ' visible" role="dialog">' +
+ '<div class="modal-dialog-message-container inner">' +
+ '<h3 data-l10n-id="error-title" class="title">' + this.getTitle() + '</h3>' +
+ '<p>' +
+ '<span data-l10n-id="error-message" class="message">' + this.getMessage() + '</span>' +
+ '</p>' +
+ '</div>' +
+ '<menu data-items="2">' +
+ '<button class="close" data-l10n-id="try-again">' + _('close') + '</button>' +
+ '<button class="reload" data-l10n-id="try-again">' + _('try-again') + '</button>' +
+ '</menu>' +
+ '</div>';
+ }
+
+ window.AppWindow = function AppWindow(configuration) {
+ for (var key in configuration) {
+ this[key] = configuration[key];
+ }
+
+ // We keep the appError object here for the purpose that
+ // we may need to export the error state of AppWindow instance to the other module
+ // in the future.
+ this.appError = new AppError(this);
+ if (ENABLE_LOG)
+ this.appLog = new AppLog(this);
+
+ return this;
+ };
+
+ AppWindow.prototype.reload = function() {
+ this.iframe.reload(true);
+ }
+
+ AppWindow.prototype.kill = function() {
+ // XXX: A workaround because a AppWindow instance shouldn't reference Window Manager directly here.
+ // In the future we should make every app maintain and execute the events in itself.
+ // Like resize, setVisibility...
+ // And Window Manager is in charge of cross app management.
+ WindowManager.kill(this.origin);
+ }
+
+}(this));
diff --git a/apps/system/js/window_manager.js b/apps/system/js/window_manager.js
new file mode 100644
index 0000000..e53605e
--- /dev/null
+++ b/apps/system/js/window_manager.js
@@ -0,0 +1,2011 @@
+/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+//
+// This file calls getElementById without waiting for an onload event, so it
+// must have a defer attribute or be included at the end of the <body>.
+//
+// This module is responsible for launching apps and for allowing
+// the user to switch among apps and kill apps. Specifically, it handles:
+// launching apps,
+// killing apps
+// keeping track of the set of running apps (which we call tasks here)
+// keeping track of which task is displayed (the foreground task)
+// changing the foreground task
+// hiding all apps to display the homescreen
+// displaying the app switcher to allow the user to switch and kill apps
+// performing appropriate transition animations between:
+// the homescreen and an app
+// the homescreen and the switcher
+// an app and the homescreen
+// the switcher and the homescreen
+// the switcher and the current foreground task
+// the switcher and a different task
+// Handling Home key events to switch to the homescreen and the switcher
+//
+// The public API of the module is small. It defines an WindowManager object
+// with these methods:
+//
+// launch(origin): switch to the specified running app
+// kill(origin, callback): stop specified app
+// reload(origin): reload the given app
+// getDisplayedApp(): return the origin of the currently displayed app
+// setOrientationForApp(origin): set the phone to orientation to a given app
+// getAppFrame(origin): returns the iframe element for the specified origin
+// which is assumed to be running. This is only currently used
+// for tests and chrome stuff: see the end of the file
+// getRunningApps(): get the app references of the running apps.
+//
+// TODO
+// The "origin" does not actually refer to app's origin but rather a identifier
+// of the app reference that one gets from |getDisplayedApp()| or
+// iterates |getRunningApps|. The string is make up of the specified
+// launching entry point, origin, or the website url launched by wrapper.
+// It would be ideal if the variable get correctly named and it's rule is being
+// properly documented.
+// See https://bugzilla.mozilla.org/show_bug.cgi?id=796629
+//
+
+var WindowManager = (function() {
+ 'use strict';
+
+ function debug(str) {
+ dump('WindowManager: ' + str + '\n');
+ }
+
+ // Holds the origin of the home screen, which should be the first
+ // app we launch through web activity during boot
+ var homescreen = null;
+ var homescreenURL = '';
+ var homescreenManifestURL = '';
+ var ftu = null;
+ var ftuManifestURL = '';
+ var ftuURL = '';
+ var isRunningFirstRunApp = false;
+ // keep the reference of inline activity frame here
+ var inlineActivityFrames = [];
+ var activityCallerOrigin = '';
+
+ // Some document elements we use
+ var windows = document.getElementById('windows');
+ var screenElement = document.getElementById('screen');
+ var wrapperHeader = document.querySelector('#wrapper-activity-indicator');
+ var wrapperFooter = document.querySelector('#wrapper-footer');
+ var kTransitionTimeout = 1000;
+
+ // Set this to true to debugging the transitions and state change
+ var slowTransition = false;
+ if (slowTransition) {
+ windows.classList.add('slow-transition');
+ }
+
+ //
+ // The set of running apps.
+ // This is a map from app origin to an object like this:
+ // {
+ // name: the app's name
+ // manifest: the app's manifest object
+ // frame: the iframe element that the app is displayed in
+ // launchTime: last time when app gets active
+ // }
+ //
+ var runningApps = {};
+ var numRunningApps = 0; // appendFrame() and removeFrame() maintain this count
+ var nextAppId = 0; // to give each app's iframe a unique id attribute
+
+ // The origin of the currently displayed app, or null if there isn't one
+ var displayedApp = null;
+
+ // Function to hide init starting logo
+ function handleInitlogo(callback) {
+ var initlogo = document.getElementById('initlogo');
+ initlogo.classList.add('hide');
+ initlogo.addEventListener('transitionend', function delInitlogo() {
+ initlogo.removeEventListener('transitionend', delInitlogo);
+ initlogo.parentNode.removeChild(initlogo);
+ if (callback) {
+ callback();
+ }
+ });
+ };
+
+ // Public function. Return the origin of the currently displayed app
+ // or null if there is none.
+ function getDisplayedApp() {
+ return displayedApp || null;
+ }
+
+ function requireFullscreen(origin) {
+ var app = runningApps[origin];
+ if (!app)
+ return false;
+
+ var manifest = app.manifest;
+ if (manifest.entry_points && manifest.type == 'certified') {
+ var entryPoint = manifest.entry_points[origin.split('/')[3]];
+ if (entryPoint)
+ return entryPoint.fullscreen;
+ return false;
+ } else {
+ return manifest.fullscreen;
+ }
+ }
+
+ // Make the specified app the displayed app.
+ // Public function. Pass null to make the homescreen visible
+ function launch(origin) {
+ // If the origin is indeed valid we make that app as the displayed app.
+ if (isRunning(origin)) {
+ setDisplayedApp(origin);
+ return;
+ }
+
+ // If the origin is null, make the homescreen visible.
+ if (origin == null) {
+ setDisplayedApp(homescreen);
+ return;
+ }
+
+ // At this point, we have no choice but to show the homescreen.
+ // We cannot launch/relaunch a given app based on the "origin" because
+ // we would need the manifest URL and the specific entry point.
+ console.warn('No running app is being identified as "' + origin + '". ' +
+ 'Showing home screen instead.');
+ setDisplayedApp(homescreen);
+ }
+
+ function isRunning(origin) {
+ return runningApps.hasOwnProperty(origin);
+ }
+
+ function getAppFrame(origin) {
+ if (isRunning(origin))
+ return runningApps[origin].frame;
+ else
+ return null;
+ }
+
+ // Set the size of the app's iframe to match the size of the screen.
+ // We have to call this on resize events (which happen when the
+ // phone orientation is changed). And also when an app is launched
+ // and each time an app is brought to the front, since the
+ // orientation could have changed since it was last displayed
+ function setAppSize(origin, changeActivityFrame) {
+ var app = runningApps[origin];
+ if (!app)
+ return;
+
+ var frame = app.frame;
+ var manifest = app.manifest;
+
+ var cssWidth = window.innerWidth + 'px';
+ var cssHeight = window.innerHeight - StatusBar.height;
+ if ('wrapper' in frame.dataset) {
+ cssHeight -= 10;
+ }
+ cssHeight += 'px';
+
+ if (!screenElement.classList.contains('attention') &&
+ requireFullscreen(origin)) {
+ cssHeight = window.innerHeight + 'px';
+ }
+
+ frame.style.width = cssWidth;
+ frame.style.height = cssHeight;
+
+ // We will call setInlineActivityFrameSize()
+ // if changeActivityFrame is not explicitly set to false.
+ if (changeActivityFrame !== false)
+ setInlineActivityFrameSize();
+ }
+
+ // App's height is relevant to keyboard height
+ function setAppHeight(keyboardHeight) {
+ var app = runningApps[displayedApp];
+ if (!app)
+ return;
+
+ var frame = app.frame;
+ var manifest = app.manifest;
+
+ var cssHeight =
+ window.innerHeight - StatusBar.height - keyboardHeight + 'px';
+
+ if (!screenElement.classList.contains('attention') &&
+ requireFullscreen(displayedApp)) {
+ cssHeight = window.innerHeight - keyboardHeight + 'px';
+ }
+
+ frame.style.height = cssHeight;
+
+ setInlineActivityFrameSize();
+ }
+
+ // Copy the dimension of the currently displayed app
+ function setInlineActivityFrameSize() {
+ if (!inlineActivityFrames.length)
+ return;
+
+ var app = runningApps[displayedApp];
+ var appFrame = app.frame;
+ var frame = inlineActivityFrames[inlineActivityFrames.length - 1];
+
+ frame.style.width = appFrame.style.width;
+
+ if (document.mozFullScreen) {
+ frame.style.height = window.innerHeight + 'px';
+ frame.style.top = '0px';
+ } else {
+ if ('wrapper' in appFrame.dataset) {
+ frame.style.height = window.innerHeight - StatusBar.height + 'px';
+ } else {
+ frame.style.height = appFrame.style.height;
+ }
+ frame.style.top = appFrame.offsetTop + 'px';
+ }
+ }
+
+ function setFrameBackgroundBlob(frame, blob, transparent) {
+ URL.revokeObjectURL(frame.dataset.bgObjectURL);
+ delete frame.dataset.bgObjectURL;
+
+ var objectURL = URL.createObjectURL(blob);
+ frame.dataset.bgObjectURL = objectURL;
+ var backgroundCSS =
+ '-moz-linear-gradient(top, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0.5) 100%),' +
+ 'url(' + objectURL + '),' +
+ ((transparent) ? 'transparent' : '#fff');
+
+ frame.style.background = backgroundCSS;
+ }
+
+ function clearFrameBackground(frame) {
+ if (!('bgObjectURL' in frame.dataset))
+ return;
+
+ URL.revokeObjectURL(frame.dataset.bgObjectURL);
+ delete frame.dataset.bgObjectURL;
+ frame.style.background = '';
+ }
+
+ var openFrame = null;
+ var closeFrame = null;
+ var openCallback = null;
+ var closeCallback = null;
+ var transitionOpenCallback = null;
+ var transitionCloseCallback = null;
+
+ // Use setOpenFrame() to reset the CSS classes set
+ // to the current openFrame (before overwriting the reference)
+ function setOpenFrame(frame) {
+ if (openFrame) {
+ removeFrameClasses(openFrame);
+ }
+
+ openFrame = frame;
+ }
+
+ // Use setCloseFrame() to reset the CSS classes set
+ // to the current closeFrame (before overwriting the reference)
+ function setCloseFrame(frame) {
+ if (closeFrame) {
+ removeFrameClasses(closeFrame);
+ // closeFrame should not be set to active
+ closeFrame.classList.remove('active');
+ }
+
+ closeFrame = frame;
+ }
+
+ // Remove these visible className from frame so we will not ended
+ // up having a frozen frame in the middle of the transition
+ function removeFrameClasses(frame) {
+ var classNames = ['opening', 'closing', 'opening-switching',
+ 'opening-card', 'closing-card'];
+
+ var classList = frame.classList;
+
+ classNames.forEach(function removeClass(className) {
+ classList.remove(className);
+ });
+ }
+
+ windows.addEventListener('transitionend', function frameTransitionend(evt) {
+ var prop = evt.propertyName;
+ var frame = evt.target;
+ if (prop !== 'transform')
+ return;
+
+ var classList = frame.classList;
+
+ if (classList.contains('inlineActivity')) {
+ if (classList.contains('active')) {
+ if (openFrame)
+ openFrame.firstChild.focus();
+
+ setOpenFrame(null);
+ } else {
+ windows.removeChild(frame);
+ }
+
+ return;
+ }
+
+ if (screenElement.classList.contains('switch-app')) {
+ if (classList.contains('closing')) {
+ classList.remove('closing');
+ classList.add('closing-card');
+
+ if (openFrame) {
+ if (openFrame.classList.contains('opening-card')) {
+ openFrame.classList.remove('opening-card');
+ openFrame.classList.add('opening-switching');
+ } else {
+ // Skip the opening-card and opening-switching transition
+ // because the closing-card transition had already finished here.
+ if (openFrame.classList.contains('fullscreen-app')) {
+ screenElement.classList.add('fullscreen-app');
+ }
+ openFrame.classList.add('opening');
+ }
+ }
+ } else if (classList.contains('closing-card')) {
+ windowClosed(frame);
+ setTimeout(closeCallback);
+ closeCallback = null;
+
+ } else if (classList.contains('opening-switching')) {
+ // If the opening app need to be full screen, switch to full screen
+ if (classList.contains('fullscreen-app')) {
+ screenElement.classList.add('fullscreen-app');
+ }
+
+ classList.remove('opening-switching');
+ classList.add('opening');
+ } else if (classList.contains('opening')) {
+ windowOpened(frame);
+
+ setTimeout(openCallback);
+ openCallback = null;
+
+ setCloseFrame(null);
+ setOpenFrame(null);
+ screenElement.classList.remove('switch-app');
+ }
+
+ return;
+ }
+
+ if (classList.contains('opening')) {
+ windowOpened(frame);
+
+ setTimeout(openCallback);
+ openCallback = null;
+
+ setOpenFrame(null);
+ } else if (classList.contains('closing')) {
+ windowClosed(frame);
+
+ setTimeout(closeCallback);
+ closeCallback = null;
+
+ setCloseFrame(null);
+ }
+ });
+
+ // Executes when the opening transition scale the app
+ // to full size.
+ function windowOpened(frame) {
+ var iframe = frame.firstChild;
+
+ frame.classList.add('active');
+ windows.classList.add('active');
+
+ if ('wrapper' in frame.dataset) {
+ wrapperFooter.classList.add('visible');
+ }
+
+ // Take the focus away from the currently displayed app
+ var app = runningApps[displayedApp];
+ if (app && app.iframe)
+ app.iframe.blur();
+
+ // Give the focus to the frame
+ iframe.focus();
+
+ if (!TrustedUIManager.isVisible() && !isRunningFirstRunApp) {
+ // Set homescreen visibility to false
+ toggleHomescreen(false);
+ }
+
+ // Set displayedApp to the new value
+ displayedApp = iframe.dataset.frameOrigin;
+
+ // Set orientation for the new app
+ setOrientationForApp(displayedApp);
+
+ // Dispatch an 'appopen' event.
+ var manifestURL = runningApps[displayedApp].manifestURL;
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('appopen', true, false, {
+ manifestURL: manifestURL,
+ origin: displayedApp
+ });
+ frame.dispatchEvent(evt);
+ }
+
+ // Executes when app closing transition finishes.
+ function windowClosed(frame) {
+ var iframe = frame.firstChild;
+
+ // If the FTU is closing, make sure we save this state
+ if (iframe.src == ftuURL) {
+ isRunningFirstRunApp = false;
+ document.getElementById('screen').classList.remove('ftu');
+ window.asyncStorage.setItem('ftu.enabled', false);
+ // Done with FTU, letting everyone know
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('ftudone',
+ /* canBubble */ true, /* cancelable */ false, {});
+ window.dispatchEvent(evt);
+ }
+
+ frame.classList.remove('active');
+ windows.classList.remove('active');
+
+ // set the closed frame visibility to false
+ if ('setVisible' in iframe)
+ iframe.setVisible(false);
+
+ screenElement.classList.remove('fullscreen-app');
+ }
+
+ // The following things needs to happen when firstpaint happens.
+ // We centralize all that here but not all of them applies.
+ windows.addEventListener('mozbrowserfirstpaint', function firstpaint(evt) {
+ var iframe = evt.target;
+ var frame = iframe.parentNode;
+
+ // remove the unpainted flag
+ delete iframe.dataset.unpainted;
+
+ setTimeout(function firstpainted() {
+ // Save the screenshot
+ // Remove the background only until we actually got the screenshot,
+ // because the getScreenshot() call will be pushed back by
+ // painting/loading in the child process; when we got the screenshot,
+ // that means the app is mostly loaded.
+ // (as opposed to plain white firstpaint)
+ saveAppScreenshot(frame, function screenshotTaken() {
+ // Remove the default background
+ frame.classList.remove('default-background');
+
+ // Remove the screenshot from frame
+ clearFrameBackground(frame);
+ });
+ });
+ });
+
+ // setFrameBackground() will attach the screenshot background to
+ // the given frame.
+ // The callback could be sync or async (depend on whether we need
+ // the screenshot from database or not)
+ function setFrameBackground(frame, callback, transparent) {
+ var iframe = frame.firstChild;
+ // If the frame is painted, or there is already background image present
+ // start the transition right away.
+ if (!('unpainted' in iframe.dataset) ||
+ ('bgObjectURL' in frame.dataset)) {
+ callback();
+ return;
+ }
+
+ // Get the screenshot from the database
+ getAppScreenshotFromDatabase(iframe.src || iframe.dataset.frameOrigin,
+ function(screenshot) {
+ // If firstpaint is faster than database, we will not transition
+ // with screenshot.
+ if (!('unpainted' in iframe.dataset)) {
+ callback();
+ return;
+ }
+
+ if (!screenshot) {
+ // put a default background
+ frame.classList.add('default-background');
+ callback();
+ return;
+ }
+
+ // set the screenshot as the background of the frame itself.
+ // we are safe to do so since there is nothing on it yet.
+ setFrameBackgroundBlob(frame, screenshot, transparent);
+
+ // start the transition
+ callback();
+ });
+ }
+
+ // On-disk database for window manager.
+ // It's only for app screenshots right now.
+ var database = null;
+ var DB_SCREENSHOT_OBJSTORE = 'screenshots';
+
+ (function openDatabase() {
+ var DB_VERSION = 2;
+ var DB_NAME = 'window_manager';
+
+ var req = window.indexedDB.open(DB_NAME, DB_VERSION);
+ req.onerror = function() {
+ console.error('Window Manager: opening database failed.');
+ };
+ req.onupgradeneeded = function databaseUpgradeneeded() {
+ database = req.result;
+
+ if (database.objectStoreNames.contains(DB_SCREENSHOT_OBJSTORE))
+ database.deleteObjectStore(DB_SCREENSHOT_OBJSTORE);
+
+ var store = database.createObjectStore(
+ DB_SCREENSHOT_OBJSTORE, { keyPath: 'url' });
+ };
+
+ req.onsuccess = function databaseSuccess() {
+ database = req.result;
+ };
+ })();
+
+ function putAppScreenshotToDatabase(url, data) {
+ if (!database)
+ return;
+
+ var txn = database.transaction(DB_SCREENSHOT_OBJSTORE, 'readwrite');
+ txn.onerror = function() {
+ console.warn(
+ 'Window Manager: transaction error while trying to save screenshot.');
+ };
+ var store = txn.objectStore(DB_SCREENSHOT_OBJSTORE);
+ var req = store.put({
+ url: url,
+ screenshot: data
+ });
+ req.onerror = function(evt) {
+ console.warn(
+ 'Window Manager: put error while trying to save screenshot.');
+ };
+ }
+
+ function getAppScreenshotFromDatabase(url, callback) {
+ if (!database) {
+ console.warn(
+ 'Window Manager: Neither database nor app frame is ' +
+ 'ready for getting screenshot.');
+
+ callback();
+ return;
+ }
+
+ var req = database.transaction(DB_SCREENSHOT_OBJSTORE)
+ .objectStore(DB_SCREENSHOT_OBJSTORE).get(url);
+ req.onsuccess = function() {
+ if (!req.result) {
+ console.log('Window Manager: No screenshot in database. ' +
+ 'This is expected from a fresh installed app.');
+ callback();
+
+ return;
+ }
+
+ callback(req.result.screenshot, true);
+ }
+ req.onerror = function(evt) {
+ console.warn('Window Manager: get screenshot from database failed.');
+ callback();
+ };
+ }
+
+ function deleteAppScreenshotFromDatabase(url) {
+ var txn = database.transaction(DB_SCREENSHOT_OBJSTORE);
+ var store = txn.objectStore(DB_SCREENSHOT_OBJSTORE);
+
+ store.delete(url);
+ }
+
+ function getAppScreenshotFromFrame(frame, callback) {
+ if (!frame) {
+ callback();
+ return;
+ }
+
+ var iframe = frame.firstChild;
+ var req = iframe.getScreenshot(iframe.offsetWidth, iframe.offsetHeight);
+
+ req.onsuccess = function gotScreenshotFromFrame(evt) {
+ var result = evt.target.result;
+ callback(result, false);
+ };
+
+ req.onerror = function gotScreenshotFromFrameError(evt) {
+ console.warn('Window Manager: getScreenshot failed.');
+ callback();
+ };
+ }
+
+ // Meta method for get the screenshot from the app frame,
+ // and save it to database.
+ function saveAppScreenshot(frame, callback) {
+ getAppScreenshotFromFrame(frame, function gotScreenshot(screenshot) {
+ if (callback)
+ callback(screenshot);
+
+ if (!screenshot)
+ return;
+
+ var iframe = frame.firstChild;
+ putAppScreenshotToDatabase(iframe.src || iframe.dataset.frameOrigin,
+ screenshot);
+ });
+ }
+
+ // Perform an "open" animation for the app's iframe
+ function openWindow(origin, callback) {
+ var app = runningApps[origin];
+ setOpenFrame(app.frame);
+
+ openCallback = callback || function() {};
+
+ // set the size of the opening app
+ setAppSize(origin);
+
+ if (origin === homescreen) {
+ // We cannot apply background screenshot to home screen app since
+ // the screenshot is encoded in JPEG and the alpha channel is
+ // not perserved. See
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=801676#c33
+ // If that resolves,
+ // setFrameBackground(openFrame, gotBackground, true);
+ // will simply work here.
+
+ // Call the openCallback only once. We have to use tmp var as
+ // openCallback can be a method calling the callback
+ // (like the `removeFrame` callback in `kill()` ).
+ var tmpCallback = openCallback;
+ openCallback = null;
+ tmpCallback();
+
+ windows.classList.add('active');
+ openFrame.classList.add('homescreen');
+ openFrame.firstChild.focus();
+ setOpenFrame(null);
+ displayedApp = origin;
+
+ return;
+ }
+
+ if (requireFullscreen(origin))
+ screenElement.classList.add('fullscreen-app');
+
+ transitionOpenCallback = function startOpeningTransition() {
+ // We have been canceled by another transition.
+ if (!openFrame || transitionOpenCallback != startOpeningTransition)
+ return;
+
+ // Make sure we're not called twice.
+ transitionOpenCallback = null;
+
+ if (!screenElement.classList.contains('switch-app')) {
+ openFrame.classList.add('opening');
+ } else if (!openFrame.classList.contains('opening')) {
+ openFrame.classList.add('opening-card');
+ }
+ };
+
+ if ('unpainted' in openFrame.firstChild.dataset) {
+ waitForNextPaintOrBackground(openFrame, transitionOpenCallback);
+ } else {
+ waitForNextPaint(openFrame, transitionOpenCallback);
+ }
+
+ // Set the frame to be visible.
+ if ('setVisible' in openFrame.firstChild) {
+ if (!AttentionScreen.isFullyVisible()) {
+ openFrame.firstChild.setVisible(true);
+ } else {
+ // If attention screen is fully visible now,
+ // don't give the open frame visible.
+ // This is the case that homescreen is restarted behind attention screen
+ openFrame.firstChild.setVisible(false);
+ }
+ }
+ }
+
+ function waitForNextPaintOrBackground(frame, callback) {
+ var waiting = true;
+ function proceed() {
+ if (waiting) {
+ waiting = false;
+ callback();
+ }
+ }
+
+ waitForNextPaint(frame, proceed);
+ setFrameBackground(frame, proceed);
+ }
+
+ function waitForNextPaint(frame, callback) {
+ function onNextPaint() {
+ clearTimeout(timeout);
+ callback();
+ }
+
+ var iframe = frame.firstChild;
+
+ // Register a timeout in case we don't receive
+ // nextpaint in an acceptable time frame.
+ var timeout = setTimeout(function() {
+ if ('removeNextPaintListener' in iframe)
+ iframe.removeNextPaintListener(onNextPaint);
+ callback();
+ }, kTransitionTimeout);
+
+ if ('addNextPaintListener' in iframe)
+ iframe.addNextPaintListener(onNextPaint);
+ }
+
+ // Perform a "close" animation for the app's iframe
+ function closeWindow(origin, callback) {
+ var app = runningApps[origin];
+ setCloseFrame(app.frame);
+ closeCallback = callback || function() {};
+
+ // Animate the window close. Ensure the homescreen is in the
+ // foreground since it will be shown during the animation.
+ var homescreenFrame = ensureHomescreen();
+
+ // invoke openWindow to show homescreen here
+ openWindow(homescreen, null);
+
+ // Take keyboard focus away from the closing window
+ closeFrame.firstChild.blur();
+
+ // set orientation for homescreen app
+ setOrientationForApp(homescreen);
+
+ // Set the size of both homescreen app and the closing app
+ // since the orientation had changed.
+ setAppSize(homescreen);
+ setAppSize(origin);
+
+ // Send a synthentic 'appwillclose' event.
+ // The keyboard uses this and the appclose event to know when to close
+ // See https://github.com/andreasgal/gaia/issues/832
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('appwillclose', true, false, { origin: origin });
+ closeFrame.dispatchEvent(evt);
+
+ if ('wrapper' in closeFrame.dataset) {
+ wrapperHeader.classList.remove('visible');
+ wrapperFooter.classList.remove('visible');
+ }
+
+ transitionCloseCallback = function startClosingTransition() {
+ // We have been canceled by another transition.
+ if (!closeFrame || transitionCloseCallback != startClosingTransition)
+ return;
+
+ // Make sure we're not called twice.
+ transitionCloseCallback = null;
+
+ // Start the transition
+ closeFrame.classList.add('closing');
+ closeFrame.classList.remove('active');
+ };
+
+ waitForNextPaint(homescreenFrame, transitionCloseCallback);
+ }
+
+ // Perform a "switching" animation for the closing frame and the opening frame
+ function switchWindow(origin, callback) {
+ // This will trigger different transition to both openWindow()
+ // and closeWindow() transition.
+ screenElement.classList.add('switch-app');
+
+ // Ask closeWindow() to start closing the displayedApp
+ closeWindow(displayedApp, callback);
+
+ // Ask openWindow() to show a card on the right waiting to be opened
+ openWindow(origin);
+ }
+
+ // Ensure the homescreen is loaded and return its frame. Restarts
+ // the homescreen app if it was killed in the background.
+ // Note: this function would not invoke openWindow(homescreen),
+ // which should be handled in setDisplayedApp and in closeWindow()
+ function ensureHomescreen(reset) {
+ // If the url of the homescreen is not known at this point do nothing.
+ if (!homescreen || !homescreenManifestURL) {
+ return null;
+ }
+
+ if (!isRunning(homescreen)) {
+ var app = Applications.getByManifestURL(homescreenManifestURL);
+ appendFrame(null, homescreen, homescreenURL,
+ app.manifest.name, app.manifest, app.manifestURL);
+ runningApps[homescreen].iframe.dataset.start = Date.now();
+ setAppSize(homescreen);
+ } else if (reset) {
+ runningApps[homescreen].iframe.src = homescreenURL;
+ setAppSize(homescreen);
+ }
+
+ return runningApps[homescreen].frame;
+ }
+
+ function retrieveHomescreen(callback) {
+ var lock = navigator.mozSettings.createLock();
+ var setting = lock.get('homescreen.manifestURL');
+ setting.onsuccess = function() {
+ var app =
+ Applications.getByManifestURL(this.result['homescreen.manifestURL']);
+
+ // XXX This is a one-day workaround to not break everybody and make sure
+ // work can continue.
+ if (!app) {
+ var tmpURL = document.location.toString()
+ .replace('system', 'homescreen')
+ .replace('index.html', 'manifest.webapp');
+ app = Applications.getByManifestURL(tmpURL);
+ }
+
+ if (app) {
+ homescreenManifestURL = app.manifestURL;
+ homescreen = app.origin;
+ homescreenURL = app.origin + '/index.html#root';
+
+ callback(app);
+ }
+ }
+ }
+
+ function skipFTU() {
+ document.getElementById('screen').classList.remove('ftuStarting');
+ handleInitlogo();
+ setDisplayedApp(homescreen);
+ }
+
+ // Check if the FTU was executed or not, if not, get a
+ // reference to the app and launch it.
+ function retrieveFTU() {
+ window.asyncStorage.getItem('ftu.enabled', function getItem(launchFTU) {
+ document.getElementById('screen').classList.add('ftuStarting');
+ if (launchFTU === false) {
+ skipFTU();
+ return;
+ }
+ var lock = navigator.mozSettings.createLock();
+ var req = lock.get('ftu.manifestURL');
+ req.onsuccess = function() {
+ ftuManifestURL = this.result['ftu.manifestURL'];
+ if (!ftuManifestURL) {
+ dump('FTU manifest cannot be found skipping.\n');
+ skipFTU();
+ return;
+ }
+ ftu = Applications.getByManifestURL(ftuManifestURL);
+ if (!ftu) {
+ dump('Opps, bogus FTU manifest.\n');
+ skipFTU();
+ return;
+ }
+ ftuURL = ftu.origin + ftu.manifest.entry_points['ftu'].launch_path;
+ ftu.launch('ftu');
+ };
+ req.onerror = function() {
+ dump('Couldn\'t get the ftu manifestURL.\n');
+ skipFTU();
+ };
+ });
+ }
+
+ // Hide current app
+ function hideCurrentApp(callback) {
+ if (displayedApp == null || displayedApp == homescreen)
+ return;
+
+ toggleHomescreen(true);
+ var frame = getAppFrame(displayedApp);
+ frame.classList.add('back');
+ frame.classList.remove('restored');
+ if (callback) {
+ frame.addEventListener('transitionend', function execCallback() {
+ frame.style.visibility = 'hidden';
+ frame.removeEventListener('transitionend', execCallback);
+ callback();
+ });
+ }
+ }
+
+ // If app parameter is passed,
+ // it means there's a specific app needs to be restored
+ // instead of current app
+ function restoreCurrentApp(app) {
+ if (app) {
+ // Restore app visibility immediately but don't open it.
+ var frame = getAppFrame(app);
+ frame.style.visibility = 'visible';
+ frame.classList.remove('back');
+ } else {
+ app = displayedApp;
+ toggleHomescreen(false);
+ var frame = getAppFrame(app);
+ frame.style.visibility = 'visible';
+ frame.classList.remove('back');
+ frame.classList.add('restored');
+ frame.addEventListener('transitionend', function removeRestored() {
+ frame.removeEventListener('transitionend', removeRestored);
+ frame.classList.remove('restored');
+ });
+ }
+ }
+
+ function toggleHomescreen(visible) {
+ var homescreenFrame = ensureHomescreen();
+ if (homescreenFrame && 'setVisible' in homescreenFrame.firstChild)
+ homescreenFrame.firstChild.setVisible(visible);
+ }
+
+ // Switch to a different app
+ function setDisplayedApp(origin, callback) {
+ var currentApp = displayedApp, newApp = origin || homescreen;
+ var isFirstRunApplication = !currentApp && (origin == ftuURL);
+
+ var homescreenFrame = null;
+ if (!isFirstRunApplication) {
+ // Returns the frame reference of the home screen app.
+ // Restarts the homescreen app if it was killed in the background.
+ homescreenFrame = ensureHomescreen();
+ }
+
+ // Cancel transitions waiting to be started.
+ transitionOpenCallback = null;
+ transitionCloseCallback = null;
+
+ // Discard any existing activity
+ stopInlineActivity(true);
+
+ // Before starting a new transition, let's make sure current transitions
+ // are stopped and the state classes are cleaned up.
+ // visibility status should also be reset.
+ if (openFrame && 'setVisible' in openFrame.firstChild)
+ openFrame.firstChild.setVisible(false);
+ if (closeFrame && 'setVisible' in closeFrame.firstChild)
+ closeFrame.firstChild.setVisible(false);
+
+ if (!isFirstRunApplication && newApp == homescreen && !AttentionScreen.isFullyVisible()) {
+ toggleHomescreen(true);
+ }
+
+ setOpenFrame(null);
+ setCloseFrame(null);
+ screenElement.classList.remove('switch-app');
+ screenElement.classList.remove('fullscreen-app');
+
+ // Dispatch an appwillopen event only when we open an app
+ if (newApp != currentApp) {
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('appwillopen', true, true, { origin: newApp });
+
+ var app = runningApps[newApp];
+ // Allows listeners to cancel app opening and so stay on homescreen
+ if (!app.frame.dispatchEvent(evt)) {
+ if (typeof(callback) == 'function')
+ callback();
+ return;
+ }
+
+ var iframe = app.iframe;
+
+ // unpainted means that the app is cold booting
+ // if it is, we're going to listen for Browser API's loadend event
+ // which indicates that the iframe's document load is complete
+ //
+ // if the app is not cold booting (is in memory) we will listen
+ // to appopen event, which is fired when the transition to the
+ // app window is complete.
+ //
+ // [w] - warm boot (app is in memory, just transition to it)
+ // [c] - cold boot (app has to be booted, we show it's document load
+ // time)
+ var type;
+ if ('unpainted' in iframe.dataset) {
+ type = 'mozbrowserloadend';
+ } else {
+ iframe.dataset.start = Date.now();
+ type = 'appopen';
+ }
+
+ app.frame.addEventListener(type, function apploaded(e) {
+ e.target.removeEventListener(e.type, apploaded);
+
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('apploadtime', true, false, {
+ time: parseInt(Date.now() - iframe.dataset.start),
+ type: (e.type == 'appopen') ? 'w' : 'c'
+ });
+ iframe.dispatchEvent(evt);
+ });
+ }
+
+ // Case 1: the app is already displayed
+ if (currentApp && currentApp == newApp) {
+ if (newApp == homescreen) {
+ // relaunch homescreen
+ openWindow(homescreen, callback);
+ } else if (callback) {
+ // Just run the callback right away if it is not homescreen
+ callback();
+ }
+ }
+ // Case 2: null --> app
+ else if (isFirstRunApplication) {
+ isRunningFirstRunApp = true;
+ openWindow(newApp, function windowOpened() {
+ handleInitlogo(function() {
+ var mainScreen = document.getElementById('screen');
+ mainScreen.classList.add('ftu');
+ mainScreen.classList.remove('ftuStarting');
+ });
+ });
+ }
+ // Case 3: null->homescreen || homescreen->app
+ else if ((!currentApp && newApp == homescreen) ||
+ (currentApp == homescreen && newApp)) {
+ openWindow(newApp, callback);
+ }
+ // Case 4: app->homescreen
+ else if (currentApp && currentApp != homescreen && newApp == homescreen) {
+ // For screenshot to catch current window size
+ closeWindow(currentApp, callback);
+ }
+ // Case 5: app-to-app transition
+ else {
+ switchWindow(newApp, callback);
+ }
+ // Set homescreen as active,
+ // to control the z-index between homescreen & keyboard iframe
+ if ((newApp == homescreen) && homescreenFrame) {
+ homescreenFrame.classList.add('active');
+ } else {
+ homescreenFrame.classList.remove('active');
+ }
+
+ // Record the time when app was launched,
+ // need this to display apps in proper order on CardsView.
+ // We would also need this to determined the freshness of the frame
+ // for making screenshots.
+ if (newApp)
+ runningApps[newApp].launchTime = Date.now();
+
+ // If the app has a attention screen open, displaying it
+ AttentionScreen.showForOrigin(newApp);
+ }
+
+ function setOrientationForApp(origin) {
+ if (origin == null) { // No app is currently running.
+ screen.mozLockOrientation('portrait-primary');
+ return;
+ }
+
+ var app = runningApps[origin];
+ if (!app)
+ return;
+ var manifest = app.manifest;
+
+ if (manifest.orientation) {
+ var rv = screen.mozLockOrientation(manifest.orientation);
+ if (rv === false) {
+ console.warn('screen.mozLockOrientation() returned false for',
+ origin, 'orientation', manifest.orientation);
+ }
+ }
+ else { // If no orientation was requested, then let it rotate
+ screen.mozUnlockOrientation();
+ }
+ }
+
+ var isOutOfProcessDisabled = false;
+ SettingsListener.observe('debug.oop.disabled', false, function(value) {
+ isOutOfProcessDisabled = value;
+ });
+
+ function createFrame(origFrame, origin, url, name, manifest, manifestURL) {
+ var iframe = origFrame || document.createElement('iframe');
+ iframe.setAttribute('mozallowfullscreen', 'true');
+
+ var frame = document.createElement('div');
+ frame.appendChild(iframe);
+ frame.className = 'appWindow';
+
+ iframe.dataset.frameOrigin = origin;
+ // Save original frame URL in order to restore it on frame load error
+ iframe.dataset.frameURL = url;
+
+ // Note that we don't set the frame size here. That will happen
+ // when we display the app in setDisplayedApp()
+
+ // frames are began unpainted.
+ iframe.dataset.unpainted = true;
+
+ if (!manifestURL) {
+ frame.setAttribute('data-wrapper', 'true');
+ return frame;
+ }
+
+ // Most apps currently need to be hosted in a special 'mozbrowser' iframe.
+ // They also need to be marked as 'mozapp' to be recognized as apps by the
+ // platform.
+ iframe.setAttribute('mozbrowser', 'true');
+
+ // These apps currently have bugs preventing them from being
+ // run out of process. All other apps will be run OOP.
+ //
+ var outOfProcessBlackList = [
+ 'Browser'
+ // Requires nested content processes (bug 761935). This is not
+ // on the schedule for v1.
+ ];
+
+ if (!isOutOfProcessDisabled &&
+ outOfProcessBlackList.indexOf(name) === -1) {
+ // FIXME: content shouldn't control this directly
+ iframe.setAttribute('remote', 'true');
+ }
+
+ iframe.setAttribute('mozapp', manifestURL);
+ iframe.src = url;
+ return frame;
+ }
+
+ function appendFrame(origFrame, origin, url, name, manifest, manifestURL) {
+ // Create the <iframe mozbrowser mozapp> that hosts the app
+ var frame =
+ createFrame(origFrame, origin, url, name, manifest, manifestURL);
+ var iframe = frame.firstChild;
+ frame.id = 'appframe' + nextAppId++;
+ iframe.dataset.frameType = 'window';
+
+ // Give a name to the frame for differentiating between main frame and
+ // inline frame. With the name we can get frames of the same app using the
+ // window.open method.
+ iframe.name = 'main';
+
+ // If this frame corresponds to the homescreen, set mozapptype=homescreen
+ // so we're less likely to kill this frame's process when we're running low
+ // on memory.
+ //
+ // We must do this before we the appendChild() call below. Once
+ // we add this frame to the document, we can't change its app type.
+ if (origin === homescreen) {
+ iframe.setAttribute('mozapptype', 'homescreen');
+ }
+
+ // Add the iframe to the document
+ windows.appendChild(frame);
+
+ // And map the app origin to the info we need for the app
+ var app = new AppWindow({
+ origin: origin,
+ name: name,
+ manifest: manifest,
+ manifestURL: manifestURL,
+ frame: frame,
+ iframe: iframe,
+ launchTime: 0
+ });
+ runningApps[origin] = app;
+
+ if (requireFullscreen(origin)) {
+ frame.classList.add('fullscreen-app');
+ }
+ if (origin === ftuURL) {
+ // Add a way to identify ftu app
+ // (Used by SimLock)
+ frame.classList.add('ftu');
+ }
+
+ // A frame should start with visible false
+ if ('setVisible' in iframe)
+ iframe.setVisible(false);
+
+ numRunningApps++;
+
+ return app;
+ }
+
+ function startInlineActivity(origin, url, name, manifest, manifestURL) {
+ // Create the <iframe mozbrowser mozapp> that hosts the app
+ var frame = createFrame(null, origin, url, name, manifest, manifestURL);
+ var iframe = frame.firstChild;
+ frame.classList.add('inlineActivity');
+ iframe.dataset.frameType = 'inline-activity';
+
+ // Give a name to the frame for differentiating between main frame and
+ // inline frame. With the name we can get frames of the same app using the
+ // window.open method.
+ iframe.name = 'inline';
+
+ // Save the reference
+ inlineActivityFrames.push(frame);
+
+ // Set the size
+ setInlineActivityFrameSize();
+
+ // Add the iframe to the document
+ windows.appendChild(frame);
+
+ // Open the frame, first, store the reference
+ openFrame = frame;
+
+ // set the frame to visible state
+ if ('setVisible' in iframe)
+ iframe.setVisible(true);
+
+ setFrameBackground(openFrame, function gotBackground() {
+ // Start the transition when this async/sync callback is called.
+ openFrame.classList.add('active');
+ if (inlineActivityFrames.length == 1)
+ activityCallerOrigin = displayedApp;
+ if ('wrapper' in runningApps[displayedApp].frame.dataset) {
+ wrapperFooter.classList.remove('visible');
+ wrapperHeader.classList.remove('visible');
+ }
+ });
+ }
+
+ function removeFrame(origin) {
+ var app = runningApps[origin];
+ var frame = app.frame;
+
+ if (frame) {
+ windows.removeChild(frame);
+ clearFrameBackground(frame);
+ }
+
+ if (openFrame == frame) {
+ setOpenFrame(null);
+ setTimeout(openCallback);
+ openCallback = null;
+ }
+ if (closeFrame == frame) {
+ setCloseFrame(null);
+ setTimeout(closeCallback);
+ closeCallback = null;
+ }
+
+ delete runningApps[origin];
+ numRunningApps--;
+ }
+
+ function removeInlineFrame(frame) {
+ // If frame is transitioning we should remove the reference
+ if (openFrame == frame)
+ setOpenFrame(null);
+
+ // If frame is never set visible, we can remove the frame directly
+ // without closing transition
+ if (!frame.classList.contains('active')) {
+ windows.removeChild(frame);
+ return;
+ }
+ // Take keyboard focus away from the closing window
+ frame.firstChild.blur();
+ // Remove the active class and start the closing transition
+ frame.classList.remove('active');
+ }
+
+ // If all is not specified,
+ // remove the top most frame
+ function stopInlineActivity(all) {
+ if (!inlineActivityFrames.length)
+ return;
+
+ if (!all) {
+ var frame = inlineActivityFrames.pop();
+ removeInlineFrame(frame);
+ } else {
+ // stop all activity frames
+ // Remore the inlineActivityFrame reference
+ for (var frame of inlineActivityFrames) {
+ removeInlineFrame(frame);
+ }
+ inlineActivityFrames = [];
+ }
+
+ if (!inlineActivityFrames.length) {
+ // Give back focus to the displayed app
+ var app = runningApps[displayedApp];
+ if (app && app.iframe) {
+ app.iframe.focus();
+ if ('wrapper' in app.frame.dataset) {
+ wrapperFooter.classList.add('visible');
+ }
+ }
+ screenElement.classList.remove('inline-activity');
+ }
+ }
+
+ // Watch activity completion here instead of activity.js
+ // Because we know when and who to re-launch when activity ends.
+ window.addEventListener('mozChromeEvent', function(e) {
+ if (e.detail.type == 'activity-done') {
+ // Remove the top most frame every time we get an 'activity-done' event.
+ stopInlineActivity();
+ if (!inlineActivityFrames.length) {
+ setDisplayedApp(activityCallerOrigin);
+ activityCallerOrigin = '';
+ }
+ }
+ });
+
+ // There are two types of mozChromeEvent we need to handle
+ // in order to launch the app for Gecko
+ window.addEventListener('mozChromeEvent', function(e) {
+ var startTime = Date.now();
+
+ var manifestURL = e.detail.manifestURL;
+ if (!manifestURL)
+ return;
+
+ var app = Applications.getByManifestURL(manifestURL);
+ if (!app)
+ return;
+
+ var manifest = app.manifest;
+ var name = new ManifestHelper(manifest).name;
+ var origin = app.origin;
+
+ // Check if it's a virtual app from a entry point.
+ // If so, change the app name and origin to the
+ // entry point.
+ var entryPoints = manifest.entry_points;
+ if (entryPoints && manifest.type == 'certified') {
+ var givenPath = e.detail.url.substr(origin.length);
+
+ // Workaround here until the bug (to be filed) is fixed
+ // Basicly, gecko is sending the URL without launch_path sometimes
+ for (var ep in entryPoints) {
+ var currentEp = entryPoints[ep];
+ var path = givenPath;
+ if (path.indexOf('?') != -1) {
+ path = path.substr(0, path.indexOf('?'));
+ }
+
+ //Remove the origin and / to find if if the url is the entry point
+ if (path.indexOf('/' + ep) == 0 &&
+ (currentEp.launch_path == path)) {
+ origin = origin + currentEp.launch_path;
+ name = new ManifestHelper(currentEp).name;
+ }
+ }
+ }
+ switch (e.detail.type) {
+ // mozApps API is asking us to launch the app
+ // We will launch it in foreground
+ case 'webapps-launch':
+ if (origin == homescreen) {
+ // No need to append a frame if is homescreen
+ setDisplayedApp();
+ } else {
+ if (!isRunning(origin)) {
+ appendFrame(null, origin, e.detail.url,
+ name, app.manifest, app.manifestURL);
+ }
+ runningApps[origin].iframe.dataset.start = startTime;
+ setDisplayedApp(origin, null, 'window');
+ }
+ break;
+ // System Message Handler API is asking us to open the specific URL
+ // that handles the pending system message.
+ // We will launch it in background if it's not handling an activity.
+ case 'open-app':
+ // If the system message goes to System app,
+ // we should not be launching that in a frame.
+ if (e.detail.url === window.location.href)
+ return;
+
+ if (e.detail.isActivity && e.detail.target.disposition &&
+ e.detail.target.disposition == 'inline') {
+ // Inline activities behaves more like a dialog,
+ // let's deal them here.
+
+ startInlineActivity(origin, e.detail.url,
+ name, manifest, app.manifestURL);
+
+ return;
+ }
+
+ if (isRunning(origin)) {
+ // If the app is in foreground, it's too risky to change it's
+ // URL. We'll ignore this request.
+ if (displayedApp !== origin) {
+ var iframe = getAppFrame(origin).firstChild;
+
+ // If the app is opened and it is loaded to the correct page,
+ // then there is nothing to do.
+ if (iframe.src !== e.detail.url) {
+ // Rewrite the URL of the app frame to the requested URL.
+ // XXX: We could ended opening URls not for the app frame
+ // in the app frame. But we don't care.
+ iframe.src = e.detail.url;
+ }
+ }
+ } else if (origin !== homescreen) {
+ // XXX: We could ended opening URls not for the app frame
+ // in the app frame. But we don't care.
+ appendFrame(null, origin, e.detail.url,
+ name, manifest, app.manifestURL);
+
+ // set the size of the iframe
+ // so Cards View will get a correct screenshot of the frame
+ if (!e.detail.isActivity)
+ setAppSize(origin, false);
+ } else {
+ ensureHomescreen();
+ }
+
+ // We will only bring web activity handling apps to the foreground
+ if (!e.detail.isActivity)
+ return;
+
+ // XXX: the correct way would be for UtilityTray to close itself
+ // when there is a appwillopen/appopen event.
+ UtilityTray.hide();
+
+ setDisplayedApp(origin);
+
+ break;
+ }
+ });
+
+ // If the application tried to close themselves by calling window.close()
+ // we will handle that here.
+ // XXX: this event is fired twice:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=814583
+ window.addEventListener('mozbrowserclose', function(e) {
+ if (!'frameType' in e.target.dataset)
+ return;
+
+ switch (e.target.dataset.frameType) {
+ case 'window':
+ kill(e.target.dataset.frameOrigin);
+ break;
+
+ case 'inline-activity':
+ stopInlineActivity(true);
+ break;
+ }
+ });
+
+ // Deal with locationchange
+ window.addEventListener('mozbrowserlocationchange', function(e) {
+ if (!'frameType' in e.target.dataset)
+ return;
+
+ e.target.dataset.url = e.detail;
+ });
+
+ // Deal with application uninstall event
+ // if the application is being uninstalled, we ensure it stop running here.
+ window.addEventListener('applicationuninstall', function(e) {
+ kill(e.detail.application.origin);
+
+ deleteAppScreenshotFromDatabase(e.detail.application.origin);
+ });
+
+ // When an UI layer is overlapping the current app,
+ // WindowManager should set the visibility of app iframe to false
+ // And reset to true when the layer is gone.
+ // We may need to handle windowclosing, windowopened in the future.
+ var attentionScreenTimer = null;
+
+ var overlayEvents = [
+ 'lock',
+ 'will-unlock',
+ 'attentionscreenshow',
+ 'attentionscreenhide',
+ 'status-active',
+ 'status-inactive'
+ ];
+
+ function overlayEventHandler(evt) {
+ if (attentionScreenTimer)
+ clearTimeout(attentionScreenTimer);
+ switch (evt.type) {
+ case 'status-active':
+ case 'attentionscreenhide':
+ case 'will-unlock':
+ if (LockScreen.locked)
+ return;
+ if (inlineActivityFrames.length) {
+ setVisibilityForInlineActivity(true);
+ } else {
+ setVisibilityForCurrentApp(true);
+ }
+ break;
+ case 'lock':
+ setVisibilityForCurrentApp(false);
+ break;
+
+ /*
+ * Because in-transition is needed in attention screen,
+ * We set a timer here to deal with visibility change
+ */
+ case 'status-inactive':
+ if (!AttentionScreen.isVisible())
+ return;
+ case 'attentionscreenshow':
+ if (evt.detail && evt.detail.origin &&
+ evt.detail.origin != displayedApp) {
+ attentionScreenTimer = setTimeout(function setVisibility() {
+ if (inlineActivityFrames.length) {
+ setVisibilityForInlineActivity(false);
+ } else {
+ setVisibilityForCurrentApp(false);
+ }
+ }, 3000);
+
+ // Immediatly blur the frame in order to ensure hiding the keyboard
+ var app = runningApps[displayedApp];
+ if (app)
+ app.iframe.blur();
+ }
+ break;
+ }
+ }
+
+ overlayEvents.forEach(function overlayEventIterator(event) {
+ window.addEventListener(event, overlayEventHandler);
+ });
+
+ function setVisibilityForInlineActivity(visible) {
+ if (!inlineActivityFrames.length)
+ return;
+
+ var topFrame = inlineActivityFrames[inlineActivityFrames.length - 1].firstChild;
+ if ('setVisible' in topFrame) {
+ topFrame.setVisible(visible);
+ }
+
+ // Restore/give away focus on visiblity change
+ // so that the app can take back its focus
+ if (visible) {
+ topFrame.focus();
+ } else {
+ topFrame.blur();
+ }
+ }
+
+ function setVisibilityForCurrentApp(visible) {
+ var app = runningApps[displayedApp];
+ if (!app)
+ return;
+ if ('setVisible' in app.iframe)
+ app.iframe.setVisible(visible);
+
+ // Restore/give away focus on visiblity change
+ // so that the app can take back its focus
+ if (visible)
+ app.iframe.focus();
+ else
+ app.iframe.blur();
+ }
+
+ function handleAppCrash(origin, manifestURL) {
+ if (origin && manifestURL) {
+ // When inline activity frame crashes,
+ // query the localized name from manifest
+ var app = Applications.getByManifestURL(manifestURL);
+ CrashReporter.setAppName(getAppName(origin, app.manifest));
+ } else {
+ var app = runningApps[displayedApp];
+ CrashReporter.setAppName(app.name);
+ }
+ }
+
+ function getAppName(origin, manifest) {
+ if (!manifest)
+ return '';
+
+ if (manifest.entry_points && manifest.type == 'certified') {
+ var entryPoint = manifest.entry_points[origin.split('/')[3]];
+ return new ManifestHelper(entryPoint).name;
+ }
+ return new ManifestHelper(manifest).name;
+ }
+
+ // Deal with crashed apps
+ window.addEventListener('mozbrowsererror', function(e) {
+ if (!'frameType' in e.target.dataset)
+ return;
+
+ var origin = e.target.dataset.frameOrigin;
+ var manifestURL = e.target.getAttribute('mozapp');
+
+ if (e.target.dataset.frameType == 'inline-activity') {
+ stopInlineActivity(true);
+ handleAppCrash(origin, manifestURL);
+ return;
+ }
+
+ if (e.target.dataset.frameType !== 'window')
+ return;
+
+ /*
+ detail.type = error (Server Not Found case)
+ is handled in Modal Dialog
+ */
+ if (e.detail.type !== 'fatal')
+ return;
+
+ // If the crashing app is currently displayed, we will present
+ // the user with a banner notification.
+ if (displayedApp == origin)
+ handleAppCrash();
+
+ // If the crashing app is the home screen app and it is the displaying app
+ // we will need to relaunch it right away.
+ // Alternatively, if home screen is not the displaying app,
+ // we will not relaunch it until the foreground app is closed.
+ // (to be dealt in setDisplayedApp(), not here)
+ if (displayedApp == homescreen) {
+ kill(origin, function relaunchHomescreen() {
+ setDisplayedApp(homescreen);
+ });
+ return;
+ }
+
+ // Actually remove the frame, and trigger the closing transition
+ // if the app is currently displaying
+ kill(origin);
+ });
+
+
+ function hasPermission(app, permission) {
+ var mozPerms = navigator.mozPermissionSettings;
+ if (!mozPerms)
+ return false;
+
+ var value = mozPerms.get(permission, app.manifestURL, app.origin, false);
+
+ return (value === 'allow');
+ }
+
+ // Use a setting in order to be "called" by settings app
+ navigator.mozSettings.addObserver(
+ 'clear.remote-windows.data',
+ function clearRemoteWindowsData(setting) {
+ var shouldClear = setting.settingValue;
+ if (!shouldClear)
+ return;
+
+ // Delete all storage and cookies from our content processes
+ var request = navigator.mozApps.getSelf();
+ request.onsuccess = function() {
+ request.result.clearBrowserData();
+ };
+
+ // Reset the setting value to false
+ var lock = navigator.mozSettings.createLock();
+ lock.set({'clear.remote-windows.data': false});
+ });
+
+ // Watch for window.open usages in order to open wrapper frames
+ window.addEventListener('mozbrowseropenwindow', function handleWrapper(evt) {
+ var detail = evt.detail;
+ var features;
+ try {
+ features = JSON.parse(detail.features);
+ } catch (e) {
+ features = {};
+ }
+
+ // Handles only call to window.open with `{remote: true}` feature.
+ if (!features.remote)
+ return;
+
+ // XXX bug 819882: for now, only allows homescreen to open oop windows
+ var callerIframe = evt.target;
+ var callerFrame = callerIframe.parentNode;
+ var manifestURL = callerIframe.getAttribute('mozapp');
+ var callerApp = Applications.getByManifestURL(manifestURL);
+ if (!callerApp || !callerFrame.classList.contains('homescreen'))
+ return;
+ var callerOrigin = callerApp.origin;
+
+ // So, we are going to open a remote window.
+ // Now, avoid PopupManager listener to be fired.
+ evt.stopImmediatePropagation();
+
+ var name = detail.name;
+ var url = detail.url;
+
+ // Use fake origin for named windows in order to be able to reuse them,
+ // otherwise always open a new window for '_blank'.
+ var origin = null;
+ var app = null;
+ if (name == '_blank') {
+ origin = url;
+
+ // Just bring on top if a wrapper window is already running with this url
+ if (origin in runningApps &&
+ runningApps[origin].windowName == '_blank') {
+ setDisplayedApp(origin);
+ return;
+ }
+ } else {
+ origin = 'window:' + name + ',source:' + callerOrigin;
+
+ var runningApp = runningApps[origin];
+ if (runningApp && runningApp.windowName === name) {
+ if (runningApp.iframe.src === url) {
+ // If the url is already loaded, just display the app
+ setDisplayedApp(origin);
+ return;
+ } else {
+ // Wrapper context shouldn't be shared between two apps -> killing
+ kill(origin);
+ }
+ }
+ }
+
+ var title = '', icon = '', remote = false, useAsyncPanZoom = false;
+ var originName, originURL, searchName, searchURL;
+
+ try {
+ var features = JSON.parse(detail.features);
+ var regExp = new RegExp('&nbsp;', 'g');
+
+ title = features.name.replace(regExp, ' ') || url;
+ icon = features.icon || '';
+
+ if (features.origin) {
+ originName = features.origin.name.replace(regExp, ' ');
+ originURL = decodeURIComponent(features.origin.url);
+ }
+
+ if (features.search) {
+ searchName = features.search.name.replace(regExp, ' ');
+ searchURL = decodeURIComponent(features.search.url);
+ }
+
+ if (features.useAsyncPanZoom)
+ useAsyncPanZoom = true;
+ } catch (ex) { }
+
+ // If we don't reuse an existing app, open a brand new one
+ var iframe;
+ if (!app) {
+ // Bug 807438: Move new window document OOP
+ // Ignore `event.detail.frameElement` for now in order
+ // to create a remote system app frame.
+ // So that new window documents are going to share
+ // system app content processes data jar.
+ iframe = document.createElement('iframe');
+ iframe.setAttribute('mozbrowser', 'true');
+ iframe.setAttribute('remote', 'true');
+
+ iframe.addEventListener('mozbrowserloadstart', function start() {
+ iframe.dataset.loading = true;
+ wrapperHeader.classList.add('visible');
+ });
+
+ iframe.addEventListener('mozbrowserloadend', function end() {
+ delete iframe.dataset.loading;
+ wrapperHeader.classList.remove('visible');
+ });
+
+ // `mozasyncpanzoom` only works when added before attaching the iframe
+ // node to the document.
+ if (useAsyncPanZoom) {
+ iframe.dataset.useAsyncPanZoom = true;
+ iframe.setAttribute('mozasyncpanzoom', 'true');
+ }
+
+ var app = appendFrame(iframe, origin, url, title, {
+ 'name': title
+ }, null);
+
+ // Set the window name in order to reuse this app if we try to open
+ // a new window with same name
+ app.windowName = name;
+ } else {
+ iframe = app.iframe;
+
+ // Update app name for the card view
+ app.manifest.name = title;
+ }
+
+ iframe.dataset.name = title;
+ iframe.dataset.icon = icon;
+
+ if (originName)
+ iframe.dataset.originName = originName;
+ if (originURL)
+ iframe.dataset.originURL = originURL;
+
+ if (searchName)
+ iframe.dataset.searchName = searchName;
+ if (searchURL)
+ iframe.dataset.searchURL = searchURL;
+
+ // First load blank page in order to hide previous website
+ iframe.src = url;
+
+ setDisplayedApp(origin);
+ }, true); // Use capture in order to catch the event before PopupManager does
+
+
+ // Stop running the app with the specified origin
+ function kill(origin, callback) {
+ if (!isRunning(origin))
+ return;
+
+ // As we can't immediatly remove runningApps entry,
+ // we flag it as being killed in order to avoid trying to remove it twice.
+ // (Check required because of bug 814583)
+ if (runningApps[origin].killed)
+ return;
+ runningApps[origin].killed = true;
+
+ // If the app is the currently displayed app, switch to the homescreen
+ if (origin === displayedApp) {
+ // when the homescreen is displayed and being
+ // killed we need to forcibly restart it...
+ if (origin === homescreen) {
+ removeFrame(origin);
+
+ // XXX workaround bug 810431.
+ // we need this here and not in other situations
+ // as it is expected that homescreen frame is available.
+ setTimeout(function() {
+ setDisplayedApp();
+
+ if (callback) {
+ callback();
+ }
+ });
+ } else {
+ setDisplayedApp(homescreen, function() {
+ removeFrame(origin);
+ if (callback)
+ setTimeout(callback);
+ });
+ }
+
+ } else {
+ removeFrame(origin);
+ }
+
+ // Send a synthentic 'appterminated' event.
+ // Let other system app module know an app is
+ // being killed, removed or crashed.
+ var evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent('appterminated', true, false, { origin: origin });
+ window.dispatchEvent(evt);
+ }
+
+ // Reload the frame of the running app
+ function reload(origin) {
+ if (!isRunning(origin))
+ return;
+
+ var app = runningApps[origin];
+ app.reload();
+ }
+
+ // When a resize event occurs, resize the running app, if there is one
+ // When the status bar is active it doubles in height so we need a resize
+ var appResizeEvents = ['resize', 'status-active', 'status-inactive',
+ 'keyboardchange', 'keyboardhide',
+ 'attentionscreenhide'];
+ appResizeEvents.forEach(function eventIterator(event) {
+ window.addEventListener(event, function on(evt) {
+ if (event == 'keyboardchange') {
+ // Cancel fullscreen if keyboard pops
+ if (document.mozFullScreen)
+ document.mozCancelFullScreen();
+
+ setAppHeight(evt.detail.height);
+ } else if (displayedApp) {
+ setAppSize(displayedApp);
+ }
+ });
+ });
+
+ window.addEventListener('home', function(e) {
+ // If the lockscreen is active, it will stop propagation on this event
+ // and we'll never see it here. Similarly, other overlays may use this
+ // event to hide themselves and may prevent the event from getting here.
+ // Note that for this to work, the lockscreen and other overlays must
+ // be included in index.html before this one, so they can register their
+ // event handlers before we do.
+
+ // If we are currently transitioning, the user would like to cancel
+ // it instead of toggling homescreen panels.
+ var inTransition = !!(openFrame || closeFrame);
+
+ if (document.mozFullScreen) {
+ document.mozCancelFullScreen();
+ }
+
+ if (displayedApp !== homescreen || inTransition) {
+ if (displayedApp != ftuURL) {
+ setDisplayedApp(homescreen);
+ } else {
+ e.preventDefault();
+ }
+ } else {
+ stopInlineActivity(true);
+ ensureHomescreen(true);
+ }
+ });
+
+ // Cancel dragstart event to workaround
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=783076
+ // which stops OOP home screen pannable with left mouse button on
+ // B2G/Desktop.
+ windows.addEventListener('dragstart', function(evt) {
+ evt.preventDefault();
+ }, true);
+
+ // With all important event handlers in place, we can now notify
+ // Gecko that we're ready for certain system services to send us
+ // messages (e.g. the radio).
+ // Note that shell.js starts listen for the mozContentEvent event at
+ // mozbrowserloadstart, which sometimes does not happen till window.onload.
+ window.addEventListener('load', function wm_loaded() {
+ window.removeEventListener('load', wm_loaded);
+
+ var evt = new CustomEvent('mozContentEvent',
+ { bubbles: true, cancelable: false,
+ detail: { type: 'system-message-listener-ready' } });
+ window.dispatchEvent(evt);
+ });
+
+ // This is code copied from
+ // http://dl.dropbox.com/u/8727858/physical-events/index.html
+ // It appears to workaround the Nexus S bug where we're not
+ // getting orientation data. See:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=753245
+ // It seems it needs to be in both window_manager.js and bootstrap.js.
+ function dumbListener2(event) {}
+ window.addEventListener('devicemotion', dumbListener2);
+
+ window.setTimeout(function() {
+ window.removeEventListener('devicemotion', dumbListener2);
+ }, 2000);
+
+ // Return the object that holds the public API
+ return {
+ isFtuRunning: function() {
+ return isRunningFirstRunApp;
+ },
+ launch: launch,
+ kill: kill,
+ reload: reload,
+ getDisplayedApp: getDisplayedApp,
+ setOrientationForApp: setOrientationForApp,
+ getAppFrame: getAppFrame,
+ getRunningApps: function() {
+ return runningApps;
+ },
+ setDisplayedApp: setDisplayedApp,
+ getCurrentDisplayedApp: function() {
+ return runningApps[displayedApp];
+ },
+ hideCurrentApp: hideCurrentApp,
+ restoreCurrentApp: restoreCurrentApp,
+ retrieveHomescreen: retrieveHomescreen,
+ retrieveFTU: retrieveFTU
+ };
+}());
+
diff --git a/apps/system/js/wrapper.js b/apps/system/js/wrapper.js
new file mode 100644
index 0000000..160d70b
--- /dev/null
+++ b/apps/system/js/wrapper.js
@@ -0,0 +1,198 @@
+/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+var Launcher = (function() {
+ function log(str) {
+ dump(' -+- Launcher -+-: ' + str + '\n');
+ }
+
+ function currentAppFrame() {
+ return WindowManager.getAppFrame(WindowManager.getDisplayedApp());
+ }
+
+
+ function currentAppIframe() {
+ return currentAppFrame().firstChild;
+ }
+
+ var _ = navigator.mozL10n.get;
+
+ var BUTTONBAR_TIMEOUT = 5000;
+ var BUTTONBAR_INITIAL_OPEN_TIMEOUT = 1500;
+
+ var footer = document.querySelector('#wrapper-footer');
+ window.addEventListener('appopen', function onAppOpen(e) {
+ if ('wrapper' in currentAppFrame().dataset) {
+ window.addEventListener('mozbrowserlocationchange', onLocationChange);
+ onLocationChange();
+ onDisplayedApplicationChange();
+ }
+ });
+
+ window.addEventListener('appwillclose', function onAppClose(e) {
+ if ('wrapper' in currentAppFrame().dataset) {
+ window.removeEventListener('mozbrowserlocationchange', onLocationChange);
+ clearTimeout(buttonBarTimeout);
+ footer.classList.add('closed');
+ isButtonBarDisplayed = false;
+ }
+ });
+
+ window.addEventListener('keyboardchange', function onKeyboardChange(e) {
+ if ('wrapper' in currentAppFrame().dataset) {
+ if (footer.classList.contains('visible')) {
+ footer.classList.remove('visible');
+ }
+ }
+ });
+
+ window.addEventListener('keyboardhide', function onKeyboardChange(e) {
+ if ('wrapper' in currentAppFrame().dataset) {
+ if (!footer.classList.contains('visible')) {
+ footer.classList.add('visible');
+ }
+ }
+ });
+
+ var buttonBarTimeout;
+
+ var isButtonBarDisplayed = false;
+ function toggleButtonBar(time) {
+ clearTimeout(buttonBarTimeout);
+ footer.classList.toggle('closed');
+ isButtonBarDisplayed = !isButtonBarDisplayed;
+ if (isButtonBarDisplayed) {
+ buttonBarTimeout = setTimeout(toggleButtonBar, time || BUTTONBAR_TIMEOUT);
+ }
+ }
+
+ function clearButtonBarTimeout() {
+ clearTimeout(buttonBarTimeout);
+ buttonBarTimeout = setTimeout(toggleButtonBar, BUTTONBAR_TIMEOUT);
+ }
+
+ document.getElementById('handler').
+ addEventListener('mousedown', function open() { toggleButtonBar() });
+
+ document.getElementById('close-button').
+ addEventListener('mousedown', function close() { toggleButtonBar() });
+
+ var reload = document.getElementById('reload-button');
+ reload.addEventListener('click', function doReload(evt) {
+ clearButtonBarTimeout();
+ currentAppIframe().reload(true);
+ });
+
+ var back = document.getElementById('back-button');
+ back.addEventListener('click', function goBack() {
+ clearButtonBarTimeout();
+ currentAppIframe().goBack();
+ });
+
+ var forward = document.getElementById('forward-button');
+ forward.addEventListener('click', function goForward() {
+ clearButtonBarTimeout();
+ currentAppIframe().goForward();
+ });
+
+ function onLocationChange() {
+ currentAppIframe().getCanGoForward().onsuccess = function forwardSuccess(e) {
+ if (e.target.result === true) {
+ delete forward.dataset.disabled;
+ } else {
+ forward.dataset.disabled = true;
+ }
+ }
+
+ currentAppIframe().getCanGoBack().onsuccess = function backSuccess(e) {
+ if (e.target.result === true) {
+ delete back.dataset.disabled;
+ } else {
+ back.dataset.disabled = true;
+ }
+ }
+ }
+
+ window.addEventListener('mozbrowserlocationchange', onLocationChange);
+
+ var bookmarkButton = document.getElementById('bookmark-button');
+ function onDisplayedApplicationChange() {
+ toggleButtonBar(BUTTONBAR_INITIAL_OPEN_TIMEOUT);
+
+ var dataset = currentAppIframe().dataset;
+ if (dataset.originURL || dataset.searchURL) {
+ delete bookmarkButton.dataset.disabled;
+ return;
+ }
+
+ bookmarkButton.dataset.disabled = true;
+ }
+
+ bookmarkButton.addEventListener('click', function doBookmark(evt) {
+ if (bookmarkButton.dataset.disabled)
+ return;
+
+ clearButtonBarTimeout();
+ var dataset = currentAppIframe().dataset;
+
+ function selected(value) {
+ if (!value)
+ return;
+
+ var name, url;
+ if (value === 'origin') {
+ name = dataset.originName;
+ url = dataset.originURL;
+ }
+
+ if (value === 'search') {
+ name = dataset.searchName;
+ url = dataset.searchURL;
+ }
+
+ var activity = new MozActivity({
+ name: 'save-bookmark',
+ data: {
+ type: 'url',
+ url: url,
+ name: name,
+ icon: dataset.icon,
+ useAsyncPanZoom: dataset.useAsyncPanZoom,
+ iconable: false
+ }
+ });
+
+ activity.onsuccess = function onsuccess() {
+ if (value === 'origin') {
+ delete currentAppIframe().dataset.originURL;
+ }
+
+ if (value === 'search') {
+ delete currentAppIframe().dataset.searchURL;
+ }
+
+ if (!currentAppIframe().dataset.originURL &&
+ !currentAppIframe().dataset.searchURL) {
+ bookmarkButton.dataset.disabled = true;
+ }
+ }
+ }
+
+ var data = {
+ title: _('add-to-home-screen'),
+ options: []
+ };
+
+ if (dataset.originURL) {
+ data.options.push({ id: 'origin', text: dataset.originName });
+ }
+
+ if (dataset.searchURL) {
+ data.options.push({ id: 'search', text: dataset.searchName });
+ }
+
+ ModalDialog.selectOne(data, selected);
+ });
+}());