mobile/android/modules/WebappManager.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/mobile/android/modules/WebappManager.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,566 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +"use strict";
     1.9 +
    1.10 +this.EXPORTED_SYMBOLS = ["WebappManager"];
    1.11 +
    1.12 +const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
    1.13 +
    1.14 +const UPDATE_URL_PREF = "browser.webapps.updateCheckUrl";
    1.15 +
    1.16 +Cu.import("resource://gre/modules/AppsUtils.jsm");
    1.17 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.18 +Cu.import("resource://gre/modules/Services.jsm");
    1.19 +Cu.import("resource://gre/modules/NetUtil.jsm");
    1.20 +Cu.import("resource://gre/modules/FileUtils.jsm");
    1.21 +Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
    1.22 +Cu.import("resource://gre/modules/Webapps.jsm");
    1.23 +Cu.import("resource://gre/modules/osfile.jsm");
    1.24 +Cu.import("resource://gre/modules/Promise.jsm");
    1.25 +Cu.import("resource://gre/modules/Task.jsm");
    1.26 +
    1.27 +XPCOMUtils.defineLazyModuleGetter(this, "Notifications", "resource://gre/modules/Notifications.jsm");
    1.28 +XPCOMUtils.defineLazyModuleGetter(this, "sendMessageToJava", "resource://gre/modules/Messaging.jsm");
    1.29 +XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm");
    1.30 +
    1.31 +XPCOMUtils.defineLazyGetter(this, "Strings", function() {
    1.32 +  return Services.strings.createBundle("chrome://browser/locale/webapp.properties");
    1.33 +});
    1.34 +
    1.35 +function debug(aMessage) {
    1.36 +  // We use *dump* instead of Services.console.logStringMessage so the messages
    1.37 +  // have the INFO level of severity instead of the ERROR level.  And we don't
    1.38 +  // append a newline character to the end of the message because *dump* spills
    1.39 +  // into the Android native logging system, which strips newlines from messages
    1.40 +  // and breaks messages into lines automatically at display time (i.e. logcat).
    1.41 +#ifdef DEBUG
    1.42 +  dump(aMessage);
    1.43 +#endif
    1.44 +}
    1.45 +
    1.46 +this.WebappManager = {
    1.47 +  __proto__: DOMRequestIpcHelper.prototype,
    1.48 +
    1.49 +  get _testing() {
    1.50 +    try {
    1.51 +      return Services.prefs.getBoolPref("browser.webapps.testing");
    1.52 +    } catch(ex) {
    1.53 +      return false;
    1.54 +    }
    1.55 +  },
    1.56 +
    1.57 +  install: function(aMessage, aMessageManager) {
    1.58 +    if (this._testing) {
    1.59 +      // Go directly to DOM.  Do not download/install APK, do not collect $200.
    1.60 +      DOMApplicationRegistry.doInstall(aMessage, aMessageManager);
    1.61 +      return;
    1.62 +    }
    1.63 +
    1.64 +    this._installApk(aMessage, aMessageManager);
    1.65 +  },
    1.66 +
    1.67 +  installPackage: function(aMessage, aMessageManager) {
    1.68 +    if (this._testing) {
    1.69 +      // Go directly to DOM.  Do not download/install APK, do not collect $200.
    1.70 +      DOMApplicationRegistry.doInstallPackage(aMessage, aMessageManager);
    1.71 +      return;
    1.72 +    }
    1.73 +
    1.74 +    this._installApk(aMessage, aMessageManager);
    1.75 +  },
    1.76 +
    1.77 +  _installApk: function(aMessage, aMessageManager) { return Task.spawn((function*() {
    1.78 +    let filePath;
    1.79 +
    1.80 +    try {
    1.81 +      filePath = yield this._downloadApk(aMessage.app.manifestURL);
    1.82 +    } catch(ex) {
    1.83 +      aMessage.error = ex;
    1.84 +      aMessageManager.sendAsyncMessage("Webapps:Install:Return:KO", aMessage);
    1.85 +      debug("error downloading APK: " + ex);
    1.86 +      return;
    1.87 +    }
    1.88 +
    1.89 +    sendMessageToJava({
    1.90 +      type: "Webapps:InstallApk",
    1.91 +      filePath: filePath,
    1.92 +      data: JSON.stringify(aMessage),
    1.93 +    });
    1.94 +  }).bind(this)); },
    1.95 +
    1.96 +  _downloadApk: function(aManifestUrl) {
    1.97 +    debug("_downloadApk for " + aManifestUrl);
    1.98 +    let deferred = Promise.defer();
    1.99 +
   1.100 +    // Get the endpoint URL and convert it to an nsIURI/nsIURL object.
   1.101 +    const GENERATOR_URL_PREF = "browser.webapps.apkFactoryUrl";
   1.102 +    const GENERATOR_URL_BASE = Services.prefs.getCharPref(GENERATOR_URL_PREF);
   1.103 +    let generatorUrl = NetUtil.newURI(GENERATOR_URL_BASE).QueryInterface(Ci.nsIURL);
   1.104 +
   1.105 +    // Populate the query part of the URL with the manifest URL parameter.
   1.106 +    let params = {
   1.107 +      manifestUrl: aManifestUrl,
   1.108 +    };
   1.109 +    generatorUrl.query =
   1.110 +      [p + "=" + encodeURIComponent(params[p]) for (p in params)].join("&");
   1.111 +    debug("downloading APK from " + generatorUrl.spec);
   1.112 +
   1.113 +    let file = Cc["@mozilla.org/download-manager;1"].
   1.114 +               getService(Ci.nsIDownloadManager).
   1.115 +               defaultDownloadsDirectory.
   1.116 +               clone();
   1.117 +    file.append(aManifestUrl.replace(/[^a-zA-Z0-9]/gi, "") + ".apk");
   1.118 +    file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
   1.119 +    debug("downloading APK to " + file.path);
   1.120 +
   1.121 +    let worker = new ChromeWorker("resource://gre/modules/WebappManagerWorker.js");
   1.122 +    worker.onmessage = function(event) {
   1.123 +      let { type, message } = event.data;
   1.124 +
   1.125 +      worker.terminate();
   1.126 +
   1.127 +      if (type == "success") {
   1.128 +        deferred.resolve(file.path);
   1.129 +      } else { // type == "failure"
   1.130 +        debug("error downloading APK: " + message);
   1.131 +        deferred.reject(message);
   1.132 +      }
   1.133 +    }
   1.134 +
   1.135 +    // Trigger the download.
   1.136 +    worker.postMessage({ url: generatorUrl.spec, path: file.path });
   1.137 +
   1.138 +    return deferred.promise;
   1.139 +  },
   1.140 +
   1.141 +  askInstall: function(aData) {
   1.142 +    let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
   1.143 +    file.initWithPath(aData.profilePath);
   1.144 +
   1.145 +    // We don't yet support pre-installing an appcache because it isn't clear
   1.146 +    // how to do it without degrading the user experience (since users expect
   1.147 +    // apps to be available after the system tells them they've been installed,
   1.148 +    // which has already happened) and because nsCacheService shuts down
   1.149 +    // when we trigger the native install dialog and doesn't re-init itself
   1.150 +    // afterward (TODO: file bug about this behavior).
   1.151 +    if ("appcache_path" in aData.app.manifest) {
   1.152 +      debug("deleting appcache_path from manifest: " + aData.app.manifest.appcache_path);
   1.153 +      delete aData.app.manifest.appcache_path;
   1.154 +    }
   1.155 +
   1.156 +    DOMApplicationRegistry.registryReady.then(() => {
   1.157 +      DOMApplicationRegistry.confirmInstall(aData, file, (function(aManifest) {
   1.158 +        let localeManifest = new ManifestHelper(aManifest, aData.app.origin);
   1.159 +
   1.160 +        // aData.app.origin may now point to the app: url that hosts this app.
   1.161 +        sendMessageToJava({
   1.162 +          type: "Webapps:Postinstall",
   1.163 +          apkPackageName: aData.app.apkPackageName,
   1.164 +          origin: aData.app.origin,
   1.165 +        });
   1.166 +
   1.167 +        this.writeDefaultPrefs(file, localeManifest);
   1.168 +      }).bind(this));
   1.169 +    });
   1.170 +  },
   1.171 +
   1.172 +  launch: function({ manifestURL, origin }) {
   1.173 +    debug("launchWebapp: " + manifestURL);
   1.174 +
   1.175 +    sendMessageToJava({
   1.176 +      type: "Webapps:Open",
   1.177 +      manifestURL: manifestURL,
   1.178 +      origin: origin
   1.179 +    });
   1.180 +  },
   1.181 +
   1.182 +  uninstall: function(aData) {
   1.183 +    debug("uninstall: " + aData.manifestURL);
   1.184 +
   1.185 +    if (this._testing) {
   1.186 +      // We don't have to do anything, as the registry does all the work.
   1.187 +      return;
   1.188 +    }
   1.189 +
   1.190 +    // TODO: uninstall the APK.
   1.191 +  },
   1.192 +
   1.193 +  autoInstall: function(aData) {
   1.194 +    let oldApp = DOMApplicationRegistry.getAppByManifestURL(aData.manifestURL);
   1.195 +    if (oldApp) {
   1.196 +      // If the app is already installed, update the existing installation.
   1.197 +      this._autoUpdate(aData, oldApp);
   1.198 +      return;
   1.199 +    }
   1.200 +
   1.201 +    let mm = {
   1.202 +      sendAsyncMessage: function (aMessageName, aData) {
   1.203 +        // TODO hook this back to Java to report errors.
   1.204 +        debug("sendAsyncMessage " + aMessageName + ": " + JSON.stringify(aData));
   1.205 +      }
   1.206 +    };
   1.207 +
   1.208 +    let origin = Services.io.newURI(aData.manifestURL, null, null).prePath;
   1.209 +
   1.210 +    let message = aData.request || {
   1.211 +      app: {
   1.212 +        origin: origin,
   1.213 +        receipts: [],
   1.214 +      }
   1.215 +    };
   1.216 +
   1.217 +    if (aData.updateManifest) {
   1.218 +      if (aData.zipFilePath) {
   1.219 +        aData.updateManifest.package_path = aData.zipFilePath;
   1.220 +      }
   1.221 +      message.app.updateManifest = aData.updateManifest;
   1.222 +    }
   1.223 +
   1.224 +    // The manifest url may be subtly different between the
   1.225 +    // time the APK was built and the APK being installed.
   1.226 +    // Thus, we should take the APK as the source of truth.
   1.227 +    message.app.manifestURL = aData.manifestURL;
   1.228 +    message.app.manifest = aData.manifest;
   1.229 +    message.app.apkPackageName = aData.apkPackageName;
   1.230 +    message.profilePath = aData.profilePath;
   1.231 +    message.mm = mm;
   1.232 +    message.apkInstall = true;
   1.233 +
   1.234 +    DOMApplicationRegistry.registryReady.then(() => {
   1.235 +      switch (aData.type) { // can be hosted or packaged.
   1.236 +        case "hosted":
   1.237 +          DOMApplicationRegistry.doInstall(message, mm);
   1.238 +          break;
   1.239 +
   1.240 +        case "packaged":
   1.241 +          message.isPackage = true;
   1.242 +          DOMApplicationRegistry.doInstallPackage(message, mm);
   1.243 +          break;
   1.244 +      }
   1.245 +    });
   1.246 +  },
   1.247 +
   1.248 +  _autoUpdate: function(aData, aOldApp) { return Task.spawn((function*() {
   1.249 +    debug("_autoUpdate app of type " + aData.type);
   1.250 +
   1.251 +    if (aOldApp.apkPackageName != aData.apkPackageName) {
   1.252 +      // This happens when the app was installed as a shortcut via the old
   1.253 +      // runtime and is now being updated to an APK.
   1.254 +      debug("update apkPackageName from " + aOldApp.apkPackageName + " to " + aData.apkPackageName);
   1.255 +      aOldApp.apkPackageName = aData.apkPackageName;
   1.256 +    }
   1.257 +
   1.258 +    if (aData.type == "hosted") {
   1.259 +      let oldManifest = yield DOMApplicationRegistry.getManifestFor(aData.manifestURL);
   1.260 +      DOMApplicationRegistry.updateHostedApp(aData, aOldApp.id, aOldApp, oldManifest, aData.manifest);
   1.261 +    } else {
   1.262 +      DOMApplicationRegistry.updatePackagedApp(aData, aOldApp.id, aOldApp, aData.manifest);
   1.263 +    }
   1.264 +  }).bind(this)); },
   1.265 +
   1.266 +  _checkingForUpdates: false,
   1.267 +
   1.268 +  checkForUpdates: function(userInitiated) { return Task.spawn((function*() {
   1.269 +    debug("checkForUpdates");
   1.270 +
   1.271 +    // Don't start checking for updates if we're already doing so.
   1.272 +    // TODO: Consider cancelling the old one and starting a new one anyway
   1.273 +    // if the user requested this one.
   1.274 +    if (this._checkingForUpdates) {
   1.275 +      debug("already checking for updates");
   1.276 +      return;
   1.277 +    }
   1.278 +    this._checkingForUpdates = true;
   1.279 +
   1.280 +    try {
   1.281 +      let installedApps = yield this._getInstalledApps();
   1.282 +      if (installedApps.length === 0) {
   1.283 +        return;
   1.284 +      }
   1.285 +
   1.286 +      // Map APK names to APK versions.
   1.287 +      let apkNameToVersion = yield this._getAPKVersions(installedApps.map(app =>
   1.288 +        app.apkPackageName).filter(apkPackageName => !!apkPackageName)
   1.289 +      );
   1.290 +
   1.291 +      // Map manifest URLs to APK versions, which is what the service needs
   1.292 +      // in order to tell us which apps are outdated; and also map them to app
   1.293 +      // objects, which the downloader/installer uses to download/install APKs.
   1.294 +      // XXX Will this cause us to update apps without packages, and if so,
   1.295 +      // does that satisfy the legacy migration story?
   1.296 +      let manifestUrlToApkVersion = {};
   1.297 +      let manifestUrlToApp = {};
   1.298 +      for (let app of installedApps) {
   1.299 +        manifestUrlToApkVersion[app.manifestURL] = apkNameToVersion[app.apkPackageName] || 0;
   1.300 +        manifestUrlToApp[app.manifestURL] = app;
   1.301 +      }
   1.302 +
   1.303 +      let outdatedApps = yield this._getOutdatedApps(manifestUrlToApkVersion, userInitiated);
   1.304 +
   1.305 +      if (outdatedApps.length === 0) {
   1.306 +        // If the user asked us to check for updates, tell 'em we came up empty.
   1.307 +        if (userInitiated) {
   1.308 +          this._notify({
   1.309 +            title: Strings.GetStringFromName("noUpdatesTitle"),
   1.310 +            message: Strings.GetStringFromName("noUpdatesMessage"),
   1.311 +            icon: "drawable://alert_app",
   1.312 +          });
   1.313 +        }
   1.314 +        return;
   1.315 +      }
   1.316 +
   1.317 +      let names = [manifestUrlToApp[url].name for (url of outdatedApps)].join(", ");
   1.318 +      let accepted = yield this._notify({
   1.319 +        title: PluralForm.get(outdatedApps.length, Strings.GetStringFromName("downloadUpdateTitle")).
   1.320 +               replace("#1", outdatedApps.length),
   1.321 +        message: Strings.formatStringFromName("downloadUpdateMessage", [names], 1),
   1.322 +        icon: "drawable://alert_app",
   1.323 +      }).dismissed;
   1.324 +
   1.325 +      if (accepted) {
   1.326 +        yield this._updateApks([manifestUrlToApp[url] for (url of outdatedApps)]);
   1.327 +      }
   1.328 +    }
   1.329 +    // There isn't a catch block because we want the error to propagate through
   1.330 +    // the promise chain, so callers can receive it and choose to respond to it.
   1.331 +    finally {
   1.332 +      // Ensure we update the _checkingForUpdates flag even if there's an error;
   1.333 +      // otherwise the process will get stuck and never check for updates again.
   1.334 +      this._checkingForUpdates = false;
   1.335 +    }
   1.336 +  }).bind(this)); },
   1.337 +
   1.338 +  _getAPKVersions: function(packageNames) {
   1.339 +    let deferred = Promise.defer();
   1.340 +
   1.341 +    sendMessageToJava({
   1.342 +      type: "Webapps:GetApkVersions",
   1.343 +      packageNames: packageNames 
   1.344 +    }, data => deferred.resolve(data.versions));
   1.345 +
   1.346 +    return deferred.promise;
   1.347 +  },
   1.348 +
   1.349 +  _getInstalledApps: function() {
   1.350 +    let deferred = Promise.defer();
   1.351 +    DOMApplicationRegistry.getAll(apps => deferred.resolve(apps));
   1.352 +    return deferred.promise;
   1.353 +  },
   1.354 +
   1.355 +  _getOutdatedApps: function(installedApps, userInitiated) {
   1.356 +    let deferred = Promise.defer();
   1.357 +
   1.358 +    let data = JSON.stringify({ installed: installedApps });
   1.359 +
   1.360 +    let notification;
   1.361 +    if (userInitiated) {
   1.362 +      notification = this._notify({
   1.363 +        title: Strings.GetStringFromName("checkingForUpdatesTitle"),
   1.364 +        message: Strings.GetStringFromName("checkingForUpdatesMessage"),
   1.365 +        // TODO: replace this with an animated icon.
   1.366 +        icon: "drawable://alert_app",
   1.367 +        progress: NaN,
   1.368 +      });
   1.369 +    }
   1.370 +
   1.371 +    let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
   1.372 +                  createInstance(Ci.nsIXMLHttpRequest).
   1.373 +                  QueryInterface(Ci.nsIXMLHttpRequestEventTarget);
   1.374 +    request.mozBackgroundRequest = true;
   1.375 +    request.open("POST", Services.prefs.getCharPref(UPDATE_URL_PREF), true);
   1.376 +    request.channel.loadFlags = Ci.nsIChannel.LOAD_ANONYMOUS |
   1.377 +                                Ci.nsIChannel.LOAD_BYPASS_CACHE |
   1.378 +                                Ci.nsIChannel.INHIBIT_CACHING;
   1.379 +    request.onload = function() {
   1.380 +      if (userInitiated) {
   1.381 +        notification.cancel();
   1.382 +      }
   1.383 +      deferred.resolve(JSON.parse(this.response).outdated);
   1.384 +    };
   1.385 +    request.onerror = function() {
   1.386 +      if (userInitiated) {
   1.387 +        notification.cancel();
   1.388 +      }
   1.389 +      deferred.reject(this.status || this.statusText);
   1.390 +    };
   1.391 +    request.setRequestHeader("Content-Type", "application/json");
   1.392 +    request.setRequestHeader("Content-Length", data.length);
   1.393 +
   1.394 +    request.send(data);
   1.395 +
   1.396 +    return deferred.promise;
   1.397 +  },
   1.398 +
   1.399 +  _updateApks: function(aApps) { return Task.spawn((function*() {
   1.400 +    // Notify the user that we're in the progress of downloading updates.
   1.401 +    let downloadingNames = [app.name for (app of aApps)].join(", ");
   1.402 +    let notification = this._notify({
   1.403 +      title: PluralForm.get(aApps.length, Strings.GetStringFromName("downloadingUpdateTitle")).
   1.404 +             replace("#1", aApps.length),
   1.405 +      message: Strings.formatStringFromName("downloadingUpdateMessage", [downloadingNames], 1),
   1.406 +      // TODO: replace this with an animated icon.  UpdateService uses
   1.407 +      // android.R.drawable.stat_sys_download, but I don't think we can reference
   1.408 +      // a system icon with a drawable: URL here, so we'll have to craft our own.
   1.409 +      icon: "drawable://alert_download",
   1.410 +      // TODO: make this a determinate progress indicator once we can determine
   1.411 +      // the sizes of the APKs and observe their progress.
   1.412 +      progress: NaN,
   1.413 +    });
   1.414 +
   1.415 +    // Download the APKs for the given apps.  We do this serially to avoid
   1.416 +    // saturating the user's network connection.
   1.417 +    // TODO: download APKs in parallel (or at least more than one at a time)
   1.418 +    // if it seems reasonable.
   1.419 +    let downloadedApks = [];
   1.420 +    let downloadFailedApps = [];
   1.421 +    for (let app of aApps) {
   1.422 +      try {
   1.423 +        let filePath = yield this._downloadApk(app.manifestURL);
   1.424 +        downloadedApks.push({ app: app, filePath: filePath });
   1.425 +      } catch(ex) {
   1.426 +        downloadFailedApps.push(app);
   1.427 +      }
   1.428 +    }
   1.429 +
   1.430 +    notification.cancel();
   1.431 +
   1.432 +    // Notify the user if any downloads failed, but don't do anything
   1.433 +    // when the user accepts/cancels the notification.
   1.434 +    // In the future, we might prompt the user to retry the download.
   1.435 +    if (downloadFailedApps.length > 0) {
   1.436 +      let downloadFailedNames = [app.name for (app of downloadFailedApps)].join(", ");
   1.437 +      this._notify({
   1.438 +        title: PluralForm.get(downloadFailedApps.length, Strings.GetStringFromName("downloadFailedTitle")).
   1.439 +               replace("#1", downloadFailedApps.length),
   1.440 +        message: Strings.formatStringFromName("downloadFailedMessage", [downloadFailedNames], 1),
   1.441 +        icon: "drawable://alert_app",
   1.442 +      });
   1.443 +    }
   1.444 +
   1.445 +    // If we weren't able to download any APKs, then there's nothing more to do.
   1.446 +    if (downloadedApks.length === 0) {
   1.447 +      return;
   1.448 +    }
   1.449 +
   1.450 +    // Prompt the user to update the apps for which we downloaded APKs, and wait
   1.451 +    // until they accept/cancel the notification.
   1.452 +    let downloadedNames = [apk.app.name for (apk of downloadedApks)].join(", ");
   1.453 +    let accepted = yield this._notify({
   1.454 +      title: PluralForm.get(downloadedApks.length, Strings.GetStringFromName("installUpdateTitle")).
   1.455 +             replace("#1", downloadedApks.length),
   1.456 +      message: Strings.formatStringFromName("installUpdateMessage", [downloadedNames], 1),
   1.457 +      icon: "drawable://alert_app",
   1.458 +    }).dismissed;
   1.459 +
   1.460 +    if (accepted) {
   1.461 +      // The user accepted the notification, so install the downloaded APKs.
   1.462 +      for (let apk of downloadedApks) {
   1.463 +        let msg = {
   1.464 +          app: apk.app,
   1.465 +          // TODO: figure out why Webapps:InstallApk needs the "from" property.
   1.466 +          from: apk.app.installOrigin,
   1.467 +        };
   1.468 +        sendMessageToJava({
   1.469 +          type: "Webapps:InstallApk",
   1.470 +          filePath: apk.filePath,
   1.471 +          data: JSON.stringify(msg),
   1.472 +        });
   1.473 +      }
   1.474 +    } else {
   1.475 +      // The user cancelled the notification, so remove the downloaded APKs.
   1.476 +      for (let apk of downloadedApks) {
   1.477 +        try {
   1.478 +          yield OS.file.remove(apk.filePath);
   1.479 +        } catch(ex) {
   1.480 +          debug("error removing " + apk.filePath + " for cancelled update: " + ex);
   1.481 +        }
   1.482 +      }
   1.483 +    }
   1.484 +
   1.485 +  }).bind(this)); },
   1.486 +
   1.487 +  _notify: function(aOptions) {
   1.488 +    dump("_notify: " + aOptions.title);
   1.489 +
   1.490 +    // Resolves to true if the notification is "clicked" (i.e. touched)
   1.491 +    // and false if the notification is "cancelled" by swiping it away.
   1.492 +    let dismissed = Promise.defer();
   1.493 +
   1.494 +    // TODO: make notifications expandable so users can expand them to read text
   1.495 +    // that gets cut off in standard notifications.
   1.496 +    let id = Notifications.create({
   1.497 +      title: aOptions.title,
   1.498 +      message: aOptions.message,
   1.499 +      icon: aOptions.icon,
   1.500 +      progress: aOptions.progress,
   1.501 +      onClick: function(aId, aCookie) {
   1.502 +        dismissed.resolve(true);
   1.503 +      },
   1.504 +      onCancel: function(aId, aCookie) {
   1.505 +        dismissed.resolve(false);
   1.506 +      },
   1.507 +    });
   1.508 +
   1.509 +    // Return an object with a promise that resolves when the notification
   1.510 +    // is dismissed by the user along with a method for cancelling it,
   1.511 +    // so callers who want to wait for user action can do so, while those
   1.512 +    // who want to control the notification's lifecycle can do that instead.
   1.513 +    return {
   1.514 +      dismissed: dismissed.promise,
   1.515 +      cancel: function() {
   1.516 +        Notifications.cancel(id);
   1.517 +      },
   1.518 +    };
   1.519 +  },
   1.520 +
   1.521 +  autoUninstall: function(aData) {
   1.522 +    DOMApplicationRegistry.registryReady.then(() => {
   1.523 +      for (let id in DOMApplicationRegistry.webapps) {
   1.524 +        let app = DOMApplicationRegistry.webapps[id];
   1.525 +        if (aData.apkPackageNames.indexOf(app.apkPackageName) > -1) {
   1.526 +          debug("attempting to uninstall " + app.name);
   1.527 +          DOMApplicationRegistry.uninstall(
   1.528 +            app.manifestURL,
   1.529 +            function() {
   1.530 +              debug("success uninstalling " + app.name);
   1.531 +            },
   1.532 +            function(error) {
   1.533 +              debug("error uninstalling " + app.name + ": " + error);
   1.534 +            }
   1.535 +          );
   1.536 +        }
   1.537 +      }
   1.538 +    });
   1.539 +  },
   1.540 +
   1.541 +  writeDefaultPrefs: function(aProfile, aManifest) {
   1.542 +      // build any app specific default prefs
   1.543 +      let prefs = [];
   1.544 +      if (aManifest.orientation) {
   1.545 +        let orientation = aManifest.orientation;
   1.546 +        if (Array.isArray(orientation)) {
   1.547 +          orientation = orientation.join(",");
   1.548 +        }
   1.549 +        prefs.push({ name: "app.orientation.default", value: orientation });
   1.550 +      }
   1.551 +
   1.552 +      // write them into the app profile
   1.553 +      let defaultPrefsFile = aProfile.clone();
   1.554 +      defaultPrefsFile.append(this.DEFAULT_PREFS_FILENAME);
   1.555 +      this._writeData(defaultPrefsFile, prefs);
   1.556 +  },
   1.557 +
   1.558 +  _writeData: function(aFile, aPrefs) {
   1.559 +    if (aPrefs.length > 0) {
   1.560 +      let array = new TextEncoder().encode(JSON.stringify(aPrefs));
   1.561 +      OS.File.writeAtomic(aFile.path, array, { tmpPath: aFile.path + ".tmp" }).then(null, function onError(reason) {
   1.562 +        debug("Error writing default prefs: " + reason);
   1.563 +      });
   1.564 +    }
   1.565 +  },
   1.566 +
   1.567 +  DEFAULT_PREFS_FILENAME: "default-prefs.js",
   1.568 +
   1.569 +};

mercurial