mobile/android/modules/WebappManager.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 "use strict";
     7 this.EXPORTED_SYMBOLS = ["WebappManager"];
     9 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
    11 const UPDATE_URL_PREF = "browser.webapps.updateCheckUrl";
    13 Cu.import("resource://gre/modules/AppsUtils.jsm");
    14 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    15 Cu.import("resource://gre/modules/Services.jsm");
    16 Cu.import("resource://gre/modules/NetUtil.jsm");
    17 Cu.import("resource://gre/modules/FileUtils.jsm");
    18 Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
    19 Cu.import("resource://gre/modules/Webapps.jsm");
    20 Cu.import("resource://gre/modules/osfile.jsm");
    21 Cu.import("resource://gre/modules/Promise.jsm");
    22 Cu.import("resource://gre/modules/Task.jsm");
    24 XPCOMUtils.defineLazyModuleGetter(this, "Notifications", "resource://gre/modules/Notifications.jsm");
    25 XPCOMUtils.defineLazyModuleGetter(this, "sendMessageToJava", "resource://gre/modules/Messaging.jsm");
    26 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm");
    28 XPCOMUtils.defineLazyGetter(this, "Strings", function() {
    29   return Services.strings.createBundle("chrome://browser/locale/webapp.properties");
    30 });
    32 function debug(aMessage) {
    33   // We use *dump* instead of Services.console.logStringMessage so the messages
    34   // have the INFO level of severity instead of the ERROR level.  And we don't
    35   // append a newline character to the end of the message because *dump* spills
    36   // into the Android native logging system, which strips newlines from messages
    37   // and breaks messages into lines automatically at display time (i.e. logcat).
    38 #ifdef DEBUG
    39   dump(aMessage);
    40 #endif
    41 }
    43 this.WebappManager = {
    44   __proto__: DOMRequestIpcHelper.prototype,
    46   get _testing() {
    47     try {
    48       return Services.prefs.getBoolPref("browser.webapps.testing");
    49     } catch(ex) {
    50       return false;
    51     }
    52   },
    54   install: function(aMessage, aMessageManager) {
    55     if (this._testing) {
    56       // Go directly to DOM.  Do not download/install APK, do not collect $200.
    57       DOMApplicationRegistry.doInstall(aMessage, aMessageManager);
    58       return;
    59     }
    61     this._installApk(aMessage, aMessageManager);
    62   },
    64   installPackage: function(aMessage, aMessageManager) {
    65     if (this._testing) {
    66       // Go directly to DOM.  Do not download/install APK, do not collect $200.
    67       DOMApplicationRegistry.doInstallPackage(aMessage, aMessageManager);
    68       return;
    69     }
    71     this._installApk(aMessage, aMessageManager);
    72   },
    74   _installApk: function(aMessage, aMessageManager) { return Task.spawn((function*() {
    75     let filePath;
    77     try {
    78       filePath = yield this._downloadApk(aMessage.app.manifestURL);
    79     } catch(ex) {
    80       aMessage.error = ex;
    81       aMessageManager.sendAsyncMessage("Webapps:Install:Return:KO", aMessage);
    82       debug("error downloading APK: " + ex);
    83       return;
    84     }
    86     sendMessageToJava({
    87       type: "Webapps:InstallApk",
    88       filePath: filePath,
    89       data: JSON.stringify(aMessage),
    90     });
    91   }).bind(this)); },
    93   _downloadApk: function(aManifestUrl) {
    94     debug("_downloadApk for " + aManifestUrl);
    95     let deferred = Promise.defer();
    97     // Get the endpoint URL and convert it to an nsIURI/nsIURL object.
    98     const GENERATOR_URL_PREF = "browser.webapps.apkFactoryUrl";
    99     const GENERATOR_URL_BASE = Services.prefs.getCharPref(GENERATOR_URL_PREF);
   100     let generatorUrl = NetUtil.newURI(GENERATOR_URL_BASE).QueryInterface(Ci.nsIURL);
   102     // Populate the query part of the URL with the manifest URL parameter.
   103     let params = {
   104       manifestUrl: aManifestUrl,
   105     };
   106     generatorUrl.query =
   107       [p + "=" + encodeURIComponent(params[p]) for (p in params)].join("&");
   108     debug("downloading APK from " + generatorUrl.spec);
   110     let file = Cc["@mozilla.org/download-manager;1"].
   111                getService(Ci.nsIDownloadManager).
   112                defaultDownloadsDirectory.
   113                clone();
   114     file.append(aManifestUrl.replace(/[^a-zA-Z0-9]/gi, "") + ".apk");
   115     file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
   116     debug("downloading APK to " + file.path);
   118     let worker = new ChromeWorker("resource://gre/modules/WebappManagerWorker.js");
   119     worker.onmessage = function(event) {
   120       let { type, message } = event.data;
   122       worker.terminate();
   124       if (type == "success") {
   125         deferred.resolve(file.path);
   126       } else { // type == "failure"
   127         debug("error downloading APK: " + message);
   128         deferred.reject(message);
   129       }
   130     }
   132     // Trigger the download.
   133     worker.postMessage({ url: generatorUrl.spec, path: file.path });
   135     return deferred.promise;
   136   },
   138   askInstall: function(aData) {
   139     let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
   140     file.initWithPath(aData.profilePath);
   142     // We don't yet support pre-installing an appcache because it isn't clear
   143     // how to do it without degrading the user experience (since users expect
   144     // apps to be available after the system tells them they've been installed,
   145     // which has already happened) and because nsCacheService shuts down
   146     // when we trigger the native install dialog and doesn't re-init itself
   147     // afterward (TODO: file bug about this behavior).
   148     if ("appcache_path" in aData.app.manifest) {
   149       debug("deleting appcache_path from manifest: " + aData.app.manifest.appcache_path);
   150       delete aData.app.manifest.appcache_path;
   151     }
   153     DOMApplicationRegistry.registryReady.then(() => {
   154       DOMApplicationRegistry.confirmInstall(aData, file, (function(aManifest) {
   155         let localeManifest = new ManifestHelper(aManifest, aData.app.origin);
   157         // aData.app.origin may now point to the app: url that hosts this app.
   158         sendMessageToJava({
   159           type: "Webapps:Postinstall",
   160           apkPackageName: aData.app.apkPackageName,
   161           origin: aData.app.origin,
   162         });
   164         this.writeDefaultPrefs(file, localeManifest);
   165       }).bind(this));
   166     });
   167   },
   169   launch: function({ manifestURL, origin }) {
   170     debug("launchWebapp: " + manifestURL);
   172     sendMessageToJava({
   173       type: "Webapps:Open",
   174       manifestURL: manifestURL,
   175       origin: origin
   176     });
   177   },
   179   uninstall: function(aData) {
   180     debug("uninstall: " + aData.manifestURL);
   182     if (this._testing) {
   183       // We don't have to do anything, as the registry does all the work.
   184       return;
   185     }
   187     // TODO: uninstall the APK.
   188   },
   190   autoInstall: function(aData) {
   191     let oldApp = DOMApplicationRegistry.getAppByManifestURL(aData.manifestURL);
   192     if (oldApp) {
   193       // If the app is already installed, update the existing installation.
   194       this._autoUpdate(aData, oldApp);
   195       return;
   196     }
   198     let mm = {
   199       sendAsyncMessage: function (aMessageName, aData) {
   200         // TODO hook this back to Java to report errors.
   201         debug("sendAsyncMessage " + aMessageName + ": " + JSON.stringify(aData));
   202       }
   203     };
   205     let origin = Services.io.newURI(aData.manifestURL, null, null).prePath;
   207     let message = aData.request || {
   208       app: {
   209         origin: origin,
   210         receipts: [],
   211       }
   212     };
   214     if (aData.updateManifest) {
   215       if (aData.zipFilePath) {
   216         aData.updateManifest.package_path = aData.zipFilePath;
   217       }
   218       message.app.updateManifest = aData.updateManifest;
   219     }
   221     // The manifest url may be subtly different between the
   222     // time the APK was built and the APK being installed.
   223     // Thus, we should take the APK as the source of truth.
   224     message.app.manifestURL = aData.manifestURL;
   225     message.app.manifest = aData.manifest;
   226     message.app.apkPackageName = aData.apkPackageName;
   227     message.profilePath = aData.profilePath;
   228     message.mm = mm;
   229     message.apkInstall = true;
   231     DOMApplicationRegistry.registryReady.then(() => {
   232       switch (aData.type) { // can be hosted or packaged.
   233         case "hosted":
   234           DOMApplicationRegistry.doInstall(message, mm);
   235           break;
   237         case "packaged":
   238           message.isPackage = true;
   239           DOMApplicationRegistry.doInstallPackage(message, mm);
   240           break;
   241       }
   242     });
   243   },
   245   _autoUpdate: function(aData, aOldApp) { return Task.spawn((function*() {
   246     debug("_autoUpdate app of type " + aData.type);
   248     if (aOldApp.apkPackageName != aData.apkPackageName) {
   249       // This happens when the app was installed as a shortcut via the old
   250       // runtime and is now being updated to an APK.
   251       debug("update apkPackageName from " + aOldApp.apkPackageName + " to " + aData.apkPackageName);
   252       aOldApp.apkPackageName = aData.apkPackageName;
   253     }
   255     if (aData.type == "hosted") {
   256       let oldManifest = yield DOMApplicationRegistry.getManifestFor(aData.manifestURL);
   257       DOMApplicationRegistry.updateHostedApp(aData, aOldApp.id, aOldApp, oldManifest, aData.manifest);
   258     } else {
   259       DOMApplicationRegistry.updatePackagedApp(aData, aOldApp.id, aOldApp, aData.manifest);
   260     }
   261   }).bind(this)); },
   263   _checkingForUpdates: false,
   265   checkForUpdates: function(userInitiated) { return Task.spawn((function*() {
   266     debug("checkForUpdates");
   268     // Don't start checking for updates if we're already doing so.
   269     // TODO: Consider cancelling the old one and starting a new one anyway
   270     // if the user requested this one.
   271     if (this._checkingForUpdates) {
   272       debug("already checking for updates");
   273       return;
   274     }
   275     this._checkingForUpdates = true;
   277     try {
   278       let installedApps = yield this._getInstalledApps();
   279       if (installedApps.length === 0) {
   280         return;
   281       }
   283       // Map APK names to APK versions.
   284       let apkNameToVersion = yield this._getAPKVersions(installedApps.map(app =>
   285         app.apkPackageName).filter(apkPackageName => !!apkPackageName)
   286       );
   288       // Map manifest URLs to APK versions, which is what the service needs
   289       // in order to tell us which apps are outdated; and also map them to app
   290       // objects, which the downloader/installer uses to download/install APKs.
   291       // XXX Will this cause us to update apps without packages, and if so,
   292       // does that satisfy the legacy migration story?
   293       let manifestUrlToApkVersion = {};
   294       let manifestUrlToApp = {};
   295       for (let app of installedApps) {
   296         manifestUrlToApkVersion[app.manifestURL] = apkNameToVersion[app.apkPackageName] || 0;
   297         manifestUrlToApp[app.manifestURL] = app;
   298       }
   300       let outdatedApps = yield this._getOutdatedApps(manifestUrlToApkVersion, userInitiated);
   302       if (outdatedApps.length === 0) {
   303         // If the user asked us to check for updates, tell 'em we came up empty.
   304         if (userInitiated) {
   305           this._notify({
   306             title: Strings.GetStringFromName("noUpdatesTitle"),
   307             message: Strings.GetStringFromName("noUpdatesMessage"),
   308             icon: "drawable://alert_app",
   309           });
   310         }
   311         return;
   312       }
   314       let names = [manifestUrlToApp[url].name for (url of outdatedApps)].join(", ");
   315       let accepted = yield this._notify({
   316         title: PluralForm.get(outdatedApps.length, Strings.GetStringFromName("downloadUpdateTitle")).
   317                replace("#1", outdatedApps.length),
   318         message: Strings.formatStringFromName("downloadUpdateMessage", [names], 1),
   319         icon: "drawable://alert_app",
   320       }).dismissed;
   322       if (accepted) {
   323         yield this._updateApks([manifestUrlToApp[url] for (url of outdatedApps)]);
   324       }
   325     }
   326     // There isn't a catch block because we want the error to propagate through
   327     // the promise chain, so callers can receive it and choose to respond to it.
   328     finally {
   329       // Ensure we update the _checkingForUpdates flag even if there's an error;
   330       // otherwise the process will get stuck and never check for updates again.
   331       this._checkingForUpdates = false;
   332     }
   333   }).bind(this)); },
   335   _getAPKVersions: function(packageNames) {
   336     let deferred = Promise.defer();
   338     sendMessageToJava({
   339       type: "Webapps:GetApkVersions",
   340       packageNames: packageNames 
   341     }, data => deferred.resolve(data.versions));
   343     return deferred.promise;
   344   },
   346   _getInstalledApps: function() {
   347     let deferred = Promise.defer();
   348     DOMApplicationRegistry.getAll(apps => deferred.resolve(apps));
   349     return deferred.promise;
   350   },
   352   _getOutdatedApps: function(installedApps, userInitiated) {
   353     let deferred = Promise.defer();
   355     let data = JSON.stringify({ installed: installedApps });
   357     let notification;
   358     if (userInitiated) {
   359       notification = this._notify({
   360         title: Strings.GetStringFromName("checkingForUpdatesTitle"),
   361         message: Strings.GetStringFromName("checkingForUpdatesMessage"),
   362         // TODO: replace this with an animated icon.
   363         icon: "drawable://alert_app",
   364         progress: NaN,
   365       });
   366     }
   368     let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
   369                   createInstance(Ci.nsIXMLHttpRequest).
   370                   QueryInterface(Ci.nsIXMLHttpRequestEventTarget);
   371     request.mozBackgroundRequest = true;
   372     request.open("POST", Services.prefs.getCharPref(UPDATE_URL_PREF), true);
   373     request.channel.loadFlags = Ci.nsIChannel.LOAD_ANONYMOUS |
   374                                 Ci.nsIChannel.LOAD_BYPASS_CACHE |
   375                                 Ci.nsIChannel.INHIBIT_CACHING;
   376     request.onload = function() {
   377       if (userInitiated) {
   378         notification.cancel();
   379       }
   380       deferred.resolve(JSON.parse(this.response).outdated);
   381     };
   382     request.onerror = function() {
   383       if (userInitiated) {
   384         notification.cancel();
   385       }
   386       deferred.reject(this.status || this.statusText);
   387     };
   388     request.setRequestHeader("Content-Type", "application/json");
   389     request.setRequestHeader("Content-Length", data.length);
   391     request.send(data);
   393     return deferred.promise;
   394   },
   396   _updateApks: function(aApps) { return Task.spawn((function*() {
   397     // Notify the user that we're in the progress of downloading updates.
   398     let downloadingNames = [app.name for (app of aApps)].join(", ");
   399     let notification = this._notify({
   400       title: PluralForm.get(aApps.length, Strings.GetStringFromName("downloadingUpdateTitle")).
   401              replace("#1", aApps.length),
   402       message: Strings.formatStringFromName("downloadingUpdateMessage", [downloadingNames], 1),
   403       // TODO: replace this with an animated icon.  UpdateService uses
   404       // android.R.drawable.stat_sys_download, but I don't think we can reference
   405       // a system icon with a drawable: URL here, so we'll have to craft our own.
   406       icon: "drawable://alert_download",
   407       // TODO: make this a determinate progress indicator once we can determine
   408       // the sizes of the APKs and observe their progress.
   409       progress: NaN,
   410     });
   412     // Download the APKs for the given apps.  We do this serially to avoid
   413     // saturating the user's network connection.
   414     // TODO: download APKs in parallel (or at least more than one at a time)
   415     // if it seems reasonable.
   416     let downloadedApks = [];
   417     let downloadFailedApps = [];
   418     for (let app of aApps) {
   419       try {
   420         let filePath = yield this._downloadApk(app.manifestURL);
   421         downloadedApks.push({ app: app, filePath: filePath });
   422       } catch(ex) {
   423         downloadFailedApps.push(app);
   424       }
   425     }
   427     notification.cancel();
   429     // Notify the user if any downloads failed, but don't do anything
   430     // when the user accepts/cancels the notification.
   431     // In the future, we might prompt the user to retry the download.
   432     if (downloadFailedApps.length > 0) {
   433       let downloadFailedNames = [app.name for (app of downloadFailedApps)].join(", ");
   434       this._notify({
   435         title: PluralForm.get(downloadFailedApps.length, Strings.GetStringFromName("downloadFailedTitle")).
   436                replace("#1", downloadFailedApps.length),
   437         message: Strings.formatStringFromName("downloadFailedMessage", [downloadFailedNames], 1),
   438         icon: "drawable://alert_app",
   439       });
   440     }
   442     // If we weren't able to download any APKs, then there's nothing more to do.
   443     if (downloadedApks.length === 0) {
   444       return;
   445     }
   447     // Prompt the user to update the apps for which we downloaded APKs, and wait
   448     // until they accept/cancel the notification.
   449     let downloadedNames = [apk.app.name for (apk of downloadedApks)].join(", ");
   450     let accepted = yield this._notify({
   451       title: PluralForm.get(downloadedApks.length, Strings.GetStringFromName("installUpdateTitle")).
   452              replace("#1", downloadedApks.length),
   453       message: Strings.formatStringFromName("installUpdateMessage", [downloadedNames], 1),
   454       icon: "drawable://alert_app",
   455     }).dismissed;
   457     if (accepted) {
   458       // The user accepted the notification, so install the downloaded APKs.
   459       for (let apk of downloadedApks) {
   460         let msg = {
   461           app: apk.app,
   462           // TODO: figure out why Webapps:InstallApk needs the "from" property.
   463           from: apk.app.installOrigin,
   464         };
   465         sendMessageToJava({
   466           type: "Webapps:InstallApk",
   467           filePath: apk.filePath,
   468           data: JSON.stringify(msg),
   469         });
   470       }
   471     } else {
   472       // The user cancelled the notification, so remove the downloaded APKs.
   473       for (let apk of downloadedApks) {
   474         try {
   475           yield OS.file.remove(apk.filePath);
   476         } catch(ex) {
   477           debug("error removing " + apk.filePath + " for cancelled update: " + ex);
   478         }
   479       }
   480     }
   482   }).bind(this)); },
   484   _notify: function(aOptions) {
   485     dump("_notify: " + aOptions.title);
   487     // Resolves to true if the notification is "clicked" (i.e. touched)
   488     // and false if the notification is "cancelled" by swiping it away.
   489     let dismissed = Promise.defer();
   491     // TODO: make notifications expandable so users can expand them to read text
   492     // that gets cut off in standard notifications.
   493     let id = Notifications.create({
   494       title: aOptions.title,
   495       message: aOptions.message,
   496       icon: aOptions.icon,
   497       progress: aOptions.progress,
   498       onClick: function(aId, aCookie) {
   499         dismissed.resolve(true);
   500       },
   501       onCancel: function(aId, aCookie) {
   502         dismissed.resolve(false);
   503       },
   504     });
   506     // Return an object with a promise that resolves when the notification
   507     // is dismissed by the user along with a method for cancelling it,
   508     // so callers who want to wait for user action can do so, while those
   509     // who want to control the notification's lifecycle can do that instead.
   510     return {
   511       dismissed: dismissed.promise,
   512       cancel: function() {
   513         Notifications.cancel(id);
   514       },
   515     };
   516   },
   518   autoUninstall: function(aData) {
   519     DOMApplicationRegistry.registryReady.then(() => {
   520       for (let id in DOMApplicationRegistry.webapps) {
   521         let app = DOMApplicationRegistry.webapps[id];
   522         if (aData.apkPackageNames.indexOf(app.apkPackageName) > -1) {
   523           debug("attempting to uninstall " + app.name);
   524           DOMApplicationRegistry.uninstall(
   525             app.manifestURL,
   526             function() {
   527               debug("success uninstalling " + app.name);
   528             },
   529             function(error) {
   530               debug("error uninstalling " + app.name + ": " + error);
   531             }
   532           );
   533         }
   534       }
   535     });
   536   },
   538   writeDefaultPrefs: function(aProfile, aManifest) {
   539       // build any app specific default prefs
   540       let prefs = [];
   541       if (aManifest.orientation) {
   542         let orientation = aManifest.orientation;
   543         if (Array.isArray(orientation)) {
   544           orientation = orientation.join(",");
   545         }
   546         prefs.push({ name: "app.orientation.default", value: orientation });
   547       }
   549       // write them into the app profile
   550       let defaultPrefsFile = aProfile.clone();
   551       defaultPrefsFile.append(this.DEFAULT_PREFS_FILENAME);
   552       this._writeData(defaultPrefsFile, prefs);
   553   },
   555   _writeData: function(aFile, aPrefs) {
   556     if (aPrefs.length > 0) {
   557       let array = new TextEncoder().encode(JSON.stringify(aPrefs));
   558       OS.File.writeAtomic(aFile.path, array, { tmpPath: aFile.path + ".tmp" }).then(null, function onError(reason) {
   559         debug("Error writing default prefs: " + reason);
   560       });
   561     }
   562   },
   564   DEFAULT_PREFS_FILENAME: "default-prefs.js",
   566 };

mercurial