/* global AppUpdatable, LazyLoader, MozActivity, NotificationScreen, Service, SettingsListener, SystemBanner, SystemUpdatable */ '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 */ (function(exports) { var UpdateManager = { _mgmt: null, _downloading: false, _uncompressing: false, _downloadedBytes: 0, _errorTimeout: null, _wifiLock: null, _systemUpdateDisplayed: false, _startedDownloadUsingDataConnection: false, _settings: null, _hasDialog: false, UPDATE_NOTIF_ID: 'update-notification', NOTIFICATION_BUFFERING_TIMEOUT: 30 * 1000, TOASTER_TIMEOUT: 1200, UPDATE_2G_SETT: 'update.2g.enabled', UPDATE_2G: false, ROAMING_SETTING_KEY: 'ril.data.roaming_enabled', DATA_TYPES_NO_ALLOWED: ['edge', 'gprs', '1xrtt', 'is95a', 'is95b'], WIFI_PRIORITIZED: true, WIFI_PRIORITIZED_KEY: 'app.update.wifi-prioritized', connection2G: false, container: null, message: null, toaster: null, toasterMessage: null, laterButton: null, notnowButton: null, downloadButton: null, downloadViaDataConnectionButton: null, downloadDialog: null, downloadViaDataConnectionTitle: null, downloadViaDataConnectionDialog: null, downloadViaDataConnectionMessage: null, downloadDialogTitle: null, downloadDialogList: null, lastUpdatesAvailable: 0, _notificationTimeout: null, updatableApps: [], systemUpdatable: null, updatesQueue: [], downloadsQueue: [], start: function() { 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) { /* jshint nonew: false */ 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('.title-container'); 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.downloadViaDataConnectionMessage = this.downloadViaDataConnectionDialog.querySelector('p'); this.downloadViaDataConnectionTitle = this.downloadViaDataConnectionDialog.querySelector('h1'); this.container.onclick = this.containerClicked.bind(this); this.laterButton.onclick = this.cancelPrompt.bind(this); this.toaster.onclick = this.toasterClicked.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); window.addEventListener('lockscreen-appopened', this); window.addEventListener('home', this); window.addEventListener('holdhome', 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(); }, requestDownloads: function um_requestDownloads(evt) { evt.preventDefault(); if (evt.target == this.downloadViaDataConnectionButton) { this._startedDownloadUsingDataConnection = true; this.startDownloads(); } else { this.promptOrDownload(); } }, startDownloads: function um_startDownloads() { this.downloadViaDataConnectionDialog.classList.remove('visible'); this._closeDownloadDialog(); Service.request('UtilityTray:show'); var checkValues = {}; var dialog = this.downloadDialogList; var checkboxes = dialog.querySelectorAll('gaia-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() { Service.request('hideCustomDialog'); this._hasDialog = false; // 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) {/* jshint nonew: false */ return; } var self = this; this._errorTimeout = setTimeout(function waitForMore() { LazyLoader.load(['js/system_banner.js']).then(function() { var systemBanner = new SystemBanner(); systemBanner.show('downloadError'); self._errorTimeout = null; })['catch'](function(err) { console.error(err); }); }, this.NOTIFICATION_BUFFERING_TIMEOUT); }, promptOrDownload: function um_promptOrDownload() { var self = this; if (self.downloadDialog.dataset.online == 'false') { self.showPromptNoConnection(); return; } if (self._wifiAvailable()) { self._startedDownloadUsingDataConnection = false; self.startDownloads(); return; } var wifiPrioritized = self.getWifiPrioritized(); var update2GEnabled = self.getUpdate2GEnabled(); // We can download the update only if the current connection // is not forbidden for download var conns = window.navigator.mozMobileConnections; if (!conns) { // B2G desktop/Mulet if (navigator.onLine) { self.startDownloads(); return; } console.error('mozMobileConnections is not available we can ' + 'not update the phone.'); self.showPromptNoConnection(); return; } var dataType; // In DualSim only one of them will have data active for (var i = 0; i < conns.length && !dataType; i++) { dataType = conns[i].data.type; } if (!dataType) { console.error('There are not wifi connection nor data ' + 'connection. We can not download update'); self.showForbiddenDownload(); return; } if (self.DATA_TYPES_NO_ALLOWED.indexOf(dataType) >= 0) { self.connection2G = true; } else { self.connection2G = false; } // If it's not connected to a wifi we need to verify what kind of // connection it has Promise.all([wifiPrioritized, update2GEnabled]).then(function(values) { var prioritized = values[0]; var update2G = values[1]; // If update 2G is available we don't need to know what kind // of connection the phone has if (update2G) { if (prioritized) { self.showPromptWifiPrioritized(); } else { self.showPrompt3GAdditionalCostIfNeeded(); } return; } //2G connection if (self.connection2G && self._systemUpdateDisplayed) { self.showForbiddenDownload(); return; } //3G connection if (prioritized) { self.showPromptWifiPrioritized(); } else { self.showPrompt3GAdditionalCostIfNeeded(); } }); }, containerClicked: function um_containerClicker() { if (this._downloading) { if (this._uncompressing) { // If notification was clicked during uncompression, do nothing. return; } var cancel = { title: 'no', callback: this.cancelPrompt.bind(this) }; var confirm = { title: 'yes', callback: this.cancelAllDownloads.bind(this) }; Service.request('showCustomDialog', 'cancelAllDownloads', 'wantToCancelAll', cancel, confirm ); this._hasDialog = true; } else { this.showDownloadPrompt(); } Service.request('UtilityTray:hide'); }, toasterClicked: function um_toasterClicked() { if (this._downloading) { return; } this.showDownloadPrompt(); Service.request('UtilityTray:hide'); }, showForbiddenDownload: function um_showForbiddenDownload() { //Close any dialog if there is any open Service.request('hideCustomDialog'); var ok = { title: 'ok', callback: this.cancelPrompt.bind(this) }; window.dispatchEvent(new CustomEvent('updatepromptshown')); Service.request('showCustomDialog', 'systemUpdate', 'downloadUpdatesVia2GForbidden3', ok, null); this._hasDialog = true; }, showPromptNoConnection: function um_showPromptNoConnection() { //Close any dialog if there is any open Service.request('hideCustomDialog'); var ok = { title: 'ok', callback: this.cancelPrompt.bind(this) }; window.dispatchEvent(new CustomEvent('updatepromptshown')); Service.request('showCustomDialog', 'systemUpdate', 'downloadOfflineWarning2', ok, null); this._hasDialog = true; }, showDownloadPrompt: function um_showDownloadPrompt() { var _localize = navigator.mozL10n.setAttributes; this._systemUpdateDisplayed = false; _localize(this.downloadDialogTitle, 'numberOfUpdates', { n: this.updatesQueue.length }); // 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'); var nameDetails; // The user can choose not to update an app if (updatable instanceof SystemUpdatable) { var checkContainer = document.createElement('label'); _localize(checkContainer, 'required'); checkContainer.classList.add('required'); this._systemUpdateDisplayed = true; listItem.appendChild(checkContainer); nameDetails = listItem; } else { var checkbox = document.createElement('gaia-checkbox'); checkbox.dataset.position = index; checkbox.checked = true; var label = document.createElement('label'); nameDetails = label; checkbox.appendChild(label); listItem.appendChild(checkbox); } var name = document.createElement('span'); name.classList.add('name'); if (updatable.nameL10nId) { _localize(name, updatable.nameL10nId, updatable.nameL10nArgs); } else { name.textContent = updatable.name; } nameDetails.appendChild(name); if (updatable.size) { var sizeItem = document.createElement('span'); sizeItem.textContent = this._humanizeSize(updatable.size); nameDetails.appendChild(sizeItem); } else { nameDetails.classList.add('nosize'); } if (updatable.buildID) { var buildId = document.createElement('span'); buildId.classList.add('font-light'); _localize(buildId, 'build-id', { buildid: updatable.buildID }); listItem.appendChild(buildId); } if (updatable.detailsURL && (updatable.detailsURL !== 'about:blank')) { var detailsUrl = document.createElement('a'); _localize(detailsUrl, 'view-release-notes'); listItem.appendChild(detailsUrl); listItem.onclick = (function(event) { event.preventDefault(); var activity = new MozActivity({ name: 'view', data: { type: 'url', url: updatable.detailsURL } }); activity.onsuccess = this.cancelPrompt.bind(this); }).bind(this); } this.downloadDialogList.appendChild(listItem); }, this); window.dispatchEvent(new CustomEvent('updatepromptshown')); this._hasDialog = true; this.downloadDialog.classList.add('visible'); this.updateDownloadButton(); }, updateDownloadButton: function() { if (this._systemUpdateDisplayed) { this.downloadButton.disabled = false; return; } var disabled = true; var dialog = this.downloadDialogList; var checkboxes = dialog.querySelectorAll('gaia-checkbox'); for (var i = 0; i < checkboxes.length; i++) { if (checkboxes[i].checked) { disabled = false; break; } } this.downloadButton.disabled = disabled; }, cancelPrompt: function um_cancelPrompt() { Service.request('hideCustomDialog'); this._hasDialog = false; this._closeDownloadDialog(); }, cancelDataConnectionUpdatesPrompt: function um_cancelDCUpdatesPrompt() { Service.request('hideCustomDialog'); this._hasDialog = false; this.downloadViaDataConnectionDialog.classList.remove('visible'); this._closeDownloadDialog(); }, getWifiPrioritized: function um_getWifiPrioritized() { var wifiPrioritized = this.WIFI_PRIORITIZED; var settings = window.navigator.mozSettings; var self = this; var getRequest = settings.createLock().get(this.WIFI_PRIORITIZED_KEY); return new Promise(function(resolve, reject) { getRequest.onerror = function() { resolve(wifiPrioritized); }; getRequest.onsuccess = function() { var prioritized = getRequest.result[self.WIFI_PRIORITIZED_KEY]; if (typeof prioritized !== 'boolean') { prioritized = wifiPrioritized; } resolve(prioritized); }; }); }, getUpdate2GEnabled: function um_getUpdate2GEnabled() { var update2G = this.UPDATE_2G; var settings = window.navigator.mozSettings; var self = this; var getRequest = settings.createLock().get(this.UPDATE_2G_SETT); return new Promise(function(resolve, reject) { getRequest.onerror = function() { resolve(update2G); }; getRequest.onsuccess = function() { var setting = getRequest.result[self.UPDATE_2G_SETT]; if (typeof setting !== 'boolean') { setting = update2G; } resolve(setting); }; }); }, showPrompt3GAdditionalCostIfNeeded: function um_showPrompt3GAdditionalCostIfNeeded() { this._openDownloadViaDataDialog(); Service.request('hideCustomDialog'); this._hasDialog = false; }, showPromptWifiPrioritized: function um_showPromptWifiPrioritized(downloadCallback) { if (!downloadCallback) { downloadCallback = this.showPrompt3GAdditionalCostIfNeeded; } var notNow = { title: 'notNow', callback: this.cancelPrompt.bind(this) }; var download = { title: 'download', recommend: true, callback: downloadCallback.bind(this) }; var messageL10n = this.connection2G ? 'downloadWifiPrioritizedUsing2G' : 'downloadWifiPrioritized3'; this._closeDownloadDialog(); window.dispatchEvent(new CustomEvent('updatepromptshown')); Service.request('UtilityTray:hide'); Service.request('showCustomDialog', 'systemUpdate', messageL10n, notNow, download ); this._hasDialog = true; }, downloadProgressed: function um_downloadProgress(bytes) { if (bytes > 0) { this._downloadedBytes += bytes; this.render(); } }, downloaded: function um_downloaded(udatable) { if (this._startedDownloadUsingDataConnection) { this._startedDownloadUsingDataConnection = false; } }, startedUncompressing: function um_startedUncompressing() { this._uncompressing = true; this.render(); }, render: function um_render() { var _localize = navigator.mozL10n.setAttributes; var l10nId = 'updateAvailableInfo'; if ((this.updatesQueue.length === 1) && (this.updatesQueue[0] instanceof SystemUpdatable)) { l10nId = 'systemUpdateAvailableInfo'; } _localize(this.toasterMessage, l10nId, { n: this.updatesQueue.length - this.lastUpdatesAvailable }); if (this._downloading) { if (this._uncompressing && this.downloadsQueue.length === 1) { _localize(this.message, 'uncompressingMessage'); } else { _localize(this.message, 'downloadingUpdateMessage', { progress: this._humanizeSize(this._downloadedBytes) }); } } else { _localize(this.message, l10nId, { n: this.updatesQueue.length }); } 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; 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; Service.request('incDownloads'); 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; this._uncompressing = false; Service.request('decDownloads'); 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.removeUnreadNotification(this.UPDATE_NOTIF_ID); } }, displayNotificationIfHidden: function() { if (!this.container.classList.contains('displayed')) { this.container.classList.add('displayed'); NotificationScreen.addUnreadNotification(this.UPDATE_NOTIF_ID); } }, 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; /* jshint nonew: false */ 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 'offline': this.updateOnlineStatus(); break; case 'online': this.updateOnlineStatus(); break; case 'wifi-statuschange': this.updateWifiStatus(); break; case 'lockscreen-appopened': if (this.systemUpdatable.showingApplyPrompt) { this.systemUpdatable.declineInstallWait(); } this.downloadViaDataConnectionDialog.classList.remove('visible'); this._closeDownloadDialog(); // only request while update manager shows a dialog if (this._hasDialog) { Service.request('hideCustomDialog'); } break; case 'home': case 'holdhome': if (this._hasDialog) { this.cancelPrompt(); } break; } if (evt.type !== 'mozChromeEvent') { return; } var detail = evt.detail; if (detail.type && detail.type === 'update-available') { this.systemUpdatable.size = detail.size; this.systemUpdatable.buildID = detail.buildID; this.systemUpdatable.detailsURL = detail.detailsURL; this.systemUpdatable.nameL10nArgs = { version: detail.displayVersion }; 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; }, _wifiAvailable: function su_wifiAvailable() { var wifiManager = window.navigator.mozWifiManager; if (!wifiManager) { return; } return wifiManager.connection.status == 'connected'; }, updateWifiStatus: function su_updateWifiStatus() { this.downloadDialog.dataset.nowifi = !this._wifiAvailable(); }, 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 }); }, _openDownloadViaDataDialog: function um_downloadViaDataDialog() { var _ = navigator.mozL10n.setAttributes; var connections = window.navigator.mozMobileConnections; var dataType; var sim; if (!connections) { this.showForbiddenDownload(); return; } // In DualSim only one of them will have data active for (var i = 0; i < connections.length && !dataType; i++) { dataType = connections[i].data.type; sim = connections[i]; } if (!dataType) { //No connection available self.showForbiddenDownload(); return; } var dataRoamingSettingPromise = this._getDataRoamingSetting(); dataRoamingSettingPromise.then(function(roaming) { if (roaming && sim.data.roaming) { _(this.downloadViaDataConnectionTitle, 'downloadUpdatesViaDataRoamingConnection'); _(this.downloadViaDataConnectionMessage, 'downloadUpdatesViaDataRoamingConnectionMessage'); window.dispatchEvent(new CustomEvent('updatepromptshown')); this.downloadViaDataConnectionDialog.classList.add('visible'); return; } this._startedDownloadUsingDataConnection = true; this.startDownloads(); }.bind(this)); }, _getDataRoamingSetting: function um_getDataRoamingSetting() { var lock = this._settings.createLock(); var reqDataRoaming = lock.get(this.ROAMING_SETTING_KEY); var dataRoamingSettingPromise; var self = this; dataRoamingSettingPromise = new Promise(function(resolve, reject) { reqDataRoaming.onsuccess = function() { resolve(reqDataRoaming.result[self.ROAMING_SETTING_KEY]); }; reqDataRoaming.onerror = function() { resolve(false); }; }); return dataRoamingSettingPromise; }, _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]); }, _closeDownloadDialog: function um_closeDownloadDialog() { window.dispatchEvent(new CustomEvent('updateprompthidden')); this.downloadDialog.classList.remove('visible'); } }; exports.UpdateManager = UpdateManager; }(window));