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.

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

mercurial