/* 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 `
  • `; }, 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 `
    `; 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));