Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
601 lines
16 KiB
JavaScript
601 lines
16 KiB
JavaScript
'use strict';
|
|
|
|
/* global
|
|
asyncStorage,
|
|
ManifestHelper,
|
|
NotificationHelper,
|
|
Service,
|
|
UpdateManager,
|
|
IconsHelper,
|
|
MozActivity
|
|
*/
|
|
|
|
/*
|
|
* 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
|
|
*/
|
|
|
|
// Wrapper to read a settings value
|
|
var setting = (function() {
|
|
var values = {};
|
|
return key => {
|
|
|
|
if (key in values) {
|
|
return values[key];
|
|
}
|
|
|
|
// Cache the promise straight away so multiple callers dont
|
|
// create multiple listeners
|
|
values[key] = new Promise(resolve => {
|
|
// We store the value of the setting and the observer is
|
|
// in charge of keeping it up to date
|
|
function cacheAndResolve() {
|
|
var result = (key in req.result) ? req.result[key] : undefined;
|
|
values[key] = Promise.resolve(result);
|
|
resolve(result);
|
|
}
|
|
|
|
var lock = navigator.mozSettings.createLock();
|
|
var req = lock.get(key);
|
|
|
|
req.onerror = cacheAndResolve;
|
|
req.onsuccess = cacheAndResolve;
|
|
|
|
navigator.mozSettings.addObserver(key, e => {
|
|
values[key] = Promise.resolve(e.settingValue);
|
|
});
|
|
});
|
|
|
|
return values[key];
|
|
};
|
|
})();
|
|
|
|
// Should we automatically update?
|
|
AppUpdatable.AUTO_UPDATE = 'addons.auto_update';
|
|
|
|
// Notify the users after we have auto updated?
|
|
AppUpdatable.NOTIFY_UPDATE = 'addons.update_notify';
|
|
|
|
// Notify the user about auto updates on first update
|
|
AppUpdatable.AUTO_UPDATES_NOTIFIED = 'notified-autoupdate';
|
|
|
|
/* === 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.nameL10nId = '';
|
|
this.nameL10nArgs = null;
|
|
|
|
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') {
|
|
return;
|
|
}
|
|
|
|
// If its not an addon, add it to the update queue
|
|
if (this.app.manifest.role !== 'addon') {
|
|
this.queueUpdate();
|
|
return;
|
|
}
|
|
|
|
let settings = [
|
|
setting(AppUpdatable.AUTO_UPDATE),
|
|
setting(AppUpdatable.NOTIFY_UPDATE)
|
|
];
|
|
|
|
Promise.all(settings).then(results => {
|
|
|
|
var autoAddonUpdates = results[0];
|
|
var notifyAddonUpdates = results[1];
|
|
|
|
// If we have autoUpdates turned off for addons, they
|
|
// go via the usual install flow
|
|
if (!autoAddonUpdates) {
|
|
this.queueUpdate();
|
|
return;
|
|
}
|
|
|
|
// We fail silently, the addon will be updated next time
|
|
// we check for updates
|
|
this.app.ondownloaderror = err => {
|
|
console.error('failed to auto update', this.name,
|
|
err.application.downloadError.name);
|
|
};
|
|
|
|
this.app.ondownloadsuccess = this.applyUpdate.bind(this);
|
|
this.app.onprogress = null;
|
|
this.app.ondownloadapplied = null;
|
|
|
|
if (notifyAddonUpdates) {
|
|
this.app.ondownloadapplied = this.notifyAutoUpdate.bind(this);
|
|
}
|
|
|
|
this.app.download();
|
|
this.maybeNotifyUpdate();
|
|
});
|
|
};
|
|
|
|
AppUpdatable.prototype.queueUpdate = function() {
|
|
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.notifyAutoUpdate = function(iconUrl) {
|
|
IconsHelper.getIcon(this.app.origin, 32, null, this.app).then(iconUrl => {
|
|
var title = {id: 'addonUpdated', args: { name: this.name}};
|
|
var opts = {
|
|
tag: 'addonUpdated-' + this.name,
|
|
bodyL10n: 'addonUpdatedDetails',
|
|
mozbehavior: {showOnlyOnce: true },
|
|
icon: iconUrl,
|
|
closeOnClick: true
|
|
};
|
|
NotificationHelper.send(title, opts).then(n => {
|
|
n.addEventListener('click', () => {
|
|
/*jshint -W031 */
|
|
new MozActivity({
|
|
name: 'configure',
|
|
data: {
|
|
target: 'device',
|
|
section: 'addon-details',
|
|
options: {
|
|
manifestURL: this.app.manifestURL
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
// If this is the first auto update to addons, notify the
|
|
// user that updates are being installed automatically
|
|
AppUpdatable.prototype.maybeNotifyUpdate = function() {
|
|
asyncStorage.getItem(AppUpdatable.AUTO_UPDATES_NOTIFIED, val => {
|
|
if (val) {
|
|
return;
|
|
}
|
|
// Ensure we dont show the notification again
|
|
asyncStorage.setItem(AppUpdatable.AUTO_UPDATES_NOTIFIED, true);
|
|
|
|
var opts = {
|
|
bodyL10n: 'addonUpdateDetails',
|
|
tag: 'addonUpdate',
|
|
mozbehavior: {showOnlyOnce: true },
|
|
closeOnClick: true,
|
|
icon: '/style/notifications/images/download.png'
|
|
};
|
|
|
|
NotificationHelper.send('addonUpdate', opts).then(n => {
|
|
n.addEventListener('click', function() {
|
|
/*jshint -W031 */
|
|
new MozActivity({
|
|
name: 'configure',
|
|
data: {target: 'device', section: 'about'}
|
|
});
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
AppUpdatable.prototype.successCallBack = function() {
|
|
var app = this.app;
|
|
if (Service.query('AppWindowManager.getActiveWindow') &&
|
|
Service.query('AppWindowManager.getActiveWindow').origin !==
|
|
app.origin) {
|
|
this.applyUpdate();
|
|
} else {
|
|
var self = this;
|
|
window.addEventListener('appwillclose', function waitClose() {
|
|
window.removeEventListener('appwillclose', waitClose);
|
|
self.applyUpdate();
|
|
});
|
|
}
|
|
|
|
UpdateManager.downloaded(this);
|
|
UpdateManager.removeFromDownloadsQueue(this);
|
|
UpdateManager.removeFromUpdatesQueue(this);
|
|
};
|
|
|
|
AppUpdatable.prototype.applyUpdate = function() {
|
|
Service.request('kill', this.app.origin);
|
|
this._mgmt.applyDownload(this.app);
|
|
};
|
|
|
|
AppUpdatable.prototype.appliedCallBack = function() {
|
|
this.clean();
|
|
};
|
|
|
|
AppUpdatable.prototype.errorCallBack = function(e) {
|
|
var app = e.application;
|
|
var errorName = app.downloadError.name;
|
|
console.info('downloadError event, error code is', errorName);
|
|
UpdateManager.requestErrorBanner();
|
|
UpdateManager.removeFromDownloadsQueue(this);
|
|
if (!app.downloadAvailable) {
|
|
UpdateManager.removeFromUpdatesQueue(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() {
|
|
this.nameL10nId = 'systemUpdateWithVersion';
|
|
this.nameL10nArgs = null;
|
|
this.size = 0;
|
|
this.buildID = null;
|
|
this.detailsURL = null;
|
|
this.downloading = false;
|
|
this.paused = false;
|
|
this.showingApplyPrompt = 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));
|
|
|
|
// We need to make sure that SystemUpdatable has an event listener ready
|
|
// to catch mozChromeEvent to be sure we can get "update-error" events.
|
|
// This was broken for the case of system updates error because of a race:
|
|
// Gecko was sending the |update-error| event before the |mozChromeEvent|
|
|
// could get installed.
|
|
window.addEventListener('mozChromeEvent', this);
|
|
this._dispatchEvent('update-prompt-ready');
|
|
}
|
|
|
|
SystemUpdatable.KNOWN_UPDATE_FLAG = 'known-sysupdate';
|
|
|
|
SystemUpdatable.prototype.download = function() {
|
|
if (this.downloading) {
|
|
return;
|
|
}
|
|
|
|
this.downloading = true;
|
|
this.paused = false;
|
|
UpdateManager.addToDownloadsQueue(this);
|
|
this.progress = 0;
|
|
this._dispatchEvent('update-available-result', 'download');
|
|
};
|
|
|
|
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(detail);
|
|
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;
|
|
UpdateManager.downloaded(this);
|
|
UpdateManager.removeFromDownloadsQueue(this);
|
|
this.showApplyPrompt(detail.isOSUpdate);
|
|
break;
|
|
case 'update-prompt-apply':
|
|
this.showApplyPrompt(detail.isOSUpdate);
|
|
break;
|
|
}
|
|
};
|
|
|
|
SystemUpdatable.prototype.errorCallBack = function(aUpdate) {
|
|
// Show notification for update installation failures
|
|
console.debug('Handling systemUpdatable error: updateType',
|
|
aUpdate.updateType, 'state', aUpdate.state);
|
|
if (aUpdate.updateType === 'complete' && aUpdate.state === 'failed') {
|
|
var errorOptions = {
|
|
bodyL10n: 'systemUpdateErrorDetails',
|
|
icon: '/style/notifications/images/system_update_error.svg',
|
|
tag: 'systemUpdateError',
|
|
mozbehavior: {
|
|
showOnlyOnce: true
|
|
},
|
|
closeOnClick: false
|
|
};
|
|
|
|
NotificationHelper.send('systemUpdateError', errorOptions).then(n => {
|
|
n.addEventListener('click',
|
|
this.showUpdateErrorDetails.bind(this, aUpdate));
|
|
});
|
|
} else {
|
|
// Keep the old banner for the others for now
|
|
UpdateManager.requestErrorBanner();
|
|
}
|
|
|
|
UpdateManager.removeFromDownloadsQueue(this);
|
|
this.downloading = false;
|
|
};
|
|
|
|
SystemUpdatable.prototype.showUpdateErrorDetails = function(updateError) {
|
|
var cancel = {
|
|
title: 'later',
|
|
callback: function() {
|
|
Service.request('hideCustomDialog');
|
|
}
|
|
};
|
|
|
|
var confirm = {
|
|
title: 'report',
|
|
callback: function() {
|
|
Service.request('hideCustomDialog');
|
|
Notification.get({ tag: 'systemUpdateError' }).then(ns => {
|
|
ns.forEach(n => {
|
|
n && n.close();
|
|
});
|
|
});
|
|
window.dispatchEvent(new CustomEvent('requestSystemLogs'));
|
|
},
|
|
recommend: true
|
|
};
|
|
|
|
Service.request('UtilityTray:hide');
|
|
Service.request('showCustomDialog',
|
|
'systemUpdateError',
|
|
{ id: 'wantToReportNow',
|
|
args: {
|
|
version: updateError.appVersion,
|
|
buildID: updateError.buildID
|
|
}
|
|
},
|
|
cancel,
|
|
confirm
|
|
);
|
|
};
|
|
|
|
// isOsUpdate comes from Gecko's update object passed in the mozChromeEvent
|
|
// and is expected to be true in case of an update package that gets applied
|
|
// in recovery mode (FOTA). We want to show the battery warning only in this
|
|
// case as described in bug 959195
|
|
SystemUpdatable.prototype.showApplyPrompt = function(isOsUpdate) {
|
|
var batteryLevel = window.navigator.battery.level * 100;
|
|
this.getBatteryPercentageThreshold().then(function(threshold) {
|
|
this.showingApplyPrompt = true;
|
|
if (isOsUpdate && batteryLevel < threshold) {
|
|
this.showApplyPromptBatteryNok(threshold);
|
|
} else {
|
|
this.showApplyPromptBatteryOk();
|
|
}
|
|
}.bind(this));
|
|
};
|
|
|
|
SystemUpdatable.prototype.BATTERY_FALLBACK_THRESHOLD = 25;
|
|
|
|
SystemUpdatable.prototype.getBatteryPercentageThreshold = function() {
|
|
var fallbackThreshold = this.BATTERY_FALLBACK_THRESHOLD;
|
|
|
|
var isCharging = window.navigator.battery.charging;
|
|
var batteryThresholdKey =
|
|
'app.update.battery-threshold.' + (isCharging ? 'plugged' : 'unplugged');
|
|
|
|
var settings = window.navigator.mozSettings;
|
|
var getRequest = settings.createLock().get(batteryThresholdKey);
|
|
|
|
return new Promise(function(resolve, reject) {
|
|
getRequest.onerror = function() {
|
|
resolve(fallbackThreshold);
|
|
};
|
|
getRequest.onsuccess = function() {
|
|
var threshold = getRequest.result[batteryThresholdKey];
|
|
if (typeof threshold !== 'number') {
|
|
threshold = fallbackThreshold;
|
|
}
|
|
if (threshold < 0 || threshold > 100) {
|
|
threshold = fallbackThreshold;
|
|
}
|
|
resolve(threshold);
|
|
};
|
|
});
|
|
};
|
|
|
|
SystemUpdatable.prototype.showApplyPromptBatteryNok = function(minBattery) {
|
|
var ok = {
|
|
title: 'ok',
|
|
callback: this.declineInstallBattery.bind(this)
|
|
};
|
|
|
|
Service.request('UtilityTray:hide');
|
|
Service.request('showCustomDialog',
|
|
'systemUpdateReady',
|
|
{ id: 'systemUpdateLowBatteryThreshold', args: { threshold: minBattery } },
|
|
ok,
|
|
null
|
|
);
|
|
};
|
|
|
|
SystemUpdatable.prototype.showApplyPromptBatteryOk = function() {
|
|
// Update will be completed after restart
|
|
this.forgetKnownUpdate();
|
|
|
|
var cancel = {
|
|
title: 'later',
|
|
callback: this.declineInstallWait.bind(this)
|
|
};
|
|
|
|
var confirm = {
|
|
title: 'installNow',
|
|
callback: this.acceptInstall.bind(this),
|
|
recommend: true
|
|
};
|
|
|
|
Service.request('UtilityTray:hide');
|
|
Service.request('showCustomDialog',
|
|
'systemUpdateReady',
|
|
'wantToInstallNow',
|
|
cancel,
|
|
confirm
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Decline install of update, forwarding `reason` to UpdatePrompt.jsm.
|
|
* `reason` is either 'wait' or 'low-battery'. 'wait' corresponds to the user
|
|
* deciding to delay the update, in which case the prompt will reappear after a
|
|
* few minutes of idle time. 'low-battery' means the battery is currently too
|
|
* low for an update to take place and the update prompt will not reappear.
|
|
* @param {String} reason
|
|
*/
|
|
SystemUpdatable.prototype.declineInstall = function(reason) {
|
|
this.showingApplyPrompt = false;
|
|
Service.request('hideCustomDialog');
|
|
this._dispatchEvent('update-prompt-apply-result', reason);
|
|
};
|
|
|
|
SystemUpdatable.prototype.declineInstallBattery = function() {
|
|
this.declineInstall('low-battery');
|
|
};
|
|
|
|
SystemUpdatable.prototype.declineInstallWait = function() {
|
|
this.declineInstall('wait');
|
|
};
|
|
|
|
|
|
SystemUpdatable.prototype.acceptInstall = function() {
|
|
Service.request('hideCustomDialog');
|
|
|
|
// Display a splash-screen so the user knows an update is being applied
|
|
var splash = document.createElement('form');
|
|
splash.id = 'system-update-splash';
|
|
['label', 'divider', 'icon'].forEach(function(name) {
|
|
var child = document.createElement('div');
|
|
child.id = name;
|
|
splash.appendChild(child);
|
|
});
|
|
splash.firstChild.setAttribute('data-l10n-id', 'systemUpdate');
|
|
|
|
var screen = document.getElementById('screen');
|
|
screen.appendChild(splash);
|
|
|
|
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);
|
|
};
|