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.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,647 @@
/* 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();