Wed, 31 Dec 2014 06:09:35 +0100
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 | }; |