Bug 1069552 - Add WebIDE scanner / runtime API. r=paul
authorJ. Ryan Stinnett <jryans@gmail.com>
Thu, 23 Oct 2014 08:24:00 +0200
changeset 212137 dfff6e9b01f66e643a5feef745750ba60b05bc58
parent 212136 63decad50695bc398e49dac1243b9e107681bff7
child 212138 805c0b62d31853c33caf5d666487444e45922ba8
push id27698
push usercbook@mozilla.com
push dateFri, 24 Oct 2014 13:53:50 +0000
treeherdermozilla-central@6e35802ae3e2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspaul
bugs1069552
milestone36.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1069552 - Add WebIDE scanner / runtime API. r=paul
browser/devtools/webide/content/monitor.js
browser/devtools/webide/content/prefs.xhtml
browser/devtools/webide/content/runtimedetails.js
browser/devtools/webide/content/webide.js
browser/devtools/webide/content/webide.xul
browser/devtools/webide/modules/app-manager.js
browser/devtools/webide/modules/runtimes.js
browser/devtools/webide/test/browser_tabs.js
browser/devtools/webide/test/head.js
browser/devtools/webide/test/test_autoconnect_runtime.html
browser/devtools/webide/test/test_deviceinfo.html
browser/devtools/webide/test/test_runtime.html
browser/devtools/webide/test/test_telemetry.html
browser/devtools/webide/themes/webide.css
browser/devtools/webide/webide-prefs.js
browser/locales/en-US/chrome/browser/devtools/webide.dtd
toolkit/components/telemetry/Histograms.json
--- a/browser/devtools/webide/content/monitor.js
+++ b/browser/devtools/webide/content/monitor.js
@@ -1,17 +1,16 @@
 /* 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/. */
 
 const Cu = Components.utils;
 Cu.import('resource:///modules/devtools/gDevTools.jsm');
 const {require} = Cu.import('resource://gre/modules/devtools/Loader.jsm', {}).devtools;
 const {Services} = Cu.import('resource://gre/modules/Services.jsm');
