Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
647 lines
19 KiB
JavaScript
Executable file
647 lines
19 KiB
JavaScript
Executable file
/* jshint moz:true */
|
|
/* global FtuLauncher */
|
|
/* global KeyboardHelper */
|
|
/* global KeyboardManager */
|
|
/* global LazyLoader */
|
|
/* global ManifestHelper */
|
|
/* global ModalDialog */
|
|
/* global SystemBanner */
|
|
/* global Template */
|
|
/* global focusManager */
|
|
/* global AppInstallDialogs */
|
|
/* global AppWindowManager */
|
|
/* global applications */
|
|
|
|
'use strict';
|
|
|
|
// note: we removed the download cancel dialog because we don't have an UI to
|
|
// remove it.
|
|
|
|
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.appInstallDialogs = new AppInstallDialogs(
|
|
document.getElementById('app-install-dialogs'));
|
|
this.appInstallDialogs.start();
|
|
this.systemBanner = new SystemBanner();
|
|
this.imeLayoutDialog = document.getElementById('ime-layout-dialog');
|
|
this.imeListTemplate = document.getElementById('ime-list-template');
|
|
this.imeList = document.getElementById('ime-list');
|
|
this.imeCancelButton = document.getElementById('ime-cancel-button');
|
|
this.imeConfirmButton = document.getElementById('ime-confirm-button');
|
|
|
|
focusManager.addUI(this);
|
|
|
|
this.appInfos = {};
|
|
this.setupQueue = [];
|
|
this.isSetupInProgress = false;
|
|
this.pausePreview = false;
|
|
this.previewOpenedTimes = {};
|
|
|
|
window.addEventListener('mozChromeEvent', (evt) => {
|
|
var detail = evt.detail;
|
|
switch (detail.type) {
|
|
case 'webapps-ask-install':
|
|
if (this.isMarketplaceAppActive()) {
|
|
if (this.getPreviewAppManifestURL()) {
|
|
// Deny the install request if there's already a preview window.
|
|
this.dispatchResponse(detail.id, 'webapps-install-denied');
|
|
} else {
|
|
this.dispatchResponse(detail.id, 'webapps-install-granted');
|
|
if (!this.pausePreview) {
|
|
this.setPreviewAppManifestURL(detail.app.manifestURL);
|
|
}
|
|
}
|
|
} else {
|
|
this.handleAppInstallPrompt(detail);
|
|
}
|
|
break;
|
|
case 'webapps-ask-uninstall':
|
|
if (this.getPreviewAppManifestURL() === detail.app.manifestURL) {
|
|
this.dispatchResponse(detail.id, 'webapps-uninstall-granted');
|
|
this.setPreviewAppManifestURL('');
|
|
} else {
|
|
this.handleAppUninstallPrompt(detail);
|
|
}
|
|
break;
|
|
}
|
|
});
|
|
|
|
window.addEventListener('applicationinstall',
|
|
this.handleApplicationInstall.bind(this));
|
|
|
|
window.addEventListener('applicationuninstall',
|
|
this.handleApplicationUninstall.bind(this));
|
|
|
|
this.imeCancelButton.onclick = this.hideIMEList.bind(this);
|
|
this.imeConfirmButton.onclick = this.handleImeConfirmAction.bind(this);
|
|
LazyLoader.load(['shared/js/template.js',
|
|
'shared/js/homescreens/confirm_dialog_helper.js']);
|
|
|
|
// 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);
|
|
|
|
// TODO: write an integration test for pressing home.
|
|
window.addEventListener('home', this.hideAllDialogs.bind(this));
|
|
},
|
|
|
|
isMarketplaceAppActive: function ai_isMarketplaceAppActive() {
|
|
const MARKETPLACE_DOMAIN = 'https://marketplace.firefox.com/';
|
|
|
|
var activeApp = AppWindowManager.getActiveApp();
|
|
return activeApp && activeApp.manifestURL.startsWith(MARKETPLACE_DOMAIN);
|
|
},
|
|
|
|
hideAllDialogs: function ai_hideAllDialogs(e) {
|
|
this.appInstallDialogs.hideAll();
|
|
if (this.imeLayoutDialog.classList.contains('visible')) {
|
|
this.imeLayoutDialog.classList.remove('visible');
|
|
this.hideIMEList();
|
|
}
|
|
},
|
|
|
|
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);
|
|
|
|
// Uninstall the previously-previewed app in case TV is turned off during
|
|
// the preview stage last time.
|
|
this.uninstallPreviewApp();
|
|
},
|
|
|
|
// start of app install
|
|
handleApplicationInstall: function ai_handleApplicationInstallEvent(e) {
|
|
var app = e.detail.application;
|
|
|
|
if (app.installState === 'installed') {
|
|
this.handleInstallSuccess(app);
|
|
return;
|
|
}
|
|
|
|
this.prepareForDownload(app);
|
|
},
|
|
|
|
handleAppInstallPrompt: function ai_handleInstallPrompt(detail) {
|
|
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;
|
|
}
|
|
|
|
// Wrap manifest to get localized properties
|
|
manifest = new ManifestHelper(manifest);
|
|
|
|
var id = detail.id;
|
|
var TYPES = AppInstallDialogs.TYPES;
|
|
var options = { 'manifest': manifest };
|
|
this.appInstallDialogs.show(TYPES.InstallDialog, options).then(
|
|
this.handleInstall.bind(this), this.showInstallCancelDialog.bind(this)
|
|
).catch(function(e) {
|
|
console.error(e);
|
|
});
|
|
|
|
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(err) {
|
|
if (err instanceof Error) {
|
|
// show error in promise
|
|
console.error(err);
|
|
}
|
|
if (this.installCallback) {
|
|
this.installCallback();
|
|
}
|
|
this.installCallback = null;
|
|
},
|
|
|
|
handleInstallCancel: function ai_handleInstallCancel(err) {
|
|
if (err instanceof Error) {
|
|
// show error in promise
|
|
console.error(err);
|
|
}
|
|
if (this.installCancelCallback) {
|
|
this.installCancelCallback();
|
|
}
|
|
this.installCancelCallback = null;
|
|
},
|
|
// end of app install
|
|
|
|
// start of app uninstall
|
|
handleApplicationUninstall: function ai_handleApplicationUninstall(e) {
|
|
var app = e.detail.application;
|
|
|
|
this.onDownloadStop(app);
|
|
this.onDownloadFinish(app);
|
|
},
|
|
|
|
handleAppUninstallPrompt: function ai_handleUninstallPrompt(detail) {
|
|
var app = detail.app;
|
|
var id = detail.id;
|
|
|
|
// updateManifest is used by packaged apps until they are installed
|
|
var manifest = app.manifest ? app.manifest : app.updateManifest;
|
|
if (!manifest) {
|
|
return;
|
|
}
|
|
|
|
// Wrap manifest to get localized properties
|
|
manifest = new ManifestHelper(manifest);
|
|
|
|
var unrecoverable = app.installState === 'pending' &&
|
|
!app.downloadAvailable &&
|
|
!app.readyToApplyDownload;
|
|
|
|
var TYPES = AppInstallDialogs.TYPES;
|
|
|
|
var options = { 'manifest': manifest, 'unrecoverable': unrecoverable };
|
|
|
|
this.appInstallDialogs.show(TYPES.UninstallDialog, options).then(
|
|
this.handleUninstall.bind(this), this.hideUninstallCancel.bind(this)
|
|
).catch(function(e) {
|
|
console.error(e);
|
|
});
|
|
|
|
this.uninstallCallback = (function ai_uninstallCallback() {
|
|
this.dispatchResponse(id, 'webapps-uninstall-granted');
|
|
}).bind(this);
|
|
|
|
this.uninstallCancelCallback = (function ai_cancelCallback() {
|
|
this.dispatchResponse(id, 'webapps-uninstall-denied');
|
|
}).bind(this);
|
|
},
|
|
|
|
handleUninstall: function ai_handleUninstallDialog() {
|
|
if (this.uninstallCallback) {
|
|
this.uninstallCallback();
|
|
}
|
|
this.uninstallCallback = null;
|
|
},
|
|
|
|
hideUninstallCancel: function ai_hideUninstallCancelDialog(err) {
|
|
if (err instanceof Error) {
|
|
// show error in promise
|
|
console.error(err);
|
|
}
|
|
if (this.uninstallCancelCallback) {
|
|
this.uninstallCancelCallback();
|
|
}
|
|
this.uninstallCancelCallback = null;
|
|
},
|
|
// end of app uninstall
|
|
|
|
// start of setup app
|
|
configurations: {
|
|
'input': {
|
|
fnName: 'showIMEList'
|
|
}
|
|
},
|
|
|
|
showSetupDialog: function ai_showSetupDialog() {
|
|
var app = this.setupQueue[0];
|
|
var options = { 'manifest': new ManifestHelper(app.manifest) };
|
|
var TYPES = AppInstallDialogs.TYPES;
|
|
|
|
this.appInstallDialogs.show(TYPES.SetupAppDialog, options).then(
|
|
this.handleSetupConfirm.bind(this), this.handleSetupCancel.bind(this)
|
|
).catch(function(e) {
|
|
console.error(e);
|
|
});
|
|
|
|
window.dispatchEvent(new CustomEvent('applicationsetupdialogshow'));
|
|
},
|
|
|
|
handleSetupCancel: function ai_handleSetupCancelAction(err) {
|
|
if (err instanceof Error) {
|
|
// show error in promise
|
|
console.error(err);
|
|
}
|
|
this.completedSetupTask();
|
|
},
|
|
|
|
handleSetupConfirm: function ai_handleSetupConfirmAction() {
|
|
var fnName = this.configurations[this.setupQueue[0].manifest.role].fnName;
|
|
this[fnName].call(this);
|
|
},
|
|
// end of setup app
|
|
|
|
// start of install cancel dialog
|
|
showInstallCancelDialog: function ai_showInstallCancelDialog(err) {
|
|
if (err instanceof Error) {
|
|
// show error in promise
|
|
console.error(err);
|
|
}
|
|
var TYPES = AppInstallDialogs.TYPES;
|
|
|
|
this.appInstallDialogs.show(TYPES.InstallCancelDialog, {}).then(
|
|
this.handleInstallCancel.bind(this),
|
|
this.handleInstall.bind(this) // cancel means to continue to install.
|
|
).catch(function(e) {
|
|
console.error(e);
|
|
});
|
|
},
|
|
// end of install cancel dialog
|
|
|
|
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;
|
|
},
|
|
|
|
previewApp: function ai_previewApp(app) {
|
|
if (!this.isMarketplaceAppActive()) {
|
|
this.uninstallPreviewApp();
|
|
return;
|
|
}
|
|
|
|
var marketplaceApp = AppWindowManager.getActiveApp();
|
|
marketplaceApp.publish('launchpreviewapp', {
|
|
url: app.origin + app.manifest.launch_path,
|
|
origin: app.origin,
|
|
manifestURL: app.manifestURL
|
|
});
|
|
},
|
|
|
|
getPreviewAppManifestURL: function() {
|
|
return localStorage.getItem('preview-app');
|
|
},
|
|
|
|
setPreviewAppManifestURL: function(manifestURL) {
|
|
localStorage.setItem('preview-app', manifestURL);
|
|
},
|
|
|
|
uninstallPreviewApp: function() {
|
|
var previewAppManifestURL = this.getPreviewAppManifestURL();
|
|
if (previewAppManifestURL) {
|
|
var app = applications.getByManifestURL(previewAppManifestURL);
|
|
if (app) {
|
|
navigator.mozApps.mgmt.uninstall(app);
|
|
} else {
|
|
this.setPreviewAppManifestURL('');
|
|
}
|
|
}
|
|
},
|
|
|
|
getAppAddedState: function(manifestURL) {
|
|
return this.getPreviewAppManifestURL() !== manifestURL &&
|
|
applications.getByManifestURL(manifestURL);
|
|
},
|
|
|
|
handleAddAppToApps: function(app) {
|
|
this.setPreviewAppManifestURL('');
|
|
this.showInstallSuccess(app);
|
|
},
|
|
|
|
handleInstallSuccess: function ai_handleInstallSuccess(app) {
|
|
var manifest = app.manifest || app.updateManifest;
|
|
var role = manifest.role;
|
|
|
|
// We must stop 3rd-party keyboard app from being installed
|
|
// if the feature is not enabled.
|
|
if (role === 'input' && !KeyboardManager.isOutOfProcessEnabled) {
|
|
navigator.mozApps.mgmt.uninstall(app);
|
|
return;
|
|
}
|
|
|
|
if (!this.pausePreview &&
|
|
app.manifestURL === this.getPreviewAppManifestURL()) {
|
|
this.previewApp(app);
|
|
return;
|
|
}
|
|
|
|
this.pausePreview = false;
|
|
|
|
if (this.configurations[role]) {
|
|
this.setupQueue.push(app);
|
|
this.checkSetupQueue();
|
|
} else {
|
|
this.showInstallSuccess(app);
|
|
}
|
|
// send event
|
|
var evt = new CustomEvent('applicationinstallsuccess',
|
|
{ detail: { application: app } });
|
|
window.dispatchEvent(evt);
|
|
},
|
|
|
|
showInstallSuccess: function ai_showInstallSuccess(app) {
|
|
if (FtuLauncher.isFtuRunning()) {
|
|
return;
|
|
}
|
|
var manifest = app.manifest || app.updateManifest;
|
|
var appManifest = new ManifestHelper(manifest);
|
|
var name = appManifest.name;
|
|
var msgID = this.isMarketplaceAppActive() ?
|
|
'added-to-apps' : 'app-install-success';
|
|
|
|
this.systemBanner.show({
|
|
id: msgID,
|
|
args: {
|
|
appName: name
|
|
}
|
|
});
|
|
},
|
|
|
|
checkSetupQueue: function ai_checkSetupQueue() {
|
|
if (this.setupQueue.length && !(this.isSetupInProgress)) {
|
|
this.isSetupInProgress = true;
|
|
this.showSetupDialog();
|
|
}
|
|
},
|
|
|
|
completedSetupTask: function ai_completedSetupTask() {
|
|
// clean completed app
|
|
this.setupQueue.shift();
|
|
this.isSetupInProgress = false;
|
|
this.checkSetupQueue();
|
|
},
|
|
|
|
showIMEList: function ai_showIMEList() {
|
|
var app = this.setupQueue[0];
|
|
var inputs = app.manifest.inputs;
|
|
if (typeof inputs !== 'object') {
|
|
console.error('inputs must be an object for ' +
|
|
'third-party keyboard layouts');
|
|
this.completedSetupTask();
|
|
return;
|
|
}
|
|
|
|
// Check permission level is correct
|
|
var hasInputPermission = (app.manifest.type === 'certified' ||
|
|
app.manifest.type === 'privileged') &&
|
|
(app.manifest.permissions &&
|
|
'input' in app.manifest.permissions);
|
|
if (!hasInputPermission) {
|
|
console.error('third-party IME does not have correct input permission');
|
|
this.completedSetupTask();
|
|
return;
|
|
}
|
|
|
|
// build the list of keyboard layouts
|
|
var listHtml = '';
|
|
var imeListWrap = Template(this.imeListTemplate);
|
|
for (var name in inputs) {
|
|
var displayIMEName = new ManifestHelper(inputs[name]).name;
|
|
listHtml += imeListWrap.interpolate({
|
|
imeName: name,
|
|
displayName: displayIMEName
|
|
});
|
|
}
|
|
// keeping li template
|
|
this.imeList.innerHTML = listHtml;
|
|
this.imeLayoutDialog.classList.add('visible');
|
|
focusManager.focus();
|
|
},
|
|
|
|
hideIMEList: function ai_hideIMEList() {
|
|
this.imeLayoutDialog.classList.remove('visible');
|
|
this.imeList.innerHTML = '';
|
|
this.completedSetupTask();
|
|
},
|
|
|
|
handleImeConfirmAction: function ai_handleImeConfirmAction() {
|
|
var manifestURL = this.setupQueue[0].manifestURL;
|
|
var keyboards = this.imeList.getElementsByTagName('input');
|
|
for (var i = 0, l = keyboards.length; i < l; i++) {
|
|
var keyboardIME = keyboards[i];
|
|
if (keyboardIME.checked) {
|
|
KeyboardHelper.setLayoutEnabled(manifestURL, keyboardIME.value, true);
|
|
KeyboardHelper.saveToSettings();
|
|
}
|
|
}
|
|
this.hideIMEList();
|
|
},
|
|
|
|
handleDownloadSuccess: function ai_handleDownloadSuccess(evt) {
|
|
var app = evt.application;
|
|
this.handleInstallSuccess(app);
|
|
this.onDownloadStop(app);
|
|
this.onDownloadFinish(app);
|
|
},
|
|
|
|
handleDownloadError: function ai_handleDownloadError(evt) {
|
|
var app = evt.application;
|
|
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 = {
|
|
id: 'app-install-' + key,
|
|
args: { appName: name }
|
|
};
|
|
this.systemBanner.show(msg);
|
|
}
|
|
|
|
this.onDownloadStop(app);
|
|
},
|
|
|
|
onDownloadStart: function ai_onDownloadStart(app) {
|
|
var manifestURL = app.manifestURL,
|
|
appInfo = this.appInfos[manifestURL];
|
|
if (!appInfo.isDownloading) {
|
|
appInfo.isDownloading = true;
|
|
this.requestWifiLock(app);
|
|
}
|
|
},
|
|
|
|
onDownloadStop: function ai_onDownloadStop(app) {
|
|
var manifestURL = app.manifestURL,
|
|
appInfo = this.appInfos[manifestURL];
|
|
if (appInfo && appInfo.isDownloading) {
|
|
this.releaseWifiLock(app);
|
|
appInfo.isDownloading = false;
|
|
}
|
|
},
|
|
|
|
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;
|
|
}
|
|
},
|
|
|
|
handleProgress: function ai_handleProgress(evt) {
|
|
this.onDownloadStart(evt.application);
|
|
},
|
|
|
|
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]);
|
|
},
|
|
|
|
isFocusable: function() {
|
|
if (this.imeLayoutDialog.classList.contains('visible')) {
|
|
return true;
|
|
} else {
|
|
return this.appInstallDialogs.hasVisibleDialog();
|
|
}
|
|
},
|
|
|
|
getElement: function() {
|
|
if (this.imeLayoutDialog.classList.contains('visible')) {
|
|
return this.imeLayoutDialog;
|
|
} else {
|
|
return this.appInstallDialogs.getTopMostDialogElement();
|
|
}
|
|
},
|
|
|
|
focus: function() {
|
|
if (this.imeLayoutDialog.classList.contains('visible')) {
|
|
document.activeElement.blur();
|
|
this.imeCancelButton.focus();
|
|
} else {
|
|
this.appInstallDialogs.focus();
|
|
}
|
|
}
|
|
};
|
|
|
|
AppInstallManager.init();
|