From db078bd435b42d687e0bcbee9c9c30c01294cdec Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Fri, 6 Jun 2014 12:10:59 -0700 Subject: [PATCH] Bug 1013433 - complete the packaged app update process; r=mfinkle,marco --- dom/apps/src/Webapps.jsm | 181 ++++++++++++----------- mobile/android/modules/WebappManager.jsm | 88 ++++++++--- 2 files changed, 156 insertions(+), 113 deletions(-) diff --git a/dom/apps/src/Webapps.jsm b/dom/apps/src/Webapps.jsm index 9f80b317871c..40814766fa17 100755 --- a/dom/apps/src/Webapps.jsm +++ b/dom/apps/src/Webapps.jsm @@ -1333,12 +1333,12 @@ this.DOMApplicationRegistry = { let app = this.webapps[id]; if (!app) { debug("startDownload: No app found for " + aManifestURL); - return; + throw new Error("NO_SUCH_APP"); } if (app.downloading) { debug("app is already downloading. Ignoring."); - return; + throw new Error("APP_IS_DOWNLOADING"); } // If the caller is trying to start a download but we have nothing to @@ -1352,7 +1352,7 @@ this.DOMApplicationRegistry = { eventType: "downloaderror", manifestURL: app.manifestURL }); - return; + throw new Error("NO_DOWNLOAD_AVAILABLE"); } // First of all, we check if the download is supposed to update an @@ -1409,7 +1409,7 @@ this.DOMApplicationRegistry = { if (!json) { debug("startDownload: No update manifest found at " + file.path + " " + aManifestURL); - return; + throw new Error("MISSING_UPDATE_MANIFEST"); } let manifest = new ManifestHelper(json, app.manifestURL); @@ -1450,104 +1450,103 @@ this.DOMApplicationRegistry = { } }), - applyDownload: function applyDownload(aManifestURL) { + applyDownload: Task.async(function*(aManifestURL) { debug("applyDownload for " + aManifestURL); let id = this._appIdForManifestURL(aManifestURL); let app = this.webapps[id]; - if (!app || (app && !app.readyToApplyDownload)) { - return; + if (!app) { + throw new Error("NO_SUCH_APP"); + } + if (!app.readyToApplyDownload) { + throw new Error("NOT_READY_TO_APPLY_DOWNLOAD"); } // We need to get the old manifest to unregister web activities. - this.getManifestFor(aManifestURL).then((aOldManifest) => { - // Move the application.zip and manifest.webapp files out of TmpD - let tmpDir = FileUtils.getDir("TmpD", ["webapps", id], true, true); - let manFile = tmpDir.clone(); - manFile.append("manifest.webapp"); - let appFile = tmpDir.clone(); - appFile.append("application.zip"); + let oldManifest = yield this.getManifestFor(aManifestURL); + // Move the application.zip and manifest.webapp files out of TmpD + let tmpDir = FileUtils.getDir("TmpD", ["webapps", id], true, true); + let manFile = tmpDir.clone(); + manFile.append("manifest.webapp"); + let appFile = tmpDir.clone(); + appFile.append("application.zip"); - let dir = FileUtils.getDir(DIRECTORY_NAME, ["webapps", id], true, true); - appFile.moveTo(dir, "application.zip"); - manFile.moveTo(dir, "manifest.webapp"); + let dir = FileUtils.getDir(DIRECTORY_NAME, ["webapps", id], true, true); + appFile.moveTo(dir, "application.zip"); + manFile.moveTo(dir, "manifest.webapp"); - // Move the staged update manifest to a non staged one. - let staged = dir.clone(); - staged.append("staged-update.webapp"); + // Move the staged update manifest to a non staged one. + let staged = dir.clone(); + staged.append("staged-update.webapp"); - // If we are applying after a restarted download, we have no - // staged update manifest. - if (staged.exists()) { - staged.moveTo(dir, "update.webapp"); + // If we are applying after a restarted download, we have no + // staged update manifest. + if (staged.exists()) { + staged.moveTo(dir, "update.webapp"); + } + + try { + tmpDir.remove(true); + } catch(e) { } + + // Clean up the deprecated manifest cache if needed. + if (id in this._manifestCache) { + delete this._manifestCache[id]; + } + + // Flush the zip reader cache to make sure we use the new application.zip + // when re-launching the application. + let zipFile = dir.clone(); + zipFile.append("application.zip"); + Services.obs.notifyObservers(zipFile, "flush-cache-entry", null); + + // Get the manifest, and set properties. + let newManifest = yield this.getManifestFor(aManifestURL); + app.downloading = false; + app.downloadAvailable = false; + app.downloadSize = 0; + app.installState = "installed"; + app.readyToApplyDownload = false; + + // Update the staged properties. + if (app.staged) { + for (let prop in app.staged) { + app[prop] = app.staged[prop]; } + delete app.staged; + } - try { - tmpDir.remove(true); - } catch(e) { } + delete app.retryingDownload; - // Clean up the deprecated manifest cache if needed. - if (id in this._manifestCache) { - delete this._manifestCache[id]; - } + // Update the asm.js scripts we need to compile. + yield ScriptPreloader.preload(app, newManifest); + yield this._saveApps(); + // Update the handlers and permissions for this app. + this.updateAppHandlers(oldManifest, newManifest, app); - // Flush the zip reader cache to make sure we use the new application.zip - // when re-launching the application. - let zipFile = dir.clone(); - zipFile.append("application.zip"); - Services.obs.notifyObservers(zipFile, "flush-cache-entry", null); + let updateManifest = yield AppsUtils.loadJSONAsync(staged.path); + let appObject = AppsUtils.cloneAppObject(app); + appObject.updateManifest = updateManifest; + this.notifyUpdateHandlers(appObject, newManifest, appFile.path); - // Get the manifest, and set properties. - this.getManifestFor(aManifestURL).then((aData) => { - app.downloading = false; - app.downloadAvailable = false; - app.downloadSize = 0; - app.installState = "installed"; - app.readyToApplyDownload = false; - - // Update the staged properties. - if (app.staged) { - for (let prop in app.staged) { - app[prop] = app.staged[prop]; - } - delete app.staged; - } - - delete app.retryingDownload; - - // Update the asm.js scripts we need to compile. - ScriptPreloader.preload(app, aData) - .then(() => this._saveApps()).then(() => { - // Update the handlers and permissions for this app. - this.updateAppHandlers(aOldManifest, aData, app); - - AppsUtils.loadJSONAsync(staged.path).then((aUpdateManifest) => { - let appObject = AppsUtils.cloneAppObject(app); - appObject.updateManifest = aUpdateManifest; - this.notifyUpdateHandlers(appObject, aData, appFile.path); - }); - - if (supportUseCurrentProfile()) { - PermissionsInstaller.installPermissions( - { manifest: aData, - origin: app.origin, - manifestURL: app.manifestURL }, - true); - } - this.updateDataStore(this.webapps[id].localId, app.origin, - app.manifestURL, aData, app.appStatus); - this.broadcastMessage("Webapps:UpdateState", { - app: app, - manifest: aData, - manifestURL: app.manifestURL - }); - this.broadcastMessage("Webapps:FireEvent", { - eventType: "downloadapplied", - manifestURL: app.manifestURL - }); - }); - }); + if (supportUseCurrentProfile()) { + PermissionsInstaller.installPermissions( + { manifest: newManifest, + origin: app.origin, + manifestURL: app.manifestURL }, + true); + } + this.updateDataStore(this.webapps[id].localId, app.origin, + app.manifestURL, newManifest, app.appStatus); + this.broadcastMessage("Webapps:UpdateState", { + app: app, + manifest: newManifest, + manifestURL: app.manifestURL }); - }, + this.broadcastMessage("Webapps:FireEvent", { + eventType: "downloadapplied", + manifestURL: app.manifestURL + }); + }), startOfflineCacheDownload: function(aManifest, aApp, aProfileDir, aIsUpdate) { if (!aManifest.appcache_path) { @@ -2773,9 +2772,11 @@ this.DOMApplicationRegistry = { if (oldPackage) { debug("package's etag or hash unchanged; sending 'applied' event"); // The package's Etag or hash has not changed. - // We send a "applied" event right away. + // We send an "applied" event right away so code awaiting that event + // can proceed to access the app. We also throw an error to alert + // the caller that the package wasn't downloaded. this._sendAppliedEvent(aNewApp, oldApp, id); - return; + throw new Error("PACKAGE_UNCHANGED"); } let newManifest = yield this._openAndReadPackage(zipFile, oldApp, aNewApp, @@ -3488,6 +3489,8 @@ this.DOMApplicationRegistry = { }); }); AppDownloadManager.remove(aNewApp.manifestURL); + + throw aError; }, doUninstall: function(aData, aMm) { diff --git a/mobile/android/modules/WebappManager.jsm b/mobile/android/modules/WebappManager.jsm index 91654e048508..c215187ee799 100644 --- a/mobile/android/modules/WebappManager.jsm +++ b/mobile/android/modules/WebappManager.jsm @@ -158,37 +158,46 @@ this.WebappManager = { return deferred.promise; }, - askInstall: function(aData) { - let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); - file.initWithPath(aData.profilePath); - + _deleteAppcachePath: function(aManifest) { // We don't yet support pre-installing an appcache because it isn't clear // how to do it without degrading the user experience (since users expect // apps to be available after the system tells them they've been installed, // which has already happened) and because nsCacheService shuts down // when we trigger the native install dialog and doesn't re-init itself // afterward (TODO: file bug about this behavior). - if ("appcache_path" in aData.app.manifest) { - debug("deleting appcache_path from manifest: " + aData.app.manifest.appcache_path); - delete aData.app.manifest.appcache_path; + if ("appcache_path" in aManifest) { + debug("deleting appcache_path from manifest: " + aManifest.appcache_path); + delete aManifest.appcache_path; } + }, + + askInstall: function(aData) { + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); + file.initWithPath(aData.profilePath); + + this._deleteAppcachePath(aData.app.manifest); DOMApplicationRegistry.registryReady.then(() => { DOMApplicationRegistry.confirmInstall(aData, file, (function(aManifest) { - let localeManifest = new ManifestHelper(aManifest, aData.app.origin); - - // aData.app.origin may now point to the app: url that hosts this app. - sendMessageToJava({ - type: "Webapps:Postinstall", - apkPackageName: aData.app.apkPackageName, - origin: aData.app.origin, - }); - - this.writeDefaultPrefs(file, localeManifest); + this._postInstall(aData.profilePath, aManifest, aData.app.origin, aData.app.apkPackageName); }).bind(this)); }); }, + _postInstall: function(aProfilePath, aNewManifest, aOrigin, aApkPackageName) { + // aOrigin may now point to the app: url that hosts this app. + sendMessageToJava({ + type: "Webapps:Postinstall", + apkPackageName: aApkPackageName, + origin: aOrigin, + }); + + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); + file.initWithPath(aProfilePath); + let localeManifest = new ManifestHelper(aNewManifest, aOrigin); + this.writeDefaultPrefs(file, localeManifest); + }, + launch: function({ manifestURL, origin }) { debug("launchWebapp: " + manifestURL); @@ -211,11 +220,16 @@ this.WebappManager = { }, autoInstall: function(aData) { - let oldApp = DOMApplicationRegistry.getAppByManifestURL(aData.manifestURL); - if (oldApp) { - // If the app is already installed, update the existing installation. - this._autoUpdate(aData, oldApp); - return; + debug("autoInstall " + aData.manifestURL); + + // If the app is already installed, update the existing installation. + // We should be able to use DOMApplicationRegistry.getAppByManifestURL, + // but it returns a mozIApplication, while _autoUpdate needs the original + // object from DOMApplicationRegistry.webapps in order to modify it. + for (let [ , app] in Iterator(DOMApplicationRegistry.webapps)) { + if (app.manifestURL == aData.manifestURL) { + return this._autoUpdate(aData, app); + } } let mm = { @@ -276,13 +290,39 @@ this.WebappManager = { } if (aData.type == "hosted") { + this._deleteAppcachePath(aData.manifest); let oldManifest = yield DOMApplicationRegistry.getManifestFor(aData.manifestURL); - DOMApplicationRegistry.updateHostedApp(aData, aOldApp.id, aOldApp, oldManifest, aData.manifest); + yield DOMApplicationRegistry.updateHostedApp(aData, aOldApp.id, aOldApp, oldManifest, aData.manifest); } else { - DOMApplicationRegistry.updatePackagedApp(aData, aOldApp.id, aOldApp, aData.manifest); + yield this._autoUpdatePackagedApp(aData, aOldApp); } + + this._postInstall(aData.profilePath, aData.manifest, aOldApp.origin, aOldApp.apkPackageName); }).bind(this)); }, + _autoUpdatePackagedApp: Task.async(function*(aData, aOldApp) { + debug("_autoUpdatePackagedApp: " + aData.manifestURL); + + if (aData.updateManifest && aData.zipFilePath) { + aData.updateManifest.package_path = aData.zipFilePath; + } + + // updatePackagedApp just prepares the update, after which we must + // download the package via the misnamed startDownload and then apply it + // via applyDownload. + yield DOMApplicationRegistry.updatePackagedApp(aData, aOldApp.id, aOldApp, aData.updateManifest); + + try { + yield DOMApplicationRegistry.startDownload(aData.manifestURL); + } catch (ex if ex.message == "PACKAGE_UNCHANGED") { + debug("package unchanged"); + // If the package is unchanged, then there's nothing more to do. + return; + } + + yield DOMApplicationRegistry.applyDownload(aData.manifestURL); + }), + _checkingForUpdates: false, checkForUpdates: function(userInitiated) { return Task.spawn((function*() {