summaryrefslogtreecommitdiffstats
path: root/devtools/shared/apps
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/apps')
-rw-r--r--devtools/shared/apps/Devices.jsm53
-rw-r--r--devtools/shared/apps/Simulator.jsm44
-rw-r--r--devtools/shared/apps/app-actor-front.js840
-rw-r--r--devtools/shared/apps/moz.build10
4 files changed, 947 insertions, 0 deletions
diff --git a/devtools/shared/apps/Devices.jsm b/devtools/shared/apps/Devices.jsm
new file mode 100644
index 000000000..d3390b34b
--- /dev/null
+++ b/devtools/shared/apps/Devices.jsm
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+Components.utils.import("resource://devtools/shared/event-emitter.js");
+
+const EXPORTED_SYMBOLS = ["Devices"];
+
+var addonInstalled = false;
+
+const Devices = {
+ _devices: {},
+
+ get helperAddonInstalled() {
+ return addonInstalled;
+ },
+ set helperAddonInstalled(v) {
+ addonInstalled = v;
+ if (!addonInstalled) {
+ for (let name in this._devices) {
+ this.unregister(name);
+ }
+ }
+ this.emit("addon-status-updated", v);
+ },
+
+ register: function (name, device) {
+ this._devices[name] = device;
+ this.emit("register");
+ },
+
+ unregister: function (name) {
+ delete this._devices[name];
+ this.emit("unregister");
+ },
+
+ available: function () {
+ return Object.keys(this._devices).sort();
+ },
+
+ getByName: function (name) {
+ return this._devices[name];
+ }
+};
+Object.defineProperty(this, "Devices", {
+ value: Devices,
+ enumerable: true,
+ writable: false
+});
+
+EventEmitter.decorate(Devices);
diff --git a/devtools/shared/apps/Simulator.jsm b/devtools/shared/apps/Simulator.jsm
new file mode 100644
index 000000000..d7d70842a
--- /dev/null
+++ b/devtools/shared/apps/Simulator.jsm
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+Components.utils.import("resource://devtools/shared/event-emitter.js");
+
+/**
+ * TODO (Bug 1132453) The `Simulator` module is deprecated, and should be
+ * removed once all simulator addons stop using it (see bug 1132452).
+ *
+ * If you want to register, unregister, or otherwise deal with installed
+ * simulators, please use the `Simulators` module defined in:
+ *
+ * devtools/client/webide/modules/simulators.js
+ */
+
+this.EXPORTED_SYMBOLS = ["Simulator"];
+
+let Simulator = this.Simulator = {
+ _simulators: {},
+
+ register: function (name, simulator) {
+ // simulators register themselves as "Firefox OS X.Y"
+ this._simulators[name] = simulator;
+ this.emit("register", name);
+ },
+
+ unregister: function (name) {
+ delete this._simulators[name];
+ this.emit("unregister", name);
+ },
+
+ availableNames: function () {
+ return Object.keys(this._simulators).sort();
+ },
+
+ getByName: function (name) {
+ return this._simulators[name];
+ },
+};
+
+EventEmitter.decorate(Simulator);
diff --git a/devtools/shared/apps/app-actor-front.js b/devtools/shared/apps/app-actor-front.js
new file mode 100644
index 000000000..6cd793679
--- /dev/null
+++ b/devtools/shared/apps/app-actor-front.js
@@ -0,0 +1,840 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {Ci, Cc, Cr} = require("chrome");
+const {OS} = require("resource://gre/modules/osfile.jsm");
+const {FileUtils} = require("resource://gre/modules/FileUtils.jsm");
+const {NetUtil} = require("resource://gre/modules/NetUtil.jsm");
+const promise = require("promise");
+const defer = require("devtools/shared/defer");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+// Bug 1188401: When loaded from xpcshell tests, we do not have browser/ files
+// and can't load target.js. Should be fixed by bug 912121.
+loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
+
+// XXX: bug 912476 make this module a real protocol.js front
+// by converting webapps actor to protocol.js
+
+const PR_USEC_PER_MSEC = 1000;
+const PR_RDWR = 0x04;
+const PR_CREATE_FILE = 0x08;
+const PR_TRUNCATE = 0x20;
+
+const CHUNK_SIZE = 10000;
+
+const appTargets = new Map();
+
+function addDirToZip(writer, dir, basePath) {
+ let files = dir.directoryEntries;
+
+ while (files.hasMoreElements()) {
+ let file = files.getNext().QueryInterface(Ci.nsIFile);
+
+ if (file.isHidden() ||
+ file.isSpecial() ||
+ file.equals(writer.file))
+ {
+ continue;
+ }
+
+ if (file.isDirectory()) {
+ writer.addEntryDirectory(basePath + file.leafName + "/",
+ file.lastModifiedTime * PR_USEC_PER_MSEC,
+ true);
+ addDirToZip(writer, file, basePath + file.leafName + "/");
+ } else {
+ writer.addEntryFile(basePath + file.leafName,
+ Ci.nsIZipWriter.COMPRESSION_DEFAULT,
+ file,
+ true);
+ }
+ }
+}
+
+/**
+ * Convert an XPConnect result code to its name and message.
+ * We have to extract them from an exception per bug 637307 comment 5.
+ */
+function getResultText(code) {
+ let regexp =
+ /^\[Exception... "(.*)" nsresult: "0x[0-9a-fA-F]* \((.*)\)" location: ".*" data: .*\]$/;
+ let ex = Cc["@mozilla.org/js/xpc/Exception;1"].
+ createInstance(Ci.nsIXPCException);
+ ex.initialize(null, code, null, null, null, null);
+ let [, message, name] = regexp.exec(ex.toString());
+ return { name: name, message: message };
+}
+
+function zipDirectory(zipFile, dirToArchive) {
+ let deferred = defer();
+ let writer = Cc["@mozilla.org/zipwriter;1"].createInstance(Ci.nsIZipWriter);
+ writer.open(zipFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE);
+
+ this.addDirToZip(writer, dirToArchive, "");
+
+ writer.processQueue({
+ onStartRequest: function onStartRequest(request, context) {},
+ onStopRequest: (request, context, status) => {
+ if (status == Cr.NS_OK) {
+ writer.close();
+ deferred.resolve(zipFile);
+ }
+ else {
+ let { name, message } = getResultText(status);
+ deferred.reject(name + ": " + message);
+ }
+ }
+ }, null);
+
+ return deferred.promise;
+}
+
+function uploadPackage(client, webappsActor, packageFile, progressCallback) {
+ if (client.traits.bulk) {
+ return uploadPackageBulk(client, webappsActor, packageFile, progressCallback);
+ } else {
+ return uploadPackageJSON(client, webappsActor, packageFile, progressCallback);
+ }
+}
+
+function uploadPackageJSON(client, webappsActor, packageFile, progressCallback) {
+ let deferred = defer();
+
+ let request = {
+ to: webappsActor,
+ type: "uploadPackage"
+ };
+ client.request(request, (res) => {
+ openFile(res.actor);
+ });
+
+ let fileSize;
+ let bytesRead = 0;
+
+ function emitProgress() {
+ progressCallback({
+ bytesSent: bytesRead,
+ totalBytes: fileSize
+ });
+ }
+
+ function openFile(actor) {
+ let openedFile;
+ OS.File.open(packageFile.path)
+ .then(file => {
+ openedFile = file;
+ return openedFile.stat();
+ })
+ .then(fileInfo => {
+ fileSize = fileInfo.size;
+ emitProgress();
+ uploadChunk(actor, openedFile);
+ });
+ }
+ function uploadChunk(actor, file) {
+ file.read(CHUNK_SIZE)
+ .then(function (bytes) {
+ bytesRead += bytes.length;
+ emitProgress();
+ // To work around the fact that JSON.stringify translates the typed
+ // array to object, we are encoding the typed array here into a string
+ let chunk = String.fromCharCode.apply(null, bytes);
+
+ let request = {
+ to: actor,
+ type: "chunk",
+ chunk: chunk
+ };
+ client.request(request, (res) => {
+ if (bytes.length == CHUNK_SIZE) {
+ uploadChunk(actor, file);
+ } else {
+ file.close().then(function () {
+ endsUpload(actor);
+ });
+ }
+ });
+ });
+ }
+ function endsUpload(actor) {
+ let request = {
+ to: actor,
+ type: "done"
+ };
+ client.request(request, (res) => {
+ deferred.resolve(actor);
+ });
+ }
+ return deferred.promise;
+}
+
+function uploadPackageBulk(client, webappsActor, packageFile, progressCallback) {
+ let deferred = defer();
+
+ let request = {
+ to: webappsActor,
+ type: "uploadPackage",
+ bulk: true
+ };
+ client.request(request, (res) => {
+ startBulkUpload(res.actor);
+ });
+
+ function startBulkUpload(actor) {
+ console.log("Starting bulk upload");
+ let fileSize = packageFile.fileSize;
+ console.log("File size: " + fileSize);
+
+ let request = client.startBulkRequest({
+ actor: actor,
+ type: "stream",
+ length: fileSize
+ });
+
+ request.on("bulk-send-ready", ({copyFrom}) => {
+ NetUtil.asyncFetch({
+ uri: NetUtil.newURI(packageFile),
+ loadUsingSystemPrincipal: true
+ }, function (inputStream) {
+ let copying = copyFrom(inputStream);
+ copying.on("progress", (e, progress) => {
+ progressCallback(progress);
+ });
+ copying.then(() => {
+ console.log("Bulk upload done");
+ inputStream.close();
+ deferred.resolve(actor);
+ });
+ });
+ });
+ }
+
+ return deferred.promise;
+}
+
+function removeServerTemporaryFile(client, fileActor) {
+ let request = {
+ to: fileActor,
+ type: "remove"
+ };
+ client.request(request);
+}
+
+/**
+ * progressCallback argument:
+ * Function called as packaged app installation proceeds.
+ * The progress object passed to this function contains:
+ * * bytesSent: The number of bytes sent so far
+ * * totalBytes: The total number of bytes to send
+ */
+function installPackaged(client, webappsActor, packagePath, appId, progressCallback) {
+ let deferred = defer();
+ let file = FileUtils.File(packagePath);
+ let packagePromise;
+ if (file.isDirectory()) {
+ let tmpZipFile = FileUtils.getDir("TmpD", [], true);
+ tmpZipFile.append("application.zip");
+ tmpZipFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
+ packagePromise = zipDirectory(tmpZipFile, file);
+ } else {
+ packagePromise = promise.resolve(file);
+ }
+ packagePromise.then((zipFile) => {
+ uploadPackage(client, webappsActor, zipFile, progressCallback)
+ .then((fileActor) => {
+ let request = {
+ to: webappsActor,
+ type: "install",
+ appId: appId,
+ upload: fileActor
+ };
+ client.request(request, (res) => {
+ // If the install method immediatly fails,
+ // reject immediatly the installPackaged promise.
+ // Otherwise, wait for webappsEvent for completion
+ if (res.error) {
+ deferred.reject(res);
+ }
+ if ("error" in res)
+ deferred.reject({error: res.error, message: res.message});
+ else
+ deferred.resolve({appId: res.appId});
+ });
+ // Ensure deleting the temporary package file, but only if that a temporary
+ // package created when we pass a directory as `packagePath`
+ if (zipFile != file)
+ zipFile.remove(false);
+ // In case of success or error, ensure deleting the temporary package file
+ // also created on the device, but only once install request is done
+ deferred.promise.then(
+ () => removeServerTemporaryFile(client, fileActor),
+ () => removeServerTemporaryFile(client, fileActor));
+ });
+ });
+ return deferred.promise;
+}
+exports.installPackaged = installPackaged;
+
+function installHosted(client, webappsActor, appId, metadata, manifest) {
+ let deferred = defer();
+ let request = {
+ to: webappsActor,
+ type: "install",
+ appId: appId,
+ metadata: metadata,
+ manifest: manifest
+ };
+ client.request(request, (res) => {
+ if (res.error) {
+ deferred.reject(res);
+ }
+ if ("error" in res)
+ deferred.reject({error: res.error, message: res.message});
+ else
+ deferred.resolve({appId: res.appId});
+ });
+ return deferred.promise;
+}
+exports.installHosted = installHosted;
+
+function getTargetForApp(client, webappsActor, manifestURL) {
+ // Ensure always returning the exact same JS object for a target
+ // of the same app in order to show only one toolbox per app and
+ // avoid re-creating lot of objects twice.
+ let existingTarget = appTargets.get(manifestURL);
+ if (existingTarget)
+ return promise.resolve(existingTarget);
+
+ let deferred = defer();
+ let request = {
+ to: webappsActor,
+ type: "getAppActor",
+ manifestURL: manifestURL,
+ };
+ client.request(request, (res) => {
+ if (res.error) {
+ deferred.reject(res.error);
+ } else {
+ let options = {
+ form: res.actor,
+ client: client,
+ chrome: false
+ };
+
+ TargetFactory.forRemoteTab(options).then((target) => {
+ target.isApp = true;
+ appTargets.set(manifestURL, target);
+ target.on("close", () => {
+ appTargets.delete(manifestURL);
+ });
+ deferred.resolve(target);
+ }, (error) => {
+ deferred.reject(error);
+ });
+ }
+ });
+ return deferred.promise;
+}
+exports.getTargetForApp = getTargetForApp;
+
+function reloadApp(client, webappsActor, manifestURL) {
+ return getTargetForApp(client,
+ webappsActor,
+ manifestURL).
+ then((target) => {
+ // Request the ContentActor to reload the app
+ let request = {
+ to: target.form.actor,
+ type: "reload",
+ options: {
+ force: true
+ },
+ manifestURL: manifestURL
+ };
+ return client.request(request);
+ }, () => {
+ throw new Error("Not running");
+ });
+}
+exports.reloadApp = reloadApp;
+
+function launchApp(client, webappsActor, manifestURL) {
+ return client.request({
+ to: webappsActor,
+ type: "launch",
+ manifestURL: manifestURL
+ });
+}
+exports.launchApp = launchApp;
+
+function closeApp(client, webappsActor, manifestURL) {
+ return client.request({
+ to: webappsActor,
+ type: "close",
+ manifestURL: manifestURL
+ });
+}
+exports.closeApp = closeApp;
+
+function getTarget(client, form) {
+ let deferred = defer();
+ let options = {
+ form: form,
+ client: client,
+ chrome: false
+ };
+
+ TargetFactory.forRemoteTab(options).then((target) => {
+ target.isApp = true;
+ deferred.resolve(target);
+ }, (error) => {
+ deferred.reject(error);
+ });
+ return deferred.promise;
+}
+
+/**
+ * `App` instances are client helpers to manage a given app
+ * and its the tab actors
+ */
+function App(client, webappsActor, manifest) {
+ this.client = client;
+ this.webappsActor = webappsActor;
+ this.manifest = manifest;
+
+ // This attribute is managed by the AppActorFront
+ this.running = false;
+
+ this.iconURL = null;
+}
+
+App.prototype = {
+ getForm: function () {
+ if (this._form) {
+ return promise.resolve(this._form);
+ }
+ let request = {
+ to: this.webappsActor,
+ type: "getAppActor",
+ manifestURL: this.manifest.manifestURL
+ };
+ return this.client.request(request)
+ .then(res => {
+ return this._form = res.actor;
+ });
+ },
+
+ getTarget: function () {
+ if (this._target) {
+ return promise.resolve(this._target);
+ }
+ return this.getForm().
+ then((form) => getTarget(this.client, form)).
+ then((target) => {
+ target.on("close", () => {
+ delete this._form;
+ delete this._target;
+ });
+ return this._target = target;
+ });
+ },
+
+ launch: function () {
+ return launchApp(this.client, this.webappsActor,
+ this.manifest.manifestURL);
+ },
+
+ reload: function () {
+ return reloadApp(this.client, this.webappsActor,
+ this.manifest.manifestURL);
+ },
+
+ close: function () {
+ return closeApp(this.client, this.webappsActor,
+ this.manifest.manifestURL);
+ },
+
+ getIcon: function () {
+ if (this.iconURL) {
+ return promise.resolve(this.iconURL);
+ }
+
+ let deferred = defer();
+
+ let request = {
+ to: this.webappsActor,
+ type: "getIconAsDataURL",
+ manifestURL: this.manifest.manifestURL
+ };
+
+ this.client.request(request, res => {
+ if (res.error) {
+ deferred.reject(res.message || res.error);
+ } else if (res.url) {
+ this.iconURL = res.url;
+ deferred.resolve(res.url);
+ } else {
+ deferred.reject("Unable to fetch app icon");
+ }
+ });
+
+ return deferred.promise;
+ }
+};
+
+
+/**
+ * `AppActorFront` is a client for the webapps actor.
+ */
+function AppActorFront(client, form) {
+ this.client = client;
+ this.actor = form.webappsActor;
+
+ this._clientListener = this._clientListener.bind(this);
+ this._onInstallProgress = this._onInstallProgress.bind(this);
+
+ this._listeners = [];
+ EventEmitter.decorate(this);
+}
+
+AppActorFront.prototype = {
+ /**
+ * List `App` instances for all currently running apps.
+ */
+ get runningApps() {
+ if (!this._apps) {
+ throw new Error("Can't get running apps before calling watchApps.");
+ }
+ let r = new Map();
+ for (let [manifestURL, app] of this._apps) {
+ if (app.running) {
+ r.set(manifestURL, app);
+ }
+ }
+ return r;
+ },
+
+ /**
+ * List `App` instances for all installed apps.
+ */
+ get apps() {
+ if (!this._apps) {
+ throw new Error("Can't get apps before calling watchApps.");
+ }
+ return this._apps;
+ },
+
+ /**
+ * Returns a `App` object instance for the given manifest URL
+ * (and cache it per AppActorFront object)
+ */
+ _getApp: function (manifestURL) {
+ let app = this._apps ? this._apps.get(manifestURL) : null;
+ if (app) {
+ return promise.resolve(app);
+ } else {
+ let request = {
+ to: this.actor,
+ type: "getApp",
+ manifestURL: manifestURL
+ };
+ return this.client.request(request)
+ .then(res => {
+ let app = new App(this.client, this.actor, res.app);
+ if (this._apps) {
+ this._apps.set(manifestURL, app);
+ }
+ return app;
+ }, e => {
+ console.error("Unable to retrieve app", manifestURL, e);
+ });
+ }
+ },
+
+ /**
+ * Starts watching for app opening/closing installing/uninstalling.
+ * Needs to be called before using `apps` or `runningApps` attributes.
+ */
+ watchApps: function (listener) {
+ // Fixes race between two references to the same front
+ // calling watchApps at the same time
+ if (this._loadingPromise) {
+ return this._loadingPromise;
+ }
+
+ // Only call watchApps for the first listener being register,
+ // for all next ones, just send fake appOpen events for already
+ // opened apps
+ if (this._apps) {
+ this.runningApps.forEach((app, manifestURL) => {
+ listener("appOpen", app);
+ });
+ return promise.resolve();
+ }
+
+ // First retrieve all installed apps and create
+ // related `App` object for each
+ let request = {
+ to: this.actor,
+ type: "getAll"
+ };
+ return this._loadingPromise = this.client.request(request)
+ .then(res => {
+ delete this._loadingPromise;
+ this._apps = new Map();
+ for (let a of res.apps) {
+ let app = new App(this.client, this.actor, a);
+ this._apps.set(a.manifestURL, app);
+ }
+ })
+ .then(() => {
+ // Then retrieve all running apps in order to flag them as running
+ let request = {
+ to: this.actor,
+ type: "listRunningApps"
+ };
+ return this.client.request(request)
+ .then(res => res.apps);
+ })
+ .then(apps => {
+ let promises = apps.map(manifestURL => {
+ // _getApp creates `App` instance and register it to AppActorFront
+ return this._getApp(manifestURL)
+ .then(app => {
+ app.running = true;
+ // Fake appOpen event for all already opened
+ this._notifyListeners("appOpen", app);
+ });
+ });
+ return promise.all(promises);
+ })
+ .then(() => {
+ // Finally ask to receive all app events
+ return this._listenAppEvents(listener);
+ });
+ },
+
+ fetchIcons: function () {
+ // On demand, retrieve apps icons in order to be able
+ // to synchronously retrieve it on `App` objects
+ let promises = [];
+ for (let [manifestURL, app] of this._apps) {
+ promises.push(app.getIcon());
+ }
+
+ return DevToolsUtils.settleAll(promises)
+ .then(null, () => {});
+ },
+
+ _listenAppEvents: function (listener) {
+ this._listeners.push(listener);
+
+ if (this._listeners.length > 1) {
+ return promise.resolve();
+ }
+
+ let client = this.client;
+ let f = this._clientListener;
+ client.addListener("appOpen", f);
+ client.addListener("appClose", f);
+ client.addListener("appInstall", f);
+ client.addListener("appUninstall", f);
+
+ let request = {
+ to: this.actor,
+ type: "watchApps"
+ };
+ return this.client.request(request);
+ },
+
+ _unlistenAppEvents: function (listener) {
+ let idx = this._listeners.indexOf(listener);
+ if (idx != -1) {
+ this._listeners.splice(idx, 1);
+ }
+
+ // Until we released all listener, we don't ask to stop sending events
+ if (this._listeners.length != 0) {
+ return promise.resolve();
+ }
+
+ let client = this.client;
+ let f = this._clientListener;
+ client.removeListener("appOpen", f);
+ client.removeListener("appClose", f);
+ client.removeListener("appInstall", f);
+ client.removeListener("appUninstall", f);
+
+ // Remove `_apps` in order to allow calling watchApps again
+ // and repopulate the apps Map.
+ delete this._apps;
+
+ let request = {
+ to: this.actor,
+ type: "unwatchApps"
+ };
+ return this.client.request(request);
+ },
+
+ _clientListener: function (type, message) {
+ let { manifestURL } = message;
+
+ // Reset the app object to get a fresh copy when we (re)install the app.
+ if (type == "appInstall" && this._apps && this._apps.has(manifestURL)) {
+ this._apps.delete(manifestURL);
+ }
+
+ this._getApp(manifestURL).then((app) => {
+ switch (type) {
+ case "appOpen":
+ app.running = true;
+ this._notifyListeners("appOpen", app);
+ break;
+ case "appClose":
+ app.running = false;
+ this._notifyListeners("appClose", app);
+ break;
+ case "appInstall":
+ // The call to _getApp is going to create App object
+
+ // This app may have been running while being installed, so check the list
+ // of running apps again to get the right answer.
+ let request = {
+ to: this.actor,
+ type: "listRunningApps"
+ };
+ this.client.request(request)
+ .then(res => {
+ if (res.apps.indexOf(manifestURL) !== -1) {
+ app.running = true;
+ this._notifyListeners("appInstall", app);
+ this._notifyListeners("appOpen", app);
+ } else {
+ this._notifyListeners("appInstall", app);
+ }
+ });
+ break;
+ case "appUninstall":
+ // Fake a appClose event if we didn't got one before uninstall
+ if (app.running) {
+ app.running = false;
+ this._notifyListeners("appClose", app);
+ }
+ this._apps.delete(manifestURL);
+ this._notifyListeners("appUninstall", app);
+ break;
+ default:
+ return;
+ }
+ });
+ },
+
+ _notifyListeners: function (type, app) {
+ this._listeners.forEach(f => {
+ f(type, app);
+ });
+ },
+
+ unwatchApps: function (listener) {
+ return this._unlistenAppEvents(listener);
+ },
+
+ /*
+ * Install a packaged app.
+ *
+ * Events are going to be emitted on the front
+ * as install progresses. Events will have the following fields:
+ * * bytesSent: The number of bytes sent so far
+ * * totalBytes: The total number of bytes to send
+ */
+ installPackaged: function (packagePath, appId) {
+ let request = () => {
+ return installPackaged(this.client, this.actor, packagePath, appId,
+ this._onInstallProgress)
+ .then(response => ({
+ appId: response.appId,
+ manifestURL: "app://" + response.appId + "/manifest.webapp"
+ }));
+ };
+ return this._install(request);
+ },
+
+ _onInstallProgress: function (progress) {
+ this.emit("install-progress", progress);
+ },
+
+ _install: function (request) {
+ let deferred = defer();
+ let finalAppId = null, manifestURL = null;
+ let installs = {};
+
+ // We need to resolve only once the request is done *AND*
+ // once we receive the related appInstall message for
+ // the same manifestURL
+ let resolve = app => {
+ this._unlistenAppEvents(listener);
+ installs = null;
+ deferred.resolve({ app: app, appId: finalAppId });
+ };
+
+ // Listen for appInstall event, in order to resolve with
+ // the matching app object.
+ let listener = (type, app) => {
+ if (type == "appInstall") {
+ // Resolves immediately if the request has already resolved
+ // or just flag the installed app to eventually resolve
+ // when the request gets its response.
+ if (app.manifest.manifestURL === manifestURL) {
+ resolve(app);
+ } else {
+ installs[app.manifest.manifestURL] = app;
+ }
+ }
+ };
+ this._listenAppEvents(listener)
+ // Execute the request
+ .then(request)
+ .then(response => {
+ finalAppId = response.appId;
+ manifestURL = response.manifestURL;
+
+ // Resolves immediately if the appInstall event
+ // was dispatched during the request.
+ if (manifestURL in installs) {
+ resolve(installs[manifestURL]);
+ }
+ }, deferred.reject);
+
+ return deferred.promise;
+
+ },
+
+ /*
+ * Install a hosted app.
+ *
+ * Events are going to be emitted on the front
+ * as install progresses. Events will have the following fields:
+ * * bytesSent: The number of bytes sent so far
+ * * totalBytes: The total number of bytes to send
+ */
+ installHosted: function (appId, metadata, manifest) {
+ let manifestURL = metadata.manifestURL ||
+ metadata.origin + "/manifest.webapp";
+ let request = () => {
+ return installHosted(this.client, this.actor, appId, metadata,
+ manifest)
+ .then(response => ({
+ appId: response.appId,
+ manifestURL: manifestURL
+ }));
+ };
+ return this._install(request);
+ }
+};
+
+exports.AppActorFront = AppActorFront;
diff --git a/devtools/shared/apps/moz.build b/devtools/shared/apps/moz.build
new file mode 100644
index 000000000..8ab1a9eb9
--- /dev/null
+++ b/devtools/shared/apps/moz.build
@@ -0,0 +1,10 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ 'app-actor-front.js',
+ 'Devices.jsm',
+ 'Simulator.jsm'
+)