mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
561 lines
20 KiB
JavaScript
561 lines
20 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
this.EXPORTED_SYMBOLS = ["WebappManager"];
|
|
|
|
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
|
|
|
const UPDATE_URL_PREF = "browser.webapps.updateCheckUrl";
|
|
|
|
Cu.import("resource://gre/modules/AppsUtils.jsm");
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
Cu.import("resource://gre/modules/NetUtil.jsm");
|
|
Cu.import("resource://gre/modules/FileUtils.jsm");
|
|
Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
|
|
Cu.import("resource://gre/modules/Webapps.jsm");
|
|
Cu.import("resource://gre/modules/osfile.jsm");
|
|
Cu.import("resource://gre/modules/Promise.jsm");
|
|
Cu.import("resource://gre/modules/Task.jsm");
|
|
Cu.import("resource://gre/modules/PluralForm.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "Notifications", "resource://gre/modules/Notifications.jsm");
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "Strings", function() {
|
|
return Services.strings.createBundle("chrome://browser/locale/webapp.properties");
|
|
});
|
|
|
|
function debug(aMessage) {
|
|
// We use *dump* instead of Services.console.logStringMessage so the messages
|
|
// have the INFO level of severity instead of the ERROR level. And we don't
|
|
// append a newline character to the end of the message because *dump* spills
|
|
// into the Android native logging system, which strips newlines from messages
|
|
// and breaks messages into lines automatically at display time (i.e. logcat).
|
|
#ifdef MOZ_DEBUG
|
|
dump(aMessage);
|
|
#endif
|
|
}
|
|
|
|
function sendMessageToJava(aMessage) {
|
|
return Services.androidBridge.handleGeckoMessage(JSON.stringify(aMessage));
|
|
}
|
|
|
|
this.WebappManager = {
|
|
__proto__: DOMRequestIpcHelper.prototype,
|
|
|
|
get _testing() {
|
|
try {
|
|
return Services.prefs.getBoolPref("browser.webapps.testing");
|
|
} catch(ex) {
|
|
return false;
|
|
}
|
|
},
|
|
|
|
install: function(aMessage, aMessageManager) {
|
|
if (this._testing) {
|
|
// Go directly to DOM. Do not download/install APK, do not collect $200.
|
|
DOMApplicationRegistry.doInstall(aMessage, aMessageManager);
|
|
return;
|
|
}
|
|
|
|
this._installApk(aMessage, aMessageManager);
|
|
},
|
|
|
|
installPackage: function(aMessage, aMessageManager) {
|
|
if (this._testing) {
|
|
// Go directly to DOM. Do not download/install APK, do not collect $200.
|
|
DOMApplicationRegistry.doInstallPackage(aMessage, aMessageManager);
|
|
return;
|
|
}
|
|
|
|
this._installApk(aMessage, aMessageManager);
|
|
},
|
|
|
|
_installApk: function(aMessage, aMessageManager) { return Task.spawn((function*() {
|
|
let filePath;
|
|
|
|
try {
|
|
filePath = yield this._downloadApk(aMessage.app.manifestURL);
|
|
} catch(ex) {
|
|
aMessage.error = ex;
|
|
aMessageManager.sendAsyncMessage("Webapps:Install:Return:KO", aMessage);
|
|
debug("error downloading APK: " + ex);
|
|
return;
|
|
}
|
|
|
|
sendMessageToJava({
|
|
type: "Webapps:InstallApk",
|
|
filePath: filePath,
|
|
data: JSON.stringify(aMessage),
|
|
});
|
|
}).bind(this)); },
|
|
|
|
_downloadApk: function(aManifestUrl) {
|
|
debug("_downloadApk for " + aManifestUrl);
|
|
let deferred = Promise.defer();
|
|
|
|
// Get the endpoint URL and convert it to an nsIURI/nsIURL object.
|
|
const GENERATOR_URL_PREF = "browser.webapps.apkFactoryUrl";
|
|
const GENERATOR_URL_BASE = Services.prefs.getCharPref(GENERATOR_URL_PREF);
|
|
let generatorUrl = NetUtil.newURI(GENERATOR_URL_BASE).QueryInterface(Ci.nsIURL);
|
|
|
|
// Populate the query part of the URL with the manifest URL parameter.
|
|
let params = {
|
|
manifestUrl: aManifestUrl,
|
|
};
|
|
generatorUrl.query =
|
|
[p + "=" + encodeURIComponent(params[p]) for (p in params)].join("&");
|
|
debug("downloading APK from " + generatorUrl.spec);
|
|
|
|
let file = Cc["@mozilla.org/download-manager;1"].
|
|
getService(Ci.nsIDownloadManager).
|
|
defaultDownloadsDirectory.
|
|
clone();
|
|
file.append(aManifestUrl.replace(/[^a-zA-Z0-9]/gi, "") + ".apk");
|
|
file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
|
|
debug("downloading APK to " + file.path);
|
|
|
|
let worker = new ChromeWorker("resource://gre/modules/WebappManagerWorker.js");
|
|
worker.onmessage = function(event) {
|
|
let { type, message } = event.data;
|
|
|
|
worker.terminate();
|
|
|
|
if (type == "success") {
|
|
deferred.resolve(file.path);
|
|
} else { // type == "failure"
|
|
debug("error downloading APK: " + message);
|
|
deferred.reject(message);
|
|
}
|
|
}
|
|
|
|
// Trigger the download.
|
|
worker.postMessage({ url: generatorUrl.spec, path: file.path });
|
|
|
|
return deferred.promise;
|
|
},
|
|
|
|
askInstall: function(aData) {
|
|
let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
|
|
file.initWithPath(aData.profilePath);
|
|
|
|
// We don't yet support pre-installing an appcache because it isn't clear
|
|
// how to do it without degrading the user experience (since users expect
|
|
// apps to be available after the system tells them they've been installed,
|
|
// which has already happened) and because nsCacheService shuts down
|
|
// when we trigger the native install dialog and doesn't re-init itself
|
|
// afterward (TODO: file bug about this behavior).
|
|
if ("appcache_path" in aData.app.manifest) {
|
|
debug("deleting appcache_path from manifest: " + aData.app.manifest.appcache_path);
|
|
delete aData.app.manifest.appcache_path;
|
|
}
|
|
|
|
DOMApplicationRegistry.registryReady.then(() => {
|
|
DOMApplicationRegistry.confirmInstall(aData, file, (function(aManifest) {
|
|
let localeManifest = new ManifestHelper(aManifest, aData.app.origin);
|
|
|
|
// aData.app.origin may now point to the app: url that hosts this app.
|
|
sendMessageToJava({
|
|
type: "Webapps:Postinstall",
|
|
apkPackageName: aData.app.apkPackageName,
|
|
origin: aData.app.origin,
|
|
});
|
|
|
|
this.writeDefaultPrefs(file, localeManifest);
|
|
}).bind(this));
|
|
});
|
|
},
|
|
|
|
launch: function({ manifestURL, origin }) {
|
|
debug("launchWebapp: " + manifestURL);
|
|
|
|
sendMessageToJava({
|
|
type: "Webapps:Open",
|
|
manifestURL: manifestURL,
|
|
origin: origin
|
|
});
|
|
},
|
|
|
|
uninstall: function(aData) {
|
|
debug("uninstall: " + aData.manifestURL);
|
|
|
|
if (this._testing) {
|
|
// We don't have to do anything, as the registry does all the work.
|
|
return;
|
|
}
|
|
|
|
// TODO: uninstall the APK.
|
|
},
|
|
|
|
autoInstall: function(aData) {
|
|
let oldApp = DOMApplicationRegistry.getAppByManifestURL(aData.manifestURL);
|
|
if (oldApp) {
|
|
// If the app is already installed, update the existing installation.
|
|
this._autoUpdate(aData, oldApp);
|
|
return;
|
|
}
|
|
|
|
let mm = {
|
|
sendAsyncMessage: function (aMessageName, aData) {
|
|
// TODO hook this back to Java to report errors.
|
|
debug("sendAsyncMessage " + aMessageName + ": " + JSON.stringify(aData));
|
|
}
|
|
};
|
|
|
|
let origin = Services.io.newURI(aData.manifestURL, null, null).prePath;
|
|
|
|
let message = aData.request || {
|
|
app: {
|
|
origin: origin,
|
|
receipts: [],
|
|
}
|
|
};
|
|
|
|
if (aData.updateManifest) {
|
|
if (aData.zipFilePath) {
|
|
aData.updateManifest.package_path = aData.zipFilePath;
|
|
}
|
|
message.app.updateManifest = aData.updateManifest;
|
|
}
|
|
|
|
// The manifest url may be subtly different between the
|
|
// time the APK was built and the APK being installed.
|
|
// Thus, we should take the APK as the source of truth.
|
|
message.app.manifestURL = aData.manifestURL;
|
|
message.app.manifest = aData.manifest;
|
|
message.app.apkPackageName = aData.apkPackageName;
|
|
message.profilePath = aData.profilePath;
|
|
message.autoInstall = true;
|
|
message.mm = mm;
|
|
|
|
DOMApplicationRegistry.registryReady.then(() => {
|
|
switch (aData.type) { // can be hosted or packaged.
|
|
case "hosted":
|
|
DOMApplicationRegistry.doInstall(message, mm);
|
|
break;
|
|
|
|
case "packaged":
|
|
message.isPackage = true;
|
|
DOMApplicationRegistry.doInstallPackage(message, mm);
|
|
break;
|
|
}
|
|
});
|
|
},
|
|
|
|
_autoUpdate: function(aData, aOldApp) { return Task.spawn((function*() {
|
|
debug("_autoUpdate app of type " + aData.type);
|
|
|
|
if (aData.type == "hosted") {
|
|
let oldManifest = yield DOMApplicationRegistry.getManifestFor(aData.manifestURL);
|
|
DOMApplicationRegistry.updateHostedApp(aData, aOldApp.id, aOldApp, oldManifest, aData.manifest);
|
|
} else {
|
|
DOMApplicationRegistry.updatePackagedApp(aData, aOldApp.id, aOldApp, aData.manifest);
|
|
}
|
|
}).bind(this)); },
|
|
|
|
_checkingForUpdates: false,
|
|
|
|
checkForUpdates: function(userInitiated) { return Task.spawn((function*() {
|
|
debug("checkForUpdates");
|
|
|
|
// Don't start checking for updates if we're already doing so.
|
|
// TODO: Consider cancelling the old one and starting a new one anyway
|
|
// if the user requested this one.
|
|
if (this._checkingForUpdates) {
|
|
debug("already checking for updates");
|
|
return;
|
|
}
|
|
this._checkingForUpdates = true;
|
|
|
|
try {
|
|
let installedApps = yield this._getInstalledApps();
|
|
if (installedApps.length === 0) {
|
|
return;
|
|
}
|
|
|
|
// Map APK names to APK versions.
|
|
let apkNameToVersion = yield this._getAPKVersions(installedApps.map(app =>
|
|
app.packageName).filter(packageName => !!packageName)
|
|
);
|
|
|
|
// Map manifest URLs to APK versions, which is what the service needs
|
|
// in order to tell us which apps are outdated; and also map them to app
|
|
// objects, which the downloader/installer uses to download/install APKs.
|
|
// XXX Will this cause us to update apps without packages, and if so,
|
|
// does that satisfy the legacy migration story?
|
|
let manifestUrlToApkVersion = {};
|
|
let manifestUrlToApp = {};
|
|
for (let app of installedApps) {
|
|
manifestUrlToApkVersion[app.manifestURL] = apkNameToVersion[app.packageName] || 0;
|
|
manifestUrlToApp[app.manifestURL] = app;
|
|
}
|
|
|
|
let outdatedApps = yield this._getOutdatedApps(manifestUrlToApkVersion, userInitiated);
|
|
|
|
if (outdatedApps.length === 0) {
|
|
// If the user asked us to check for updates, tell 'em we came up empty.
|
|
if (userInitiated) {
|
|
this._notify({
|
|
title: Strings.GetStringFromName("noUpdatesTitle"),
|
|
message: Strings.GetStringFromName("noUpdatesMessage"),
|
|
icon: "drawable://alert_app",
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
let names = [manifestUrlToApp[url].name for (url of outdatedApps)].join(", ");
|
|
let accepted = yield this._notify({
|
|
title: PluralForm.get(outdatedApps.length, Strings.GetStringFromName("downloadUpdateTitle")).
|
|
replace("#1", outdatedApps.length),
|
|
message: Strings.formatStringFromName("downloadUpdateMessage", [names], 1),
|
|
icon: "drawable://alert_download",
|
|
}).dismissed;
|
|
|
|
if (accepted) {
|
|
yield this._updateApks([manifestUrlToApp[url] for (url of outdatedApps)]);
|
|
}
|
|
}
|
|
// There isn't a catch block because we want the error to propagate through
|
|
// the promise chain, so callers can receive it and choose to respond to it.
|
|
finally {
|
|
// Ensure we update the _checkingForUpdates flag even if there's an error;
|
|
// otherwise the process will get stuck and never check for updates again.
|
|
this._checkingForUpdates = false;
|
|
}
|
|
}).bind(this)); },
|
|
|
|
_getAPKVersions: function(packageNames) {
|
|
let deferred = Promise.defer();
|
|
|
|
sendMessageToJava({
|
|
type: "Webapps:GetApkVersions",
|
|
packageNames: packageNames
|
|
}, data => deferred.resolve(JSON.parse(data).versions));
|
|
|
|
return deferred.promise;
|
|
},
|
|
|
|
_getInstalledApps: function() {
|
|
let deferred = Promise.defer();
|
|
DOMApplicationRegistry.getAll(apps => deferred.resolve(apps));
|
|
return deferred.promise;
|
|
},
|
|
|
|
_getOutdatedApps: function(installedApps, userInitiated) {
|
|
let deferred = Promise.defer();
|
|
|
|
let data = JSON.stringify({ installed: installedApps });
|
|
|
|
let notification;
|
|
if (userInitiated) {
|
|
notification = this._notify({
|
|
title: Strings.GetStringFromName("checkingForUpdatesTitle"),
|
|
message: Strings.GetStringFromName("checkingForUpdatesMessage"),
|
|
// TODO: replace this with an animated icon.
|
|
icon: "drawable://alert_app",
|
|
progress: NaN,
|
|
});
|
|
}
|
|
|
|
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
|
|
createInstance(Ci.nsIXMLHttpRequest).
|
|
QueryInterface(Ci.nsIXMLHttpRequestEventTarget);
|
|
request.mozBackgroundRequest = true;
|
|
request.open("POST", Services.prefs.getCharPref(UPDATE_URL_PREF), true);
|
|
request.channel.loadFlags = Ci.nsIChannel.LOAD_ANONYMOUS |
|
|
Ci.nsIChannel.LOAD_BYPASS_CACHE |
|
|
Ci.nsIChannel.INHIBIT_CACHING;
|
|
request.onload = function() {
|
|
notification.cancel();
|
|
deferred.resolve(JSON.parse(this.response).outdated);
|
|
};
|
|
request.onerror = function() {
|
|
if (userInitiated) {
|
|
notification.cancel();
|
|
}
|
|
deferred.reject(this.status || this.statusText);
|
|
};
|
|
request.setRequestHeader("Content-Type", "application/json");
|
|
request.setRequestHeader("Content-Length", data.length);
|
|
|
|
request.send(data);
|
|
|
|
return deferred.promise;
|
|
},
|
|
|
|
_updateApks: function(aApps) { return Task.spawn((function*() {
|
|
// Notify the user that we're in the progress of downloading updates.
|
|
let downloadingNames = [app.name for (app of aApps)].join(", ");
|
|
let notification = this._notify({
|
|
title: PluralForm.get(aApps.length, Strings.GetStringFromName("downloadingUpdateTitle")).
|
|
replace("#1", aApps.length),
|
|
message: Strings.formatStringFromName("downloadingUpdateMessage", [downloadingNames], 1),
|
|
// TODO: replace this with an animated icon. UpdateService uses
|
|
// android.R.drawable.stat_sys_download, but I don't think we can reference
|
|
// a system icon with a drawable: URL here, so we'll have to craft our own.
|
|
icon: "drawable://alert_download",
|
|
// TODO: make this a determinate progress indicator once we can determine
|
|
// the sizes of the APKs and observe their progress.
|
|
progress: NaN,
|
|
});
|
|
|
|
// Download the APKs for the given apps. We do this serially to avoid
|
|
// saturating the user's network connection.
|
|
// TODO: download APKs in parallel (or at least more than one at a time)
|
|
// if it seems reasonable.
|
|
let downloadedApks = [];
|
|
let downloadFailedApps = [];
|
|
for (let app of aApps) {
|
|
try {
|
|
let filePath = yield this._downloadApk(app.manifestURL);
|
|
downloadedApks.push({ app: app, filePath: filePath });
|
|
} catch(ex) {
|
|
downloadFailedApps.push(app);
|
|
}
|
|
}
|
|
|
|
notification.cancel();
|
|
|
|
// Notify the user if any downloads failed, but don't do anything
|
|
// when the user accepts/cancels the notification.
|
|
// In the future, we might prompt the user to retry the download.
|
|
if (downloadFailedApps.length > 0) {
|
|
let downloadFailedNames = [app.name for (app of downloadFailedApps)].join(", ");
|
|
this._notify({
|
|
title: PluralForm.get(downloadFailedApps.length, Strings.GetStringFromName("downloadFailedTitle")).
|
|
replace("#1", downloadFailedApps.length),
|
|
message: Strings.formatStringFromName("downloadFailedMessage", [downloadFailedNames], 1),
|
|
icon: "drawable://alert_app",
|
|
});
|
|
}
|
|
|
|
// If we weren't able to download any APKs, then there's nothing more to do.
|
|
if (downloadedApks.length === 0) {
|
|
return;
|
|
}
|
|
|
|
// Prompt the user to update the apps for which we downloaded APKs, and wait
|
|
// until they accept/cancel the notification.
|
|
let downloadedNames = [apk.app.name for (apk of downloadedApks)].join(", ");
|
|
let accepted = yield this._notify({
|
|
title: PluralForm.get(downloadedApks.length, Strings.GetStringFromName("installUpdateTitle")).
|
|
replace("#1", downloadedApks.length),
|
|
message: Strings.formatStringFromName("installUpdateMessage", [downloadedNames], 1),
|
|
icon: "drawable://alert_app",
|
|
}).dismissed;
|
|
|
|
if (accepted) {
|
|
// The user accepted the notification, so install the downloaded APKs.
|
|
for (let apk of downloadedApks) {
|
|
let msg = {
|
|
app: apk.app,
|
|
// TODO: figure out why Webapps:InstallApk needs the "from" property.
|
|
from: apk.app.installOrigin,
|
|
};
|
|
sendMessageToJava({
|
|
type: "Webapps:InstallApk",
|
|
filePath: apk.filePath,
|
|
data: JSON.stringify(msg),
|
|
});
|
|
}
|
|
} else {
|
|
// The user cancelled the notification, so remove the downloaded APKs.
|
|
for (let apk of downloadedApks) {
|
|
try {
|
|
yield OS.file.remove(apk.filePath);
|
|
} catch(ex) {
|
|
debug("error removing " + apk.filePath + " for cancelled update: " + ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
}).bind(this)); },
|
|
|
|
_notify: function(aOptions) {
|
|
dump("_notify: " + aOptions.title);
|
|
|
|
// Resolves to true if the notification is "clicked" (i.e. touched)
|
|
// and false if the notification is "cancelled" by swiping it away.
|
|
let dismissed = Promise.defer();
|
|
|
|
// TODO: make notifications expandable so users can expand them to read text
|
|
// that gets cut off in standard notifications.
|
|
let id = Notifications.create({
|
|
title: aOptions.title,
|
|
message: aOptions.message,
|
|
icon: aOptions.icon,
|
|
progress: aOptions.progress,
|
|
onClick: function(aId, aCookie) {
|
|
dismissed.resolve(true);
|
|
},
|
|
onCancel: function(aId, aCookie) {
|
|
dismissed.resolve(false);
|
|
},
|
|
});
|
|
|
|
// Return an object with a promise that resolves when the notification
|
|
// is dismissed by the user along with a method for cancelling it,
|
|
// so callers who want to wait for user action can do so, while those
|
|
// who want to control the notification's lifecycle can do that instead.
|
|
return {
|
|
dismissed: dismissed.promise,
|
|
cancel: function() {
|
|
Notifications.cancel(id);
|
|
},
|
|
};
|
|
},
|
|
|
|
autoUninstall: function(aData) {
|
|
DOMApplicationRegistry.registryReady.then(() => {
|
|
for (let id in DOMApplicationRegistry.webapps) {
|
|
let app = DOMApplicationRegistry.webapps[id];
|
|
if (aData.apkPackageNames.indexOf(app.apkPackageName) > -1) {
|
|
debug("attempting to uninstall " + app.name);
|
|
DOMApplicationRegistry.uninstall(
|
|
app.manifestURL,
|
|
function() {
|
|
debug("success uninstalling " + app.name);
|
|
},
|
|
function(error) {
|
|
debug("error uninstalling " + app.name + ": " + error);
|
|
}
|
|
);
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
writeDefaultPrefs: function(aProfile, aManifest) {
|
|
// build any app specific default prefs
|
|
let prefs = [];
|
|
if (aManifest.orientation) {
|
|
let orientation = aManifest.orientation;
|
|
if (Array.isArray(orientation)) {
|
|
orientation = orientation.join(",");
|
|
}
|
|
prefs.push({ name: "app.orientation.default", value: orientation });
|
|
}
|
|
|
|
// write them into the app profile
|
|
let defaultPrefsFile = aProfile.clone();
|
|
defaultPrefsFile.append(this.DEFAULT_PREFS_FILENAME);
|
|
this._writeData(defaultPrefsFile, prefs);
|
|
},
|
|
|
|
_writeData: function(aFile, aPrefs) {
|
|
if (aPrefs.length > 0) {
|
|
let array = new TextEncoder().encode(JSON.stringify(aPrefs));
|
|
OS.File.writeAtomic(aFile.path, array, { tmpPath: aFile.path + ".tmp" }).then(null, function onError(reason) {
|
|
debug("Error writing default prefs: " + reason);
|
|
});
|
|
}
|
|
},
|
|
|
|
DEFAULT_PREFS_FILENAME: "default-prefs.js",
|
|
|
|
};
|