VulpesOS/assets/webapps/system.gaiamobile.org/js/app_install_manager.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

872 lines
28 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* jshint moz:true */
/* global ConfirmDialogHelper */
/* global KeyboardHelper */
/* global LazyLoader */
/* global ManifestHelper */
/* global MatchPattern */
/* global ModalDialog */
/* global NotificationScreen */
/* global Service */
/* global SystemBanner */
/* global Sanitizer */
/* global UtilityTray */
/* global applications */
'use strict';
(function(exports) {
var AppInstallManager = {
DEBUG: false,
name: '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',
'DOWNLOAD_CANCELED':'download-canceled'
},
start: function() {
var lazyLoadScripts = [
'js/system_banner.js',
'shared/js/addons/match_pattern.js'
];
LazyLoader.load(lazyLoadScripts).then(() => {
this.systemBanner = new SystemBanner();
}).catch((err) => {
console.error(err);
});
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.imeLayoutDialog = document.getElementById('ime-layout-dialog');
this.imeList = document.getElementById('ime-list');
this.imeCancelButton = document.getElementById('ime-cancel-button');
this.imeConfirmButton = document.getElementById('ime-confirm-button');
this.setupCancelButton =
document.getElementById('setup-cancel-button');
this.setupConfirmButton =
document.getElementById('setup-confirm-button');
this.installCancelDialog =
document.getElementById('app-install-cancel-dialog');
this.downloadCancelDialog =
document.getElementById('app-download-cancel-dialog');
this.setupInstalledAppDialog =
document.getElementById('setup-installed-app-dialog');
this.confirmCancelButton =
document.getElementById('app-install-confirm-cancel-button');
this.setupAppName = document.getElementById('setup-app-name');
this.setupAppDescription =
document.getElementById('setup-app-description');
this.resumeButton = document.getElementById('app-install-resume-button');
this.notifContainer =
document.getElementById('install-manager-notification-container');
this.appInfos = {};
this.setupQueue = [];
this.isSetupInProgress = false;
window.addEventListener('mozChromeEvent', this);
window.addEventListener('applicationinstall', this);
window.addEventListener('applicationuninstall', 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);
this.setupCancelButton.onclick = this.handleSetupCancelAction.bind(this);
this.setupConfirmButton.onclick =
this.handleSetupConfirmAction.bind(this);
this.imeCancelButton.onclick = this.hideIMEList.bind(this);
this.imeConfirmButton.onclick = this.handleImeConfirmAction.bind(this);
LazyLoader.load(['shared/js/homescreens/confirm_dialog_helper.js']);
window.addEventListener('applicationready', this);
window.addEventListener('home', this);
window.addEventListener('holdhome', 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);
},
stop: function ai_stop() {
window.removeEventListener('mozChromeEvent', this);
window.removeEventListener('applicationinstall', this);
window.removeEventListener('applicationuninstall', this);
window.removeEventListener('applicationready', this);
window.removeEventListener('home', this);
window.removeEventListener('holdhome', this);
},
handleEvent: function ai_handleEvent(evt) {
switch (evt.type) {
case 'mozChromeEvent':
return this.handleMozChromeEvent(evt);
case 'applicationready':
return this.handleApplicationReady(evt);
case 'home':
case 'holdhome':
return this.cancelInstallation(evt);
case 'applicationinstall':
return this.handleApplicationInstall(evt);
case 'applicationuninstall':
return this.handleApplicationUninstall(evt);
}
},
imeListView: function({displayName, imeName}) {
return Sanitizer.createSafeHTML `
<li>
<gaia-checkbox class="ime inline" name="keyboards" value="${imeName}">
<label>${displayName}</label>
</gaia-checkbox>
</li>`;
},
cancelInstallation: function ai_cancelInstallation() {
this.dialog.classList.remove('visible');
this.dispatchPromptEvent('hidden');
this.handleInstallCancel();
// hide IME setup dialog if presented
if (this.setupInstalledAppDialog.classList.contains('visible') ) {
this.handleSetupCancelAction();
}
// hide IME layout list if presented
if (this.imeLayoutDialog.classList.contains('visible')) {
this.hideIMEList();
}
},
handleMozChromeEvent: function ai_handleMozChromeEvent(e) {
switch (e.detail.type) {
case 'webapps-ask-install':
var manifestURL = e.detail.app && e.detail.app.manifestURL;
var isCustomizing = Service.query('ftuCustomizationContains',
manifestURL);
this.debug('handling webapps-ask-install, ' +
'manifestURL: ' + manifestURL + ', ' +
'isCustomizing: ' + isCustomizing);
if (manifestURL && isCustomizing) {
var grantedEvent = new CustomEvent('mozContentEvent', {
bubbles: true,
cancelable: true,
detail: {
id: e.detail.id,
type: 'webapps-install-granted'
}
});
window.dispatchEvent(grantedEvent);
} else {
this.handleAppInstallPrompt(e.detail);
}
break;
case 'webapps-ask-uninstall':
this.handleAppUninstallPrompt(e.detail);
break;
}
},
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.handleInstallSuccess(app);
return;
}
this.prepareForDownload(app);
},
handleApplicationUninstall: function ai_handleApplicationUninstall(e) {
var app = e.detail.application;
this.onDownloadStop(app);
this.onDownloadFinish(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;
}
this.dialog.classList.add('visible');
this.dispatchPromptEvent('shown');
var id = detail.id;
if (manifest.size) {
this.size.removeAttribute('data-l10n-id');
this.size.textContent = this.humanizeSize(manifest.size);
} else {
this.size.setAttribute('data-l10n-id', 'size-unknown');
}
// Wrap manifest to get localized properties
manifest = new ManifestHelper(manifest);
navigator.mozL10n.setAttributes(this.msg, 'install-app', {
'name': manifest.displayName
});
if (manifest.developer) {
if (manifest.developer.name) {
this.authorName.removeAttribute('data-l10n-id');
this.authorName.textContent = manifest.developer.name;
} else {
this.authorName.setAttribute('data-l10n-id', 'author-unknown');
}
this.authorUrl.textContent = manifest.developer.url || '';
} else {
this.authorName.setAttribute('data-l10n-id', 'author-unknown');
this.authorUrl.textContent = '';
}
// Warn about system level addons?
var warningEl = document.getElementById('system-addon-warning');
warningEl.hidden = !this.requiresRestart(manifest.content_scripts);
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);
},
requiresRestart: function ai_requiresRestart(contentScripts) {
var restartRequired = false;
if (contentScripts) {
var patterns = contentScripts.map(contentScript => {
return {
matches: new MatchPattern(contentScript.matches),
excludeMatches: new MatchPattern(contentScript.exclude_matches)
};
});
appLoop:
for (var appKey in applications.installedApps) {
if (!appKey) {
continue;
}
var installedApp = applications.installedApps[appKey];
var launchPath = installedApp.manifest.launch_path || '';
if (!installedApp.manifest.origin) {
continue;
}
if (installedApp.manifest.role !== 'system' &&
installedApp.manifest.role !== 'homescreen') {
continue;
}
var launchURL = new URL(launchPath, installedApp.manifest.origin);
for(var i = 0; i < patterns.length; i++) {
var pattern = patterns[i];
if (!pattern.excludeMatches.matches(launchURL) &&
pattern.matches.matches(launchURL)) {
restartRequired = true;
break appLoop;
}
}
}
}
return restartRequired;
},
handleInstall: function ai_handleInstall(evt) {
if (evt) {
evt.preventDefault();
}
if (this.installCallback) {
this.installCallback();
}
this.installCallback = null;
this.installCancelCallback = null;
this.dialog.classList.remove('visible');
this.dispatchPromptEvent('hidden');
},
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);
if (manifest.role === 'theme') {
this.dispatchResponse(id, 'webapps-uninstall-granted');
return;
}
var unrecoverable = app.installState === 'pending' &&
!app.downloadAvailable &&
!app.readyToApplyDownload;
var dialogConfig;
if (unrecoverable) {
dialogConfig = {
type: 'unrecoverable',
title: 'unrecoverable-error-title',
body: 'unrecoverable-error-body',
confirm: {
title: 'unrecoverable-error-action',
cb: () => {
this.dispatchResponse(id, 'webapps-uninstall-granted');
}
}
};
} else {
var nameObj = { name: manifest.displayName };
dialogConfig = {
type: 'remove',
title: {id: 'delete-title', args: nameObj},
body: {id: 'delete-body', args: nameObj},
cancel: {
title: 'cancel',
cb: () => { this.dispatchResponse(id, 'webapps-uninstall-denied'); }
},
confirm: {
title: 'delete',
type: 'danger',
cb: () => {
this.dispatchResponse(id, 'webapps-uninstall-granted');
}
}
};
}
var dialog = new ConfirmDialogHelper(dialogConfig);
dialog.show(document.getElementById('app-uninstall-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;
},
configurations: {
'input': {
fnName: 'showIMEList'
}
},
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' &&
!Service.query('isOutOfProcessEnabled')) {
navigator.mozApps.mgmt.uninstall(app);
return;
}
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 (Service.query('isFtuRunning')) {
return;
}
var manifest = app.manifest || app.updateManifest;
var appManifest = new ManifestHelper(manifest);
var name = appManifest.displayName;
var l10nId = appManifest.role === 'langpack' ?
'langpack-install-success2' : 'app-install-success';
this.systemBanner.show({id: l10nId, 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();
},
hideSetupDialog: function ai_hideSetupDialog() {
this.setupAppName.textContent = '';
this.setupAppDescription.textContent = '';
this.setupInstalledAppDialog.classList.remove('visible');
this.dispatchPromptEvent('hidden');
},
showSetupDialog: function ai_showSetupDialog() {
var app = this.setupQueue[0];
var manifest = app.manifest;
var appManifest = new ManifestHelper(manifest);
var appName = appManifest.displayName;
var appDescription = appManifest.description;
this.setupAppDescription.textContent = appDescription;
navigator.mozL10n.setAttributes(this.setupAppName,
'app-install-success',
{ appName: appName });
this.setupInstalledAppDialog.classList.add('visible');
this.dispatchPromptEvent('shown');
window.dispatchEvent(new CustomEvent('applicationsetupdialogshow'));
},
handleSetupCancelAction: function ai_handleSetupCancelAction() {
this.hideSetupDialog();
this.completedSetupTask();
},
handleSetupConfirmAction: function ai_handleSetupConfirmAction() {
var fnName = this.configurations[this.setupQueue[0].manifest.role].fnName;
this[fnName].call(this);
this.hideSetupDialog();
},
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 = [];
for (var name in inputs) {
var displayIMEName = new ManifestHelper(inputs[name]).displayName;
listHtml.push(this.imeListView({
imeName: name,
displayName: displayIMEName
}));
}
// keeping li template
this.imeList.innerHTML = Sanitizer.unwrapSafeHTML(...listHtml);
this.imeLayoutDialog.classList.add('visible');
this.dispatchPromptEvent('shown');
},
hideIMEList: function ai_hideIMEList() {
this.imeLayoutDialog.classList.remove('visible');
this.dispatchPromptEvent('hidden');
this.imeList.innerHTML = '';
this.completedSetupTask();
},
handleImeConfirmAction: function ai_handleImeConfirmAction() {
var manifestURL = this.setupQueue[0].manifestURL;
var keyboards = this.imeList.getElementsByTagName('gaia-checkbox');
for (var i = 0, l = keyboards.length; i < l; i++) {
var keyboardIME = keyboards[i];
if (keyboardIME.checked) {
KeyboardHelper.setLayoutEnabled(manifestURL,
keyboardIME.getAttribute('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).displayName;
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';
this.systemBanner.show({
id: 'app-install-' + key,
args: { appName: name }
});
}
this.onDownloadStop(app);
},
onDownloadStart: function ai_onDownloadStart(app) {
if (! this.hasNotification(app)) {
Service.request('incDownloads');
this.addNotification(app);
this.requestWifiLock(app);
}
},
onDownloadStop: function ai_onDownloadStop(app) {
if (this.hasNotification(app)) {
Service.request('decDownloads');
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,
appInfo = this.appInfos[manifestURL];
if (appInfo.installNotification) {
return;
}
var newNotif = Sanitizer.createSafeHTML `
<div class="fake-notification" role="link">
<div data-icon="rocket" class="alert" aria-hidden="true"></div>
<div class="title-container"></div>
<progress></progress>
</div>`;
this.notifContainer.insertAdjacentHTML('afterbegin',
Sanitizer.unwrapSafeHTML(newNotif));
var newNode = this.notifContainer.firstElementChild;
newNode.dataset.manifest = manifestURL;
var manifest = app.manifest || app.updateManifest;
navigator.mozL10n.setAttributes(
newNode.querySelector('.title-container'),
'downloadingAppMessage',
{ appName: new ManifestHelper(manifest).displayName }
);
var progressNode = newNode.querySelector('progress');
if (app.updateManifest && app.updateManifest.size) {
progressNode.max = app.updateManifest.size;
appInfo.hasMax = true;
}
appInfo.installNotification = newNode;
NotificationScreen.addUnreadNotification(manifestURL);
},
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;
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 = {
id: 'downloadingAppProgressIndeterminate',
args: null
};
progressNode.removeAttribute('value'); // switch to indeterminate state
} else if (appInfo.hasMax) {
message = {
id: 'downloadingAppProgress',
args: {
progress: this.humanizeSize(app.progress),
max: this.humanizeSize(progressNode.max)
}
};
progressNode.value = app.progress;
} else {
message = {
id: 'downloadingAppProgressNoMax',
args: { progress: this.humanizeSize(app.progress) }
};
}
navigator.mozL10n.setAttributes(
progressNode,
message.id,
message.args);
},
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.removeUnreadNotification(manifestURL);
},
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');
navigator.mozL10n.setAttributes(title, 'stopDownloading', {
app: new ManifestHelper(manifest).displayName
});
dialog.classList.add('visible');
this.dispatchPromptEvent('shown');
dialog.dataset.manifest = manifestURL;
UtilityTray.hide();
},
handleInstallCancel: function ai_handleInstallCancel() {
if (this.installCancelCallback) {
this.installCancelCallback();
}
this.installCallback = null;
this.installCancelCallback = null;
this.installCancelDialog.classList.remove('visible');
this.dispatchPromptEvent('hidden');
},
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');
this.dispatchPromptEvent('hidden');
delete dialog.dataset.manifest;
},
dispatchPromptEvent: function(state) {
window.dispatchEvent(new CustomEvent('installprompt' + state));
},
debug: function() {
if (this.DEBUG) {
console.log('[' + this.name + ']' +
'[' + Service.currentTime() + '] ' +
Array.slice(arguments).concat());
if (this.TRACE) {
console.trace();
}
}
}
};
exports.AppInstallManager = AppInstallManager;
}(window));