michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cu = Components.utils; michael@0: const Cr = Components.results; michael@0: Cu.import("resource:///modules/devtools/gDevTools.jsm"); michael@0: const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); michael@0: const {require} = devtools; michael@0: const {ConnectionManager, Connection} = require("devtools/client/connection-manager"); michael@0: const {AppProjects} = require("devtools/app-manager/app-projects"); michael@0: const {AppValidator} = require("devtools/app-manager/app-validator"); michael@0: const {Services} = Cu.import("resource://gre/modules/Services.jsm"); michael@0: const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm"); michael@0: const {installHosted, installPackaged, getTargetForApp, michael@0: reloadApp, launchApp, closeApp} = require("devtools/app-actor-front"); michael@0: const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js"); michael@0: michael@0: const promise = require("devtools/toolkit/deprecated-sync-thenables"); michael@0: michael@0: const MANIFEST_EDITOR_ENABLED = "devtools.appmanager.manifestEditor.enabled"; michael@0: michael@0: window.addEventListener("message", function(event) { michael@0: try { michael@0: let json = JSON.parse(event.data); michael@0: if (json.name == "connection") { michael@0: let cid = parseInt(json.cid); michael@0: for (let c of ConnectionManager.connections) { michael@0: if (c.uid == cid) { michael@0: UI.connection = c; michael@0: UI.onNewConnection(); michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: } catch(e) {} michael@0: }); michael@0: michael@0: window.addEventListener("unload", function onUnload() { michael@0: window.removeEventListener("unload", onUnload); michael@0: UI.destroy(); michael@0: }); michael@0: michael@0: let UI = { michael@0: isReady: false, michael@0: michael@0: onload: function() { michael@0: if (Services.prefs.getBoolPref(MANIFEST_EDITOR_ENABLED)) { michael@0: document.querySelector("#lense").setAttribute("manifest-editable", ""); michael@0: } michael@0: michael@0: this.template = new Template(document.body, AppProjects.store, Utils.l10n); michael@0: this.template.start(); michael@0: michael@0: AppProjects.load().then(() => { michael@0: AppProjects.store.object.projects.forEach(UI.validate); michael@0: this.isReady = true; michael@0: this.emit("ready"); michael@0: }); michael@0: }, michael@0: michael@0: destroy: function() { michael@0: if (this.connection) { michael@0: this.connection.off(Connection.Events.STATUS_CHANGED, this._onConnectionStatusChange); michael@0: } michael@0: this.template.destroy(); michael@0: }, michael@0: michael@0: onNewConnection: function() { michael@0: this.connection.on(Connection.Events.STATUS_CHANGED, this._onConnectionStatusChange); michael@0: this._onConnectionStatusChange(); michael@0: }, michael@0: michael@0: _onConnectionStatusChange: function() { michael@0: if (this.connection.status != Connection.Status.CONNECTED) { michael@0: document.body.classList.remove("connected"); michael@0: this.listTabsResponse = null; michael@0: } else { michael@0: document.body.classList.add("connected"); michael@0: this.connection.client.listTabs( michael@0: response => {this.listTabsResponse = response} michael@0: ); michael@0: } michael@0: }, michael@0: michael@0: get connected() { return !!this.listTabsResponse; }, michael@0: michael@0: _selectFolder: function() { michael@0: let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); michael@0: fp.init(window, Utils.l10n("project.filePickerTitle"), Ci.nsIFilePicker.modeGetFolder); michael@0: let res = fp.show(); michael@0: if (res != Ci.nsIFilePicker.returnCancel) michael@0: return fp.file; michael@0: return null; michael@0: }, michael@0: michael@0: addPackaged: function(folder) { michael@0: if (!folder) { michael@0: folder = this._selectFolder(); michael@0: } michael@0: if (!folder) michael@0: return; michael@0: return AppProjects.addPackaged(folder) michael@0: .then(function (project) { michael@0: UI.validate(project); michael@0: UI.selectProject(project.location); michael@0: }); michael@0: }, michael@0: michael@0: addHosted: function() { michael@0: let form = document.querySelector("#new-hosted-project-wrapper"); michael@0: if (!form.checkValidity()) michael@0: return; michael@0: michael@0: let urlInput = document.querySelector("#url-input"); michael@0: let manifestURL = urlInput.value; michael@0: return AppProjects.addHosted(manifestURL) michael@0: .then(function (project) { michael@0: UI.validate(project); michael@0: UI.selectProject(project.location); michael@0: }); michael@0: }, michael@0: michael@0: _getLocalIconURL: function(project, manifest) { michael@0: let icon; michael@0: if (manifest.icons) { michael@0: let size = Object.keys(manifest.icons).sort(function(a, b) b - a)[0]; michael@0: if (size) { michael@0: icon = manifest.icons[size]; michael@0: } michael@0: } michael@0: if (!icon) michael@0: return "chrome://browser/skin/devtools/app-manager/default-app-icon.png"; michael@0: if (project.type == "hosted") { michael@0: let manifestURL = Services.io.newURI(project.location, null, null); michael@0: let origin = Services.io.newURI(manifestURL.prePath, null, null); michael@0: return Services.io.newURI(icon, null, origin).spec; michael@0: } else if (project.type == "packaged") { michael@0: let projectFolder = FileUtils.File(project.location); michael@0: let folderURI = Services.io.newFileURI(projectFolder).spec; michael@0: return folderURI + icon.replace(/^\/|\\/, ""); michael@0: } michael@0: }, michael@0: michael@0: validate: function(project) { michael@0: let validation = new AppValidator(project); michael@0: return validation.validate() michael@0: .then(function () { michael@0: if (validation.manifest) { michael@0: project.icon = UI._getLocalIconURL(project, validation.manifest); michael@0: project.manifest = validation.manifest; michael@0: } michael@0: michael@0: project.validationStatus = "valid"; michael@0: michael@0: if (validation.warnings.length > 0) { michael@0: project.warningsCount = validation.warnings.length; michael@0: project.warnings = validation.warnings.join(",\n "); michael@0: project.validationStatus = "warning"; michael@0: } else { michael@0: project.warnings = ""; michael@0: project.warningsCount = 0; michael@0: } michael@0: michael@0: if (validation.errors.length > 0) { michael@0: project.errorsCount = validation.errors.length; michael@0: project.errors = validation.errors.join(",\n "); michael@0: project.validationStatus = "error"; michael@0: } else { michael@0: project.errors = ""; michael@0: project.errorsCount = 0; michael@0: } michael@0: michael@0: if (project.warningsCount && project.errorsCount) { michael@0: project.validationStatus = "error warning"; michael@0: } michael@0: michael@0: }); michael@0: michael@0: }, michael@0: michael@0: update: function(button, location) { michael@0: button.disabled = true; michael@0: let project = AppProjects.get(location); michael@0: michael@0: // Update the manifest editor view, in case the manifest was modified michael@0: // outside of the app manager. This can happen in parallel with the other michael@0: // steps. michael@0: this._showManifestEditor(project); michael@0: michael@0: this.validate(project) michael@0: .then(() => { michael@0: // Install the app to the device if we are connected, michael@0: // and there is no error michael@0: if (project.errorsCount == 0 && this.connected) { michael@0: return this.install(project); michael@0: } michael@0: }) michael@0: .then(() => { michael@0: button.disabled = false; michael@0: // Finally try to reload the app if it is already opened michael@0: if (this.connected) { michael@0: this.reload(project); michael@0: } michael@0: }, michael@0: (res) => { michael@0: button.disabled = false; michael@0: let message = res.error + ": " + res.message; michael@0: alert(message); michael@0: this.connection.log(message); michael@0: }); michael@0: }, michael@0: michael@0: saveManifest: function(button) { michael@0: button.disabled = true; michael@0: this.manifestEditor.save().then(() => button.disabled = false); michael@0: }, michael@0: michael@0: reload: function (project) { michael@0: if (!this.connected) { michael@0: return promise.reject(); michael@0: } michael@0: return reloadApp(this.connection.client, michael@0: this.listTabsResponse.webappsActor, michael@0: this._getProjectManifestURL(project)). michael@0: then(() => { michael@0: this.connection.log("App reloaded"); michael@0: }); michael@0: }, michael@0: michael@0: remove: function(location, event) { michael@0: if (event) { michael@0: // We don't want the "click" event to be propagated to the project item. michael@0: // That would trigger `selectProject()`. michael@0: event.stopPropagation(); michael@0: } michael@0: michael@0: let item = document.getElementById(location); michael@0: michael@0: let toSelect = document.querySelector(".project-item.selected"); michael@0: toSelect = toSelect ? toSelect.id : ""; michael@0: michael@0: if (toSelect == location) { michael@0: toSelect = null; michael@0: let sibling; michael@0: if (item.previousElementSibling) { michael@0: sibling = item.previousElementSibling; michael@0: } else { michael@0: sibling = item.nextElementSibling; michael@0: } michael@0: if (sibling && !!AppProjects.get(sibling.id)) { michael@0: toSelect = sibling.id; michael@0: } michael@0: } michael@0: michael@0: AppProjects.remove(location).then(() => { michael@0: this.selectProject(toSelect); michael@0: }); michael@0: }, michael@0: michael@0: _getProjectManifestURL: function (project) { michael@0: if (project.type == "packaged") { michael@0: return "app://" + project.packagedAppOrigin + "/manifest.webapp"; michael@0: } else if (project.type == "hosted") { michael@0: return project.location; michael@0: } michael@0: }, michael@0: michael@0: install: function(project) { michael@0: if (!this.connected) { michael@0: return promise.reject(); michael@0: } michael@0: this.connection.log("Installing the " + project.manifest.name + " app..."); michael@0: let installPromise; michael@0: if (project.type == "packaged") { michael@0: installPromise = installPackaged(this.connection.client, this.listTabsResponse.webappsActor, project.location, project.packagedAppOrigin) michael@0: .then(({ appId }) => { michael@0: // If the packaged app specified a custom origin override, michael@0: // we need to update the local project origin michael@0: project.packagedAppOrigin = appId; michael@0: // And ensure the indexed db on disk is also updated michael@0: AppProjects.update(project); michael@0: }); michael@0: } else { michael@0: let manifestURLObject = Services.io.newURI(project.location, null, null); michael@0: let origin = Services.io.newURI(manifestURLObject.prePath, null, null); michael@0: let appId = origin.host; michael@0: let metadata = { michael@0: origin: origin.spec, michael@0: manifestURL: project.location michael@0: }; michael@0: installPromise = installHosted(this.connection.client, this.listTabsResponse.webappsActor, appId, metadata, project.manifest); michael@0: } michael@0: michael@0: installPromise.then(() => { michael@0: this.connection.log("Install completed."); michael@0: }, () => { michael@0: this.connection.log("Install failed."); michael@0: }); michael@0: michael@0: return installPromise; michael@0: }, michael@0: michael@0: start: function(project) { michael@0: if (!this.connected) { michael@0: return promise.reject(); michael@0: } michael@0: let manifestURL = this._getProjectManifestURL(project); michael@0: return launchApp(this.connection.client, michael@0: this.listTabsResponse.webappsActor, michael@0: manifestURL); michael@0: }, michael@0: michael@0: stop: function(location) { michael@0: if (!this.connected) { michael@0: return promise.reject(); michael@0: } michael@0: let project = AppProjects.get(location); michael@0: let manifestURL = this._getProjectManifestURL(project); michael@0: return closeApp(this.connection.client, michael@0: this.listTabsResponse.webappsActor, michael@0: manifestURL); michael@0: }, michael@0: michael@0: debug: function(button, location) { michael@0: if (!this.connected) { michael@0: return promise.reject(); michael@0: } michael@0: button.disabled = true; michael@0: let project = AppProjects.get(location); michael@0: michael@0: let onFailedToStart = (error) => { michael@0: // If not installed, install and open it michael@0: if (error == "NO_SUCH_APP") { michael@0: return this.install(project); michael@0: } else { michael@0: throw error; michael@0: } michael@0: }; michael@0: let onStarted = () => { michael@0: // Once we asked the app to launch, the app isn't necessary completely loaded. michael@0: // launch request only ask the app to launch and immediatly returns. michael@0: // We have to keep trying to get app tab actors required to create its target. michael@0: let deferred = promise.defer(); michael@0: let loop = (count) => { michael@0: // Ensure not looping for ever michael@0: if (count >= 100) { michael@0: deferred.reject("Unable to connect to the app"); michael@0: return; michael@0: } michael@0: // Also, in case the app wasn't installed yet, we also have to keep asking the michael@0: // app to launch, as launch request made right after install may race. michael@0: this.start(project); michael@0: getTargetForApp( michael@0: this.connection.client, michael@0: this.listTabsResponse.webappsActor, michael@0: this._getProjectManifestURL(project)). michael@0: then(deferred.resolve, michael@0: (err) => { michael@0: if (err == "appNotFound") michael@0: setTimeout(loop, 500, count + 1); michael@0: else michael@0: deferred.reject(err); michael@0: }); michael@0: }; michael@0: loop(0); michael@0: return deferred.promise; michael@0: }; michael@0: michael@0: // First try to open the app michael@0: this.start(project) michael@0: .then(null, onFailedToStart) michael@0: .then(onStarted) michael@0: .then((target) => michael@0: top.UI.openAndShowToolboxForTarget(target, michael@0: project.manifest.name, michael@0: project.icon)) michael@0: .then(() => { michael@0: // And only when the toolbox is opened, release the button michael@0: button.disabled = false; michael@0: }, michael@0: (err) => { michael@0: button.disabled = false; michael@0: let message = err.error ? err.error + ": " + err.message : String(err); michael@0: alert(message); michael@0: this.connection.log(message); michael@0: }); michael@0: }, michael@0: michael@0: reveal: function(location) { michael@0: let project = AppProjects.get(location); michael@0: if (project.type == "packaged") { michael@0: let projectFolder = FileUtils.File(project.location); michael@0: projectFolder.reveal(); michael@0: } else { michael@0: // TODO: eventually open hosted apps in firefox michael@0: // when permissions are correctly supported by firefox michael@0: } michael@0: }, michael@0: michael@0: selectProject: function(location) { michael@0: let projects = AppProjects.store.object.projects; michael@0: let idx = 0; michael@0: for (; idx < projects.length; idx++) { michael@0: if (projects[idx].location == location) { michael@0: break; michael@0: } michael@0: } michael@0: michael@0: let oldButton = document.querySelector(".project-item.selected"); michael@0: if (oldButton) { michael@0: oldButton.classList.remove("selected"); michael@0: } michael@0: michael@0: if (idx == projects.length) { michael@0: // Not found. Empty lense. michael@0: let lense = document.querySelector("#lense"); michael@0: lense.setAttribute("template-for", '{"path":"","childSelector":""}'); michael@0: this.template._processFor(lense); michael@0: return; michael@0: } michael@0: michael@0: let button = document.getElementById(location); michael@0: button.classList.add("selected"); michael@0: michael@0: let template = '{"path":"projects.' + idx + '","childSelector":"#lense-template"}'; michael@0: michael@0: let lense = document.querySelector("#lense"); michael@0: lense.setAttribute("template-for", template); michael@0: this.template._processFor(lense); michael@0: michael@0: let project = projects[idx]; michael@0: this._showManifestEditor(project).then(() => this.emit("project-selected")); michael@0: }, michael@0: michael@0: _showManifestEditor: function(project) { michael@0: if (this.manifestEditor) { michael@0: this.manifestEditor.destroy(); michael@0: } michael@0: let editorContainer = document.querySelector("#lense .manifest-editor"); michael@0: this.manifestEditor = new ManifestEditor(project); michael@0: return this.manifestEditor.show(editorContainer); michael@0: } michael@0: }; michael@0: michael@0: // This must be bound immediately, as it might be used via the message listener michael@0: // before UI.onload() has been called. michael@0: UI._onConnectionStatusChange = UI._onConnectionStatusChange.bind(UI); michael@0: michael@0: EventEmitter.decorate(UI);