Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
559 lines
15 KiB
JavaScript
Executable file
559 lines
15 KiB
JavaScript
Executable file
/* global AppUpdatable, SystemUpdatable, SettingsCache, CustomDialog,
|
|
SystemBanner, focusManager */
|
|
'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,
|
|
_settings: null,
|
|
UPDATE_NOTIF_ID: 'update-notification',
|
|
NOTIFICATION_BUFFERING_TIMEOUT: 30 * 1000,
|
|
TOASTER_TIMEOUT: 1200,
|
|
|
|
container: null,
|
|
message: null,
|
|
toaster: null,
|
|
toasterMessage: null,
|
|
laterButton: null,
|
|
notnowButton: null,
|
|
downloadButton: null,
|
|
downloadDialog: 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) {
|
|
/* jshint nonew: false */
|
|
new AppUpdatable(app);
|
|
});
|
|
}).bind(this);
|
|
|
|
this._settings = navigator.mozSettings;
|
|
|
|
this.systemUpdatable = new SystemUpdatable();
|
|
|
|
this.toaster = document.getElementById('update-manager-toaster');
|
|
this.toasterMessage = this.toaster.querySelector('.message');
|
|
|
|
this.laterButton = document.getElementById('updates-later-button');
|
|
this.downloadButton = document.getElementById('updates-download-button');
|
|
this.downloadDialog = document.getElementById('updates-download-dialog');
|
|
this.downloadDialogTitle = this.downloadDialog.querySelector('h1');
|
|
this.downloadDialogList = this.downloadDialog.querySelector('ul');
|
|
|
|
this.toaster.onclick = this.showDownloadPrompt.bind(this);
|
|
this.laterButton.onclick = this.cancelPrompt.bind(this);
|
|
this.downloadButton.onclick = this.requestDownloads.bind(this);
|
|
this.downloadDialogList.onchange = this.updateDownloadButton.bind(this);
|
|
|
|
window.addEventListener('mozChromeEvent', this);
|
|
window.addEventListener('applicationinstall', this);
|
|
window.addEventListener('applicationuninstall', this);
|
|
window.addEventListener('online', this);
|
|
window.addEventListener('offline', this);
|
|
|
|
SettingsCache.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();
|
|
focusManager.addUI(this);
|
|
},
|
|
|
|
requestDownloads: function um_requestDownloads(evt) {
|
|
evt.preventDefault();
|
|
this.promptOrDownload();
|
|
},
|
|
|
|
startDownloads: function um_startDownloads() {
|
|
this.downloadDialog.classList.remove('visible');
|
|
focusManager.focus();
|
|
|
|
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 self = this;
|
|
this._errorTimeout = setTimeout(function waitForMore() {
|
|
var systemBanner = new SystemBanner();
|
|
var msg = {
|
|
id: 'downloadError',
|
|
args: null
|
|
};
|
|
systemBanner.show(msg);
|
|
self._errorTimeout = null;
|
|
}, this.NOTIFICATION_BUFFERING_TIMEOUT);
|
|
},
|
|
|
|
promptOrDownload: function um_promptOrDownload() {
|
|
if (!this._wifiAvailable()) {
|
|
return;
|
|
}
|
|
this.startDownloads();
|
|
},
|
|
|
|
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');
|
|
|
|
// The user can choose not to update an app
|
|
var checkContainer = document.createElement('label');
|
|
if (updatable instanceof SystemUpdatable) {
|
|
_localize(checkContainer, '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.classList.add('pack-checkbox');
|
|
checkContainer.appendChild(checkbox);
|
|
checkContainer.appendChild(span);
|
|
}
|
|
listItem.appendChild(checkContainer);
|
|
|
|
var name = document.createElement('div');
|
|
name.classList.add('name');
|
|
if (updatable.nameL10nId) {
|
|
_localize(name, updatable.nameL10nId);
|
|
} else {
|
|
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');
|
|
this.updateDownloadButton();
|
|
focusManager.focus();
|
|
},
|
|
|
|
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');
|
|
focusManager.focus();
|
|
},
|
|
|
|
isFocusable: function um_isFocusable() {
|
|
return this.downloadDialog.classList.contains('visible');
|
|
},
|
|
|
|
getElement: function um_getElement() {
|
|
return this.downloadDialog;
|
|
},
|
|
|
|
focus: function um_focus() {
|
|
document.activeElement.blur();
|
|
if (!this.downloadButton.disabled) {
|
|
this.downloadButton.focus();
|
|
} else {
|
|
this.laterButton.focus();
|
|
}
|
|
},
|
|
|
|
downloadProgressed: function um_downloadProgress(bytes) {
|
|
if (bytes > 0) {
|
|
this._downloadedBytes += bytes;
|
|
this.render();
|
|
}
|
|
},
|
|
|
|
downloaded: function um_downloaded(udatable) {
|
|
|
|
},
|
|
|
|
startedUncompressing: function um_startedUncompressing() {
|
|
this._uncompressing = true;
|
|
this.render();
|
|
},
|
|
|
|
render: function um_render() {
|
|
var _localize = navigator.mozL10n.setAttributes;
|
|
|
|
_localize(this.toasterMessage, 'updateAvailableInfo', {
|
|
n: this.updatesQueue.length - this.lastUpdatesAvailable
|
|
});
|
|
|
|
},
|
|
|
|
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.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;
|
|
|
|
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;
|
|
this._wifiLock = navigator.requestWakeLock('wifi');
|
|
|
|
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._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();
|
|
}
|
|
},
|
|
|
|
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;
|
|
}
|
|
|
|
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');
|
|
}
|
|
},
|
|
|
|
_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
|
|
});
|
|
},
|
|
|
|
_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]);
|
|
}
|
|
};
|
|
|
|
// unit tests call init() manually
|
|
if (navigator.mozL10n) {
|
|
navigator.mozL10n.once(UpdateManager.init.bind(UpdateManager));
|
|
}
|