Bug 790870 - Implement install/update API during installation of hosted apps - Part 2 : hosted apps implementation [r=gwagner]

This commit is contained in:
Fabrice Desré 2012-09-26 18:01:20 -07:00
parent 767bd17711
commit 1e24fe7790
6 changed files with 365 additions and 42 deletions

View File

@ -34,7 +34,13 @@ let AppsUtils = {
removable: aApp.removable,
localId: aApp.localId,
progress: aApp.progress || 0.0,
status: aApp.status || "installed"
installState: aApp.installState || "installed",
downloadAvailable: aApp.downloadAvailable,
downloading: aApp.downloading,
readyToApplyDownload: aApp.readyToApplyDownload,
downloadSize: aApp.downloadSize || 0,
lastUpdateCheck: aApp.lastUpdateCheck,
etag: aApp.etag
};
},

View File

@ -55,7 +55,6 @@ WebappsRegistry.prototype = {
Services.DOMRequest.fireSuccess(req, createApplicationObject(this._window, app));
break;
case "Webapps:Install:Return:KO":
dump("XxXxX Webapps:Install:Return:KO\n");
Services.DOMRequest.fireError(req, msg.error || "DENIED");
break;
case "Webapps:GetSelf:Return:OK":
@ -121,10 +120,12 @@ WebappsRegistry.prototype = {
} else {
let receipts = (aParams && aParams.receipts && Array.isArray(aParams.receipts)) ? aParams.receipts : [];
let categories = (aParams && aParams.categories && Array.isArray(aParams.categories)) ? aParams.categories : [];
let etag = xhr.getResponseHeader("Etag");
cpmm.sendAsyncMessage("Webapps:Install", { app: { installOrigin: installOrigin,
origin: this._getOrigin(aURL),
manifestURL: aURL,
manifest: manifest,
etag: etag,
receipts: receipts,
categories: categories },
from: installURL,
@ -259,6 +260,36 @@ WebappsRegistry.prototype = {
classDescription: "Webapps Registry"})
}
/**
* nsIDOMDOMError object
*/
function createDOMError(aError) {
let error = Cc["@mozilla.org/dom-error;1"]
.createInstance(Ci.nsIDOMDOMError);
error.wrappedJSObject.init(aError);
return error;
}
function DOMError() {
this.wrappedJSObject = this;
}
DOMError.prototype = {
init: function domerror_init(aError) {
this.name = aError;
},
classID: Components.ID("{dcc1d5b7-43d8-4740-9244-b3d8db0f503d}"),
QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMDOMError]),
classInfo: XPCOMUtils.generateCI({classID: Components.ID("{dcc1d5b7-43d8-4740-9244-b3d8db0f503d}"),
contractID: "@mozilla.org/dom-error;1",
interfaces: [Ci.nsIDOMDOMError],
flags: Ci.nsIClassInfo.DOM_OBJECT,
classDescription: "DOMError object"})
}
/**
* mozIDOMApplication object
*/
@ -279,17 +310,35 @@ WebappsApplication.prototype = {
init: function(aWindow, aApp) {
this.origin = aApp.origin;
this.manifest = ObjectWrapper.wrap(aApp.manifest, aWindow);
this.updateManifest = aApp.updateManifest ? ObjectWrapper.wrap(aApp.updateManifest, aWindow)
: null;
this.manifestURL = aApp.manifestURL;
this.receipts = aApp.receipts;
this.installOrigin = aApp.installOrigin;
this.installTime = aApp.installTime;
this.status = "installed";
this.installState = aApp.installState || "installed";
this.removable = aApp.removable;
this.lastUpdateCheck = aApp.lastUpdateCheck ? aApp.lastUpdateCheck
: Date.now();
this.progress = NaN;
this.downloadAvailable = aApp.downloadAvailable;
this.downloading = aApp.downloading;
this.readyToApplyDownload = aApp.readyToApplyDownload;
this.downloadSize = aApp.downloadSize || 0;
this._onprogress = null;
this._ondownloadsuccess = null;
this._ondownloaderror = null;
this._ondownloadavailable = null;
this._ondownloadapplied = null;
this._downloadError = null;
this.initHelper(aWindow, ["Webapps:Uninstall:Return:OK",
"Webapps:Uninstall:Return:KO",
"Webapps:OfflineCache"]);
"Webapps:OfflineCache",
"Webapps:CheckForUpdate:Return:OK",
"Webapps:CheckForUpdate:Return:KO"]);
cpmm.sendAsyncMessage("Webapps:RegisterForMessages",
["Webapps:Uninstall:Return:OK", "Webapps:OfflineCache"]);
},
@ -302,6 +351,60 @@ WebappsApplication.prototype = {
return this._onprogress;
},
set ondownloadsuccess(aCallback) {
this._ondownloadsuccess = aCallback;
},
get ondownloadsuccess() {
return this._ondownloadsuccess;
},
set ondownloaderror(aCallback) {
this._ondownloaderror = aCallback;
},
get ondownloaderror() {
return this._ondownloaderror;
},
set ondownloadavailable(aCallback) {
this._ondownloadavailable = aCallback;
},
get ondownloadavailable() {
return this._ondownloadavailable;
},
set ondownloadapplied(aCallback) {
this._ondownloadapplied = aCallback;
},
get ondownloadapplied() {
return this._ondownloadapplied;
},
get downloadError() {
return createDOMError(this._downloadError);
},
download: function() {
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
},
cancelDownload: function() {
cpmm.sendAsyncMessage("Webapps:CancelDownload",
{ manifestURL: this.manifestURL });
},
checkForUpdate: function() {
let request = this.createRequest();
cpmm.sendAsyncMessage("Webapps:CheckForUpdate",
{ manifestURL: this.manifestURL,
oid: this._id,
requestID: this.getRequestId(request) });
return request;
},
launch: function(aStartPoint) {
let request = this.createRequest();
cpmm.sendAsyncMessage("Webapps:Launch", { origin: this.origin,
@ -334,6 +437,13 @@ WebappsApplication.prototype = {
["Webapps:Uninstall:Return:OK", "Webapps:OfflineCache"]);
},
_fireEvent: function(aName, aHandler) {
if (aHandler) {
let event = new this._window.MozApplicationEvent(aName, { application: this });
aHandler.handleEvent(event);
}
},
receiveMessage: function(aMessage) {
var msg = aMessage.json;
let req = this.takeRequest(msg.requestID);
@ -346,16 +456,41 @@ WebappsApplication.prototype = {
case "Webapps:Uninstall:Return:KO":
Services.DOMRequest.fireError(req, "NOT_INSTALLED");
break;
case "Webapps:Launch:Return:KO":
Services.DOMRequest.fireError(req, "APP_INSTALL_PENDING");
break;
case "Webapps:Uninstall:Return:KO":
Services.DOMRequest.fireError(req, "NOT_INSTALLED");
break;
case "Webapps:OfflineCache":
if (msg.manifest != this.manifestURL)
return;
this.status = msg.status;
if (this._onprogress) {
let event = new this._window.MozApplicationEvent("applicationinstall", { application: this });
this._onprogress.handleEvent(event);
if (installState in msg) {
this.installState = msg.installState;
if (this.installState == "installed") {
this._fireEvent("downloadsuccess", this._ondownloadsuccess);
this._fireEvent("downloadapplied", this._ondownloadapplied);
} else {
this._fireEvent("downloadprogress", this._onprogress);
}
} else if (msg.error) {
this._downloadError = msg.error;
this._fireEvent("downloaderror", this._ondownloaderror);
}
break;
case "Webapps:CheckForUpdate:Return:OK":
for (let prop in msg.app) {
this[prop] = msg.app[prop];
if (msg.event == "downloadapplied") {
Services.DOMRequest.fireSuccess(req, this.manifestURL);
this._fireEvent("downloadapplied", this._ondownloadapplied);
}
}
break;
case "Webapps:CheckForUpdate:Return:KO":
Services.DOMRequest.fireError(req, msg.error);
break;
}
},
@ -398,10 +533,11 @@ function WebappsApplicationMgmt(aWindow) {
WebappsApplicationMgmt.prototype = {
__proto__: DOMRequestIpcHelper.prototype,
__exposedProps__: {
getAll: 'r',
getNotInstalled: 'r',
oninstall: 'rw',
onuninstall: 'rw'
applyDownload: "r",
getAll: "r",
getNotInstalled: "r",
oninstall: "rw",
onuninstall: "rw"
},
uninit: function() {
@ -411,6 +547,10 @@ WebappsApplicationMgmt.prototype = {
["Webapps:Install:Return:OK", "Webapps:Uninstall:Return:OK"]);
},
applyDownload: function(aApp) {
return Cr.NS_ERROR_NOT_IMPLEMENTED;
},
getAll: function() {
let request = this.createRequest();
cpmm.sendAsyncMessage("Webapps:GetAll", { oid: this._id,
@ -496,4 +636,6 @@ WebappsApplicationMgmt.prototype = {
classDescription: "Webapps Application Mgmt"})
}
const NSGetFactory = XPCOMUtils.generateNSGetFactory([WebappsRegistry, WebappsApplication]);
const NSGetFactory = XPCOMUtils.generateNSGetFactory([WebappsRegistry,
WebappsApplication,
DOMError]);

View File

@ -17,6 +17,10 @@ Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import('resource://gre/modules/ActivitiesService.jsm');
Cu.import("resource://gre/modules/AppsUtils.jsm");
function debug(aMsg) {
//dump("-*-*- Webapps.jsm : " + aMsg + "\n");
}
const WEBAPP_RUNTIME = Services.appinfo.ID == "webapprt@mozilla.org";
XPCOMUtils.defineLazyGetter(this, "NetUtil", function() {
@ -61,7 +65,8 @@ let DOMApplicationRegistry = {
"Webapps:Launch", "Webapps:GetAll",
"Webapps:InstallPackage", "Webapps:GetBasePath",
"Webapps:GetList", "Webapps:RegisterForMessages",
"Webapps:UnregisterForMessages"];
"Webapps:UnregisterForMessages",
"Webapps:CancelDownload", "Webapps:CheckForUpdate"];
this.frameMessages = ["Webapps:ClearBrowserData"];
@ -447,7 +452,7 @@ let DOMApplicationRegistry = {
this.uninstall(msg);
break;
case "Webapps:Launch":
Services.obs.notifyObservers(mm, "webapps-launch", JSON.stringify(msg));
this.launchApp(msg, mm);
break;
case "Webapps:IsInstalled":
this.isInstalled(msg, mm);
@ -479,6 +484,12 @@ let DOMApplicationRegistry = {
case "Webapps:GetList":
this.addMessageListener(["Webapps:AddApp", "Webapps:RemoveApp"], mm);
return this.webapps;
case "Webapps:CancelDownload":
this.cancelDowload(msg.manifestURL);
break;
case "Webapps:CheckForUpdate":
this.checkForUpdate(msg, mm);
break;
case "Activities:Register:OK":
this.activitiesRegistered++;
if (this.allActivitiesSent &&
@ -536,6 +547,157 @@ let DOMApplicationRegistry = {
});
},
launchApp: function launchApp(aData, aMm) {
let app = this.getAppByManifestURL(aData.manifestURL);
if (!app) {
aMm.sendAsyncMessage("Webapps:Launch:Return:KO", aData);
return;
}
// Fire an error when trying to launch an app that is not
// yet fully installed.
if (app.installState == "pending") {
aMm.sendAsyncMessage("Webapps:Launch:Return:KO", aData);
return;
}
Services.obs.notifyObservers(aMm, "webapps-launch", JSON.stringify(aData));
},
cancelDownload: function cancelDowload(aManifestURL) {
// We can't cancel appcache dowloads for now.
},
startOfflineCacheDownload: function startOfflineCacheDownload(aManifest, aApp, aProfileDir) {
// if the manifest has an appcache_path property, use it to populate the appcache
if (aManifest.appcache_path) {
let appcacheURI = Services.io.newURI(aManifest.fullAppcachePath(), null, null);
let updateService = Cc["@mozilla.org/offlinecacheupdate-service;1"]
.getService(Ci.nsIOfflineCacheUpdateService);
let docURI = Services.io.newURI(aManifest.fullLaunchPath(), null, null);
let cacheUpdate = aProfileDir ? updateService.scheduleCustomProfileUpdate(appcacheURI, docURI, aProfileDir)
: updateService.scheduleUpdate(appcacheURI, docURI, null);
cacheUpdate.addObserver(new AppcacheObserver(aApp), false);
if (aOfflineCacheObserver) {
cacheUpdate.addObserver(aOfflineCacheObserver, false);
}
}
},
checkForUpdate: function(aData, aMm) {
let app = this.getAppByManifestURL(aData.manifestURL);
if (!app) {
aData.error = "NO_SUCH_APP";
aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", aData);
return;
}
function sendError(aError) {
aData.error = aError;
aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:KO", aData);
}
function updatePackagedApp(aManifest) {
debug("updatePackagedApp");
}
function updateHostedApp(aManifest) {
debug("updateHostedApp");
let id = this._appId(app.origin);
#ifdef MOZ_SYS_MSG
// Update the Web Activities
this._readManifests([{ id: id }], (function unregisterManifest(aResult) {
this._unregisterActivities(aResult[0].manifest, app);
this._registerSystemMessages(aManifest, app);
this._registerActivities(aManifest, app);
}).bind(this));
#endif
// Store the new manifest.
let dir = FileUtils.getDir(DIRECTORY_NAME, ["webapps", id], true, true);
let manFile = dir.clone();
manFile.append("manifest.webapp");
this._writeFile(manFile, JSON.stringify(aManifest), function() { });
let manifest = new DOMApplicationManifest(aManifest, app.origin);
if (manifest.appcache_path) {
app.installState = "updating";
app.downloadAvailable = true;
app.downloading = true;
app.downloadsize = 0;
app.readyToApplyDownload = false;
} else {
app.installState = "installed";
app.downloadAvailable = false;
app.downloading = false;
app.readyToApplyDownload = false;
}
app.name = aManifest.name;
// Update the registry.
this.webapps[id] = app;
this._saveApps((function() {
// XXX Should we fire notifications ?
}).bind(this));
// Preload the appcache if needed.
this.startOfflineCacheDownload(manifest, app);
}
// First, we download the manifest.
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance(Ci.nsIXMLHttpRequest);
xhr.open("GET", aData.manifestURL, true);
if (aData.etag) {
xhr.setRequestHeader("If-None-Match", aData.etag);
}
xhr.addEventListener("load", (function() {
if (xhr.status == 200) {
let manifest;
try {
JSON.parse(xhr.responseText, installOrigin);
} catch(e) {
sendError("MANIFEST_PARSE_ERROR");
return;
}
if (!AppsUtils.checkManifest(manifest, installOrigin)) {
sendError("INVALID_MANIFEST");
} else {
app.etag = xhr.getResponseHeader("Etag");
app.lastCheckedUpdate = Date.now();
if (package_path in manifest) {
updatePackagedApp(manifest);
} else {
updateHostedApp(manifest);
}
}
this._saveApps();
} else if (xhr.status == 304) {
// The manifest has not changed. We just update lastCheckedUpdate.
app.lastCheckedUpdate = Date.now();
aData.event = "downloadapplied";
aData.app = {
lastCheckedUpdate: app.lastCheckedUpdate
}
aMm.sendAsyncMessage("Webapps:CheckForUpdate:Return:OK", aData);
this._saveApps();
} else {
sendError("MANIFEST_URL_ERROR");
}
}).bind(this), false);
xhr.addEventListener("error", (function() {
sendError(request, "NETWORK_ERROR");
}).bind(this), false);
xhr.send(null);
},
denyInstall: function(aData) {
let packageId = aData.app.packageId;
if (packageId) {
@ -575,6 +737,7 @@ let DOMApplicationRegistry = {
let appObject = AppsUtils.cloneAppObject(app);
appObject.appStatus = app.appStatus || Ci.nsIPrincipal.APP_STATUS_INSTALLED;
appObject.installTime = app.installTime = Date.now();
appObject.lastUpdateCheck = app.lastUpdateCheck = Date.now();
let appNote = JSON.stringify(appObject);
appNote.id = id;
@ -599,13 +762,26 @@ let DOMApplicationRegistry = {
}
}
});
this.webapps[id] = appObject;
appObject.status = "installed";
appObject.name = app.manifest.name;
let manifest = new DOMApplicationManifest(app.manifest, app.origin);
if (manifest.appcache_path) {
appObject.installState = "pending";
appObject.downloadAvailable = true;
appObject.downloading = true;
appObject.downloadsize = 0;
appObject.readyToApplyDownload = false;
} else {
appObject.installState = "installed";
appObject.downloadAvailable = false;
appObject.downloading = false;
appObject.readyToApplyDownload = false;
}
appObject.name = app.manifest.name;
this.webapps[id] = appObject;
if (!aFromSync)
this._saveApps((function() {
this.broadcastMessage("Webapps:Install:Return:OK", aData);
@ -618,19 +794,7 @@ let DOMApplicationRegistry = {
this._registerActivities(app.manifest, app);
#endif
// if the manifest has an appcache_path property, use it to populate the appcache
if (manifest.appcache_path) {
let appcacheURI = Services.io.newURI(manifest.fullAppcachePath(), null, null);
let updateService = Cc["@mozilla.org/offlinecacheupdate-service;1"]
.getService(Ci.nsIOfflineCacheUpdateService);
let docURI = Services.io.newURI(manifest.fullLaunchPath(), null, null);
let cacheUpdate = aProfileDir ? updateService.scheduleCustomProfileUpdate(appcacheURI, docURI, aProfileDir)
: updateService.scheduleUpdate(appcacheURI, docURI, null);
cacheUpdate.addObserver(new AppcacheObserver(appObject), false);
if (aOfflineCacheObserver) {
cacheUpdate.addObserver(aOfflineCacheObserver, false);
}
}
this.startOfflineCacheDownload(manifest, appObject, aProfileDir);
},
_nextLocalId: function() {
@ -653,7 +817,7 @@ let DOMApplicationRegistry = {
},
_saveApps: function(aCallback) {
this._writeFile(this.appsFile, JSON.stringify(this.webapps), function() {
this._writeFile(this.appsFile, JSON.stringify(this.webapps, null, 2), function() {
if (aCallback)
aCallback();
});
@ -844,11 +1008,11 @@ let DOMApplicationRegistry = {
let appNote = JSON.stringify(AppsUtils.cloneAppObject(app));
appNote.id = id;
this._readManifests([{ id: id }], (function unregisterManifest(aResult) {
#ifdef MOZ_SYS_MSG
this._readManifests([{ id: id }], (function unregisterManifest(aResult) {
this._unregisterActivities(aResult[0].manifest, app);
#endif
}).bind(this));
#endif
let dir = this._getAppDir(id);
try {
@ -1219,6 +1383,7 @@ let DOMApplicationRegistry = {
*/
let AppcacheObserver = function(aApp) {
this.app = aApp;
this.startStatus = aApp.installState;
};
AppcacheObserver.prototype = {
@ -1228,27 +1393,33 @@ AppcacheObserver.prototype = {
let app = this.app;
let setStatus = function appObs_setStatus(aStatus) {
mustSave = (app.status != aStatus);
app.status = aStatus;
mustSave = (app.installState != aStatus);
app.installState = aStatus;
DOMApplicationRegistry.broadcastMessage("Webapps:OfflineCache",
{ manifest: app.manifestURL,
status: aStatus });
installState: app.installState });
}
let setError = function appObs_setError(aError) {
DOMApplicationRegistry.broadcastMessage("Webapps:OfflineCache",
{ manifest: app.manifestURL,
error: aError });
}
switch (aState) {
case Ci.nsIOfflineCacheUpdateObserver.STATE_ERROR:
aUpdate.removeObserver(this);
setStatus("cache-error");
setError("APP_CACHE_DOWNLOAD_ERROR");
break;
case Ci.nsIOfflineCacheUpdateObserver.STATE_NOUPDATE:
case Ci.nsIOfflineCacheUpdateObserver.STATE_FINISHED:
aUpdate.removeObserver(this);
setStatus("cached");
setStatus("installed");
break;
case Ci.nsIOfflineCacheUpdateObserver.STATE_DOWNLOADING:
case Ci.nsIOfflineCacheUpdateObserver.STATE_ITEMSTARTED:
case Ci.nsIOfflineCacheUpdateObserver.STATE_ITEMPROGRESS:
setStatus("downloading")
setStatus(this.startStatus);
break;
}

View File

@ -5,3 +5,6 @@ category JavaScript-navigator-property mozApps @mozilla.org/webapps;1
component {723ed303-7757-4fb0-b261-4f78b1f6bd22} Webapps.js
contract @mozilla.org/webapps/application;1 {723ed303-7757-4fb0-b261-4f78b1f6bd22}
component {dcc1d5b7-43d8-4740-9244-b3d8db0f503d} Webapps.js
contract @mozilla.org/dom-error;1 {dcc1d5b7-43d8-4740-9244-b3d8db0f503d}

View File

@ -6,7 +6,7 @@
#include "nsISupports.idl"
[scriptable, builtinclass, uuid(e4e28307-d409-4cf7-93cd-6ea8e889f87a)]
[scriptable, uuid(e4e28307-d409-4cf7-93cd-6ea8e889f87a)]
interface nsIDOMDOMError : nsISupports
{
readonly attribute DOMString name;

View File

@ -36,6 +36,7 @@ for (var p in props) {
var mgmtProps = {
QueryInterface: "function",
applyDownload: "function",
getAll: "function",
getNotInstalled: "function",
oninstall: "object",