-const {Devices} = Cu.import("resource://gre/modules/devtools/Devices.jsm");
 const {AppManager} = require('devtools/webide/app-manager');
 const {AppActorFront} = require('devtools/app-actor-front');
 const {Connection} = require('devtools/client/connection-manager');
 const EventEmitter = require('devtools/toolkit/event-emitter');
 
 window.addEventListener('load', function onLoad() {
   window.removeEventListener('load', onLoad);
   window.addEventListener('resize', Monitor.resize);
@@ -222,18 +221,17 @@ let Monitor = {
    * benefits over bug 1037465's multi-process USSAgent approach, notably:
    * - Works for older Firefox OS devices (pre-2.1),
    * - Doesn't need certified-apps debugging,
    * - Polling time is synchronized for all processes.
    * TODO: After bug 1043324 lands, consider removing this hack.
    */
   pollB2GInfo: function() {
     if (AppManager.selectedRuntime) {
-      let id = AppManager.selectedRuntime.id;
-      let device = Devices.getByName(id);
+      let device = AppManager.selectedRuntime.device;
       if (device && device.shell) {
         device.shell('b2g-info').then(s => {
           let lines = s.split('\n');
           let line = '';
 
           // Find the header row to locate NAME and USS, looks like:
           // '      NAME PID NICE  USS  PSS  RSS VSIZE OOM_ADJ USER '.
           while (line.indexOf('NAME') < 0) {
--- a/browser/devtools/webide/content/prefs.xhtml
+++ b/browser/devtools/webide/content/prefs.xhtml
@@ -31,22 +31,16 @@
     <ul>
       <li>
         <label title="&prefs_options_templatesurl_tooltip;">
           <span>&prefs_options_templatesurl;</span>
           <input data-pref="devtools.webide.templatesURL"/>
         </label>
       </li>
       <li>
-        <label title="&prefs_options_enablelocalruntime_tooltip;">
-          <input type="checkbox" data-pref="devtools.webide.enableLocalRuntime"/>
-          <span>&prefs_options_enablelocalruntime;</span>
-        </label>
-      </li>
-      <li>
         <label title="&prefs_options_rememberlastproject_tooltip;">
           <input type="checkbox" data-pref="devtools.webide.restoreLastProject"/>
           <span>&prefs_options_rememberlastproject;</span>
         </label>
       </li>
       <li>
         <label title="&prefs_options_showeditor_tooltip;">
           <input type="checkbox" data-pref="devtools.webide.showProjectEditor"/>
--- a/browser/devtools/webide/content/runtimedetails.js
+++ b/browser/devtools/webide/content/runtimedetails.js
@@ -2,18 +2,17 @@
  * 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/. */
 
 const Cu = Components.utils;
 const {Services} = Cu.import("resource://gre/modules/Services.jsm");
 const {require} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
 const {AppManager} = require("devtools/webide/app-manager");
 const {Connection} = require("devtools/client/connection-manager");
-const {Devices} = Cu.import("resource://gre/modules/devtools/Devices.jsm");
-const {USBRuntime} = require("devtools/webide/runtimes");
+const {RuntimeTypes} = require("devtools/webide/runtimes");
 const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties");
 
 window.addEventListener("load", function onLoad() {
   window.removeEventListener("load", onLoad);
   document.querySelector("#close").onclick = CloseUI;
   document.querySelector("#certified-check button").onclick = EnableCertApps;
   document.querySelector("#adb-check button").onclick = RootADB;
   AppManager.on("app-manager-update", OnAppManagerUpdate);
@@ -81,18 +80,18 @@ function CheckLockState() {
 
   adbCheckResult.textContent = sUnknown;
   certCheckResult.textContent = sUnknown;
 
   if (AppManager.connection &&
       AppManager.connection.status == Connection.Status.CONNECTED) {
 
     // ADB check
-    if (AppManager.selectedRuntime instanceof USBRuntime) {
-      let device = Devices.getByName(AppManager.selectedRuntime.id);
+    if (AppManager.selectedRuntime.type === RuntimeTypes.USB) {
+      let device = AppManager.selectedRuntime.device;
       if (device && device.summonRoot) {
         device.isRoot().then(isRoot => {
           if (isRoot) {
             adbCheckResult.textContent = sYes;
             flipCertPerfButton.removeAttribute("disabled");
           } else {
             adbCheckResult.textContent = sNo;
             adbRootAction.removeAttribute("hidden");
@@ -122,21 +121,21 @@ function CheckLockState() {
       flipCertPerfAction.removeAttribute("hidden");
     }
 
   }
 
 }
 
 function EnableCertApps() {
-  let device = Devices.getByName(AppManager.selectedRuntime.id);
+  let device = AppManager.selectedRuntime.device;
   device.shell(
     "stop b2g && " +
     "cd /data/b2g/mozilla/*.default/ && " +
     "echo 'user_pref(\"devtools.debugger.forbid-certified-apps\", false);' >> prefs.js && " +
     "start b2g"
-  )
+  );
 }
 
 function RootADB() {
-  let device = Devices.getByName(AppManager.selectedRuntime.id);
+  let device = AppManager.selectedRuntime.device;
   device.summonRoot().then(CheckLockState, (e) => console.error(e));
 }
--- a/browser/devtools/webide/content/webide.js
+++ b/browser/devtools/webide/content/webide.js
@@ -17,16 +17,17 @@ const {Connection} = require("devtools/c
 const {AppManager} = require("devtools/webide/app-manager");
 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
 const ProjectEditor = require("projecteditor/projecteditor");
 const {Devices} = Cu.import("resource://gre/modules/devtools/Devices.jsm");
 const {GetAvailableAddons} = require("devtools/webide/addons");
 const {GetTemplatesJSON, GetAddonsJSON} = require("devtools/webide/remote-resources");
 const utils = require("devtools/webide/utils");
 const Telemetry = require("devtools/shared/telemetry");
+const {RuntimeScanners, WiFiScanner} = require("devtools/webide/runtimes");
 
 const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties");
 
 const HTML = "http://www.w3.org/1999/xhtml";
 const HELP_URL = "https://developer.mozilla.org/docs/Tools/WebIDE/Troubleshooting";
 
 // download template index early
 GetTemplatesJSON(true);
@@ -143,17 +144,20 @@ let UI = {
           UI.saveLastSelectedProject();
         });
         return;
       case "project-is-not-running":
       case "project-is-running":
       case "list-tabs-response":
         this.updateCommands();
         break;
-      case "runtime":
+      case "runtime-details":
+        this.updateRuntimeButton();
+        break;
+      case "runtime-changed":
         this.updateRuntimeButton();
         this.saveLastConnectedRuntime();
         break;
       case "project-validated":
         this.updateTitle();
         this.updateCommands();
         this.updateProjectButton();
         this.updateProjectEditorHeader();
@@ -300,56 +304,58 @@ let UI = {
   dismissErrorNotification: function() {
     let nbox = document.querySelector("#notificationbox");
     nbox.removeAllNotifications(true);
   },
 
   /********** RUNTIME **********/
 
   updateRuntimeList: function() {
-    let wifiHeaderNode = document.querySelector("#runtime-header-wifi-devices");
-    if (AppManager.isWiFiScanningEnabled) {
+    let wifiHeaderNode = document.querySelector("#runtime-header-wifi");
+    if (WiFiScanner.allowed) {
       wifiHeaderNode.removeAttribute("hidden");
     } else {
       wifiHeaderNode.setAttribute("hidden", "true");
     }
 
-    let USBListNode = document.querySelector("#runtime-panel-usbruntime");
-    let WiFiListNode = document.querySelector("#runtime-panel-wifi-devices");
-    let simulatorListNode = document.querySelector("#runtime-panel-simulators");
-    let customListNode = document.querySelector("#runtime-panel-custom");
+    let usbListNode = document.querySelector("#runtime-panel-usb");
+    let wifiListNode = document.querySelector("#runtime-panel-wifi");
+    let simulatorListNode = document.querySelector("#runtime-panel-simulator");
+    let otherListNode = document.querySelector("#runtime-panel-other");
 
     let noHelperNode = document.querySelector("#runtime-panel-noadbhelper");
     let noUSBNode = document.querySelector("#runtime-panel-nousbdevice");
 
     if (Devices.helperAddonInstalled) {
       noHelperNode.setAttribute("hidden", "true");
     } else {
       noHelperNode.removeAttribute("hidden");
     }
 
-    if (AppManager.runtimeList.usb.length == 0 && Devices.helperAddonInstalled) {
+    let runtimeList = AppManager.runtimeList;
+
+    if (runtimeList.usb.length === 0 && Devices.helperAddonInstalled) {
       noUSBNode.removeAttribute("hidden");
     } else {
       noUSBNode.setAttribute("hidden", "true");
     }
 
     for (let [type, parent] of [
-      ["usb", USBListNode],
-      ["wifi", WiFiListNode],
+      ["usb", usbListNode],
+      ["wifi", wifiListNode],
       ["simulator", simulatorListNode],
-      ["custom", customListNode],
+      ["other", otherListNode],
     ]) {
       while (parent.hasChildNodes()) {
         parent.firstChild.remove();
       }
-      for (let runtime of AppManager.runtimeList[type]) {
+      for (let runtime of runtimeList[type]) {
         let panelItemNode = document.createElement("toolbarbutton");
         panelItemNode.className = "panel-item runtime-panel-item-" + type;
-        panelItemNode.setAttribute("label", runtime.getName());
+        panelItemNode.setAttribute("label", runtime.name);
         parent.appendChild(panelItemNode);
         let r = runtime;
         panelItemNode.addEventListener("click", () => {
           this.hidePanels();
           this.dismissErrorNotification();
           this.connectToRuntime(r);
         }, true);
       }
@@ -361,55 +367,56 @@ let UI = {
     // if available and has an ID
     if (AppManager.selectedRuntime || !this.lastConnectedRuntime) {
       return;
     }
     let [_, type, id] = this.lastConnectedRuntime.match(/^(\w+):(.+)$/);
 
     type = type.toLowerCase();
 
-    // Local connection is mapped to AppManager.runtimeList.custom array
+    // Local connection is mapped to AppManager.runtimeList.other array
     if (type == "local") {
-      type = "custom";
+      type = "other";
     }
 
     // We support most runtimes except simulator, that needs to be manually
     // launched
-    if (type == "usb" || type == "wifi" || type == "custom") {
+    if (type == "usb" || type == "wifi" || type == "other") {
       for (let runtime of AppManager.runtimeList[type]) {
-        // Some runtimes do not expose getID function and don't support
-        // autoconnect (like remote connection)
-        if (typeof(runtime.getID) == "function" && runtime.getID() == id) {
+        // Some runtimes do not expose an id and don't support autoconnect (like
+        // remote connection)
+        if (runtime.id == id) {
           this.connectToRuntime(runtime);
         }
       }
     }
   },
 
   connectToRuntime: function(runtime) {
-    let name = runtime.getName();
+    let name = runtime.name;
     let promise = AppManager.connectToRuntime(runtime);
     promise.then(() => this.initConnectionTelemetry());
-    return this.busyUntil(promise, "connecting to runtime");
+    return this.busyUntil(promise, "connecting to runtime " + name);
   },
 
   updateRuntimeButton: function() {
     let labelNode = document.querySelector("#runtime-panel-button > .panel-button-label");
     if (!AppManager.selectedRuntime) {
       labelNode.setAttribute("value", Strings.GetStringFromName("runtimeButton_label"));
     } else {
-      let name = AppManager.selectedRuntime.getName();
+      let name = AppManager.selectedRuntime.name;
       labelNode.setAttribute("value", name);
     }
   },
 
   saveLastConnectedRuntime: function () {
     if (AppManager.selectedRuntime &&
-        typeof(AppManager.selectedRuntime.getID) === "function") {
-      this.lastConnectedRuntime = AppManager.selectedRuntime.type + ":" + AppManager.selectedRuntime.getID();
+        AppManager.selectedRuntime.id !== undefined) {
+      this.lastConnectedRuntime = AppManager.selectedRuntime.type + ":" +
+                                  AppManager.selectedRuntime.id;
     } else {
       this.lastConnectedRuntime = "";
     }
     Services.prefs.setCharPref("devtools.webide.lastConnectedRuntime",
                                this.lastConnectedRuntime);
   },
 
   _actionsToLog: new Set(),
@@ -1120,17 +1127,17 @@ let Cmds = {
           location: tab.url,
           name: tab.name
         };
       }, true);
     }
   },
 
   showRuntimePanel: function() {
-    AppManager.scanForWiFiRuntimes();
+    RuntimeScanners.scan();
 
     let panel = document.querySelector("#runtime-panel");
     let anchor = document.querySelector("#runtime-panel-button > .panel-button-anchor");
 
     let deferred = promise.defer();
     function onPopupShown() {
       panel.removeEventListener("popupshown", onPopupShown);
       deferred.resolve();
--- a/browser/devtools/webide/content/webide.xul
+++ b/browser/devtools/webide/content/webide.xul
@@ -146,27 +146,27 @@
         <label class="panel-header" id="panel-header-tabs" hidden="true">&projectPanel_tabs;</label>
         <vbox flex="1" id="project-panel-tabs"/>
       </vbox>
     </panel>
 
     <!-- Runtime panel -->
     <panel id="runtime-panel" type="arrow" position="bottomcenter topright" consumeoutsideclicks="true" animate="false">
       <vbox flex="1">
-        <label class="panel-header">&runtimePanel_USBDevices;</label>
+        <label class="panel-header">&runtimePanel_usb;</label>
         <toolbarbutton class="panel-item" label="&runtimePanel_nousbdevice;" id="runtime-panel-nousbdevice" command="cmd_showTroubleShooting"/>
         <toolbarbutton class="panel-item" label="&runtimePanel_noadbhelper;" id="runtime-panel-noadbhelper" command="cmd_showAddons"/>
-        <vbox id="runtime-panel-usbruntime"></vbox>
-        <label class="panel-header" id="runtime-header-wifi-devices">&runtimePanel_WiFiDevices;</label>
-        <vbox id="runtime-panel-wifi-devices"></vbox>
-        <label class="panel-header">&runtimePanel_simulators;</label>
-        <vbox id="runtime-panel-simulators"></vbox>
+        <vbox id="runtime-panel-usb"></vbox>
+        <label class="panel-header" id="runtime-header-wifi">&runtimePanel_wifi;</label>
+        <vbox id="runtime-panel-wifi"></vbox>
+        <label class="panel-header">&runtimePanel_simulator;</label>
+        <vbox id="runtime-panel-simulator"></vbox>
         <toolbarbutton class="panel-item" label="&runtimePanel_installsimulator;" id="runtime-panel-installsimulator" command="cmd_showAddons"/>
-        <label class="panel-header">&runtimePanel_custom;</label>
-        <vbox id="runtime-panel-custom"></vbox>
+        <label class="panel-header">&runtimePanel_other;</label>
+        <vbox id="runtime-panel-other"></vbox>
         <vbox flex="1" id="runtime-actions">
           <toolbarbutton class="panel-item" id="runtime-details" command="cmd_showRuntimeDetails"/>
           <toolbarbutton class="panel-item" id="runtime-permissions" command="cmd_showPermissionsTable"/>
           <toolbarbutton class="panel-item" id="runtime-screenshot"  command="cmd_takeScreenshot"/>
           <toolbarbutton class="panel-item" id="runtime-disconnect"  command="cmd_disconnectRuntime"/>
         </vbox>
       </vbox>
     </panel>
--- a/browser/devtools/webide/modules/app-manager.js
+++ b/browser/devtools/webide/modules/app-manager.js
@@ -2,108 +2,78 @@
  * 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/. */
 
 const {Cu} = require("chrome");
 
 let { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
 
 const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
-const {Devices} = Cu.import("resource://gre/modules/devtools/Devices.jsm");
 const {Services} = Cu.import("resource://gre/modules/Services.jsm");
 const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm");
-const {Simulator} = Cu.import("resource://gre/modules/devtools/Simulator.jsm");
-const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js");
+const EventEmitter = require("devtools/toolkit/event-emitter");
 const {TextEncoder, OS}  = Cu.import("resource://gre/modules/osfile.jsm", {});
 const {AppProjects} = require("devtools/app-manager/app-projects");
 const TabStore = require("devtools/webide/tab-store");
 const {AppValidator} = require("devtools/app-manager/app-validator");
 const {ConnectionManager, Connection} = require("devtools/client/connection-manager");
 const {AppActorFront} = require("devtools/app-actor-front");
 const {getDeviceFront} = require("devtools/server/actors/device");
 const {getPreferenceFront} = require("devtools/server/actors/preference");
 const {setTimeout} = require("sdk/timers");
 const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
-const {USBRuntime, WiFiRuntime, SimulatorRuntime,
-       gLocalRuntime, gRemoteRuntime} = require("devtools/webide/runtimes");
-const discovery = require("devtools/toolkit/discovery/discovery");
+const {RuntimeScanners, RuntimeTypes} = require("devtools/webide/runtimes");
 const {NetUtil} = Cu.import("resource://gre/modules/NetUtil.jsm", {});
 const Telemetry = require("devtools/shared/telemetry");
 
 const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties");
 
-const WIFI_SCANNING_PREF = "devtools.remote.wifi.scan";
-
-exports.AppManager = AppManager = {
+let AppManager = exports.AppManager = {
 
   // FIXME: will break when devtools/app-manager will be removed:
   DEFAULT_PROJECT_ICON: "chrome://browser/skin/devtools/app-manager/default-app-icon.png",
   DEFAULT_PROJECT_NAME: "--",
 
   init: function() {
-    let host = Services.prefs.getCharPref("devtools.debugger.remote-host");
     let port = Services.prefs.getIntPref("devtools.debugger.remote-port");
-
     this.connection = ConnectionManager.createConnection("localhost", port);
     this.onConnectionChanged = this.onConnectionChanged.bind(this);
     this.connection.on(Connection.Events.STATUS_CHANGED, this.onConnectionChanged);
 
     this.tabStore = new TabStore(this.connection);
     this.onTabNavigate = this.onTabNavigate.bind(this);
     this.onTabClosed = this.onTabClosed.bind(this);
     this.tabStore.on("navigate", this.onTabNavigate);
     this.tabStore.on("closed", this.onTabClosed);
 
-    this.runtimeList = {
-      usb: [],
-      wifi: [],
-      simulator: [],
-      custom: [gRemoteRuntime]
-    };
-    if (Services.prefs.getBoolPref("devtools.webide.enableLocalRuntime")) {
-      this.runtimeList.custom.push(gLocalRuntime);
-    }
-    this.trackUSBRuntimes();
-    this.trackWiFiRuntimes();
-    this.trackSimulatorRuntimes();
+    this._clearRuntimeList();
+    this._rebuildRuntimeList = this._rebuildRuntimeList.bind(this);
+    RuntimeScanners.on("runtime-list-updated", this._rebuildRuntimeList);
+    RuntimeScanners.enable();
+    this._rebuildRuntimeList();
 
     this.onInstallProgress = this.onInstallProgress.bind(this);
 
-    this.observe = this.observe.bind(this);
-    Services.prefs.addObserver(WIFI_SCANNING_PREF, this, false);
-
     this._telemetry = new Telemetry();
   },
 
   uninit: function() {
     this.selectedProject = null;
     this.selectedRuntime = null;
-    this.untrackUSBRuntimes();
-    this.untrackWiFiRuntimes();
-    this.untrackSimulatorRuntimes();
+    RuntimeScanners.off("runtime-list-updated", this._rebuildRuntimeList);
+    RuntimeScanners.disable();
     this.runtimeList = null;
     this.tabStore.off("navigate", this.onTabNavigate);
     this.tabStore.off("closed", this.onTabClosed);
     this.tabStore.destroy();
     this.tabStore = null;
     this.connection.off(Connection.Events.STATUS_CHANGED, this.onConnectionChanged);
     this._listTabsResponse = null;
     this.connection.disconnect();
     this.connection = null;
-    Services.prefs.removeObserver(WIFI_SCANNING_PREF, this);
-  },
-
-  observe: function(subject, topic, data) {
-    if (data !== WIFI_SCANNING_PREF) {
-      return;
-    }
-    // Cycle WiFi tracking to reflect the new value
-    this.untrackWiFiRuntimes();
-    this.trackWiFiRuntimes();
-    this._updateWiFiRuntimes();
   },
 
   update: function(what, details) {
     // Anything we want to forward to the UI
     this.emit("app-manager-update", what, details);
   },
 
   reportError: function(l10nProperty, ...l10nArgs) {
@@ -329,17 +299,17 @@ exports.AppManager = AppManager = {
   set selectedRuntime(value) {
     this._selectedRuntime = value;
     if (!value && this.selectedProject &&
         (this.selectedProject.type == "mainProcess" ||
          this.selectedProject.type == "runtimeApp" ||
          this.selectedProject.type == "tab")) {
       this.selectedProject = null;
     }
-    this.update("runtime");
+    this.update("runtime-changed");
   },
 
   get selectedRuntime() {
     return this._selectedRuntime;
   },
 
   connectToRuntime: function(runtime) {
 
@@ -602,99 +572,51 @@ exports.AppManager = AppManager = {
       if (AppManager.selectedProject === project) {
         AppManager.update("project-validated");
       }
     });
   },
 
   /* RUNTIME LIST */
 
-  trackUSBRuntimes: function() {
-    this._updateUSBRuntimes = this._updateUSBRuntimes.bind(this);
-    Devices.on("register", this._updateUSBRuntimes);
-    Devices.on("unregister", this._updateUSBRuntimes);
-    Devices.on("addon-status-updated", this._updateUSBRuntimes);
-    this._updateUSBRuntimes();
-  },
-  untrackUSBRuntimes: function() {
-    Devices.off("register", this._updateUSBRuntimes);
-    Devices.off("unregister", this._updateUSBRuntimes);
-    Devices.off("addon-status-updated", this._updateUSBRuntimes);
+  _clearRuntimeList: function() {
+    this.runtimeList = {
+      usb: [],
+      wifi: [],
+      simulator: [],
+      other: []
+    };
   },
-  _updateUSBRuntimes: function() {
-    this.runtimeList.usb = [];
-    for (let id of Devices.available()) {
-      let r = new USBRuntime(id);
-      this.runtimeList.usb.push(r);
-      r.updateNameFromADB().then(
-        () => {
-          this.update("runtimelist");
-          // Also update the runtime button label, if the currently selected
-          // runtime name changes
-          if (r == this.selectedRuntime) {
-            this.update("runtime");
-          }
-        },
-        () => {});
+
+  _rebuildRuntimeList: function() {
+    let runtimes = RuntimeScanners.listRuntimes();
+    this._clearRuntimeList();
+
+    // Reorganize runtimes by type
+    for (let runtime of runtimes) {
+      switch (runtime.type) {
+        case RuntimeTypes.USB:
+          this.runtimeList.usb.push(runtime);
+          break;
+        case RuntimeTypes.WIFI:
+          this.runtimeList.wifi.push(runtime);
+          break;
+        case RuntimeTypes.SIMULATOR:
+          this.runtimeList.simulator.push(runtime);
+          break;
+        default:
+          this.runtimeList.other.push(runtime);
+      }
     }
+
+    this.update("runtime-details");
     this.update("runtimelist");
   },
 
-  get isWiFiScanningEnabled() {
-    return Services.prefs.getBoolPref(WIFI_SCANNING_PREF);
-  },
-  scanForWiFiRuntimes: function() {
-    if (!this.isWiFiScanningEnabled) {
-      return;
-    }
-    discovery.scan();
-  },
-  trackWiFiRuntimes: function() {
-    if (!this.isWiFiScanningEnabled) {
-      return;
-    }
-    this._updateWiFiRuntimes = this._updateWiFiRuntimes.bind(this);
-    discovery.on("devtools-device-added", this._updateWiFiRuntimes);
-    discovery.on("devtools-device-updated", this._updateWiFiRuntimes);
-    discovery.on("devtools-device-removed", this._updateWiFiRuntimes);
-    this._updateWiFiRuntimes();
-  },
-  untrackWiFiRuntimes: function() {
-    if (!this.isWiFiScanningEnabled) {
-      return;
-    }
-    discovery.off("devtools-device-added", this._updateWiFiRuntimes);
-    discovery.off("devtools-device-updated", this._updateWiFiRuntimes);
-    discovery.off("devtools-device-removed", this._updateWiFiRuntimes);
-  },
-  _updateWiFiRuntimes: function() {
-    this.runtimeList.wifi = [];
-    for (let device of discovery.getRemoteDevicesWithService("devtools")) {
-      this.runtimeList.wifi.push(new WiFiRuntime(device));
-    }
-    this.update("runtimelist");
-  },
-
-  trackSimulatorRuntimes: function() {
-    this._updateSimulatorRuntimes = this._updateSimulatorRuntimes.bind(this);
-    Simulator.on("register", this._updateSimulatorRuntimes);
-    Simulator.on("unregister", this._updateSimulatorRuntimes);
-    this._updateSimulatorRuntimes();
-  },
-  untrackSimulatorRuntimes: function() {
-    Simulator.off("register", this._updateSimulatorRuntimes);
-    Simulator.off("unregister", this._updateSimulatorRuntimes);
-  },
-  _updateSimulatorRuntimes: function() {
-    this.runtimeList.simulator = [];
-    for (let version of Simulator.availableVersions()) {
-      this.runtimeList.simulator.push(new SimulatorRuntime(version));
-    }
-    this.update("runtimelist");
-  },
+  /* MANIFEST UTILS */
 
   writeManifest: function(project) {
     if (project.type != "packaged") {
       return promise.reject("Not a packaged app");
     }
 
     if (!project.manifest) {
       project.manifest = {};
@@ -702,11 +624,11 @@ exports.AppManager = AppManager = {
 
     let folder = project.location;
     let manifestPath = OS.Path.join(folder, "manifest.webapp");
     let text = JSON.stringify(project.manifest, null, 2);
     let encoder = new TextEncoder();
     let array = encoder.encode(text);
     return OS.File.writeAtomic(manifestPath, array, {tmpPath: manifestPath + ".tmp"});
   },
-}
+};
 
 EventEmitter.decorate(AppManager);
--- a/browser/devtools/webide/modules/runtimes.js
+++ b/browser/devtools/webide/modules/runtimes.js
@@ -4,145 +4,524 @@
 
 const {Cu} = require("chrome");
 const {Devices} = Cu.import("resource://gre/modules/devtools/Devices.jsm");
 const {Services} = Cu.import("resource://gre/modules/Services.jsm");
 const {Simulator} = Cu.import("resource://gre/modules/devtools/Simulator.jsm");
 const {ConnectionManager, Connection} = require("devtools/client/connection-manager");
 const {DebuggerServer} = require("resource://gre/modules/devtools/dbg-server.jsm");
 const discovery = require("devtools/toolkit/discovery/discovery");
+const EventEmitter = require("devtools/toolkit/event-emitter");
 const promise = require("promise");
 
 const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties");
 
-// These type strings are used for logging events to Telemetry
-let RuntimeTypes = {
-  usb: "USB",
-  wifi: "WIFI",
-  simulator: "SIMULATOR",
-  remote: "REMOTE",
-  local: "LOCAL"
+/**
+ * Runtime and Scanner API
+ *
+ * |RuntimeScanners| maintains a set of |Scanner| objects that produce one or
+ * more |Runtime|s to connect to.  Add-ons can extend the set of known runtimes
+ * by registering additional |Scanner|s that emit them.
+ *
+ * Each |Scanner| must support the following API:
+ *
+ * enable()
+ *   Bind any event handlers and start any background work the scanner needs to
+ *   maintain an updated set of |Runtime|s.
+ *   Called when the first consumer (such as WebIDE) actively interested in
+ *   maintaining the |Runtime| list enables the registry.
+ * disable()
+ *   Unbind any event handlers and stop any background work the scanner needs to
+ *   maintain an updated set of |Runtime|s.
+ *   Called when the last consumer (such as WebIDE) actively interested in
+ *   maintaining the |Runtime| list disables the registry.
+ * emits "runtime-list-updated"
+ *   If the set of runtimes a |Scanner| manages has changed, it must emit this
+ *   event to notify consumers of changes.
+ * scan()
+ *   Actively refreshes the list of runtimes the scanner knows about.  If your
+ *   scanner uses an active scanning approach (as opposed to listening for
+ *   events when changes occur), the bulk of the work would be done here.
+ *   @return Promise
+ *           Should be resolved when scanning is complete.  If scanning has no
+ *           well-defined end point, you can resolve immediately, as long as
+ *           update event is emitted later when changes are noticed.
+ * listRuntimes()
+ *   Return the current list of runtimes known to the |Scanner| instance.
+ *   @return Iterable
+ *
+ * Each |Runtime| must support the following API:
+ *
+ * |type| field
+ *   The |type| must be one of the values from the |RuntimeTypes| object.  This
+ *   is used for Telemetry and to support displaying sets of |Runtime|s
+ *   categorized by type.
+ * |id| field
+ *   An identifier that is unique in the set of all runtimes with the same
+ *   |type|.  WebIDE tries to save the last used runtime via type + id, and
+ *   tries to locate it again in the next session, so this value should attempt
+ *   to be stable across Firefox sessions.
+ * |name| field
+ *   A user-visible label to identify the runtime that will be displayed in a
+ *   runtime list.
+ * connect()
+ *   Configure the passed |connection| object with any settings need to
+ *   successfully connect to the runtime, and call the |connection|'s connect()
+ *   method.
+ *   @param  Connection connection
+ *           A |Connection| object from the DevTools |ConnectionManager|.
+ *   @return Promise
+ *           Resolved once you've called the |connection|'s connect() method.
+ */
+
+/* SCANNER REGISTRY */
+
+let RuntimeScanners = {
+
+  _enabledCount: 0,
+  _scanners: new Set(),
+
+  get enabled() {
+    return !!this._enabledCount;
+  },
+
+  add(scanner) {
+    if (this.enabled) {
+      // Enable any scanner added while globally enabled
+      this._enableScanner(scanner);
+    }
+    this._scanners.add(scanner);
+    this._emitUpdated();
+  },
+
+  remove(scanner) {
+    this._scanners.delete(scanner);
+    if (this.enabled) {
+      // Disable any scanner removed while globally enabled
+      this._disableScanner(scanner);
+    }
+    this._emitUpdated();
+  },
+
+  has(scanner) {
+    return this._scanners.has(scanner);
+  },
+
+  scan() {
+    if (!this.enabled) {
+      return promise.resolve();
+    }
+
+    if (this._scanPromise) {
+      return this._scanPromise;
+    }
+
+    let promises = [];
+
+    for (let scanner of this._scanners) {
+      promises.push(scanner.scan());
+    }
+
+    this._scanPromise = promise.all(promises);
+
+    // Reset pending promise
+    this._scanPromise.then(() => {
+      this._scanPromise = null;
+    }, () => {
+      this._scanPromise = null;
+    });
+
+    return this._scanPromise;
+  },
+
+  listRuntimes: function*() {
+    for (let scanner of this._scanners) {
+      for (let runtime of scanner.listRuntimes()) {
+        yield runtime;
+      }
+    }
+  },
+
+  _emitUpdated() {
+    this.emit("runtime-list-updated");
+  },
+
+  enable() {
+    if (this._enabledCount++ !== 0) {
+      // Already enabled scanners during a previous call
+      return;
+    }
+    this._emitUpdated = this._emitUpdated.bind(this);
+    for (let scanner of this._scanners) {
+      this._enableScanner(scanner);
+    }
+  },
+
+  _enableScanner(scanner) {
+    scanner.enable();
+    scanner.on("runtime-list-updated", this._emitUpdated);
+  },
+
+  disable() {
+    if (--this._enabledCount !== 0) {
+      // Already disabled scanners during a previous call
+      return;
+    }
+    for (let scanner of this._scanners) {
+      this._disableScanner(scanner);
+    }
+  },
+
+  _disableScanner(scanner) {
+    scanner.off("runtime-list-updated", this._emitUpdated);
+    scanner.disable();
+  },
+
 };
 
-function USBRuntime(id) {
-  this.id = id;
+EventEmitter.decorate(RuntimeScanners);
+
+exports.RuntimeScanners = RuntimeScanners;
+
+/* SCANNERS */
+
+let SimulatorScanner = {
+
+  _runtimes: [],
+
+  enable() {
+    this._updateRuntimes = this._updateRuntimes.bind(this);
+    Simulator.on("register", this._updateRuntimes);
+    Simulator.on("unregister", this._updateRuntimes);
+    this._updateRuntimes();
+  },
+
+  disable() {
+    Simulator.off("register", this._updateRuntimes);
+    Simulator.off("unregister", this._updateRuntimes);
+  },
+
+  _emitUpdated() {
+    this.emit("runtime-list-updated");
+  },
+
+  _updateRuntimes() {
+    this._runtimes = [];
+    for (let version of Simulator.availableVersions()) {
+      this._runtimes.push(new SimulatorRuntime(version));
+    }
+    this._emitUpdated();
+  },
+
+  scan() {
+    return promise.resolve();
+  },
+
+  listRuntimes: function() {
+    return this._runtimes;
+  }
+
+};
+
+EventEmitter.decorate(SimulatorScanner);
+RuntimeScanners.add(SimulatorScanner);
+
+/**
+ * TODO: Remove this comaptibility layer in the future (bug 1085393)
+ * This runtime exists to support the ADB Helper add-on below version 0.7.0.
+ *
+ * This scanner will list all ADB devices as runtimes, even if they may or may
+ * not actually connect (since the |DeprecatedUSBRuntime| assumes a Firefox OS
+ * device).
+ */
+let DeprecatedAdbScanner = {
+
+  _runtimes: [],
+
+  enable() {
+    this._updateRuntimes = this._updateRuntimes.bind(this);
+    Devices.on("register", this._updateRuntimes);
+    Devices.on("unregister", this._updateRuntimes);
+    Devices.on("addon-status-updated", this._updateRuntimes);
+    this._updateRuntimes();
+  },
+
+  disable() {
+    Devices.off("register", this._updateRuntimes);
+    Devices.off("unregister", this._updateRuntimes);
+    Devices.off("addon-status-updated", this._updateRuntimes);
+  },
+
+  _emitUpdated() {
+    this.emit("runtime-list-updated");
+  },
+
+  _updateRuntimes() {
+    this._runtimes = [];
+    for (let id of Devices.available()) {
+      let runtime = new DeprecatedUSBRuntime(id);
+      this._runtimes.push(runtime);
+      runtime.updateNameFromADB().then(() => {
+        this._emitUpdated();
+      }, () => {});
+    }
+    this._emitUpdated();
+  },
+
+  scan() {
+    return promise.resolve();
+  },
+
+  listRuntimes: function() {
+    return this._runtimes;
+  }
+
+};
+
+EventEmitter.decorate(DeprecatedAdbScanner);
+RuntimeScanners.add(DeprecatedAdbScanner);
+
+// ADB Helper 0.7.0 and later will replace this scanner on startup
+exports.DeprecatedAdbScanner = DeprecatedAdbScanner;
+
+let WiFiScanner = {
+
+  _runtimes: [],
+
+  init() {
+    this.updateRegistration();
+    Services.prefs.addObserver(this.ALLOWED_PREF, this, false);
+  },
+
+  enable() {
+    this._updateRuntimes = this._updateRuntimes.bind(this);
+    discovery.on("devtools-device-added", this._updateRuntimes);
+    discovery.on("devtools-device-updated", this._updateRuntimes);
+    discovery.on("devtools-device-removed", this._updateRuntimes);
+    this._updateRuntimes();
+  },
+
+  disable() {
+    discovery.off("devtools-device-added", this._updateRuntimes);
+    discovery.off("devtools-device-updated", this._updateRuntimes);
+    discovery.off("devtools-device-removed", this._updateRuntimes);
+  },
+
+  _emitUpdated() {
+    this.emit("runtime-list-updated");
+  },
+
+  _updateRuntimes() {
+    this._runtimes = [];
+    for (let device of discovery.getRemoteDevicesWithService("devtools")) {
+      this._runtimes.push(new WiFiRuntime(device));
+    }
+    this._emitUpdated();
+  },
+
+  scan() {
+    discovery.scan();
+    return promise.resolve();
+  },
+
+  listRuntimes: function() {
+    return this._runtimes;
+  },
+
+  ALLOWED_PREF: "devtools.remote.wifi.scan",
+
+  get allowed() {
+    return Services.prefs.getBoolPref(this.ALLOWED_PREF);
+  },
+
+  updateRegistration() {
+    if (this.allowed) {
+      RuntimeScanners.add(WiFiScanner);
+    } else {
+      RuntimeScanners.remove(WiFiScanner);
+    }
+    this._emitUpdated();
+  },
+
+  observe(subject, topic, data) {
+    if (data !== WiFiScanner.ALLOWED_PREF) {
+      return;
+    }
+    WiFiScanner.updateRegistration();
+  }
+
+};
+
+EventEmitter.decorate(WiFiScanner);
+WiFiScanner.init();
+
+exports.WiFiScanner = WiFiScanner;
+
+let StaticScanner = {
+  enable() {},
+  disable() {},
+  scan() { return promise.resolve(); },
+  listRuntimes() { return [gRemoteRuntime, gLocalRuntime]; }
+};
+
+EventEmitter.decorate(StaticScanner);
+RuntimeScanners.add(StaticScanner);
+
+/* RUNTIMES */
+
+// These type strings are used for logging events to Telemetry.
+// You must update Histograms.json if new types are added.
+let RuntimeTypes = exports.RuntimeTypes = {
+  USB: "USB",
+  WIFI: "WIFI",
+  SIMULATOR: "SIMULATOR",
+  REMOTE: "REMOTE",
+  LOCAL: "LOCAL",
+  OTHER: "OTHER"
+};
+
+/**
+ * TODO: Remove this comaptibility layer in the future (bug 1085393)
+ * This runtime exists to support the ADB Helper add-on below version 0.7.0.
+ *
+ * This runtime assumes it is connecting to a Firefox OS device.
+ */
+function DeprecatedUSBRuntime(id) {
+  this._id = id;
 }
 
-USBRuntime.prototype = {
-  type: RuntimeTypes.usb,
+DeprecatedUSBRuntime.prototype = {
+  type: RuntimeTypes.USB,
+  get device() {
+    return Devices.getByName(this._id);
+  },
   connect: function(connection) {
-    let device = Devices.getByName(this.id);
-    if (!device) {
-      return promise.reject("Can't find device: " + this.getName());
+    if (!this.device) {
+      return promise.reject("Can't find device: " + this.name);
     }
-    return device.connect().then((port) => {
+    return this.device.connect().then((port) => {
       connection.host = "localhost";
       connection.port = port;
       connection.connect();
     });
   },
-  getID: function() {
-    return this.id;
+  get id() {
+    return this._id;
   },
-  getName: function() {
-    return this._productModel || this.id;
+  get name() {
+    return this._productModel || this._id;
   },
   updateNameFromADB: function() {
     if (this._productModel) {
-      return promise.resolve();
+      return promise.reject();
     }
-    let device = Devices.getByName(this.id);
     let deferred = promise.defer();
-    if (device && device.shell) {
-      device.shell("getprop ro.product.model").then(stdout => {
+    if (this.device && this.device.shell) {
+      this.device.shell("getprop ro.product.model").then(stdout => {
         this._productModel = stdout;
         deferred.resolve();
       }, () => {});
     } else {
       this._productModel = null;
       deferred.reject();
     }
     return deferred.promise;
   },
-}
+};
+
+// For testing use only
+exports._DeprecatedUSBRuntime = DeprecatedUSBRuntime;
 
 function WiFiRuntime(deviceName) {
   this.deviceName = deviceName;
 }
 
 WiFiRuntime.prototype = {
-  type: RuntimeTypes.wifi,
+  type: RuntimeTypes.WIFI,
   connect: function(connection) {
     let service = discovery.getRemoteService("devtools", this.deviceName);
     if (!service) {
-      return promise.reject("Can't find device: " + this.getName());
+      return promise.reject("Can't find device: " + this.name);
     }
     connection.host = service.host;
     connection.port = service.port;
     connection.connect();
     return promise.resolve();
   },
-  getID: function() {
+  get id() {
     return this.deviceName;
   },
-  getName: function() {
+  get name() {
     return this.deviceName;
   },
-}
+};
+
+// For testing use only
+exports._WiFiRuntime = WiFiRuntime;
 
 function SimulatorRuntime(version) {
   this.version = version;
 }
 
 SimulatorRuntime.prototype = {
-  type: RuntimeTypes.simulator,
+  type: RuntimeTypes.SIMULATOR,
   connect: function(connection) {
     let port = ConnectionManager.getFreeTCPPort();
     let simulator = Simulator.getByVersion(this.version);
     if (!simulator || !simulator.launch) {
-      return promise.reject("Can't find simulator: " + this.getName());
+      return promise.reject("Can't find simulator: " + this.name);
     }
     return simulator.launch({port: port}).then(() => {
       connection.host = "localhost";
       connection.port = port;
       connection.keepConnecting = true;
       connection.once(Connection.Events.DISCONNECTED, simulator.close);
       connection.connect();
     });
   },
-  getID: function() {
+  get id() {
     return this.version;
   },
-  getName: function() {
+  get name() {
+    let simulator = Simulator.getByVersion(this.version);
+    if (!simulator) {
+      return "Unknown";
+    }
     return Simulator.getByVersion(this.version).appinfo.label;
   },
-}
+};
+
+// For testing use only
+exports._SimulatorRuntime = SimulatorRuntime;
 
 let gLocalRuntime = {
-  type: RuntimeTypes.local,
+  type: RuntimeTypes.LOCAL,
   connect: function(connection) {
     if (!DebuggerServer.initialized) {
       DebuggerServer.init();
       DebuggerServer.addBrowserActors();
     }
     connection.host = null; // Force Pipe transport
     connection.port = null;
     connection.connect();
     return promise.resolve();
   },
-  getName: function() {
+  get id() {
+    return "local";
+  },
+  get name() {
     return Strings.GetStringFromName("local_runtime");
   },
-  getID: function () {
-    return "local";
-  }
-}
+};
+
+// For testing use only
+exports._gLocalRuntime = gLocalRuntime;
 
 let gRemoteRuntime = {
-  type: RuntimeTypes.remote,
+  type: RuntimeTypes.REMOTE,
   connect: function(connection) {
     let win = Services.wm.getMostRecentWindow("devtools:webide");
     if (!win) {
       return promise.reject();
     }
     let ret = {value: connection.host + ":" + connection.port};
     let title = Strings.GetStringFromName("remote_runtime_promptTitle");
     let message = Strings.GetStringFromName("remote_runtime_promptMessage");
@@ -154,18 +533,15 @@ let gRemoteRuntime = {
     if (!host || !port) {
       return promise.reject();
     }
     connection.host = host;
     connection.port = port;
     connection.connect();
     return promise.resolve();
   },
-  getName: function() {
+  get name() {
     return Strings.GetStringFromName("remote_runtime");
   },
-}
+};
 
-exports.USBRuntime = USBRuntime;
-exports.WiFiRuntime = WiFiRuntime;
-exports.SimulatorRuntime = SimulatorRuntime;
-exports.gRemoteRuntime = gRemoteRuntime;
-exports.gLocalRuntime = gLocalRuntime;
+// For testing use only
+exports._gRemoteRuntime = gRemoteRuntime;
--- a/browser/devtools/webide/test/browser_tabs.js
+++ b/browser/devtools/webide/test/browser_tabs.js
@@ -38,17 +38,17 @@ function test() {
   }).then(finish, handleError);
 }
 
 function connectToLocal(win) {
   let deferred = promise.defer();
   win.AppManager.connection.once(
       win.Connection.Events.CONNECTED,
       () => deferred.resolve());
-  win.document.querySelectorAll(".runtime-panel-item-custom")[1].click();
+  win.document.querySelectorAll(".runtime-panel-item-other")[1].click();
   return deferred.promise;
 }
 
 function selectTabProject(win) {
   return Task.spawn(function() {
     yield win.AppManager.listTabs();
     win.Cmds.showProjectPanel();
     yield nextTick();
--- a/browser/devtools/webide/test/head.js
+++ b/browser/devtools/webide/test/head.js
@@ -17,28 +17,26 @@ const {AppProjects} = require("devtools/
 let TEST_BASE;
 if (window.location === "chrome://browser/content/browser.xul") {
   TEST_BASE = "chrome://mochitests/content/browser/browser/devtools/webide/test/";
 } else {
   TEST_BASE = "chrome://mochitests/content/chrome/browser/devtools/webide/test/";
 }
 
 Services.prefs.setBoolPref("devtools.webide.enabled", true);
-Services.prefs.setBoolPref("devtools.webide.enableLocalRuntime", true);
 
 Services.prefs.setCharPref("devtools.webide.addonsURL", TEST_BASE + "addons/simulators.json");
 Services.prefs.setCharPref("devtools.webide.simulatorAddonsURL", TEST_BASE + "addons/fxos_#SLASHED_VERSION#_simulator-#OS#.xpi");
 Services.prefs.setCharPref("devtools.webide.adbAddonURL", TEST_BASE + "addons/adbhelper-#OS#.xpi");
 Services.prefs.setCharPref("devtools.webide.adaptersAddonURL", TEST_BASE + "addons/fxdt-adapters-#OS#.xpi");
 Services.prefs.setCharPref("devtools.webide.templatesURL", TEST_BASE + "templates.json");
 
 
 SimpleTest.registerCleanupFunction(() => {
   Services.prefs.clearUserPref("devtools.webide.enabled");
-  Services.prefs.clearUserPref("devtools.webide.enableLocalRuntime");
   Services.prefs.clearUserPref("devtools.webide.autoinstallADBHelper");
   Services.prefs.clearUserPref("devtools.webide.autoinstallFxdtAdapters");
 });
 
 function openWebIDE(autoInstallAddons) {
   info("opening WebIDE");
 
   Services.prefs.setBoolPref("devtools.webide.autoinstallADBHelper", !!autoInstallAddons);
--- a/browser/devtools/webide/test/test_autoconnect_runtime.html
+++ b/browser/devtools/webide/test/test_autoconnect_runtime.html
@@ -30,21 +30,21 @@
             type: "USB",
             connect: function(connection) {
               ok(connection, win.AppManager.connection, "connection is valid");
               connection.host = null; // force connectPipe
               connection.connect();
               return promise.resolve();
             },
 
-            getID: function() {
+            get id() {
               return "fakeRuntime";
             },
 
-            getName: function() {
+            get name() {
               return "fakeRuntime";
             }
           };
           win.AppManager.runtimeList.usb.push(fakeRuntime);
           win.AppManager.update("runtimelist");
 
           let panelNode = win.document.querySelector("#runtime-panel");
           let items = panelNode.querySelectorAll(".runtime-panel-item-usb");
--- a/browser/devtools/webide/test/test_deviceinfo.html
+++ b/browser/devtools/webide/test/test_deviceinfo.html
@@ -29,21 +29,21 @@
           let win = yield openWebIDE();
 
           let permIframe = win.document.querySelector("#deck-panel-permissionstable");
           let infoIframe = win.document.querySelector("#deck-panel-runtimedetails");
 
           yield documentIsLoaded(permIframe.contentWindow.document);
           yield documentIsLoaded(infoIframe.contentWindow.document);
 
-          win.AppManager.update("runtimelist");
+          win.AppManager._rebuildRuntimeList();
 
           let panelNode = win.document.querySelector("#runtime-panel");
-          let items = panelNode.querySelectorAll(".runtime-panel-item-custom");
-          is(items.length, 2, "Found 2 custom runtimes button");
+          let items = panelNode.querySelectorAll(".runtime-panel-item-other");
+          is(items.length, 2, "Found 2 other runtimes button");
 
           let deferred = promise.defer();
           win.AppManager.on("app-manager-update", function onUpdate(e,w) {
             if (w == "list-tabs-response") {
               win.AppManager.off("app-manager-update", onUpdate);
               deferred.resolve();
             }
           });
--- a/browser/devtools/webide/test/test_runtime.html
+++ b/browser/devtools/webide/test/test_runtime.html
@@ -52,17 +52,17 @@
           win.AppManager.runtimeList.usb.push({
             connect: function(connection) {
               ok(connection, win.AppManager.connection, "connection is valid");
               connection.host = null; // force connectPipe
               connection.connect();
               return promise.resolve();
             },
 
-            getName: function() {
+            get name() {
               return "fakeRuntime";
             }
           });
 
           win.AppManager.update("runtimelist");
 
           let packagedAppLocation = getTestFilePath("app");
 
@@ -105,17 +105,17 @@
           yield win.Cmds.disconnectRuntime();
 
           is(Object.keys(DebuggerServer._connections).length, 0, "Disconnected");
 
           ok(win.AppManager.selectedProject, "A project is still selected");
           ok(!isPlayActive(), "play button is disabled 4");
           ok(!isStopActive(), "stop button is disabled 4");
 
-          win.document.querySelectorAll(".runtime-panel-item-custom")[1].click();
+          win.document.querySelectorAll(".runtime-panel-item-other")[1].click();
 
           yield waitForUpdate(win, "list-tabs-response");
 
           is(Object.keys(DebuggerServer._connections).length, 1, "Locally connected");
 
           ok(win.AppManager.isMainProcessDebuggable(), "Main process available");
 
           // Select main process
--- a/browser/devtools/webide/test/test_telemetry.html
+++ b/browser/devtools/webide/test/test_telemetry.html
@@ -11,18 +11,19 @@
     <script type="application/javascript;version=1.8" src="head.js"></script>
     <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
   </head>
 
   <body>
 
     <script type="application/javascript;version=1.8">
       const Telemetry = require("devtools/shared/telemetry");
-      const { USBRuntime, WiFiRuntime, SimulatorRuntime, gRemoteRuntime,
-              gLocalRuntime } = require("devtools/webide/runtimes");
+      const { _DeprecatedUSBRuntime, _WiFiRuntime, _SimulatorRuntime,
+              _gRemoteRuntime, _gLocalRuntime, RuntimeTypes }
+            = require("devtools/webide/runtimes");
 
       // Because we need to gather stats for the period of time that a tool has
       // been opened we make use of setTimeout() to create tool active times.
       const TOOL_DELAY = 200;
 
       function patchTelemetry() {
         Telemetry.prototype.telemetryInfo = {};
         Telemetry.prototype._oldlog = Telemetry.prototype.log;
@@ -50,59 +51,65 @@
           yield closeWebIDE(win);
         });
       }
 
       function addFakeRuntimes(win) {
         // We use the real runtimes here (and switch out some functionality)
         // so we can ensure that logging happens as it would in real use.
 
-        let usb = new USBRuntime("fakeUSB");
+        let usb = new _DeprecatedUSBRuntime("fakeUSB");
         // Use local pipe instead
         usb.connect = function(connection) {
           ok(connection, win.AppManager.connection, "connection is valid");
           connection.host = null; // force connectPipe
           connection.connect();
           return promise.resolve();
         };
         win.AppManager.runtimeList.usb.push(usb);
 
-        let wifi = new WiFiRuntime("fakeWiFi");
+        let wifi = new _WiFiRuntime("fakeWiFi");
         // Use local pipe instead
         wifi.connect = function(connection) {
           ok(connection, win.AppManager.connection, "connection is valid");
           connection.host = null; // force connectPipe
           connection.connect();
           return promise.resolve();
         };
         win.AppManager.runtimeList.wifi.push(wifi);
 
-        let sim = new SimulatorRuntime("fakeSimulator");
+        let sim = new _SimulatorRuntime("fakeSimulator");
         // Use local pipe instead
         sim.connect = function(connection) {
           ok(connection, win.AppManager.connection, "connection is valid");
           connection.host = null; // force connectPipe
           connection.connect();
           return promise.resolve();
         };
-        sim.getName = function() {
-          return this.version;
-        };
+        Object.defineProperty(sim, "name", {
+          get() {
+            return this.version;
+          }
+        });
         win.AppManager.runtimeList.simulator.push(sim);
 
-        let remote = gRemoteRuntime;
+        let remote = _gRemoteRuntime;
         // Use local pipe instead
         remote.connect = function(connection) {
           ok(connection, win.AppManager.connection, "connection is valid");
           connection.host = null; // force connectPipe
           connection.connect();
           return promise.resolve();
         };
-        let local = gLocalRuntime;
-        win.AppManager.runtimeList.custom = [gRemoteRuntime, gLocalRuntime];
+        let local = _gLocalRuntime;
+
+        let other = Object.create(_gLocalRuntime);
+        other.type = RuntimeTypes.OTHER;
+
+        win.AppManager.runtimeList.other = [remote, local, other];
 
         win.AppManager.update("runtimelist");
       }
 
       function addTestApp(win) {
         return Task.spawn(function*() {
           let packagedAppLocation = getTestFilePath("app");
           yield win.Cmds.importPackagedApp(packagedAppLocation);
@@ -159,36 +166,36 @@
             ok(value.length > 1, histId + " has more than one entry");
 
             let okay = value.every(function(element) {
               return element > 0;
             });
 
             ok(okay, "All " + histId + " entries have time > 0");
           } else if (histId === "DEVTOOLS_WEBIDE_CONNECTION_RESULT") {
-            ok(value.length === 5, histId + " has 5 connection results");
+            ok(value.length === 6, histId + " has 6 connection results");
 
             let okay = value.every(function(element) {
               return !!element;
             });
 
             ok(okay, "All " + histId + " connections succeeded");
           } else if (histId.endsWith("CONNECTION_RESULT")) {
             ok(value.length === 1 && !!value[0],
                histId + " has 1 successful connection");
           } else if (histId === "DEVTOOLS_WEBIDE_CONNECTION_TIME_SECONDS") {
-            ok(value.length === 5, histId + " has 5 connection results");
+            ok(value.length === 6, histId + " has 6 connection results");
 
             let okay = value.every(function(element) {
               return element > 0;
             });
 
             ok(okay, "All " + histId + " connections have time > 0");
           } else if (histId.endsWith("USED")) {
-            ok(value.length === 5, histId + " has 5 connection actions");
+            ok(value.length === 6, histId + " has 6 connection actions");
 
             let okay = value.every(function(element) {
               return !element;
             });
 
             ok(okay, "All " + histId + " actions were skipped");
           } else {
             ok(false, "Unexpected " + histId + " was logged");
@@ -234,19 +241,21 @@
           // Each one should log a connection result and non-zero connection
           // time
           yield connectToRuntime(win, "usb");
           yield waitForTime(TOOL_DELAY);
           yield connectToRuntime(win, "wifi");
           yield waitForTime(TOOL_DELAY);
           yield connectToRuntime(win, "simulator");
           yield waitForTime(TOOL_DELAY);
-          yield connectToRuntime(win, "custom", 0 /* remote */);
+          yield connectToRuntime(win, "other", 0 /* remote */);
           yield waitForTime(TOOL_DELAY);
-          yield connectToRuntime(win, "custom", 1 /* local */);
+          yield connectToRuntime(win, "other", 1 /* local */);
+          yield waitForTime(TOOL_DELAY);
+          yield connectToRuntime(win, "other", 2 /* other */);
           yield waitForTime(TOOL_DELAY);
           yield closeWebIDE(win);
 
           checkResults();
 
           SimpleTest.finish();
         });
       }
--- a/browser/devtools/webide/themes/webide.css
+++ b/browser/devtools/webide/themes/webide.css
@@ -204,44 +204,44 @@ panel > .panel-arrowcontainer > .panel-a
 .project-panel-item-openhosted   { -moz-image-region: rect(208px,438px,234px,412px) }
 
 /* runtime panel */
 
 #runtime-panel .panel-arrowcontent {
   padding: 12px 0 0;
 }
 
-#runtime-panel-custom {
+#runtime-panel-other {
   margin-bottom: 12px;
 }
 
 #runtime-details,
 #runtime-screenshot,
 #runtime-permissions,
 #runtime-disconnect,
 #runtime-panel-nousbdevice,
 #runtime-panel-noadbhelper,
 #runtime-panel-installsimulator,
 .runtime-panel-item-usb,
 .runtime-panel-item-wifi,
-.runtime-panel-item-custom,
+.runtime-panel-item-other,
 .runtime-panel-item-simulator {
   list-style-image: url("icons.png");
 }
 
 #runtime-details                { -moz-image-region: rect(156px,438px,182px,412px) }
 #runtime-screenshot             { -moz-image-region: rect(130px,438px,156px,412px) }
 #runtime-permissions            { -moz-image-region: rect(104px,438px,130px,412px) }
 #runtime-disconnect             { -moz-image-region: rect(52px,438px,78px,412px) }
 #runtime-panel-nousbdevice      { -moz-image-region: rect(156px,438px,182px,412px) }
 #runtime-panel-noadbhelper      { -moz-image-region: rect(234px,438px,260px,412px) }
 #runtime-panel-installsimulator { -moz-image-region: rect(0px,438px,26px,412px) }
 .runtime-panel-item-usb         { -moz-image-region: rect(52px,438px,78px,412px) }
 .runtime-panel-item-wifi        { -moz-image-region: rect(208px,438px,234px,412px) }
-.runtime-panel-item-custom      { -moz-image-region: rect(26px,438px,52px,412px) }
+.runtime-panel-item-other       { -moz-image-region: rect(26px,438px,52px,412px) }
 .runtime-panel-item-simulator   { -moz-image-region: rect(0px,438px,26px,412px) }
 
 #runtime-actions {
   border-top: 1px solid rgba(221,221,221,1);
 }
 
 
 #runtime-actions > toolbarbutton {
--- a/browser/devtools/webide/webide-prefs.js
+++ b/browser/devtools/webide/webide-prefs.js
@@ -3,17 +3,16 @@
 # 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/.
 
 pref("devtools.webide.showProjectEditor", true);
 pref("devtools.webide.templatesURL", "https://code.cdn.mozilla.net/templates/list.json");
 pref("devtools.webide.autoinstallADBHelper", true);
 pref("devtools.webide.autoinstallFxdtAdapters", false);
 pref("devtools.webide.restoreLastProject", true);
-pref("devtools.webide.enableLocalRuntime", true);
 pref("devtools.webide.addonsURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/index.json");
 pref("devtools.webide.simulatorAddonsURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/#VERSION#/#OS#/fxos_#SLASHED_VERSION#_simulator-#OS#-latest.xpi");
 pref("devtools.webide.simulatorAddonID", "fxos_#SLASHED_VERSION#_simulator@mozilla.org");
 pref("devtools.webide.adbAddonURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/adb-helper/#OS#/adbhelper-#OS#-latest.xpi");
 pref("devtools.webide.adbAddonID", "adbhelper@mozilla.org");
 pref("devtools.webide.adaptersAddonURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxdt-adapters/#OS#/fxdt-adapters-#OS#-latest.xpi");
 pref("devtools.webide.adaptersAddonID", "fxdevtools-adapters@mozilla.org");
 pref("devtools.webide.monitorWebSocketURL", "ws://localhost:9000");
--- a/browser/locales/en-US/chrome/browser/devtools/webide.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/webide.dtd
@@ -58,20 +58,20 @@
 <!-- show toolbox -->
 <!ENTITY key_toggleToolbox "VK_F12">
 <!-- toggle sidebar -->
 <!ENTITY key_toggleEditor "B">
 
 <!ENTITY projectPanel_myProjects "My Projects">
 <!ENTITY projectPanel_runtimeApps "Runtime Apps">
 <!ENTITY projectPanel_tabs "Tabs">
-<!ENTITY runtimePanel_USBDevices "USB Devices">
-<!ENTITY runtimePanel_WiFiDevices "Wi-Fi Devices">
-<!ENTITY runtimePanel_simulators "Simulators">
-<!ENTITY runtimePanel_custom "Custom">
+<!ENTITY runtimePanel_usb "USB Devices">
+<!ENTITY runtimePanel_wifi "Wi-Fi Devices">
+<!ENTITY runtimePanel_simulator "Simulators">
+<!ENTITY runtimePanel_other "Other">
 <!ENTITY runtimePanel_installsimulator "Install Simulator">
 <!ENTITY runtimePanel_noadbhelper "Install ADB Helper">
 <!ENTITY runtimePanel_nousbdevice "Can't see your device?">
 
 <!-- Lense -->
 <!ENTITY details_valid_header "valid">
 <!ENTITY details_warning_header "warnings">
 <!ENTITY details_error_header "errors">
@@ -96,18 +96,16 @@
 <!ENTITY addons_aboutaddons "Open Add-ons Manager">
 
 <!-- Prefs -->
 <!ENTITY prefs_title "Preferences">
 <!ENTITY prefs_editor_title "Editor">
 <!ENTITY prefs_general_title "General">
 <!ENTITY prefs_restore "Restore Defaults">
 <!ENTITY prefs_simulators "Manage Simulators">
-<!ENTITY prefs_options_enablelocalruntime "Enable local runtime">
-<!ENTITY prefs_options_enablelocalruntime_tooltip "Allow WebIDE to connect to its own runtime (running browser instance)">
 <!ENTITY prefs_options_rememberlastproject "Remember last project">
 <!ENTITY prefs_options_rememberlastproject_tooltip "Restore previous project when WebIDE starts">
 <!ENTITY prefs_options_templatesurl "Templates URL">
 <!ENTITY prefs_options_templatesurl_tooltip "Index of available templates">
 <!ENTITY prefs_options_showeditor "Show editor">
 <!ENTITY prefs_options_showeditor_tooltip "Show internal editor">
 <!ENTITY prefs_options_tabsize "Tab size">
 <!ENTITY prefs_options_expandtab "Soft tabs">
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -6157,16 +6157,21 @@
     "kind": "boolean",
     "description": "Did WebIDE remote runtime connection succeed?"
   },
   "DEVTOOLS_WEBIDE_LOCAL_CONNECTION_RESULT": {
     "expires_in_version": "never",
     "kind": "boolean",
     "description": "Did WebIDE local runtime connection succeed?"
   },
+  "DEVTOOLS_WEBIDE_OTHER_CONNECTION_RESULT": {
+    "expires_in_version": "never",
+    "kind": "boolean",
+    "description": "Did WebIDE other runtime connection succeed?"
+  },
   "DEVTOOLS_WEBIDE_CONNECTION_TIME_SECONDS": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": "10000000",
     "n_buckets": 100,
     "description": "How long was WebIDE connected to a runtime (seconds)?"
   },
   "DEVTOOLS_WEBIDE_CONNECTION_PLAY_USED": {