Merge fx-team to m-c a=merge
authorWes Kocher <wkocher@mozilla.com>
Thu, 19 Jun 2014 16:14:35 -0700
changeset 189602 aaaa561743612b0cb49c68220d85bbdde515be33
parent 189587 79e69d064957aca2adeac6d7abe92c1af59572e0 (current diff)
parent 189601 fb867eb7b56f00404777b84308b123555118a2b8 (diff)
child 189634 bdac18bd6c7441154559413600d80a340d026bda
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersmerge
milestone33.0a1
Merge fx-team to m-c a=merge
--- a/browser/components/translation/BingTranslator.jsm
+++ b/browser/components/translation/BingTranslator.jsm
@@ -1,17 +1,17 @@
 /* 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 {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
-this.EXPORTED_SYMBOLS = [ "BingTranslation" ];
+this.EXPORTED_SYMBOLS = [ "BingTranslator" ];
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Log.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource://services-common/rest.js");
 
@@ -35,27 +35,27 @@ const MAX_REQUESTS = 15;
  * @param translationDocument  The TranslationDocument object that represents
  *                             the webpage to be translated
  * @param sourceLanguage       The source language of the document
  * @param targetLanguage       The target language for the translation
  *
  * @returns {Promise}          A promise that will resolve when the translation
  *                             task is finished.
  */
-this.BingTranslation = function(translationDocument, sourceLanguage, targetLanguage) {
+this.BingTranslator = function(translationDocument, sourceLanguage, targetLanguage) {
   this.translationDocument = translationDocument;
   this.sourceLanguage = sourceLanguage;
   this.targetLanguage = targetLanguage;
   this._pendingRequests = 0;
   this._partialSuccess = false;
   this._serviceUnavailable = false;
   this._translatedCharacterCount = 0;
 };
 
-this.BingTranslation.prototype = {
+this.BingTranslator.prototype = {
   /**
    * Performs the translation, splitting the document into several chunks
    * respecting the data limits of the API.
    *
    * @returns {Promise}          A promise that will resolve when the translation
    *                             task is finished.
    */
   translate: function() {
@@ -277,17 +277,20 @@ function BingRequest(translationData, so
 BingRequest.prototype = {
   /**
    * Initiates the request
    */
   fireRequest: function() {
     return Task.spawn(function *(){
       let token = yield BingTokenManager.getToken();
       let auth = "Bearer " + token;
-      let request = new RESTRequest("https://api.microsofttranslator.com/v2/Http.svc/TranslateArray");
+      let url = getUrlParam("https://api.microsofttranslator.com/v2/Http.svc/TranslateArray",
+                            "browser.translation.bing.translateArrayURL",
+                            false);
+      let request = new RESTRequest(url);
       request.setHeader("Content-type", "text/xml");
       request.setHeader("Authorization", auth);
 
       let requestString =
         '<TranslateArrayRequest>' +
           '<AppId/>' +
           '<From>' + this.sourceLanguage + '</From>' +
           '<Options>' +
@@ -353,25 +356,28 @@ let BingTokenManager = {
 
   /**
    * Generates a new token from the server.
    *
    * @returns {Promise}  A promise that resolves with the token
    *                     string once it is obtained.
    */
   _getNewToken: function() {
-    let request = new RESTRequest("https://datamarket.accesscontrol.windows.net/v2/OAuth2-13");
+    let url = getUrlParam("https://datamarket.accesscontrol.windows.net/v2/OAuth2-13",
+                          "browser.translation.bing.authURL",
+                          false);
+    let request = new RESTRequest(url);
     request.setHeader("Content-type", "application/x-www-form-urlencoded");
     let params = [
       "grant_type=client_credentials",
       "scope=" + encodeURIComponent("http://api.microsofttranslator.com"),
       "client_id=" +
-      getAuthTokenParam("%BING_API_CLIENTID%", "browser.translation.bing.clientIdOverride"),
+      getUrlParam("%BING_API_CLIENTID%", "browser.translation.bing.clientIdOverride"),
       "client_secret=" +
-      getAuthTokenParam("%BING_API_KEY%", "browser.translation.bing.apiKeyOverride")
+      getUrlParam("%BING_API_KEY%", "browser.translation.bing.apiKeyOverride")
     ];
 
     let deferred = Promise.defer();
     this._pendingRequest = deferred.promise;
     request.post(params.join("&"), function(err) {
       BingTokenManager._pendingRequest = null;
 
       if (err) {
@@ -411,16 +417,15 @@ function escapeXML(aStr) {
              .replace("<", "&lt;", "g")
              .replace(">", "&gt;", "g");
 }
 
 /**
  * Fetch an auth token (clientID or client secret), which may be overridden by
  * a pref if it's set.
  */
-function getAuthTokenParam(key, prefName) {
-  let val;
-  try {
-    val = Services.prefs.getCharPref(prefName);
-  } catch(ex) {}
+function getUrlParam(paramValue, prefName, encode = true) {
+  if (Services.prefs.getPrefType(prefName))
+    paramValue = Services.prefs.getCharPref(prefName);
+  paramValue = Services.urlFormatter.formatURL(paramValue);
 
-  return encodeURIComponent(Services.urlFormatter.formatURL(val || key));
+  return encode ? encodeURIComponent(paramValue) : paramValue;
 }
--- a/browser/components/translation/Translation.jsm
+++ b/browser/components/translation/Translation.jsm
@@ -88,20 +88,26 @@ this.Translation = {
  * - showOriginalContent, method showing the original page content.
  * - showTranslatedContent, method showing the translation for an
  *   already translated page whose original content is shown.
  * - originalShown, boolean indicating if the original or translated
  *   version of the page is shown.
  */
 function TranslationUI(aBrowser) {
   this.browser = aBrowser;
-  aBrowser.messageManager.addMessageListener("Translation:Finished", this);
 }
 
 TranslationUI.prototype = {
+  get browser() this._browser,
+  set browser(aBrowser) {
+    if (this._browser)
+      this._browser.messageManager.removeMessageListener("Translation:Finished", this);
+    aBrowser.messageManager.addMessageListener("Translation:Finished", this);
+    this._browser = aBrowser;
+  },
   translate: function(aFrom, aTo) {
     if (aFrom == aTo ||
         (this.state == Translation.STATE_TRANSLATED &&
          this.translatedFrom == aFrom && this.translatedTo == aTo)) {
       // Nothing to do.
       return;
     }
 
@@ -119,17 +125,27 @@ TranslationUI.prototype = {
     let chromeWin = this.browser.ownerGlobal;
     let PopupNotifications = chromeWin.PopupNotifications;
     let removeId = this.originalShown ? "translated" : "translate";
     let notification =
       PopupNotifications.getNotification(removeId, this.browser);
     if (notification)
       PopupNotifications.remove(notification);
 
-    let callback = aTopic => {
+    let callback = (aTopic, aNewBrowser) => {
+      if (aTopic == "swapping") {
+        let infoBarVisible =
+          this.notificationBox.getNotificationWithValue("translation");
+        aNewBrowser.translationUI = this;
+        this.browser = aNewBrowser;
+        if (infoBarVisible)
+          this.showTranslationInfoBar();
+        return true;
+      }
+
       if (aTopic != "showing")
         return false;
       let notification = this.notificationBox.getNotificationWithValue("translation");
       if (notification)
         notification.close();
       else
         this.showTranslationInfoBar();
       return true;
--- a/browser/components/translation/TranslationContentHandler.jsm
+++ b/browser/components/translation/TranslationContentHandler.jsm
@@ -116,26 +116,26 @@ TranslationContentHandler.prototype = {
         Cu.import("resource:///modules/translation/BingTranslator.jsm");
 
         // If a TranslationDocument already exists for this document, it should
         // be used instead of creating a new one so that we can use the original
         // content of the page for the new translation instead of the newly
         // translated text.
         let translationDocument = this.global.content.translationDocument ||
                                   new TranslationDocument(this.global.content.document);
-        let bingTranslation = new BingTranslation(translationDocument,
-                                                  msg.data.from,
-                                                  msg.data.to);
+        let bingTranslator = new BingTranslator(translationDocument,
+                                                msg.data.from,
+                                                msg.data.to);
 
         this.global.content.translationDocument = translationDocument;
         translationDocument.translatedFrom = msg.data.from;
         translationDocument.translatedTo = msg.data.to;
         translationDocument.translationError = false;
 
-        bingTranslation.translate().then(
+        bingTranslator.translate().then(
           result => {
             this.global.sendAsyncMessage("Translation:Finished", {
               characterCount: result.characterCount,
               from: msg.data.from,
               to: msg.data.to,
               success: true
             });
             translationDocument.showTranslation();
new file mode 100644
--- /dev/null
+++ b/browser/components/translation/test/bing.sjs
@@ -0,0 +1,218 @@
+/* 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 {classes: Cc, interfaces: Ci, Constructor: CC} = Components;
+const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
+                             "nsIBinaryInputStream",
+                             "setInputStream");
+
+function handleRequest(req, res) {
+  try {
+    reallyHandleRequest(req, res);
+  } catch (ex) {
+    res.setStatusLine("1.0", 200, "AlmostOK");
+    let msg = "Error handling request: " + ex + "\n" + ex.stack;
+    log(msg);
+    res.write(msg);
+  }
+}
+
+function log(msg) {
+  // dump("BING-SERVER-MOCK: " + msg + "\n");
+}
+
+const statusCodes = {
+  400: "Bad Request",
+  401: "Unauthorized",
+  403: "Forbidden",
+  404: "Not Found",
+  405: "Method Not Allowed",
+  500: "Internal Server Error",
+  501: "Not Implemented",
+  503: "Service Unavailable"
+};
+
+function HTTPError(code = 500, message) {
+  this.code = code;
+  this.name = statusCodes[code] || "HTTPError";
+  this.message = message || this.name;
+}
+HTTPError.prototype = new Error();
+HTTPError.prototype.constructor = HTTPError;
+
+function sendError(res, err) {
+  if (!(err instanceof HTTPError)) {
+    err = new HTTPError(typeof err == "number" ? err : 500,
+                        err.message || typeof err == "string" ? err : "");
+  }
+  res.setStatusLine("1.1", err.code, err.name);
+  res.write(err.message);
+}
+
+function parseQuery(query) {
+  let ret = {};
+  for (let param of query.replace(/^[?&]/, "").split("&")) {
+    param = param.split("=");
+    if (!param[0])
+      continue;
+    ret[unescape(param[0])] = unescape(param[1]);
+  }
+  return ret;
+}
+
+function getRequestBody(req) {
+  let avail;
+  let bytes = [];
+  let body = new BinaryInputStream(req.bodyInputStream);
+
+  while ((avail = body.available()) > 0)
+    Array.prototype.push.apply(bytes, body.readByteArray(avail));
+
+  return String.fromCharCode.apply(null, bytes);
+}
+
+function sha1(str) {
+  let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+                    .createInstance(Ci.nsIScriptableUnicodeConverter);
+  converter.charset = "UTF-8";
+  // `result` is an out parameter, `result.value` will contain the array length.
+  let result = {};
+  // `data` is an array of bytes.
+  let data = converter.convertToByteArray(str, result);
+  let ch = Cc["@mozilla.org/security/hash;1"]
+             .createInstance(Ci.nsICryptoHash);
+  ch.init(ch.SHA1);
+  ch.update(data, data.length);
+  let hash = ch.finish(false);
+
+  // Return the two-digit hexadecimal code for a byte.
+  function toHexString(charCode) {
+    return ("0" + charCode.toString(16)).slice(-2);
+  }
+
+  // Convert the binary hash data to a hex string.
+  return [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
+}
+
+function parseXml(body) {
+  let DOMParser = Cc["@mozilla.org/xmlextras/domparser;1"]
+                    .createInstance(Ci.nsIDOMParser);
+  let xml = DOMParser.parseFromString(body, "text/xml");
+  if (xml.documentElement.localName == "parsererror")
+    throw new Error("Invalid XML");
+  return xml;
+}
+
+function getInputStream(path) {
+  let file = Cc["@mozilla.org/file/directory_service;1"]
+               .getService(Ci.nsIProperties)
+               .get("CurWorkD", Ci.nsILocalFile);
+  for (let part of path.split("/"))
+    file.append(part);
+  let fileStream  = Cc["@mozilla.org/network/file-input-stream;1"]
+                      .createInstance(Ci.nsIFileInputStream);
+  fileStream.init(file, 1, 0, false);
+  return fileStream;
+}
+
+function checkAuth(req) {
+  let err = new Error("Authorization failed");
+  err.code = 401;
+
+  if (!req.hasHeader("Authorization"))
+    throw new HTTPError(401, "No Authorization header provided.");
+
+  let auth = req.getHeader("Authorization");
+  if (!auth.startsWith("Bearer "))
+    throw new HTTPError(401, "Invalid Authorization header content: '" + auth + "'");
+}
+
+function reallyHandleRequest(req, res) {
+  log("method: " + req.method);
+  if (req.method != "POST") {
+    sendError(res, "Bing only deals with POST requests, not '" + req.method + "'.");
+    return;
+  }
+
+  let body = getRequestBody(req);
+  log("body: " + body);
+
+  // First, we'll see if we're dealing with an XML body:
+  let contentType = req.hasHeader("Content-Type") ? req.getHeader("Content-Type") : null;
+  log("contentType: " + contentType);
+
+  if (contentType == "text/xml") {
+    try {
+      // For all these requests the client needs to supply the correct
+      // authentication headers.
+      checkAuth(req);
+
+      let xml = parseXml(body);
+      let method = xml.documentElement.localName;
+      log("invoking method: " + method);
+      // If the requested method is supported, delegate it to its handler.
+      if (methodHandlers[method])
+        methodHandlers[method](res, xml);
+      else
+        throw new HTTPError(501);
+    } catch (ex) {
+      sendError(res, ex, ex.code);
+    }
+  } else {
+    // Not XML, so it must be a query-string.
+    let params = parseQuery(body);
+
+    // Delegate an authentication request to the correct handler.
+    if ("grant_type" in params && params.grant_type == "client_credentials")
+      methodHandlers.authenticate(res, params);
+    else
+      sendError(res, 501);
+  }
+}
+
+const methodHandlers = {
+  authenticate: function(res, params) {
+    // Validate a few required parameters.
+    if (params.scope != "http://api.microsofttranslator.com") {
+      sendError(res, "Invalid scope.");
+      return;
+    }
+    if (!params.client_id) {
+      sendError(res, "Missing client_id param.");
+      return;
+    }
+    if (!params.client_secret) {
+      sendError(res, "Missing client_secret param.");
+      return;
+    }
+
+    let content = JSON.stringify({
+      access_token: "test",
+      expires_in: 600
+    });
+
+    res.setStatusLine("1.1", 200, "OK");
+    res.setHeader("Content-Length", String(content.length));
+    res.setHeader("Content-Type", "application/json");
+    res.write(content);
+  },
+
+  TranslateArrayRequest: function(res, xml, body) {
+    let from = xml.querySelector("From").firstChild.nodeValue;
+    let to = xml.querySelector("To").firstChild.nodeValue
+    log("translating from '" + from + "' to '" + to + "'");
+
+    res.setStatusLine("1.1", 200, "OK");
+    res.setHeader("Content-Type", "text/xml");
+
+    let hash = sha1(body).substr(0, 10);
+    log("SHA1 hash of content: " + hash);
+    let inputStream = getInputStream(
+      "browser/browser/components/translation/test/fixtures/result-" + hash + ".txt");
+    res.bodyOutputStream.writeFrom(inputStream, inputStream.available());
+    inputStream.close();
+  }
+};
--- a/browser/components/translation/test/browser.ini
+++ b/browser/components/translation/test/browser.ini
@@ -1,6 +1,10 @@
 [DEFAULT]
+support-files =
+  bing.sjs
+  fixtures/bug1022725-fr.html
+  fixtures/result-da39a3ee5e.txt
 
+[browser_translation_bing.js]
 [browser_translation_fhr.js]
-skip-if = true # Needs to wait until bug 1022725.
 [browser_translation_infobar.js]
 [browser_translation_exceptions.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/translation/test/browser_translation_bing.js
@@ -0,0 +1,56 @@
+/* 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/. */
+
+// Test the Bing Translator client against a mock Bing service, bing.sjs.
+
+"use strict";
+
+const kClientIdPref = "browser.translation.bing.clientIdOverride";
+const kClientSecretPref = "browser.translation.bing.apiKeyOverride";
+
+const {BingTranslator} = Cu.import("resource:///modules/translation/BingTranslator.jsm", {});
+const {TranslationDocument} = Cu.import("resource:///modules/translation/TranslationDocument.jsm", {});
+
+function test() {
+  waitForExplicitFinish();
+
+  Services.prefs.setCharPref(kClientIdPref, "testClient");
+  Services.prefs.setCharPref(kClientSecretPref, "testSecret");
+
+  // Deduce the Mochitest server address in use from a pref that was pre-processed.
+  let server = Services.prefs.getCharPref("browser.translation.bing.authURL")
+                             .replace("http://", "");
+  server = server.substr(0, server.indexOf("/"));
+  let tab = gBrowser.addTab("http://" + server +
+    "/browser/browser/components/translation/test/fixtures/bug1022725-fr.html");
+  gBrowser.selectedTab = tab;
+
+  registerCleanupFunction(function () {
+    gBrowser.removeTab(tab);
+    Services.prefs.clearUserPref(kClientIdPref);
+    Services.prefs.clearUserPref(kClientSecretPref);
+  });
+
+  let browser = tab.linkedBrowser;
+  browser.addEventListener("load", function onload() {
+    if (browser.currentURI.spec == "about:blank")
+      return;
+
+    browser.removeEventListener("load", onload, true);
+    let client = new BingTranslator(
+      new TranslationDocument(browser.contentDocument), "fr", "en");
+
+    client.translate().then(
+      result => {
+        // XXXmikedeboer; here you would continue the test/ content inspection.
+        ok(result, "There should be a result.");
+        finish();
+      },
+      error => {
+        ok(false, "Unexpected Client Error: " + error);
+        finish();
+      }
+    );
+  }, true);
+}
--- a/browser/components/translation/test/browser_translation_fhr.js
+++ b/browser/components/translation/test/browser_translation_fhr.js
@@ -52,17 +52,18 @@ function retrieveTranslationCounts() {
     let values = yield measurement.getValues();
 
     let day = values.days.getDay(new Date());
     if (!day) {
       // This should never happen except when the test runs at midnight.
       return [0, 0];
     }
 
-    return [day.get("pageTranslatedCount"), day.get("charactersTranslatedCount")];
+    // .get() may return `undefined`, which we can't compute.
+    return [day.get("pageTranslatedCount") || 0, day.get("charactersTranslatedCount") || 0];
   });
 }
 
 function translate(text, from, to) {
   return Task.spawn(function* task_translate() {
     // Create some content to translate.
     let tab = gBrowser.selectedTab =
       gBrowser.addTab("data:text/html;charset=utf-8," + text);
new file mode 100644
--- /dev/null
+++ b/browser/components/translation/test/fixtures/bug1022725-fr.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<html lang="fr">
+  <head>
+    <!--
+     - Text retrieved from http://fr.wikipedia.org/wiki/Coupe_du_monde_de_football_de_2014
+     - at 06/13/2014, Creative Commons Attribution-ShareAlike License.
+     -->
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <title>test</title>
+  </head>
+  <body>
+    <h1>Coupe du monde de football de 2014</h1>
+    <div>La Coupe du monde de football de 2014 est la 20e édition de la Coupe du monde de football, compétition organisée par la FIFA et qui réunit les trente-deux meilleures sélections nationales. Sa phase finale a lieu à l'été 2014 au Brésil. Avec le pays organisateur, toutes les équipes championnes du monde depuis 1930 (Uruguay, Italie, Allemagne, Angleterre, Argentine, France et Espagne) se sont qualifiées pour cette compétition. Elle est aussi la première compétition internationale de la Bosnie-Herzégovine.</div>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/components/translation/test/fixtures/result-da39a3ee5e.txt
@@ -0,0 +1,22 @@
+<ArrayOfTranslateArrayResponse xmlns="http://schemas.datacontract.org/2004/07/Microsoft.MT.Web.Service.V2" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
+  <TranslateArrayResponse>
+    <From>fr</From>
+    <OriginalTextSentenceLengths xmlns:a="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
+      <a:int>34</a:int>
+    </OriginalTextSentenceLengths>
+    <TranslatedText>Football's 2014 World Cup</TranslatedText>
+    <TranslatedTextSentenceLengths xmlns:a="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
+      <a:int>25</a:int>
+    </TranslatedTextSentenceLengths>
+  </TranslateArrayResponse>
+  <TranslateArrayResponse>
+    <From>fr</From>
+    <OriginalTextSentenceLengths xmlns:a="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
+      <a:int>508</a:int>
+    </OriginalTextSentenceLengths>
+    <TranslatedText>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus diam sem, porttitor eget neque sit amet, ultricies posuere metus. Cras placerat rutrum risus, nec dignissim magna dictum vitae. Fusce eleifend fermentum lacinia. Nulla sagittis cursus nibh. Praesent adipiscing, elit at pulvinar dapibus, neque massa tincidunt sapien, eu consectetur lectus metus sit amet odio. Proin blandit consequat porttitor. Pellentesque vehicula justo sed luctus vestibulum. Donec metus.</TranslatedText>
+    <TranslatedTextSentenceLengths xmlns:a="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
+      <a:int>475</a:int>
+    </TranslatedTextSentenceLengths>
+  </TranslateArrayResponse>
+</ArrayOfTranslateArrayResponse>
--- a/browser/components/translation/translation-infobar.xml
+++ b/browser/components/translation/translation-infobar.xml
@@ -147,21 +147,16 @@
           ]]>
         </setter>
       </property>
 
       <method name="init">
         <parameter name="aTranslation"/>
         <body>
           <![CDATA[
-            if (Translation.serviceUnavailable) {
-              this.state = Translation.STATE_UNAVAILABLE;
-              return;
-            }
-
             this.translation = aTranslation;
             let bundle = Cc["@mozilla.org/intl/stringbundle;1"]
                            .getService(Ci.nsIStringBundleService)
                            .createBundle("chrome://global/locale/languageNames.properties");
 
             // Fill the lists of supported source languages.
             let detectedLanguage = this._getAnonElt("detectedLanguage");
             let fromLanguage = this._getAnonElt("fromLanguage");
--- a/browser/devtools/app-manager/content/connection-footer.xhtml
+++ b/browser/devtools/app-manager/content/connection-footer.xhtml
@@ -122,17 +122,17 @@
 
       </div>
     </div>
   </body>
 
   <template id="simulator-item-template">
   <span>
     <button class="simulator-item action-primary" onclick="UI.startSimulator(this.dataset.version)" template='{"type":"attribute","path":"version","name":"data-version"}' title="&connection.startSimulatorTooltip;">
-      <span template='{"type":"textContent", "path":"version"}'></span>
+      <span template='{"type":"textContent", "path":"label"}'></span>
     </button>
   </span>
   </template>
 
   <template id="adb-devices-template">
   <span>
     <button class="adb-device action-primary" onclick="UI.connectToAdbDevice(this.dataset.name)" template='{"type":"attribute","path":"name","name":"data-name"}'>
       <span template='{"type":"textContent", "path":"name"}'></span>
--- a/browser/devtools/app-manager/simulators-store.js
+++ b/browser/devtools/app-manager/simulators-store.js
@@ -5,17 +5,21 @@
 const {Cu} = require("chrome");
 const ObservableObject = require("devtools/shared/observable-object");
 const {Simulator} = Cu.import("resource://gre/modules/devtools/Simulator.jsm");
 
 let store = new ObservableObject({versions:[]});
 
 function feedStore() {
   store.object.versions = Simulator.availableVersions().map(v => {
-    return {version:v}
+    let simulator = Simulator.getByVersion(v);
+    return {
+      version: v,
+      label: simulator.appinfo.label
+    }
   });
 }
 
 Simulator.on("register", feedStore);
 Simulator.on("unregister", feedStore);
 feedStore();
 
 module.exports = store;
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webide/content/addons.js
@@ -0,0 +1,117 @@
+/* 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;
+const {Services} = Cu.import("resource://gre/modules/Services.jsm");
+const {require} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
+const {GetAvailableAddons} = require("devtools/webide/addons");
+const Strings = Services.strings.createBundle("chrome://webide/content/webide.properties");
+
+window.addEventListener("load", function onLoad() {
+  window.removeEventListener("load", onLoad);
+  document.querySelector("#aboutaddons").onclick = function() {
+    window.parent.UI.openInBrowser("about:addons");
+  }
+  document.querySelector("#close").onclick = CloseUI;
+  GetAvailableAddons().then(BuildUI, (e) => {
+    console.error(e);
+    window.alert(Strings.formatStringFromName("error_cantFetchAddonsJSON", [e], 1));
+  });
+}, true);
+
+function CloseUI() {
+  window.parent.UI.openProject();
+}
+
+function BuildUI(addons) {
+  BuildItem(addons.adb, true /* is adb */);
+  for (let addon of addons.simulators) {
+    BuildItem(addon, false /* is adb */);
+  }
+}
+
+function BuildItem(addon, isADB) {
+
+  function onAddonUpdate(event, arg) {
+    switch (event) {
+      case "update":
+        progress.removeAttribute("value");
+        li.setAttribute("status", addon.status);
+        status.textContent = Strings.GetStringFromName("addons_status_" + addon.status);
+        break;
+      case "failure":
+        console.error(arg);
+        window.alert(arg);
+        break;
+      case "progress":
+        if (arg == -1) {
+          progress.removeAttribute("value");
+        } else {
+          progress.value = arg;
+        }
+        break;
+    }
+  }
+
+  let events = ["update", "failure", "progress"];
+  for (let e of events) {
+    addon.on(e, onAddonUpdate);
+  }
+  window.addEventListener("unload", function onUnload() {
+    window.removeEventListener("unload", onUnload);
+    for (let e of events) {
+      addon.off(e, onAddonUpdate);
+    }
+  });
+
+  let li = document.createElement("li");
+  li.setAttribute("status", addon.status);
+
+  // Used in tests
+  if (isADB) {
+    li.setAttribute("addon", "adb");
+  } else {
+    li.setAttribute("addon", "simulator-" + addon.version);
+  }
+
+  let name = document.createElement("span");
+  name.className = "name";
+  if (isADB) {
+    name.textContent = Strings.GetStringFromName("addons_adb_label");
+  } else {
+    let stability = Strings.GetStringFromName("addons_" + addon.stability);
+    name.textContent = Strings.formatStringFromName("addons_simulator_label", [addon.version, stability], 2);
+  }
+
+  li.appendChild(name);
+
+  let status = document.createElement("span");
+  status.className = "status";
+  status.textContent = Strings.GetStringFromName("addons_status_" + addon.status);
+  li.appendChild(status);
+
+  let installButton = document.createElement("button");
+  installButton.className = "install-button";
+  installButton.onclick = () => addon.install();
+  installButton.textContent = Strings.GetStringFromName("addons_install_button");
+  li.appendChild(installButton);
+
+  let uninstallButton = document.createElement("button");
+  uninstallButton.className = "uninstall-button";
+  uninstallButton.onclick = () => addon.uninstall();
+  uninstallButton.textContent = Strings.GetStringFromName("addons_uninstall_button");
+  li.appendChild(uninstallButton);
+
+  let progress = document.createElement("progress");
+  li.appendChild(progress);
+
+  if (isADB) {
+    let warning = document.createElement("p");
+    warning.textContent = Strings.GetStringFromName("addons_adb_warning");
+    warning.className = "warning";
+    li.appendChild(warning);
+  }
+
+  document.querySelector("ul").appendChild(li);
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webide/content/addons.xhtml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE html [
+  <!ENTITY % webideDTD SYSTEM "chrome://webide/content/webide.dtd" >
+  %webideDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <meta charset="utf8"/>
+    <link rel="stylesheet" href="chrome://webide/skin/addons.css" type="text/css"/>
+    <script type="application/javascript;version=1.8" src="chrome://webide/content/addons.js"></script>
+  </head>
+  <body>
+
+    <div id="controls">
+      <a id="aboutaddons">&addons_aboutaddons;</a>
+      <a id="close">&deck_close;</a>
+    </div>
+
+    <h1>&addons_title;</h1>
+
+    <ul></ul>
+
+  </body>
+</html>
--- a/browser/devtools/webide/content/jar.mn
+++ b/browser/devtools/webide/content/jar.mn
@@ -6,14 +6,20 @@ webide.jar:
 %   content webide %content/
     content/webide.xul                (webide.xul)
     content/webide.js                 (webide.js)
     content/newapp.xul                (newapp.xul)
     content/newapp.js                 (newapp.js)
     content/details.xhtml             (details.xhtml)
     content/details.js                (details.js)
     content/cli.js                    (cli.js)
+    content/addons.js                 (addons.js)
+    content/addons.xhtml              (addons.xhtml)
+    content/permissionstable.js       (permissionstable.js)
+    content/permissionstable.xhtml    (permissionstable.xhtml)
+    content/runtimedetails.js         (runtimedetails.js)
+    content/runtimedetails.xhtml      (runtimedetails.xhtml)
 
 # Temporarily include locales in content, until we're ready
 # to localize webide
 
     content/webide.dtd                (../locales/en-US/webide.dtd)
     content/webide.properties         (../locales/en-US/webide.properties)
--- a/browser/devtools/webide/content/newapp.js
+++ b/browser/devtools/webide/content/newapp.js
@@ -12,46 +12,39 @@ Cu.import("resource://gre/modules/XPCOMU
 XPCOMUtils.defineLazyModuleGetter(this, "ZipUtils", "resource://gre/modules/ZipUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Downloads", "resource://gre/modules/Downloads.jsm");
 
 const {require} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
 const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm");
 const {AppProjects} = require("devtools/app-manager/app-projects");
 const APP_CREATOR_LIST = "devtools.webide.templatesURL";
 const {AppManager} = require("devtools/webide/app-manager");
+const {GetTemplatesJSON} = require("devtools/webide/remote-resources");
 
 let gTemplateList = null;
 
 // See bug 989619
 console.log = console.log.bind(console);
 console.warn = console.warn.bind(console);
 console.error = console.error.bind(console);
 
 window.addEventListener("load", function onLoad() {
   window.removeEventListener("load", onLoad);
   let projectNameNode = document.querySelector("#project-name");
   projectNameNode.addEventListener("input", canValidate, true);
   getJSON();
 }, true);
 
 function getJSON() {
-  let xhr = new XMLHttpRequest();
-  xhr.overrideMimeType('text/plain');
-  xhr.onload = function() {
-    let list;
-    try {
-      list = JSON.parse(this.responseText);
-      if (!Array.isArray(list)) {
-        throw new Error("JSON response not an array");
-      }
-      if (list.length == 0) {
-        throw new Error("JSON response is an empty array");
-      }
-    } catch(e) {
-      return failAndBail("Invalid response from server");
+  GetTemplatesJSON().then(list => {
+    if (!Array.isArray(list)) {
+      throw new Error("JSON response not an array");
+    }
+    if (list.length == 0) {
+      throw new Error("JSON response is an empty array");
     }
     gTemplateList = list;
     let templatelistNode = document.querySelector("#templatelist");
     templatelistNode.innerHTML = "";
     for (let template of list) {
       let richlistitemNode = document.createElement("richlistitem");
       let imageNode = document.createElement("image");
       imageNode.setAttribute("src", template.icon);
@@ -71,23 +64,19 @@ function getJSON() {
 
     /* Chrome mochitest support */
     let testOptions = window.arguments[0].testOptions;
     if (testOptions) {
       templatelistNode.selectedIndex = testOptions.index;
       document.querySelector("#project-name").value = testOptions.name;
       doOK();
     }
-  };
-  xhr.onerror = function() {
-    failAndBail("Can't download app templates");
-  };
-  let url = Services.prefs.getCharPref(APP_CREATOR_LIST);
-  xhr.open("get", url);
-  xhr.send();
+  }, (e) => {
+    failAndBail("Can't download app templates: " + e);
+  });
 }
 
 function failAndBail(msg) {
   let promptService = Cc["@mozilla.org/embedcomp/prompt-service;1"].getService(Ci.nsIPromptService);
   promptService.alert(window, "error", msg);
   window.close();
 }
 
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webide/content/permissionstable.js
@@ -0,0 +1,75 @@
+/* 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;
+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");
+
+window.addEventListener("load", function onLoad() {
+  window.removeEventListener("load", onLoad);
+  document.querySelector("#close").onclick = CloseUI;
+  AppManager.on("app-manager-update", OnAppManagerUpdate);
+  BuildUI();
+}, true);
+
+window.addEventListener("unload", function onUnload() {
+  window.removeEventListener("unload", onUnload);
+  AppManager.off("app-manager-update", OnAppManagerUpdate);
+});
+
+function CloseUI() {
+  window.parent.UI.openProject();
+}
+
+function OnAppManagerUpdate(event, what) {
+  if (what == "connection" || what == "list-tabs-response") {
+    BuildUI();
+  }
+}
+
+let getRawPermissionsTablePromise; // Used by tests
+function BuildUI() {
+  let table = document.querySelector("table");
+  let lines = table.querySelectorAll(".line");
+  for (let line of lines) {
+    line.remove();
+  }
+
+  if (AppManager.connection &&
+      AppManager.connection.status == Connection.Status.CONNECTED &&
+      AppManager.deviceFront) {
+    getRawPermissionsTablePromise = AppManager.deviceFront.getRawPermissionsTable();
+    getRawPermissionsTablePromise.then(json => {
+      let permissionsTable = json.rawPermissionsTable;
+      for (let name in permissionsTable) {
+        let tr = document.createElement("tr");
+        tr.className = "line";
+        let td = document.createElement("td");
+        td.textContent = name;
+        tr.appendChild(td);
+        for (let type of ["app","privileged","certified"]) {
+          let td = document.createElement("td");
+          if (permissionsTable[name][type] == json.ALLOW_ACTION) {
+            td.textContent = "✓";
+            td.className = "permallow";
+          }
+          if (permissionsTable[name][type] == json.PROMPT_ACTION) {
+            td.textContent = "!";
+            td.className = "permprompt";
+          }
+          if (permissionsTable[name][type] == json.DENY_ACTION) {
+            td.textContent = "✕";
+            td.className = "permdeny"
+          }
+          tr.appendChild(td);
+        }
+        table.appendChild(tr);
+      }
+    });
+  } else {
+    CloseUI();
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webide/content/permissionstable.xhtml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE html [
+  <!ENTITY % webideDTD SYSTEM "chrome://webide/content/webide.dtd" >
+  %webideDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <meta charset="utf8"/>
+    <link rel="stylesheet" href="chrome://webide/skin/tabledoc.css" type="text/css"/>
+    <script type="application/javascript;version=1.8" src="chrome://webide/content/permissionstable.js"></script>
+  </head>
+  <body>
+
+    <div id="controls">
+      <a id="close">&deck_close;</a>
+    </div>
+
+    <h1>&permissionstable_title;</h1>
+
+    <table class="permissionstable">
+      <tr>
+        <th>&permissionstable_name_header;</th>
+        <th>type:web</th>
+        <th>type:privileged</th>
+        <th>type:certified</th>
+      </tr>
+    </table>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webide/content/runtimedetails.js
@@ -0,0 +1,56 @@
+/* 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;
+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");
+
+window.addEventListener("load", function onLoad() {
+  window.removeEventListener("load", onLoad);
+  document.querySelector("#close").onclick = CloseUI;
+  AppManager.on("app-manager-update", OnAppManagerUpdate);
+  BuildUI();
+}, true);
+
+window.addEventListener("unload", function onUnload() {
+  window.removeEventListener("unload", onUnload);
+  AppManager.off("app-manager-update", OnAppManagerUpdate);
+});
+
+function CloseUI() {
+  window.parent.UI.openProject();
+}
+
+function OnAppManagerUpdate(event, what) {
+  if (what == "connection" || what == "list-tabs-response") {
+    BuildUI();
+  }
+}
+
+let getDescriptionPromise; // Used by tests
+function BuildUI() {
+  let table = document.querySelector("table");
+  table.innerHTML = "";
+  if (AppManager.connection &&
+      AppManager.connection.status == Connection.Status.CONNECTED &&
+      AppManager.deviceFront) {
+    getDescriptionPromise = AppManager.deviceFront.getDescription();
+    getDescriptionPromise.then(json => {
+      for (let name in json) {
+        let tr = document.createElement("tr");
+        let td = document.createElement("td");
+        td.textContent = name;
+        tr.appendChild(td);
+        td = document.createElement("td");
+        td.textContent = json[name];
+        tr.appendChild(td);
+        table.appendChild(tr);
+      }
+    });
+  } else {
+    CloseUI();
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webide/content/runtimedetails.xhtml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE html [
+  <!ENTITY % webideDTD SYSTEM "chrome://webide/content/webide.dtd" >
+  %webideDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <meta charset="utf8"/>
+    <link rel="stylesheet" href="chrome://webide/skin/tabledoc.css" type="text/css"/>
+    <script type="application/javascript;version=1.8" src="chrome://webide/content/runtimedetails.js"></script>
+  </head>
+  <body>
+
+    <div id="controls">
+      <a id="close">&deck_close;</a>
+    </div>
+
+    <h1>&runtimedetails_title;</h1>
+
+    <table></table>
+  </body>
+</html>
--- a/browser/devtools/webide/content/webide.js
+++ b/browser/devtools/webide/content/webide.js
@@ -13,22 +13,29 @@ Cu.import("resource://gre/modules/Task.j
 const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 const {require} = devtools;
 const {Services} = Cu.import("resource://gre/modules/Services.jsm");
 const {AppProjects} = require("devtools/app-manager/app-projects");
 const {Connection} = require("devtools/client/connection-manager");
 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 Strings = Services.strings.createBundle("chrome://webide/content/webide.properties");
 
 const HTML = "http://www.w3.org/1999/xhtml";
 const HELP_URL = "https://developer.mozilla.org/Firefox_OS/Using_the_App_Manager#Troubleshooting";
 
+// download some JSON early.
+GetTemplatesJSON(true);
+GetAddonsJSON(true);
+
 // See bug 989619
 console.log = console.log.bind(console);
 console.warn = console.warn.bind(console);
 console.error = console.error.bind(console);
 
 window.addEventListener("load", function onLoad() {
   window.removeEventListener("load", onLoad);
   UI.init();
@@ -51,28 +58,42 @@ let UI = {
 
     this.updateCommands();
     this.updateRuntimeList();
 
     this.onfocus = this.onfocus.bind(this);
     window.addEventListener("focus", this.onfocus, true);
 
     AppProjects.load().then(() => {
-      let lastProjectLocation = Services.prefs.getCharPref("devtools.webide.lastprojectlocation");
-      if (lastProjectLocation) {
-        let lastProject = AppProjects.get(lastProjectLocation);
-        if (lastProject) {
-          AppManager.selectedProject = lastProject;
-        } else {
-          AppManager.selectedProject = null;
-        }
+      this.openLastProject();
+    });
+
+    // Auto install the ADB Addon Helper. Only once.
+    // If the user decides to uninstall the addon, we won't install it again.
+    let autoInstallADBHelper = Services.prefs.getBoolPref("devtools.webide.autoinstallADBHelper");
+    if (autoInstallADBHelper && !Devices.helperAddonInstalled) {
+      GetAvailableAddons().then(addons => {
+        addons.adb.install();
+      }, console.error);
+    }
+    Services.prefs.setBoolPref("devtools.webide.autoinstallADBHelper", false);
+  },
+
+  openLastProject: function() {
+    let lastProjectLocation = Services.prefs.getCharPref("devtools.webide.lastprojectlocation");
+    if (lastProjectLocation) {
+      let lastProject = AppProjects.get(lastProjectLocation);
+      if (lastProject) {
+        AppManager.selectedProject = lastProject;
       } else {
         AppManager.selectedProject = null;
       }
-    });
+    } else {
+      AppManager.selectedProject = null;
+    }
   },
 
   uninit: function() {
     window.removeEventListener("focus", this.onfocus, true);
     AppManager.off("app-manager-update", this.appManagerUpdate);
     AppManager.uninit();
     window.removeEventListener("message", this.onMessage);
   },
@@ -198,29 +219,51 @@ let UI = {
     let buttons = [{
       label: Strings.GetStringFromName("notification_showTroubleShooting_label"),
       accessKey: Strings.GetStringFromName("notification_showTroubleShooting_accesskey"),
       callback: function () {
         Cmds.showTroubleShooting();
       }
     }];
 
-    let nbox = document.querySelector("#body");
+    let nbox = document.querySelector("#notificationbox");
     nbox.removeAllNotifications(true);
     nbox.appendNotification(text, "webide:errornotification", null,
                             nbox.PRIORITY_WARNING_LOW, buttons);
   },
 
   /********** RUNTIME **********/
 
   updateRuntimeList: function() {
     let USBListNode = document.querySelector("#runtime-panel-usbruntime");
     let simulatorListNode = document.querySelector("#runtime-panel-simulators");
     let customListNode = document.querySelector("#runtime-panel-custom");
 
+    let noHelperNode = document.querySelector("#runtime-panel-noadbhelper");
+    let noUSBNode = document.querySelector("#runtime-panel-nousbdevice");
+    let noSimulatorNode = document.querySelector("#runtime-panel-nosimulator");
+
+    if (Devices.helperAddonInstalled) {
+      noHelperNode.setAttribute("hidden", "true");
+    } else {
+      noHelperNode.removeAttribute("hidden");
+    }
+
+    if (AppManager.runtimeList.usb.length == 0 && Devices.helperAddonInstalled) {
+      noUSBNode.removeAttribute("hidden");
+    } else {
+      noUSBNode.setAttribute("hidden", "true");
+    }
+
+    if (AppManager.runtimeList.simulator.length > 0) {
+      noSimulatorNode.setAttribute("hidden", "true");
+    } else {
+      noSimulatorNode.removeAttribute("hidden");
+    }
+
     for (let [type, parent] of [
       ["usb", USBListNode],
       ["simulator", simulatorListNode],
       ["custom", customListNode],
     ]) {
       while (parent.hasChildNodes()) {
         parent.firstChild.remove();
       }
@@ -278,17 +321,17 @@ let UI = {
 
   // ProjectEditor & details screen
 
   getProjectEditor: function() {
     if (this.projecteditor) {
       return this.projecteditor.loaded;
     }
 
-    let projecteditorIframe = document.querySelector("#projecteditor");
+    let projecteditorIframe = document.querySelector("#deck-panel-projecteditor");
     this.projecteditor = ProjectEditor.ProjectEditor(projecteditorIframe);
     this.projecteditor.on("onEditorSave", (editor, resource) => {
       AppManager.validateProject(AppManager.selectedProject);
     });
     return this.projecteditor.loaded;
   },
 
   updateProjectEditorHeader: function() {
@@ -301,72 +344,84 @@ let UI = {
       status = "error";
     }
     this.getProjectEditor().then((projecteditor) => {
       projecteditor.setProjectToAppPath(project.location, {
         name: project.name,
         iconUrl: project.icon,
         projectOverviewURL: "chrome://webide/content/details.xhtml",
         validationStatus: status
-      });
+      }).then(null, console.error);
     }, console.error);
   },
 
   isProjectEditorEnabled: function() {
     return Services.prefs.getBoolPref("devtools.webide.showProjectEditor");
   },
 
   openProject: function() {
-    let detailsIframe = document.querySelector("#details");
-    let projecteditorIframe = document.querySelector("#projecteditor");
-
     let project = AppManager.selectedProject;
 
     // Nothing to show
 
     if (!project) {
-      detailsIframe.setAttribute("hidden", "true");
-      projecteditorIframe.setAttribute("hidden", "true");
-      document.commandDispatcher.focusedElement = document.documentElement;
+      this.resetDeck();
       return;
     }
 
     // Make sure the directory exist before we show Project Editor
 
     let forceDetailsOnly = false;
     if (project.type == "packaged") {
       let directory = new FileUtils.File(project.location);
       forceDetailsOnly = !directory.exists();
     }
 
     // Show only the details screen
 
     if (project.type != "packaged" ||
         !this.isProjectEditorEnabled() ||
         forceDetailsOnly) {
-      detailsIframe.removeAttribute("hidden");
-      projecteditorIframe.setAttribute("hidden", "true");
-      document.commandDispatcher.focusedElement = document.documentElement;
+      this.selectDeckPanel("details");
       return;
     }
 
     // Show ProjectEditor
 
-    detailsIframe.setAttribute("hidden", "true");
-    projecteditorIframe.removeAttribute("hidden");
+    this.selectDeckPanel("projecteditor");
 
     this.getProjectEditor().then(() => {
       this.updateProjectEditorHeader();
     }, console.error);
 
     if (project.location) {
       Services.prefs.setCharPref("devtools.webide.lastprojectlocation", project.location);
     }
   },
 
+  /********** DECK **********/
+
+  resetFocus: function() {
+    document.commandDispatcher.focusedElement = document.documentElement;
+  },
+
+  selectDeckPanel: function(id) {
+    this.hidePanels();
+    this.resetFocus();
+    let deck = document.querySelector("#deck");
+    let panel = deck.querySelector("#deck-panel-" + id);
+    deck.selectedPanel = panel;
+  },
+
+  resetDeck: function() {
+    this.resetFocus();
+    let deck = document.querySelector("#deck");
+    deck.selectedPanel = null;
+  },
+
   /********** COMMANDS **********/
 
   updateCommands: function() {
 
     if (document.querySelector("window").classList.contains("busy")) {
       document.querySelector("#cmd_newApp").setAttribute("disabled", "true");
       document.querySelector("#cmd_importPackagedApp").setAttribute("disabled", "true");
       document.querySelector("#cmd_importHostedApp").setAttribute("disabled", "true");
@@ -501,19 +556,17 @@ let UI = {
     iframe.height = height;
 
     document.querySelector("#action-button-debug").setAttribute("active", "true");
 
     return gDevTools.showToolbox(target, null, host, options);
   },
 
   closeToolboxUI: function() {
-    let body = document.querySelector("#body");
-    body.removeAttribute("hidden");
-
+    this.resetFocus();
     Services.prefs.setIntPref("devtools.toolbox.footer.height", this.toolboxIframe.height);
 
     // We have to destroy the iframe, otherwise, the keybindings of webide don't work
     // properly anymore.
     this.toolboxIframe.remove();
     this.toolboxIframe = null;
 
     let splitter = document.querySelector(".devtools-horizontal-splitter");
@@ -722,91 +775,21 @@ let Cmds = {
        return longstr.string().then(dataURL => {
          longstr.release().then(null, console.error);
          UI.openInBrowser(dataURL);
        });
     }), "taking screenshot");
   },
 
   showPermissionsTable: function() {
-    return UI.busyUntil(AppManager.deviceFront.getRawPermissionsTable().then(json => {
-      let styleContent = "";
-      styleContent += "body {background:white; font-family: monospace}";
-      styleContent += "table {border-collapse: collapse}";
-      styleContent += "th, td {padding: 5px; border: 1px solid #EEE}";
-      styleContent += "th {min-width: 130px}";
-      styleContent += "td {text-align: center}";
-      styleContent += "th:first-of-type, td:first-of-type {text-align:left}";
-      styleContent += ".permallow  {color:rgb(152, 207, 57)}";
-      styleContent += ".permprompt {color:rgb(0,158,237)}";
-      styleContent += ".permdeny   {color:rgb(204,73,8)}";
-      let style = document.createElementNS(HTML, "style");
-      style.textContent = styleContent;
-      let table = document.createElementNS(HTML, "table");
-      table.innerHTML = "<tr><th>Name</th><th>type:web</th><th>type:privileged</th><th>type:certified</th></tr>";
-      let permissionsTable = json.rawPermissionsTable;
-      for (let name in permissionsTable) {
-        let tr = document.createElementNS(HTML, "tr");
-        let td = document.createElementNS(HTML, "td");
-        td.textContent = name;
-        tr.appendChild(td);
-        for (let type of ["app","privileged","certified"]) {
-          let td = document.createElementNS(HTML, "td");
-          if (permissionsTable[name][type] == json.ALLOW_ACTION) {
-            td.textContent = "✓";
-            td.className = "permallow";
-          }
-          if (permissionsTable[name][type] == json.PROMPT_ACTION) {
-            td.textContent = "!";
-            td.className = "permprompt";
-          }
-          if (permissionsTable[name][type] == json.DENY_ACTION) {
-            td.textContent = "✕";
-            td.className = "permdeny"
-          }
-          tr.appendChild(td);
-        }
-        table.appendChild(tr);
-      }
-      let body = document.createElementNS(HTML, "body");
-      body.appendChild(style);
-      body.appendChild(table);
-      let url = "data:text/html;charset=utf-8,";
-      url += encodeURIComponent(body.outerHTML);
-      UI.openInBrowser(url);
-    }), "showing permission table");
+    UI.selectDeckPanel("permissionstable");
   },
 
   showRuntimeDetails: function() {
-    return UI.busyUntil(AppManager.deviceFront.getDescription().then(json => {
-      let styleContent = "";
-      styleContent += "body {background:white; font-family: monospace}";
-      styleContent += "table {border-collapse: collapse}";
-      styleContent += "th, td {padding: 5px; border: 1px solid #EEE}";
-      let style = document.createElementNS(HTML, "style");
-      style.textContent = styleContent;
-      let table = document.createElementNS(HTML, "table");
-      for (let name in json) {
-        let tr = document.createElementNS(HTML, "tr");
-        let td = document.createElementNS(HTML, "td");
-        td.textContent = name;
-        tr.appendChild(td);
-        td = document.createElementNS(HTML, "td");
-        td.textContent = json[name];
-        tr.appendChild(td);
-        table.appendChild(tr);
-      }
-      let body = document.createElementNS(HTML, "body");
-      body.appendChild(style);
-      body.appendChild(table);
-      let url = "data:text/html;charset=utf-8,";
-      url += encodeURIComponent(body.outerHTML);
-      UI.openInBrowser(url);
-    }), "showing runtime details");
-
+    UI.selectDeckPanel("runtimedetails");
   },
 
   play: function() {
     switch(AppManager.selectedProject.type) {
       case "packaged":
       case "hosted":
         return UI.busyUntil(AppManager.installAndRunProject(), "installing and running app");
         break;
@@ -841,9 +824,13 @@ let Cmds = {
   toggleEditors: function() {
     Services.prefs.setBoolPref("devtools.webide.showProjectEditor", !UI.isProjectEditorEnabled());
     UI.openProject();
   },
 
   showTroubleShooting: function() {
     UI.openInBrowser(HELP_URL);
   },
+
+  showAddons: function() {
+    UI.selectDeckPanel("addons");
+  },
 }
--- a/browser/devtools/webide/content/webide.xul
+++ b/browser/devtools/webide/content/webide.xul
@@ -36,16 +36,18 @@
       <command id="cmd_removeProject" oncommand="Cmds.removeProject()" label="&projectMenu_remove_label;"/>
       <command id="cmd_showProjectPanel" oncommand="Cmds.showProjectPanel()"/>
       <command id="cmd_showRuntimePanel" oncommand="Cmds.showRuntimePanel()"/>
       <command id="cmd_disconnectRuntime" oncommand="Cmds.disconnectRuntime()" label="&runtimeMenu_disconnect_label;"/>
       <command id="cmd_showPermissionsTable" oncommand="Cmds.showPermissionsTable()" label="&runtimeMenu_showPermissionTable_label;"/>
       <command id="cmd_showRuntimeDetails" oncommand="Cmds.showRuntimeDetails()" label="&runtimeMenu_showDetails_label;"/>
       <command id="cmd_takeScreenshot" oncommand="Cmds.takeScreenshot()" label="&runtimeMenu_takeScreenshot_label;"/>
       <command id="cmd_toggleEditor" oncommand="Cmds.toggleEditors()" label="&viewMenu_toggleEditor_label;"/>
+      <command id="cmd_showAddons" oncommand="Cmds.showAddons()"/>
+      <command id="cmd_showTroubleShooting" oncommand="Cmds.showTroubleShooting()"/>
       <command id="cmd_play" oncommand="Cmds.play()"/>
       <command id="cmd_stop" oncommand="Cmds.stop()"/>
       <command id="cmd_toggleToolbox" oncommand="Cmds.toggleToolbox()"/>
     </commandset>
   </commandset>
 
   <menubar id="main-menubar">
     <menu id="menu-project" label="&projectMenu_label;" accesskey="&projectMenu_accesskey;">
@@ -71,16 +73,17 @@
         <menuseparator/>
         <menuitem command="cmd_disconnectRuntime" accesskey="&runtimeMenu_disconnect_accesskey;"/>
       </menupopup>
     </menu>
 
     <menu id="menu-view" label="&viewMenu_label;" accesskey="&viewMenu_accesskey;">
       <menupopup id="menu-ViewPopup">
         <menuitem command="cmd_toggleEditor" key="key_toggleEditor" accesskey="&viewMenu_toggleEditor_accesskey;"/>
+        <menuitem command="cmd_showAddons" label="&viewMenu_showAddons_label;" accesskey="&viewMenu_showAddons_accesskey;"/>
       </menupopup>
     </menu>
 
   </menubar>
 
   <keyset id="mainKeyset">
     <key key="&key_quit;" id="key_quit" command="cmd_quit" modifiers="accel"/>
     <key key="&key_showProjectPanel;" id="key_showProjectPanel" command="cmd_showProjectPanel" modifiers="accel"/>
@@ -131,33 +134,41 @@
         <vbox flex="1" id="project-panel-runtimeapps"/>
       </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>
+        <toolbarbutton class="panel-item-help" label="&runtimePanel_nousbdevice;" id="runtime-panel-nousbdevice" command="cmd_showTroubleShooting"/>
+        <toolbarbutton class="panel-item-help" label="&runtimePanel_noadbhelper;" id="runtime-panel-noadbhelper" command="cmd_showAddons"/>
         <vbox id="runtime-panel-usbruntime"></vbox>
         <label class="panel-header">&runtimePanel_simulators;</label>
+        <toolbarbutton class="panel-item-help" label="&runtimePanel_nosimulator;" id="runtime-panel-nosimulator" command="cmd_showAddons"/>
         <vbox id="runtime-panel-simulators"></vbox>
         <label class="panel-header">&runtimePanel_custom;</label>
         <vbox id="runtime-panel-custom"></vbox>
         <vbox flex="1" id="runtime-actions" hidden="true">
           <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"/>
         </vbox>
       </vbox>
     </panel>
 
   </popupset>
 
-  <notificationbox flex="1" id="body">
-    <iframe id="details" flex="1" hidden="true" src="details.xhtml"/>
-    <iframe id="projecteditor" flex="1" hidden="true"/>
+  <notificationbox flex="1" id="notificationbox">
+    <deck flex="1" id="deck" selectedIndex="-1">
+      <iframe id="deck-panel-details" flex="1" src="details.xhtml"/>
+      <iframe id="deck-panel-projecteditor" flex="1"/>
+      <iframe id="deck-panel-addons" flex="1" src="addons.xhtml"/>
+      <iframe id="deck-panel-permissionstable" flex="1" src="permissionstable.xhtml"/>
+      <iframe id="deck-panel-runtimedetails" flex="1" src="runtimedetails.xhtml"/>
+    </deck>
   </notificationbox>
 
   <splitter hidden="true" class="devtools-horizontal-splitter" orient="vertical"/>
 
   <!-- toolbox iframe will be inserted here -->
 
 </window>
--- a/browser/devtools/webide/locales/en-US/webide.dtd
+++ b/browser/devtools/webide/locales/en-US/webide.dtd
@@ -33,16 +33,18 @@
 <!ENTITY runtimeMenu_takeScreenshot_accesskey "S">
 <!ENTITY runtimeMenu_showDetails_label "Runtime Info">
 <!ENTITY runtimeMenu_showDetails_accesskey "E">
 
 <!ENTITY viewMenu_label "View">
 <!ENTITY viewMenu_accesskey "V">
 <!ENTITY viewMenu_toggleEditor_label "Toggle Editor">
 <!ENTITY viewMenu_toggleEditor_accesskey "E">
+<!ENTITY viewMenu_showAddons_label "Manage simulators">
+<!ENTITY viewMenu_showAddons_accesskey "M">
 
 <!ENTITY projectButton_label "Open App">
 <!ENTITY runtimeButton_label "Select Runtime">
 
 <!-- We try to repicate Firefox' bindings: -->
 <!-- quit app -->
 <!ENTITY key_quit "Q">
 <!-- open menu -->
@@ -56,23 +58,42 @@
 <!-- toggle sidebar -->
 <!ENTITY key_toggleEditor "B">
 
 <!ENTITY projectPanel_myProjects "My Projects">
 <!ENTITY projectPanel_runtimeApps "Runtime Apps">
 <!ENTITY runtimePanel_USBDevices "USB Devices">
 <!ENTITY runtimePanel_simulators "Simulators">
 <!ENTITY runtimePanel_custom "Custom">
+<!ENTITY runtimePanel_nosimulator "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">
 <!ENTITY details_description "Description">
 <!ENTITY details_location "Location">
 <!ENTITY details_manifestURL "App ID">
 <!ENTITY details_removeProject_button "Remove Project">
 
 <!-- New App -->
 <!ENTITY newAppWindowTitle "New App">
 <!ENTITY newAppHeader "Select template">
 <!ENTITY newAppLoadingTemplate "Loading templates…">
 <!ENTITY newAppProjectName "Project Name:">
+
+
+<!-- Decks -->
+
+<!ENTITY deck_close "close">
+
+<!-- Addons -->
+<!ENTITY addons_title "Extra Components:">
+<!ENTITY addons_aboutaddons "Open Addons Manager">
+
+<!-- Permissions Table -->
+<!ENTITY permissionstable_title "Permissions Table">
+<!ENTITY permissionstable_name_header "Name">
+
+<!-- Runtime Details -->
+<!ENTITY runtimedetails_title "Runtime Info">
--- a/browser/devtools/webide/locales/en-US/webide.properties
+++ b/browser/devtools/webide/locales/en-US/webide.properties
@@ -21,8 +21,23 @@ notification_showTroubleShooting_label=t
 notification_showTroubleShooting_accesskey=t
 
 error_operationTimeout=Operation timed out: %1$S
 error_operationFail=Operation failed: %1$S
 error_listRunningApps=Can't get app list from device
 error_cantConnectToApp=Can't connect to app: %1$S
 error_cantInstallNotFullyConnected=Can't install project. Not fully connected.
 error_cantInstallValidationErrors=Can't install project. Validation errors.
+error_cantFetchAddonsJSON=Can't fetch the addon list: %S
+
+addons_stable=stable
+addons_unstable=unstable
+addons_simulator_label=Firefox OS %1$S Simulator (%2$S)
+addons_install_button=install
+addons_uninstall_button=uninstall
+addons_adb_label=ADB Addon Helper
+addons_adb_warning=USB devices won't be detected without this add-on
+addons_status_unknown=?
+addons_status_installed=Installed
+addons_status_uninstalled=Not Installed
+addons_status_preparing=preparing
+addons_status_downloading=downloading
+addons_status_installing=installing
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webide/modules/addons.js
@@ -0,0 +1,206 @@
+/* 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} = require("chrome");
+const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm");
+const {AddonManager} = Cu.import("resource://gre/modules/AddonManager.jsm");
+const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js");
+const {Simulator} = Cu.import("resource://gre/modules/devtools/Simulator.jsm");
+const {Devices} = Cu.import("resource://gre/modules/devtools/Devices.jsm");
+const {Services} = Cu.import("resource://gre/modules/Services.jsm");
+const {GetAddonsJSON} = require("devtools/webide/remote-resources");
+
+let SIMULATOR_LINK = Services.prefs.getCharPref("devtools.webide.simulatorAddonsURL");
+let ADB_LINK = Services.prefs.getCharPref("devtools.webide.adbAddonURL");
+let SIMULATOR_ADDON_ID = Services.prefs.getCharPref("devtools.webide.simulatorAddonID");
+let ADB_ADDON_ID = Services.prefs.getCharPref("devtools.webide.adbAddonID");
+
+let platform = Services.appShell.hiddenDOMWindow.navigator.platform;
+let OS = "";
+if (platform.indexOf("Win") != -1) {
+  OS = "win32";
+} else if (platform.indexOf("Mac") != -1) {
+  OS = "mac64";
+} else if (platform.indexOf("Linux") != -1) {
+  if (platform.indexOf("x86_64") != -1) {
+    OS = "linux64";
+  } else {
+    OS = "linux";
+  }
+}
+
+Simulator.on("unregister", updateSimulatorAddons);
+Simulator.on("register", updateSimulatorAddons);
+Devices.on("addon-status-updated", updateAdbAddon);
+
+function updateSimulatorAddons(event, version) {
+  GetAvailableAddons().then(addons => {
+    let foundAddon = null;
+    for (let addon of addons.simulators) {
+      if (addon.version == version) {
+        foundAddon = addon;
+        break;
+      }
+    }
+    if (!foundAddon) {
+      console.warn("An unknown simulator (un)registered", version);
+      return;
+    }
+    foundAddon.updateInstallStatus();
+  });
+}
+
+function updateAdbAddon() {
+  GetAvailableAddons().then(addons => {
+    addons.adb.updateInstallStatus();
+  });
+}
+
+let GetAvailableAddons_promise = null;
+let GetAvailableAddons = exports.GetAvailableAddons = function() {
+  if (!GetAvailableAddons_promise) {
+    let deferred = promise.defer();
+    GetAvailableAddons_promise = deferred.promise;
+    let addons = {
+      simulators: [],
+      adb: null
+    }
+    GetAddonsJSON().then(json => {
+      for (let stability in json) {
+        for (let version of json[stability]) {
+          addons.simulators.push(new SimulatorAddon(stability, version));
+        }
+      }
+      addons.adb = new ADBAddon();
+      deferred.resolve(addons);
+    }, e => {
+      GetAvailableAddons_promise = null;
+      deferred.reject(e);
+    });
+  }
+  return GetAvailableAddons_promise;
+}
+
+function Addon() {}
+Addon.prototype = {
+  _status: "unknown",
+  set status(value) {
+    if (this._status != value) {
+      this._status = value;
+      this.emit("update");
+    }
+  },
+  get status() {
+    return this._status;
+  },
+
+  install: function() {
+    if (this.status != "uninstalled") {
+      throw new Error("Not uninstalled");
+    }
+    this.status = "preparing";
+
+    AddonManager.getAddonByID(this.addonID, (addon) => {
+      if (addon && addon.userDisabled) {
+        addon.userDisabled = false;
+      } else {
+        AddonManager.getInstallForURL(this.xpiLink, (install) => {
+          install.addListener(this);
+          install.install();
+        }, "application/x-xpinstall");
+      }
+    });
+
+  },
+
+  uninstall: function() {
+    AddonManager.getAddonByID(this.addonID, (addon) => {
+      addon.uninstall();
+    });
+  },
+
+  installFailureHandler: function(install, message) {
+    this.status = "uninstalled";
+    this.emit("failure", message);
+  },
+
+  onDownloadStarted: function() {
+    this.status = "downloading";
+  },
+
+  onInstallStarted: function() {
+    this.status = "installing";
+  },
+
+  onDownloadProgress: function(install) {
+    if (install.maxProgress == -1) {
+      this.emit("progress", -1);
+    } else {
+      this.emit("progress", install.progress / install.maxProgress);
+    }
+  },
+
+  onInstallEnded: function({addon}) {
+    addon.userDisabled = false;
+  },
+
+  onDownloadCancelled: function(install) {
+    this.installFailureHandler(install, "Download cancelled");
+  },
+  onDownloadFailed: function(install) {
+    this.installFailureHandler(install, "Download failed");
+  },
+  onInstallCancelled: function(install) {
+    this.installFailureHandler(install, "Install cancelled");
+  },
+  onInstallFailed: function(install) {
+    this.installFailureHandler(install, "Install failed");
+  },
+}
+
+function SimulatorAddon(stability, version) {
+  EventEmitter.decorate(this);
+  this.stability = stability;
+  this.version = version;
+  this.xpiLink = SIMULATOR_LINK.replace(/#OS#/g, OS)
+                               .replace(/#VERSION#/g, version)
+                               .replace(/#SLASHED_VERSION#/g, version.replace(/\./g, "_"));
+  this.addonID = SIMULATOR_ADDON_ID.replace(/#SLASHED_VERSION#/g, version.replace(/\./g, "_"));
+  this.updateInstallStatus();
+}
+
+SimulatorAddon.prototype = Object.create(Addon.prototype, {
+  updateInstallStatus: {
+    enumerable: true,
+    value: function() {
+      let sim = Simulator.getByVersion(this.version);
+      if (sim) {
+        this.status = "installed";
+      } else {
+        this.status = "uninstalled";
+      }
+    }
+  },
+});
+
+function ADBAddon() {
+  EventEmitter.decorate(this);
+  this.xpiLink = ADB_LINK.replace(/#OS#/g, OS);
+  this.addonID = ADB_ADDON_ID;
+  this.updateInstallStatus();
+}
+
+ADBAddon.prototype = Object.create(Addon.prototype, {
+  updateInstallStatus: {
+    enumerable: true,
+    value: function() {
+      if (Devices.helperAddonInstalled) {
+        this.status = "installed";
+      } else {
+        this.status = "uninstalled";
+      }
+    }
+  },
+});
+
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webide/modules/remote-resources.js
@@ -0,0 +1,54 @@
+/* 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, CC} = require("chrome");
+const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
+const {Services} = Cu.import("resource://gre/modules/Services.jsm");
+
+const XMLHttpRequest = CC("@mozilla.org/xmlextras/xmlhttprequest;1");
+
+function getJSON(bypassCache, pref) {
+  if (!bypassCache) {
+    try {
+      let str = Services.prefs.getCharPref(pref + "_cache");
+      let json = JSON.parse(str);
+      return promise.resolve(json);
+    } catch(e) {/* no pref or invalid json. Let's continue */}
+  }
+
+
+  let deferred = promise.defer();
+
+  let xhr = new XMLHttpRequest();
+
+  xhr.onload = () => {
+    let json;
+    try {
+      json = JSON.parse(xhr.responseText);
+    } catch(e) {
+      return deferred.reject("Not valid JSON");
+    }
+    Services.prefs.setCharPref(pref + "_cache", xhr.responseText);
+    deferred.resolve(json);
+  }
+
+  xhr.onerror = (e) => {
+    deferred.reject("Network error");
+  }
+
+  xhr.open("get", Services.prefs.getCharPref(pref));
+  xhr.send();
+
+  return deferred.promise;
+}
+
+
+
+exports.GetTemplatesJSON = function(bypassCache) {
+  return getJSON(bypassCache, "devtools.webide.templatesURL");
+}
+
+exports.GetAddonsJSON = function(bypassCache) {
+  return getJSON(bypassCache, "devtools.webide.addonsURL");
+}
--- a/browser/devtools/webide/modules/runtimes.js
+++ b/browser/devtools/webide/modules/runtimes.js
@@ -51,22 +51,21 @@ SimulatorRuntime.prototype = {
       connection.keepConnecting = true;
       connection.connect();
     });
   },
   getID: function() {
     return this.version;
   },
   getName: function() {
-    return this.version;
+    return Simulator.getByVersion(this.version).appinfo.label;
   },
 }
 
 let gLocalRuntime = {
-  supportApps: false, // Temporary static value
   connect: function(connection) {
     if (!DebuggerServer.initialized) {
       DebuggerServer.init();
       DebuggerServer.addBrowserActors();
     }
     connection.port = null;
     connection.host = null; // Force Pipe transport
     connection.connect();
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..b56cc03e349e4b871ea95b61ceeb53d6969759ab
GIT binary patch
literal 1293
zc$^FHW@Zs#U|`^2U~Ag!az0|k)eVdc3<1mx3_J`n3`zO<CB-F0i3NID#i1db49q%0
zRly)!TEWf0$np}X3{3P+a?HN%Akg~$mni>ymE<Lp7Ps=}dMQq5QhdAc_*RomX0dO(
z=0@(${ZRk@M3GaB$L-VWmi&EowtDlbX%;sU&#DOAyVKBrxu|Z5{Nj&mXUI+q4b%O&
zbbC+xge2!3yt)sAJD9&nFTbm7swJRc>!`ukb75}0U3>gB%jVo$mbX(P9^Es0GBeC#
z$_~x@Q+K>hxX|+Px%b&`a_)yeI9`aFmQn7x;^;-M^$kxwmu>bl-PhW3tfFPE#Om4V
z5{~*t>sDvYsGhF)PW$SbEzzNViJR0a#O_4pn4C1onklZgHvIM6%#PRTO0mM8Yc3x?
z^K!>LyGxFXKV&)uWr}u9(pUOe>2j-H-qUu=Nx^VkSrwM!SC_7;d6F1-W%DJowu44%
zk5~S;Kk9ukGNX0*<gV1%PrWxDBwpQHsF&Y6LsUtUZ(qO*(M1kZO#XGI9N2tx-W`vR
zwYS;o&0oJ%%~3E6{L9t7<+}Hx8TwYA=kBwsnZd6X?B%%rTJ>MsYZaMu-KCl?ggq7&
z=Xm81^kv7H1#^OSGe5Pn48M~8Y{_ep&swhYm+-p(pDuB7|I-aC{?FZJ+_&;r=;bH(
z=e*AkfX6y-<7Ss8Hp56sCI$vJ76t}xV61266_+ID<meTpq=6$nN}wt@RRAN>Q-e?E
z-8K-|^ICh`sjX8|ZgwOe4xDw3S4zRLd6~wA$6hf<b5w&A!t1uY*gt(*&QUi%q4e}`
zhEMeJww1DGZ}O9@mF-)k!g8r~qSyYIcHQZZwDz2AI%=)>;J24$h1ptN4m(NDg7h3-
zx#HY^-CS&D2eg^gqOA>5%PUOkLqZj8wpK3f0=mRzMbnnW-#7Fr^iJY<=k1{3vspUC
zpMQfR`z-^Hr#DvZNu9JIaL*@}r@vf)5i@Q6vQ{S38FK=E_NzIxi5468Ki;)oUh=ol
z?=>6Oi@Phhh_Q6%pSJ$+IJ;a{?#Ar1YPWi<KV_KBKI$p8eC38sB6-Utw>$0b-f;cQ
zO1sH(#k?k`2nFvIk87FvzI3BXc*NBW<&p;Gy4A&N=gGSiFO^LW?UdT8b+S<XlhLZ@
zUTGSiMMHUS@~gXw70SzWw0oaX%M`bgYqxkWb?R5dcWaiS<QvgFzQ?vL?fbyRyC%#$
z;?Fs+H8JNc863~wu%BR^y3*wKxw{LsZizp-z3k)mejAgLypw{`YX7{Ch8p>uTB&h~
z<>-^5psvVy6C`>IdyHrK9uJ>fpDreIb?YU2zQxO@2R1G4D{Y#$xzKvU^1w^Q@f(xZ
zf1I6GEq|Y{{85|ftcRPYeeme<PO&j|OZL6__LFkW%XQmMUr%Iqo_A%>*Yj>(|119V
z30i%+y=d!w%l*r$U%dEpJ$=HKZ?B*3s`<3%^@Fo2-8bGli2Y#?@MdI^W5!jgNHBl^
zkjt>75yV0*V^|?&3|dk{HW4$$BAfUPNTH-(R!I8AFp!lEq=^X#7X#@K77z~r445He
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..b56cc03e349e4b871ea95b61ceeb53d6969759ab
GIT binary patch
literal 1293
zc$^FHW@Zs#U|`^2U~Ag!az0|k)eVdc3<1mx3_J`n3`zO<CB-F0i3NID#i1db49q%0
zRly)!TEWf0$np}X3{3P+a?HN%Akg~$mni>ymE<Lp7Ps=}dMQq5QhdAc_*RomX0dO(
z=0@(${ZRk@M3GaB$L-VWmi&EowtDlbX%;sU&#DOAyVKBrxu|Z5{Nj&mXUI+q4b%O&
zbbC+xge2!3yt)sAJD9&nFTbm7swJRc>!`ukb75}0U3>gB%jVo$mbX(P9^Es0GBeC#
z$_~x@Q+K>hxX|+Px%b&`a_)yeI9`aFmQn7x;^;-M^$kxwmu>bl-PhW3tfFPE#Om4V
z5{~*t>sDvYsGhF)PW$SbEzzNViJR0a#O_4pn4C1onklZgHvIM6%#PRTO0mM8Yc3x?
z^K!>LyGxFXKV&)uWr}u9(pUOe>2j-H-qUu=Nx^VkSrwM!SC_7;d6F1-W%DJowu44%
zk5~S;Kk9ukGNX0*<gV1%PrWxDBwpQHsF&Y6LsUtUZ(qO*(M1kZO#XGI9N2tx-W`vR
zwYS;o&0oJ%%~3E6{L9t7<+}Hx8TwYA=kBwsnZd6X?B%%rTJ>MsYZaMu-KCl?ggq7&
z=Xm81^kv7H1#^OSGe5Pn48M~8Y{_ep&swhYm+-p(pDuB7|I-aC{?FZJ+_&;r=;bH(
z=e*AkfX6y-<7Ss8Hp56sCI$vJ76t}xV61266_+ID<meTpq=6$nN}wt@RRAN>Q-e?E
z-8K-|^ICh`sjX8|ZgwOe4xDw3S4zRLd6~wA$6hf<b5w&A!t1uY*gt(*&QUi%q4e}`
zhEMeJww1DGZ}O9@mF-)k!g8r~qSyYIcHQZZwDz2AI%=)>;J24$h1ptN4m(NDg7h3-
zx#HY^-CS&D2eg^gqOA>5%PUOkLqZj8wpK3f0=mRzMbnnW-#7Fr^iJY<=k1{3vspUC
zpMQfR`z-^Hr#DvZNu9JIaL*@}r@vf)5i@Q6vQ{S38FK=E_NzIxi5468Ki;)oUh=ol
z?=>6Oi@Phhh_Q6%pSJ$+IJ;a{?#Ar1YPWi<KV_KBKI$p8eC38sB6-Utw>$0b-f;cQ
zO1sH(#k?k`2nFvIk87FvzI3BXc*NBW<&p;Gy4A&N=gGSiFO^LW?UdT8b+S<XlhLZ@
zUTGSiMMHUS@~gXw70SzWw0oaX%M`bgYqxkWb?R5dcWaiS<QvgFzQ?vL?fbyRyC%#$
z;?Fs+H8JNc863~wu%BR^y3*wKxw{LsZizp-z3k)mejAgLypw{`YX7{Ch8p>uTB&h~
z<>-^5psvVy6C`>IdyHrK9uJ>fpDreIb?YU2zQxO@2R1G4D{Y#$xzKvU^1w^Q@f(xZ
zf1I6GEq|Y{{85|ftcRPYeeme<PO&j|OZL6__LFkW%XQmMUr%Iqo_A%>*Yj>(|119V
z30i%+y=d!w%l*r$U%dEpJ$=HKZ?B*3s`<3%^@Fo2-8bGli2Y#?@MdI^W5!jgNHBl^
zkjt>75yV0*V^|?&3|dk{HW4$$BAfUPNTH-(R!I8AFp!lEq=^X#7X#@K77z~r445He
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..b56cc03e349e4b871ea95b61ceeb53d6969759ab
GIT binary patch
literal 1293
zc$^FHW@Zs#U|`^2U~Ag!az0|k)eVdc3<1mx3_J`n3`zO<CB-F0i3NID#i1db49q%0
zRly)!TEWf0$np}X3{3P+a?HN%Akg~$mni>ymE<Lp7Ps=}dMQq5QhdAc_*RomX0dO(
z=0@(${ZRk@M3GaB$L-VWmi&EowtDlbX%;sU&#DOAyVKBrxu|Z5{Nj&mXUI+q4b%O&
zbbC+xge2!3yt)sAJD9&nFTbm7swJRc>!`ukb75}0U3>gB%jVo$mbX(P9^Es0GBeC#
z$_~x@Q+K>hxX|+Px%b&`a_)yeI9`aFmQn7x;^;-M^$kxwmu>bl-PhW3tfFPE#Om4V
z5{~*t>sDvYsGhF)PW$SbEzzNViJR0a#O_4pn4C1onklZgHvIM6%#PRTO0mM8Yc3x?
z^K!>LyGxFXKV&)uWr}u9(pUOe>2j-H-qUu=Nx^VkSrwM!SC_7;d6F1-W%DJowu44%
zk5~S;Kk9ukGNX0*<gV1%PrWxDBwpQHsF&Y6LsUtUZ(qO*(M1kZO#XGI9N2tx-W`vR
zwYS;o&0oJ%%~3E6{L9t7<+}Hx8TwYA=kBwsnZd6X?B%%rTJ>MsYZaMu-KCl?ggq7&
z=Xm81^kv7H1#^OSGe5Pn48M~8Y{_ep&swhYm+-p(pDuB7|I-aC{?FZJ+_&;r=;bH(
z=e*AkfX6y-<7Ss8Hp56sCI$vJ76t}xV61266_+ID<meTpq=6$nN}wt@RRAN>Q-e?E
z-8K-|^ICh`sjX8|ZgwOe4xDw3S4zRLd6~wA$6hf<b5w&A!t1uY*gt(*&QUi%q4e}`
zhEMeJww1DGZ}O9@mF-)k!g8r~qSyYIcHQZZwDz2AI%=)>;J24$h1ptN4m(NDg7h3-
zx#HY^-CS&D2eg^gqOA>5%PUOkLqZj8wpK3f0=mRzMbnnW-#7Fr^iJY<=k1{3vspUC
zpMQfR`z-^Hr#DvZNu9JIaL*@}r@vf)5i@Q6vQ{S38FK=E_NzIxi5468Ki;)oUh=ol
z?=>6Oi@Phhh_Q6%pSJ$+IJ;a{?#Ar1YPWi<KV_KBKI$p8eC38sB6-Utw>$0b-f;cQ
zO1sH(#k?k`2nFvIk87FvzI3BXc*NBW<&p;Gy4A&N=gGSiFO^LW?UdT8b+S<XlhLZ@
zUTGSiMMHUS@~gXw70SzWw0oaX%M`bgYqxkWb?R5dcWaiS<QvgFzQ?vL?fbyRyC%#$
z;?Fs+H8JNc863~wu%BR^y3*wKxw{LsZizp-z3k)mejAgLypw{`YX7{Ch8p>uTB&h~
z<>-^5psvVy6C`>IdyHrK9uJ>fpDreIb?YU2zQxO@2R1G4D{Y#$xzKvU^1w^Q@f(xZ
zf1I6GEq|Y{{85|ftcRPYeeme<PO&j|OZL6__LFkW%XQmMUr%Iqo_A%>*Yj>(|119V
z30i%+y=d!w%l*r$U%dEpJ$=HKZ?B*3s`<3%^@Fo2-8bGli2Y#?@MdI^W5!jgNHBl^
zkjt>75yV0*V^|?&3|dk{HW4$$BAfUPNTH-(R!I8AFp!lEq=^X#7X#@K77z~r445He
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..b56cc03e349e4b871ea95b61ceeb53d6969759ab
GIT binary patch
literal 1293
zc$^FHW@Zs#U|`^2U~Ag!az0|k)eVdc3<1mx3_J`n3`zO<CB-F0i3NID#i1db49q%0
zRly)!TEWf0$np}X3{3P+a?HN%Akg~$mni>ymE<Lp7Ps=}dMQq5QhdAc_*RomX0dO(
z=0@(${ZRk@M3GaB$L-VWmi&EowtDlbX%;sU&#DOAyVKBrxu|Z5{Nj&mXUI+q4b%O&
zbbC+xge2!3yt)sAJD9&nFTbm7swJRc>!`ukb75}0U3>gB%jVo$mbX(P9^Es0GBeC#
z$_~x@Q+K>hxX|+Px%b&`a_)yeI9`aFmQn7x;^;-M^$kxwmu>bl-PhW3tfFPE#Om4V
z5{~*t>sDvYsGhF)PW$SbEzzNViJR0a#O_4pn4C1onklZgHvIM6%#PRTO0mM8Yc3x?
z^K!>LyGxFXKV&)uWr}u9(pUOe>2j-H-qUu=Nx^VkSrwM!SC_7;d6F1-W%DJowu44%
zk5~S;Kk9ukGNX0*<gV1%PrWxDBwpQHsF&Y6LsUtUZ(qO*(M1kZO#XGI9N2tx-W`vR
zwYS;o&0oJ%%~3E6{L9t7<+}Hx8TwYA=kBwsnZd6X?B%%rTJ>MsYZaMu-KCl?ggq7&
z=Xm81^kv7H1#^OSGe5Pn48M~8Y{_ep&swhYm+-p(pDuB7|I-aC{?FZJ+_&;r=;bH(
z=e*AkfX6y-<7Ss8Hp56sCI$vJ76t}xV61266_+ID<meTpq=6$nN}wt@RRAN>Q-e?E
z-8K-|^ICh`sjX8|ZgwOe4xDw3S4zRLd6~wA$6hf<b5w&A!t1uY*gt(*&QUi%q4e}`
zhEMeJww1DGZ}O9@mF-)k!g8r~qSyYIcHQZZwDz2AI%=)>;J24$h1ptN4m(NDg7h3-
zx#HY^-CS&D2eg^gqOA>5%PUOkLqZj8wpK3f0=mRzMbnnW-#7Fr^iJY<=k1{3vspUC
zpMQfR`z-^Hr#DvZNu9JIaL*@}r@vf)5i@Q6vQ{S38FK=E_NzIxi5468Ki;)oUh=ol
z?=>6Oi@Phhh_Q6%pSJ$+IJ;a{?#Ar1YPWi<KV_KBKI$p8eC38sB6-Utw>$0b-f;cQ
zO1sH(#k?k`2nFvIk87FvzI3BXc*NBW<&p;Gy4A&N=gGSiFO^LW?UdT8b+S<XlhLZ@
zUTGSiMMHUS@~gXw70SzWw0oaX%M`bgYqxkWb?R5dcWaiS<QvgFzQ?vL?fbyRyC%#$
z;?Fs+H8JNc863~wu%BR^y3*wKxw{LsZizp-z3k)mejAgLypw{`YX7{Ch8p>uTB&h~
z<>-^5psvVy6C`>IdyHrK9uJ>fpDreIb?YU2zQxO@2R1G4D{Y#$xzKvU^1w^Q@f(xZ
zf1I6GEq|Y{{85|ftcRPYeeme<PO&j|OZL6__LFkW%XQmMUr%Iqo_A%>*Yj>(|119V
z30i%+y=d!w%l*r$U%dEpJ$=HKZ?B*3s`<3%^@Fo2-8bGli2Y#?@MdI^W5!jgNHBl^
zkjt>75yV0*V^|?&3|dk{HW4$$BAfUPNTH-(R!I8AFp!lEq=^X#7X#@K77z~r445He
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..81e1abc6e1d339d1f84b102b9806821dd36a4fe1
GIT binary patch
literal 1233
zc$^FHW@Zs#U|`^2NNl;}BHupyS2ZI8!!0HT1|9|(hNS%blH!u0!~(sn;?NLI24++L
zg~1?PTEWf0$nuhzfdNePPV~>eZNPK%{XY>Gzf6IM*nYh#t$+lFcGeSm)~AeQH&1YN
zUNR}qao_J3TV7vdkS)7-U;cjelsjDCOw5|Nw93sD+AA&eKURKP8GQfHsn9&u`)oot
zicT^<oM+v_eopzu%qibEn>h_$G%T3m&iL;9Z2dozlfG=w%v}<2kV{T8K`7?%zP8!F
zcD0?it>CcvTe144&7p(bDq$}?OoF3DtSu(+u}wH*Wb1nQ$2ry}{{w5QHfbtWv^th%
z&5@4ZlYP!Qeb%`<F}CKxtbU6M{0q|O_Dr6tI(610*Y~v!JOBDl`R)?fRra&0`p%_@
zMd9a`OKO`+&M4h^LgRV*CZ63r(-vzB9xdGw{!HWD+I4l43JXi4U90~+R{QMHboPtU
z|M}0dFGgnY9-rJ9TI=c9DLi567ehVk^>w_Gdkc0Mz0g0>uqJhvi&lfZqVc;o;s*NS
z{8KhQPyey2)&J|)oEOVGgU+8d{3VuL>^bL|!}~vNi)7_5+4Fk;y70+q-4g!lKdb>L
z5y)$L@}m$i@^n}j7`TBEn3-2xl9-dDSCo>5FaA=4WAkSlh}1q0zjJYWCY#eDQy0#=
zTXZfh30pla>(V?W=IKYIZ=Bd?;<^0q_niVB8S^%rw|!nWC;M4>`=*BUD{dM)3*6%#
zFr5lXb;=XZ^R=A4t9+wz{c#DKw=IoZSCw{sa82dVUh2JJ#>&a}X9!JTx}_-a>Qck0
z*{7SU1YHk4+~~(O(Td@~gSaIcymbxf{7(8G9z^rW1cl00Uwv%*W+&rH4x>bm?j>qx
z67)0DIS*~=EDF4#vp<AabNT|IM~CWEnAC3wh8o}3zx`|8rRe)<Uvq8mGapMjKJ$OX
zx&wXsXAjwK{Qi6Ic6swBfg-MT0>2VZluR=&o^piWXO{H#t7kUIoY3QLbu{?6u}1XX
zj*V|i_qj~Y3Y=IfS+OW<)3Yo~>&`SMsYk8{z4ToB+N35<O|%q_oFhNwW{=syV;!6H
zEHwYVTy*PBe&pUqTh3M(<vHEm+JAZDA&Z=(vm5zUye58~6>j)aO8SuGtH`ZuDt<lp
zvAjEX+J|2OVNS{G_<B#*2Cm!gF@N=`eN29}_RiT>Yv%vaUv@b`=E-&)UgmRZ^6Hk|
zlNfGhoH=fD=tY^$+&|J57gl;J*3VOuca6JHptnJBy1bPQ&z|qP-+M~u{t92aO)aj!
z>|txzqz9|dJYek9xBS?sJ88N^d0lOM^IfAgyLHWPO1qsuuV?k(gIfmMp*OjkYlW^h
zJbU?vPfzf9)}@{K|EJve$sgd&$Rx*%t7MR100AJEVM!y1g<4LqLed#p8bdb`HK`(-
aco0aTBwSnuva*3RF#+LXAT7ZH;sF3L{sbTZ
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..81e1abc6e1d339d1f84b102b9806821dd36a4fe1
GIT binary patch
literal 1233
zc$^FHW@Zs#U|`^2NNl;}BHupyS2ZI8!!0HT1|9|(hNS%blH!u0!~(sn;?NLI24++L
zg~1?PTEWf0$nuhzfdNePPV~>eZNPK%{XY>Gzf6IM*nYh#t$+lFcGeSm)~AeQH&1YN
zUNR}qao_J3TV7vdkS)7-U;cjelsjDCOw5|Nw93sD+AA&eKURKP8GQfHsn9&u`)oot
zicT^<oM+v_eopzu%qibEn>h_$G%T3m&iL;9Z2dozlfG=w%v}<2kV{T8K`7?%zP8!F
zcD0?it>CcvTe144&7p(bDq$}?OoF3DtSu(+u}wH*Wb1nQ$2ry}{{w5QHfbtWv^th%
z&5@4ZlYP!Qeb%`<F}CKxtbU6M{0q|O_Dr6tI(610*Y~v!JOBDl`R)?fRra&0`p%_@
zMd9a`OKO`+&M4h^LgRV*CZ63r(-vzB9xdGw{!HWD+I4l43JXi4U90~+R{QMHboPtU
z|M}0dFGgnY9-rJ9TI=c9DLi567ehVk^>w_Gdkc0Mz0g0>uqJhvi&lfZqVc;o;s*NS
z{8KhQPyey2)&J|)oEOVGgU+8d{3VuL>^bL|!}~vNi)7_5+4Fk;y70+q-4g!lKdb>L
z5y)$L@}m$i@^n}j7`TBEn3-2xl9-dDSCo>5FaA=4WAkSlh}1q0zjJYWCY#eDQy0#=
zTXZfh30pla>(V?W=IKYIZ=Bd?;<^0q_niVB8S^%rw|!nWC;M4>`=*BUD{dM)3*6%#
zFr5lXb;=XZ^R=A4t9+wz{c#DKw=IoZSCw{sa82dVUh2JJ#>&a}X9!JTx}_-a>Qck0
z*{7SU1YHk4+~~(O(Td@~gSaIcymbxf{7(8G9z^rW1cl00Uwv%*W+&rH4x>bm?j>qx
z67)0DIS*~=EDF4#vp<AabNT|IM~CWEnAC3wh8o}3zx`|8rRe)<Uvq8mGapMjKJ$OX
zx&wXsXAjwK{Qi6Ic6swBfg-MT0>2VZluR=&o^piWXO{H#t7kUIoY3QLbu{?6u}1XX
zj*V|i_qj~Y3Y=IfS+OW<)3Yo~>&`SMsYk8{z4ToB+N35<O|%q_oFhNwW{=syV;!6H
zEHwYVTy*PBe&pUqTh3M(<vHEm+JAZDA&Z=(vm5zUye58~6>j)aO8SuGtH`ZuDt<lp
zvAjEX+J|2OVNS{G_<B#*2Cm!gF@N=`eN29}_RiT>Yv%vaUv@b`=E-&)UgmRZ^6Hk|
zlNfGhoH=fD=tY^$+&|J57gl;J*3VOuca6JHptnJBy1bPQ&z|qP-+M~u{t92aO)aj!
z>|txzqz9|dJYek9xBS?sJ88N^d0lOM^IfAgyLHWPO1qsuuV?k(gIfmMp*OjkYlW^h
zJbU?vPfzf9)}@{K|EJve$sgd&$Rx*%t7MR100AJEVM!y1g<4LqLed#p8bdb`HK`(-
aco0aTBwSnuva*3RF#+LXAT7ZH;sF3L{sbTZ
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..81e1abc6e1d339d1f84b102b9806821dd36a4fe1
GIT binary patch
literal 1233
zc$^FHW@Zs#U|`^2NNl;}BHupyS2ZI8!!0HT1|9|(hNS%blH!u0!~(sn;?NLI24++L
zg~1?PTEWf0$nuhzfdNePPV~>eZNPK%{XY>Gzf6IM*nYh#t$+lFcGeSm)~AeQH&1YN
zUNR}qao_J3TV7vdkS)7-U;cjelsjDCOw5|Nw93sD+AA&eKURKP8GQfHsn9&u`)oot
zicT^<oM+v_eopzu%qibEn>h_$G%T3m&iL;9Z2dozlfG=w%v}<2kV{T8K`7?%zP8!F
zcD0?it>CcvTe144&7p(bDq$}?OoF3DtSu(+u}wH*Wb1nQ$2ry}{{w5QHfbtWv^th%
z&5@4ZlYP!Qeb%`<F}CKxtbU6M{0q|O_Dr6tI(610*Y~v!JOBDl`R)?fRra&0`p%_@
zMd9a`OKO`+&M4h^LgRV*CZ63r(-vzB9xdGw{!HWD+I4l43JXi4U90~+R{QMHboPtU
z|M}0dFGgnY9-rJ9TI=c9DLi567ehVk^>w_Gdkc0Mz0g0>uqJhvi&lfZqVc;o;s*NS
z{8KhQPyey2)&J|)oEOVGgU+8d{3VuL>^bL|!}~vNi)7_5+4Fk;y70+q-4g!lKdb>L
z5y)$L@}m$i@^n}j7`TBEn3-2xl9-dDSCo>5FaA=4WAkSlh}1q0zjJYWCY#eDQy0#=
zTXZfh30pla>(V?W=IKYIZ=Bd?;<^0q_niVB8S^%rw|!nWC;M4>`=*BUD{dM)3*6%#
zFr5lXb;=XZ^R=A4t9+wz{c#DKw=IoZSCw{sa82dVUh2JJ#>&a}X9!JTx}_-a>Qck0
z*{7SU1YHk4+~~(O(Td@~gSaIcymbxf{7(8G9z^rW1cl00Uwv%*W+&rH4x>bm?j>qx
z67)0DIS*~=EDF4#vp<AabNT|IM~CWEnAC3wh8o}3zx`|8rRe)<Uvq8mGapMjKJ$OX
zx&wXsXAjwK{Qi6Ic6swBfg-MT0>2VZluR=&o^piWXO{H#t7kUIoY3QLbu{?6u}1XX
zj*V|i_qj~Y3Y=IfS+OW<)3Yo~>&`SMsYk8{z4ToB+N35<O|%q_oFhNwW{=syV;!6H
zEHwYVTy*PBe&pUqTh3M(<vHEm+JAZDA&Z=(vm5zUye58~6>j)aO8SuGtH`ZuDt<lp
zvAjEX+J|2OVNS{G_<B#*2Cm!gF@N=`eN29}_RiT>Yv%vaUv@b`=E-&)UgmRZ^6Hk|
zlNfGhoH=fD=tY^$+&|J57gl;J*3VOuca6JHptnJBy1bPQ&z|qP-+M~u{t92aO)aj!
z>|txzqz9|dJYek9xBS?sJ88N^d0lOM^IfAgyLHWPO1qsuuV?k(gIfmMp*OjkYlW^h
zJbU?vPfzf9)}@{K|EJve$sgd&$Rx*%t7MR100AJEVM!y1g<4LqLed#p8bdb`HK`(-
aco0aTBwSnuva*3RF#+LXAT7ZH;sF3L{sbTZ
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..81e1abc6e1d339d1f84b102b9806821dd36a4fe1
GIT binary patch
literal 1233
zc$^FHW@Zs#U|`^2NNl;}BHupyS2ZI8!!0HT1|9|(hNS%blH!u0!~(sn;?NLI24++L
zg~1?PTEWf0$nuhzfdNePPV~>eZNPK%{XY>Gzf6IM*nYh#t$+lFcGeSm)~AeQH&1YN
zUNR}qao_J3TV7vdkS)7-U;cjelsjDCOw5|Nw93sD+AA&eKURKP8GQfHsn9&u`)oot
zicT^<oM+v_eopzu%qibEn>h_$G%T3m&iL;9Z2dozlfG=w%v}<2kV{T8K`7?%zP8!F
zcD0?it>CcvTe144&7p(bDq$}?OoF3DtSu(+u}wH*Wb1nQ$2ry}{{w5QHfbtWv^th%
z&5@4ZlYP!Qeb%`<F}CKxtbU6M{0q|O_Dr6tI(610*Y~v!JOBDl`R)?fRra&0`p%_@
zMd9a`OKO`+&M4h^LgRV*CZ63r(-vzB9xdGw{!HWD+I4l43JXi4U90~+R{QMHboPtU
z|M}0dFGgnY9-rJ9TI=c9DLi567ehVk^>w_Gdkc0Mz0g0>uqJhvi&lfZqVc;o;s*NS
z{8KhQPyey2)&J|)oEOVGgU+8d{3VuL>^bL|!}~vNi)7_5+4Fk;y70+q-4g!lKdb>L
z5y)$L@}m$i@^n}j7`TBEn3-2xl9-dDSCo>5FaA=4WAkSlh}1q0zjJYWCY#eDQy0#=
zTXZfh30pla>(V?W=IKYIZ=Bd?;<^0q_niVB8S^%rw|!nWC;M4>`=*BUD{dM)3*6%#
zFr5lXb;=XZ^R=A4t9+wz{c#DKw=IoZSCw{sa82dVUh2JJ#>&a}X9!JTx}_-a>Qck0
z*{7SU1YHk4+~~(O(Td@~gSaIcymbxf{7(8G9z^rW1cl00Uwv%*W+&rH4x>bm?j>qx
z67)0DIS*~=EDF4#vp<AabNT|IM~CWEnAC3wh8o}3zx`|8rRe)<Uvq8mGapMjKJ$OX
zx&wXsXAjwK{Qi6Ic6swBfg-MT0>2VZluR=&o^piWXO{H#t7kUIoY3QLbu{?6u}1XX
zj*V|i_qj~Y3Y=IfS+OW<)3Yo~>&`SMsYk8{z4ToB+N35<O|%q_oFhNwW{=syV;!6H
zEHwYVTy*PBe&pUqTh3M(<vHEm+JAZDA&Z=(vm5zUye58~6>j)aO8SuGtH`ZuDt<lp
zvAjEX+J|2OVNS{G_<B#*2Cm!gF@N=`eN29}_RiT>Yv%vaUv@b`=E-&)UgmRZ^6Hk|
zlNfGhoH=fD=tY^$+&|J57gl;J*3VOuca6JHptnJBy1bPQ&z|qP-+M~u{t92aO)aj!
z>|txzqz9|dJYek9xBS?sJ88N^d0lOM^IfAgyLHWPO1qsuuV?k(gIfmMp*OjkYlW^h
zJbU?vPfzf9)}@{K|EJve$sgd&$Rx*%t7MR100AJEVM!y1g<4LqLed#p8bdb`HK`(-
aco0aTBwSnuva*3RF#+LXAT7ZH;sF3L{sbTZ
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..1ce3f959e1a18af8e9df037328f9a8d7d3183fac
GIT binary patch
literal 1232
zc$^FHW@Zs#U|`^2NNl;}a-;juyedWphFeSw3_J`n3`zO<CB-F0i3NID#i1db49uqf
z3xh$pw1S&~k>w>b0|S`oo#>x^+d<&y`(Gl>{(XkGCM<UHYjjXlW&3{VahB;OwKZi&
z&aIhy?Z@u&CrdJuJzl2%|MzUK`R+W{YAI<))~UOtCmgn!GyR9r&nqkQ6Q62rZ^~!p
z%-Q**q2RpiA?D|TZ+yId_Z(!AC~*ilvzX!g^RwaqJPoUEOfd^uk-*w7V!*jBG5)Y`
zZS3LcayBgU>h@gEk$d=nRY<!;W!6gFE?F55c4m*Xne!H9);woAsQw_#_U06UJ%<)p
znV#!if6w&!Tw~w#ymj+tuVhjW+@Zc>v){3krzUy%o?P_ZZo!>@YNy;6X}IpL*=?Jb
zsdG{L`Q@Ievv|(zib<LB**LN-*6sAgsU453?r0Z_e2)vaKfPneuC<G7{})cKR5_Sd
zHTS>&=d~|%wzNGyd1TeTB@2#lo(TFSF@0|M{<fYti&)7o;z<tIjAE5U56qt+S^kZC
z#`JFXQ#U?y+vGl9Kb!ZjN#LuUmTvcxa`ZYc%e-j5AF$=I^RMNJp7W#PPE7rh*}tD5
z044tT|8GAi1dKZ!76t}xVEkp~6_+ID<meTpq~VLc)Zp0s*#;uD&%^Ir+@8tiw8+$j
z^X?X%OH0C5Ps_SAPl<W@5$PKz_L+Dt|NDKXfJer>4d-p2*Uia(R^Gm;A^nP*#?Auw
zxCcz9LQ<Xb#PfVDXYVTCXk33>!scyD<JMKBT_0RiIkcC0Z<w)i^8Fb?6PRu(3cR}1
zaBBAH<|;wggAX_QaZR*hIPf5Di3V?7Lpr~c{)Y$Ad@@0yvej1~+rHV!xRS#t(W85b
z+L;9XjC9UJTRMvZZ|Lj~;nkeJK<Lq-Iu$1M8-k(6_w{f8ns+Jse%jYu+xyJNl8(>(
zAF=L0pZ?iHwj00y-n(7i{7ImQYn{Na#1kdcjEkon;rE#(z5VK$4KgS6xLX|!K5ncL
zy|-iI+tPh5ld}RRmP%GE%G&fS%hI|t%}MH!>p?F)*S<EXiBl6Tg(K(4Pr2DLJ0aO6
zQhd&oe<gvr@3yarE4-CvBWb?y-L2y<BNAoIHa@$-F0}MS)!EfEN_u%8@_bzr6=w6d
zc-fqHe%=+e8rlmD!`qKN-KTLqddd0F)Og0_`{pk+mA&TwPyBMG0pF+SX>E+@llv#n
zarJD-F?p6Omsnyw&+i|v%!?~aC)oQ>?pM;!un@m7;WWRj9NWF$)2ff{^833wELwQI
z`|b};+MXXm(>^pDnLg*o0kM<bJ=Xj8tv{G2dF}4B*>89kKTj8zEvQg7VNU$E?WSF4
zmc!?gI`-)u#ilRsY_Iov^M^gan~_P58CSs|!2kk4F2j;W5DT@KV1=YJv^0ipB5G1a
bHt_(ELP@x|3}j^kX<`Dx#Xwq|1;hgYu3idX
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..1ce3f959e1a18af8e9df037328f9a8d7d3183fac
GIT binary patch
literal 1232
zc$^FHW@Zs#U|`^2NNl;}a-;juyedWphFeSw3_J`n3`zO<CB-F0i3NID#i1db49uqf
z3xh$pw1S&~k>w>b0|S`oo#>x^+d<&y`(Gl>{(XkGCM<UHYjjXlW&3{VahB;OwKZi&
z&aIhy?Z@u&CrdJuJzl2%|MzUK`R+W{YAI<))~UOtCmgn!GyR9r&nqkQ6Q62rZ^~!p
z%-Q**q2RpiA?D|TZ+yId_Z(!AC~*ilvzX!g^RwaqJPoUEOfd^uk-*w7V!*jBG5)Y`
zZS3LcayBgU>h@gEk$d=nRY<!;W!6gFE?F55c4m*Xne!H9);woAsQw_#_U06UJ%<)p
znV#!if6w&!Tw~w#ymj+tuVhjW+@Zc>v){3krzUy%o?P_ZZo!>@YNy;6X}IpL*=?Jb
zsdG{L`Q@Ievv|(zib<LB**LN-*6sAgsU453?r0Z_e2)vaKfPneuC<G7{})cKR5_Sd
zHTS>&=d~|%wzNGyd1TeTB@2#lo(TFSF@0|M{<fYti&)7o;z<tIjAE5U56qt+S^kZC
z#`JFXQ#U?y+vGl9Kb!ZjN#LuUmTvcxa`ZYc%e-j5AF$=I^RMNJp7W#PPE7rh*}tD5
z044tT|8GAi1dKZ!76t}xVEkp~6_+ID<meTpq~VLc)Zp0s*#;uD&%^Ir+@8tiw8+$j
z^X?X%OH0C5Ps_SAPl<W@5$PKz_L+Dt|NDKXfJer>4d-p2*Uia(R^Gm;A^nP*#?Auw
zxCcz9LQ<Xb#PfVDXYVTCXk33>!scyD<JMKBT_0RiIkcC0Z<w)i^8Fb?6PRu(3cR}1
zaBBAH<|;wggAX_QaZR*hIPf5Di3V?7Lpr~c{)Y$Ad@@0yvej1~+rHV!xRS#t(W85b
z+L;9XjC9UJTRMvZZ|Lj~;nkeJK<Lq-Iu$1M8-k(6_w{f8ns+Jse%jYu+xyJNl8(>(
zAF=L0pZ?iHwj00y-n(7i{7ImQYn{Na#1kdcjEkon;rE#(z5VK$4KgS6xLX|!K5ncL
zy|-iI+tPh5ld}RRmP%GE%G&fS%hI|t%}MH!>p?F)*S<EXiBl6Tg(K(4Pr2DLJ0aO6
zQhd&oe<gvr@3yarE4-CvBWb?y-L2y<BNAoIHa@$-F0}MS)!EfEN_u%8@_bzr6=w6d
zc-fqHe%=+e8rlmD!`qKN-KTLqddd0F)Og0_`{pk+mA&TwPyBMG0pF+SX>E+@llv#n
zarJD-F?p6Omsnyw&+i|v%!?~aC)oQ>?pM;!un@m7;WWRj9NWF$)2ff{^833wELwQI
z`|b};+MXXm(>^pDnLg*o0kM<bJ=Xj8tv{G2dF}4B*>89kKTj8zEvQg7VNU$E?WSF4
zmc!?gI`-)u#ilRsY_Iov^M^gan~_P58CSs|!2kk4F2j;W5DT@KV1=YJv^0ipB5G1a
bHt_(ELP@x|3}j^kX<`Dx#Xwq|1;hgYu3idX
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..1ce3f959e1a18af8e9df037328f9a8d7d3183fac
GIT binary patch
literal 1232
zc$^FHW@Zs#U|`^2NNl;}a-;juyedWphFeSw3_J`n3`zO<CB-F0i3NID#i1db49uqf
z3xh$pw1S&~k>w>b0|S`oo#>x^+d<&y`(Gl>{(XkGCM<UHYjjXlW&3{VahB;OwKZi&
z&aIhy?Z@u&CrdJuJzl2%|MzUK`R+W{YAI<))~UOtCmgn!GyR9r&nqkQ6Q62rZ^~!p
z%-Q**q2RpiA?D|TZ+yId_Z(!AC~*ilvzX!g^RwaqJPoUEOfd^uk-*w7V!*jBG5)Y`
zZS3LcayBgU>h@gEk$d=nRY<!;W!6gFE?F55c4m*Xne!H9);woAsQw_#_U06UJ%<)p
znV#!if6w&!Tw~w#ymj+tuVhjW+@Zc>v){3krzUy%o?P_ZZo!>@YNy;6X}IpL*=?Jb
zsdG{L`Q@Ievv|(zib<LB**LN-*6sAgsU453?r0Z_e2)vaKfPneuC<G7{})cKR5_Sd
zHTS>&=d~|%wzNGyd1TeTB@2#lo(TFSF@0|M{<fYti&)7o;z<tIjAE5U56qt+S^kZC
z#`JFXQ#U?y+vGl9Kb!ZjN#LuUmTvcxa`ZYc%e-j5AF$=I^RMNJp7W#PPE7rh*}tD5
z044tT|8GAi1dKZ!76t}xVEkp~6_+ID<meTpq~VLc)Zp0s*#;uD&%^Ir+@8tiw8+$j
z^X?X%OH0C5Ps_SAPl<W@5$PKz_L+Dt|NDKXfJer>4d-p2*Uia(R^Gm;A^nP*#?Auw
zxCcz9LQ<Xb#PfVDXYVTCXk33>!scyD<JMKBT_0RiIkcC0Z<w)i^8Fb?6PRu(3cR}1
zaBBAH<|;wggAX_QaZR*hIPf5Di3V?7Lpr~c{)Y$Ad@@0yvej1~+rHV!xRS#t(W85b
z+L;9XjC9UJTRMvZZ|Lj~;nkeJK<Lq-Iu$1M8-k(6_w{f8ns+Jse%jYu+xyJNl8(>(
zAF=L0pZ?iHwj00y-n(7i{7ImQYn{Na#1kdcjEkon;rE#(z5VK$4KgS6xLX|!K5ncL
zy|-iI+tPh5ld}RRmP%GE%G&fS%hI|t%}MH!>p?F)*S<EXiBl6Tg(K(4Pr2DLJ0aO6
zQhd&oe<gvr@3yarE4-CvBWb?y-L2y<BNAoIHa@$-F0}MS)!EfEN_u%8@_bzr6=w6d
zc-fqHe%=+e8rlmD!`qKN-KTLqddd0F)Og0_`{pk+mA&TwPyBMG0pF+SX>E+@llv#n
zarJD-F?p6Omsnyw&+i|v%!?~aC)oQ>?pM;!un@m7;WWRj9NWF$)2ff{^833wELwQI
z`|b};+MXXm(>^pDnLg*o0kM<bJ=Xj8tv{G2dF}4B*>89kKTj8zEvQg7VNU$E?WSF4
zmc!?gI`-)u#ilRsY_Iov^M^gan~_P58CSs|!2kk4F2j;W5DT@KV1=YJv^0ipB5G1a
bHt_(ELP@x|3}j^kX<`Dx#Xwq|1;hgYu3idX
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..1ce3f959e1a18af8e9df037328f9a8d7d3183fac
GIT binary patch
literal 1232
zc$^FHW@Zs#U|`^2NNl;}a-;juyedWphFeSw3_J`n3`zO<CB-F0i3NID#i1db49uqf
z3xh$pw1S&~k>w>b0|S`oo#>x^+d<&y`(Gl>{(XkGCM<UHYjjXlW&3{VahB;OwKZi&
z&aIhy?Z@u&CrdJuJzl2%|MzUK`R+W{YAI<))~UOtCmgn!GyR9r&nqkQ6Q62rZ^~!p
z%-Q**q2RpiA?D|TZ+yId_Z(!AC~*ilvzX!g^RwaqJPoUEOfd^uk-*w7V!*jBG5)Y`
zZS3LcayBgU>h@gEk$d=nRY<!;W!6gFE?F55c4m*Xne!H9);woAsQw_#_U06UJ%<)p
znV#!if6w&!Tw~w#ymj+tuVhjW+@Zc>v){3krzUy%o?P_ZZo!>@YNy;6X}IpL*=?Jb
zsdG{L`Q@Ievv|(zib<LB**LN-*6sAgsU453?r0Z_e2)vaKfPneuC<G7{})cKR5_Sd
zHTS>&=d~|%wzNGyd1TeTB@2#lo(TFSF@0|M{<fYti&)7o;z<tIjAE5U56qt+S^kZC
z#`JFXQ#U?y+vGl9Kb!ZjN#LuUmTvcxa`ZYc%e-j5AF$=I^RMNJp7W#PPE7rh*}tD5
z044tT|8GAi1dKZ!76t}xVEkp~6_+ID<meTpq~VLc)Zp0s*#;uD&%^Ir+@8tiw8+$j
z^X?X%OH0C5Ps_SAPl<W@5$PKz_L+Dt|NDKXfJer>4d-p2*Uia(R^Gm;A^nP*#?Auw
zxCcz9LQ<Xb#PfVDXYVTCXk33>!scyD<JMKBT_0RiIkcC0Z<w)i^8Fb?6PRu(3cR}1
zaBBAH<|;wggAX_QaZR*hIPf5Di3V?7Lpr~c{)Y$Ad@@0yvej1~+rHV!xRS#t(W85b
z+L;9XjC9UJTRMvZZ|Lj~;nkeJK<Lq-Iu$1M8-k(6_w{f8ns+Jse%jYu+xyJNl8(>(
zAF=L0pZ?iHwj00y-n(7i{7ImQYn{Na#1kdcjEkon;rE#(z5VK$4KgS6xLX|!K5ncL
zy|-iI+tPh5ld}RRmP%GE%G&fS%hI|t%}MH!>p?F)*S<EXiBl6Tg(K(4Pr2DLJ0aO6
zQhd&oe<gvr@3yarE4-CvBWb?y-L2y<BNAoIHa@$-F0}MS)!EfEN_u%8@_bzr6=w6d
zc-fqHe%=+e8rlmD!`qKN-KTLqddd0F)Og0_`{pk+mA&TwPyBMG0pF+SX>E+@llv#n
zarJD-F?p6Omsnyw&+i|v%!?~aC)oQ>?pM;!un@m7;WWRj9NWF$)2ff{^833wELwQI
z`|b};+MXXm(>^pDnLg*o0kM<bJ=Xj8tv{G2dF}4B*>89kKTj8zEvQg7VNU$E?WSF4
zmc!?gI`-)u#ilRsY_Iov^M^gan~_P58CSs|!2kk4F2j;W5DT@KV1=YJv^0ipB5G1a
bHt_(ELP@x|3}j^kX<`Dx#Xwq|1;hgYu3idX
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ec9645da40d54ea47b2e9c7b418b6f777f9dc72e
GIT binary patch
literal 1233
zc$^FHW@Zs#U|`^2NNl;}l3KvEx0;cG;T97E0}q1?LsEWzNpVS0Vu4;(acBr91GB0B
z!e9_Ct>9*0WO>QVzyKzCC;DgKb`UuF{+CFzf1ly43HL;bxLCL}O?OO}3tv6aG;ynP
zS=!qz$L{Y{R`->5_R|0T-TeLDo2A+}F5lqLT5;!YgUqvsdyYImx!kXoTRVKGXpOW-
z!f93Zmg;u`au)pw=|Q*DdDIRpU~Wp1W8Gn&zwd*8*TuujwoG(l)vj35;90=?TPp9>
zYbm?$4-`LqdAv65GY7l=gouU87kzRA-#l=zl5j9O{YAw0v9&md9pm;_CzrK65M{f%
zvM{ga<2uXtb26=pi@)CS5w++%WP5OKsgYmUw2&+>vAy3JpMEnByeI6cdF$!xw}sxx
zopF}))7D%`Nx1e@Y4M!-Cym}1MfI(75xM>_Zsy`0+jsr)Ja+VIuISsJ{j<*~bDCW~
z{ilA`de`)YrZWC&VP92QRs9^MT{^zw&7NPTDPIn~JhfoI0Q074FNIbz{A)S0V{`n0
zJux-`$L7s{_*%^F>h;YF=c{?ynje1=-E&s8a2E64&(d9GH6H&>bY422=H5BU?#)N>
z0F($k=i-|#1dKc#76t}xU<7976_+ID<meTpq~VLd)Zp0s*#;uD&%^Ir+@8tCbWv(i
zOJ0;%X3*+TZ_`ZwiH+Wmcym(XXDzw>?{^G`O6I%`=WU<Y&B;zH|Glau{fe80Wx0FY
z1Ey0xPo46f<vFv?-cjCZTzy=^CbzY5YF0_x2iK<@-b=MN%*ecSe}>=$rdvt^uTC{+
z-8$V|C762h;YLsIiIxlp9>k?+@YXe?^E>H(co5AfV-zaScJ;CCn;ncRIf@cJy0<8v
zNmS3s<~&r=$rX4@XMYH@<n#qWj}FzTFsa`Vx*B|c{jOj0E=k`{`<iQepZS>3@tJ=k
zbPo3EpS@$dseJd`?cGtIH2RY5IsO`?Sn1EO^mtUQcDDC+=CX*`6X&>F9Sy#1tP;Jq
zqw{U)HiyZXfs;!mD^^KudX{Br+qKL|>QU-REj`!9wy23y6HP@cgWgPybbp<Y>vDC<
zoGJfG!g8OLuW>BAlV;R+E1+KXQOOEJ&f5{yGE6IjQuaP8b=q}sT0s+Am~!^Kzm|c0
z<;#`#*iBh_A!0@C>rVkwUh`ghzH8h1hRYYuFEzC_^Z6&f*;9b^sP5@Q4Vyn6FFg3j
z#KEkq;z)8&{^FBA?zb}U-6iztdEuwPH>|qPd3HbfRNLR)_<i5wc^mcx|6g_W+>zH8
z&+WP3B~&xj(njGUt9o5x_Y+aIz47<M6SniLt~;IkZJObS&zt!y_6W}6PW-p+rebH7
z<L8np_U9eNrZ3NIulIWNhdscXkx7mjSIHp300KZR!;(f23$>hJg`_jIG=^>>YEnfu
a@gR^wNw~NSWMu<sVgka&Kw5$Y!~+0TOA5jO
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ec9645da40d54ea47b2e9c7b418b6f777f9dc72e
GIT binary patch
literal 1233
zc$^FHW@Zs#U|`^2NNl;}l3KvEx0;cG;T97E0}q1?LsEWzNpVS0Vu4;(acBr91GB0B
z!e9_Ct>9*0WO>QVzyKzCC;DgKb`UuF{+CFzf1ly43HL;bxLCL}O?OO}3tv6aG;ynP
zS=!qz$L{Y{R`->5_R|0T-TeLDo2A+}F5lqLT5;!YgUqvsdyYImx!kXoTRVKGXpOW-
z!f93Zmg;u`au)pw=|Q*DdDIRpU~Wp1W8Gn&zwd*8*TuujwoG(l)vj35;90=?TPp9>
zYbm?$4-`LqdAv65GY7l=gouU87kzRA-#l=zl5j9O{YAw0v9&md9pm;_CzrK65M{f%
zvM{ga<2uXtb26=pi@)CS5w++%WP5OKsgYmUw2&+>vAy3JpMEnByeI6cdF$!xw}sxx
zopF}))7D%`Nx1e@Y4M!-Cym}1MfI(75xM>_Zsy`0+jsr)Ja+VIuISsJ{j<*~bDCW~
z{ilA`de`)YrZWC&VP92QRs9^MT{^zw&7NPTDPIn~JhfoI0Q074FNIbz{A)S0V{`n0
zJux-`$L7s{_*%^F>h;YF=c{?ynje1=-E&s8a2E64&(d9GH6H&>bY422=H5BU?#)N>
z0F($k=i-|#1dKc#76t}xU<7976_+ID<meTpq~VLd)Zp0s*#;uD&%^Ir+@8tCbWv(i
zOJ0;%X3*+TZ_`ZwiH+Wmcym(XXDzw>?{^G`O6I%`=WU<Y&B;zH|Glau{fe80Wx0FY
z1Ey0xPo46f<vFv?-cjCZTzy=^CbzY5YF0_x2iK<@-b=MN%*ecSe}>=$rdvt^uTC{+
z-8$V|C762h;YLsIiIxlp9>k?+@YXe?^E>H(co5AfV-zaScJ;CCn;ncRIf@cJy0<8v
zNmS3s<~&r=$rX4@XMYH@<n#qWj}FzTFsa`Vx*B|c{jOj0E=k`{`<iQepZS>3@tJ=k
zbPo3EpS@$dseJd`?cGtIH2RY5IsO`?Sn1EO^mtUQcDDC+=CX*`6X&>F9Sy#1tP;Jq
zqw{U)HiyZXfs;!mD^^KudX{Br+qKL|>QU-REj`!9wy23y6HP@cgWgPybbp<Y>vDC<
zoGJfG!g8OLuW>BAlV;R+E1+KXQOOEJ&f5{yGE6IjQuaP8b=q}sT0s+Am~!^Kzm|c0
z<;#`#*iBh_A!0@C>rVkwUh`ghzH8h1hRYYuFEzC_^Z6&f*;9b^sP5@Q4Vyn6FFg3j
z#KEkq;z)8&{^FBA?zb}U-6iztdEuwPH>|qPd3HbfRNLR)_<i5wc^mcx|6g_W+>zH8
z&+WP3B~&xj(njGUt9o5x_Y+aIz47<M6SniLt~;IkZJObS&zt!y_6W}6PW-p+rebH7
z<L8np_U9eNrZ3NIulIWNhdscXkx7mjSIHp300KZR!;(f23$>hJg`_jIG=^>>YEnfu
a@gR^wNw~NSWMu<sVgka&Kw5$Y!~+0TOA5jO
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ec9645da40d54ea47b2e9c7b418b6f777f9dc72e
GIT binary patch
literal 1233
zc$^FHW@Zs#U|`^2NNl;}l3KvEx0;cG;T97E0}q1?LsEWzNpVS0Vu4;(acBr91GB0B
z!e9_Ct>9*0WO>QVzyKzCC;DgKb`UuF{+CFzf1ly43HL;bxLCL}O?OO}3tv6aG;ynP
zS=!qz$L{Y{R`->5_R|0T-TeLDo2A+}F5lqLT5;!YgUqvsdyYImx!kXoTRVKGXpOW-
z!f93Zmg;u`au)pw=|Q*DdDIRpU~Wp1W8Gn&zwd*8*TuujwoG(l)vj35;90=?TPp9>
zYbm?$4-`LqdAv65GY7l=gouU87kzRA-#l=zl5j9O{YAw0v9&md9pm;_CzrK65M{f%
zvM{ga<2uXtb26=pi@)CS5w++%WP5OKsgYmUw2&+>vAy3JpMEnByeI6cdF$!xw}sxx
zopF}))7D%`Nx1e@Y4M!-Cym}1MfI(75xM>_Zsy`0+jsr)Ja+VIuISsJ{j<*~bDCW~
z{ilA`de`)YrZWC&VP92QRs9^MT{^zw&7NPTDPIn~JhfoI0Q074FNIbz{A)S0V{`n0
zJux-`$L7s{_*%^F>h;YF=c{?ynje1=-E&s8a2E64&(d9GH6H&>bY422=H5BU?#)N>
z0F($k=i-|#1dKc#76t}xU<7976_+ID<meTpq~VLd)Zp0s*#;uD&%^Ir+@8tCbWv(i
zOJ0;%X3*+TZ_`ZwiH+Wmcym(XXDzw>?{^G`O6I%`=WU<Y&B;zH|Glau{fe80Wx0FY
z1Ey0xPo46f<vFv?-cjCZTzy=^CbzY5YF0_x2iK<@-b=MN%*ecSe}>=$rdvt^uTC{+
z-8$V|C762h;YLsIiIxlp9>k?+@YXe?^E>H(co5AfV-zaScJ;CCn;ncRIf@cJy0<8v
zNmS3s<~&r=$rX4@XMYH@<n#qWj}FzTFsa`Vx*B|c{jOj0E=k`{`<iQepZS>3@tJ=k
zbPo3EpS@$dseJd`?cGtIH2RY5IsO`?Sn1EO^mtUQcDDC+=CX*`6X&>F9Sy#1tP;Jq
zqw{U)HiyZXfs;!mD^^KudX{Br+qKL|>QU-REj`!9wy23y6HP@cgWgPybbp<Y>vDC<
zoGJfG!g8OLuW>BAlV;R+E1+KXQOOEJ&f5{yGE6IjQuaP8b=q}sT0s+Am~!^Kzm|c0
z<;#`#*iBh_A!0@C>rVkwUh`ghzH8h1hRYYuFEzC_^Z6&f*;9b^sP5@Q4Vyn6FFg3j
z#KEkq;z)8&{^FBA?zb}U-6iztdEuwPH>|qPd3HbfRNLR)_<i5wc^mcx|6g_W+>zH8
z&+WP3B~&xj(njGUt9o5x_Y+aIz47<M6SniLt~;IkZJObS&zt!y_6W}6PW-p+rebH7
z<L8np_U9eNrZ3NIulIWNhdscXkx7mjSIHp300KZR!;(f23$>hJg`_jIG=^>>YEnfu
a@gR^wNw~NSWMu<sVgka&Kw5$Y!~+0TOA5jO
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ec9645da40d54ea47b2e9c7b418b6f777f9dc72e
GIT binary patch
literal 1233
zc$^FHW@Zs#U|`^2NNl;}l3KvEx0;cG;T97E0}q1?LsEWzNpVS0Vu4;(acBr91GB0B
z!e9_Ct>9*0WO>QVzyKzCC;DgKb`UuF{+CFzf1ly43HL;bxLCL}O?OO}3tv6aG;ynP
zS=!qz$L{Y{R`->5_R|0T-TeLDo2A+}F5lqLT5;!YgUqvsdyYImx!kXoTRVKGXpOW-
z!f93Zmg;u`au)pw=|Q*DdDIRpU~Wp1W8Gn&zwd*8*TuujwoG(l)vj35;90=?TPp9>
zYbm?$4-`LqdAv65GY7l=gouU87kzRA-#l=zl5j9O{YAw0v9&md9pm;_CzrK65M{f%
zvM{ga<2uXtb26=pi@)CS5w++%WP5OKsgYmUw2&+>vAy3JpMEnByeI6cdF$!xw}sxx
zopF}))7D%`Nx1e@Y4M!-Cym}1MfI(75xM>_Zsy`0+jsr)Ja+VIuISsJ{j<*~bDCW~
z{ilA`de`)YrZWC&VP92QRs9^MT{^zw&7NPTDPIn~JhfoI0Q074FNIbz{A)S0V{`n0
zJux-`$L7s{_*%^F>h;YF=c{?ynje1=-E&s8a2E64&(d9GH6H&>bY422=H5BU?#)N>
z0F($k=i-|#1dKc#76t}xU<7976_+ID<meTpq~VLd)Zp0s*#;uD&%^Ir+@8tCbWv(i
zOJ0;%X3*+TZ_`ZwiH+Wmcym(XXDzw>?{^G`O6I%`=WU<Y&B;zH|Glau{fe80Wx0FY
z1Ey0xPo46f<vFv?-cjCZTzy=^CbzY5YF0_x2iK<@-b=MN%*ecSe}>=$rdvt^uTC{+
z-8$V|C762h;YLsIiIxlp9>k?+@YXe?^E>H(co5AfV-zaScJ;CCn;ncRIf@cJy0<8v
zNmS3s<~&r=$rX4@XMYH@<n#qWj}FzTFsa`Vx*B|c{jOj0E=k`{`<iQepZS>3@tJ=k
zbPo3EpS@$dseJd`?cGtIH2RY5IsO`?Sn1EO^mtUQcDDC+=CX*`6X&>F9Sy#1tP;Jq
zqw{U)HiyZXfs;!mD^^KudX{Br+qKL|>QU-REj`!9wy23y6HP@cgWgPybbp<Y>vDC<
zoGJfG!g8OLuW>BAlV;R+E1+KXQOOEJ&f5{yGE6IjQuaP8b=q}sT0s+Am~!^Kzm|c0
z<;#`#*iBh_A!0@C>rVkwUh`ghzH8h1hRYYuFEzC_^Z6&f*;9b^sP5@Q4Vyn6FFg3j
z#KEkq;z)8&{^FBA?zb}U-6iztdEuwPH>|qPd3HbfRNLR)_<i5wc^mcx|6g_W+>zH8
z&+WP3B~&xj(njGUt9o5x_Y+aIz47<M6SniLt~;IkZJObS&zt!y_6W}6PW-p+rebH7
z<L8np_U9eNrZ3NIulIWNhdscXkx7mjSIHp300KZR!;(f23$>hJg`_jIG=^>>YEnfu
a@gR^wNw~NSWMu<sVgka&Kw5$Y!~+0TOA5jO
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webide/test/addons/simulators.json
@@ -0,0 +1,4 @@
+{
+    "stable": ["1.0", "2.0"],
+     "unstable": ["3.0"]
+}
--- a/browser/devtools/webide/test/chrome.ini
+++ b/browser/devtools/webide/test/chrome.ini
@@ -1,15 +1,34 @@
 [DEFAULT]
 support-files =
   app/index.html
   app/manifest.webapp
   app.zip
+  addons/simulators.json
+  addons/fxos_1_0_simulator-linux.xpi
+  addons/fxos_1_0_simulator-linux64.xpi
+  addons/fxos_1_0_simulator-win32.xpi
+  addons/fxos_1_0_simulator-mac64.xpi
+  addons/fxos_2_0_simulator-linux.xpi
+  addons/fxos_2_0_simulator-linux64.xpi
+  addons/fxos_2_0_simulator-win32.xpi
+  addons/fxos_2_0_simulator-mac64.xpi
+  addons/fxos_3_0_simulator-linux.xpi
+  addons/fxos_3_0_simulator-linux64.xpi
+  addons/fxos_3_0_simulator-win32.xpi
+  addons/fxos_3_0_simulator-mac64.xpi
+  addons/adbhelper-linux.xpi
+  addons/adbhelper-linux64.xpi
+  addons/adbhelper-win32.xpi
+  addons/adbhelper-mac64.xpi
   head.js
   hosted_app.manifest
   templates.json
 
 [test_basic.html]
 [test_newapp.html]
 [test_import.html]
 [test_runtime.html]
 [test_cli.html]
 [test_manifestUpdate.html]
+[test_addons.html]
+[test_deviceinfo.html]
--- a/browser/devtools/webide/test/head.js
+++ b/browser/devtools/webide/test/head.js
@@ -14,24 +14,39 @@ const {devtools} = Cu.import("resource:/
 const {require} = devtools;
 const {AppProjects} = require("devtools/app-manager/app-projects");
 
 const 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.templatesURL", TEST_BASE + "templates.json");
+
+
 SimpleTest.registerCleanupFunction(() => {
+  Services.prefs.clearUserPref("devtools.webide.templatesURL");
   Services.prefs.clearUserPref("devtools.webide.enabled");
   Services.prefs.clearUserPref("devtools.webide.enableLocalRuntime");
+  Services.prefs.clearUserPref("devtools.webide.addonsURL");
+  Services.prefs.clearUserPref("devtools.webide.simulatorAddonsURL");
+  Services.prefs.clearUserPref("devtools.webide.adbAddonURL");
+  Services.prefs.clearUserPref("devtools.webide.autoInstallADBHelper", false);
 });
 
-function openWebIDE() {
+function openWebIDE(autoInstallADBHelper) {
   info("opening WebIDE");
 
+  if (!autoInstallADBHelper) {
+    Services.prefs.setBoolPref("devtools.webide.autoinstallADBHelper", false);
+  }
+
   let deferred = promise.defer();
 
   let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].getService(Ci.nsIWindowWatcher);
   let win = ww.openWindow(null, "chrome://webide/content/", "webide", "chrome,centerscreen,resizable", null);
 
   win.addEventListener("load", function onLoad() {
     win.removeEventListener("load", onLoad);
     info("WebIDE open");
@@ -75,8 +90,23 @@ function removeAllProjects() {
 function nextTick() {
   let deferred = promise.defer();
   SimpleTest.executeSoon(() => {
     deferred.resolve();
   });
 
   return deferred.promise;
 }
+
+function documentIsLoaded(doc) {
+  let deferred = promise.defer();
+  if (doc.readyState == "complete") {
+    deferred.resolve();
+  } else {
+    doc.addEventListener("readystatechange", function onChange() {
+      if (doc.readyState == "complete") {
+        doc.removeEventListener("readystatechange", onChange);
+        deferred.resolve();
+      }
+    });
+  }
+  return deferred.promise;
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webide/test/test_addons.html
@@ -0,0 +1,168 @@
+<!DOCTYPE html>
+
+<html>
+
+  <head>
+    <meta charset="utf8">
+    <title></title>
+
+    <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+    <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
+    <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">
+      window.onload = function() {
+        SimpleTest.waitForExplicitFinish();
+
+        const {GetAvailableAddons} = require("devtools/webide/addons");
+        const {Devices} = Cu.import("resource://gre/modules/devtools/Devices.jsm");
+        const {Simulator} = Cu.import("resource://gre/modules/devtools/Simulator.jsm");
+
+        let adbAddonsInstalled = promise.defer();
+        Devices.on("addon-status-updated", function onUpdate1() {
+          Devices.off("addon-status-updated", onUpdate1);
+          adbAddonsInstalled.resolve();
+        });
+
+        function onSimulatorInstalled(version) {
+          let deferred = promise.defer();
+          Simulator.on("register", function onUpdate() {
+            if (Simulator.getByVersion(version)) {
+              Simulator.off("register", onUpdate);
+              nextTick().then(deferred.resolve);
+            }
+          });
+          return deferred.promise;
+        }
+
+        function installSimulatorFromUI(doc, version) {
+          let li = doc.querySelector('[addon="simulator-' + version + '"]');
+          li.querySelector(".install-button").click();
+          return onSimulatorInstalled(version);
+        }
+
+        function uninstallSimulatorFromUI(doc, version) {
+          let deferred = promise.defer();
+          Simulator.on("unregister", function onUpdate() {
+            nextTick().then(() => {
+              let li = doc.querySelector('[status="uninstalled"][addon="simulator-' + version + '"]');
+              if (li) {
+                Simulator.off("unregister", onUpdate);
+                deferred.resolve();
+              } else {
+                deferred.reject("Can't find item");
+              }
+            })
+          });
+          let li = doc.querySelector('[status="installed"][addon="simulator-' + version + '"]');
+          li.querySelector(".uninstall-button").click();
+          return deferred.promise;
+        }
+
+        function uninstallADBFromUI(doc) {
+          let deferred = promise.defer();
+          Devices.on("addon-status-updated", function onUpdate() {
+            nextTick().then(() => {
+              let li = doc.querySelector('[status="uninstalled"][addon="adb"]');
+              if (li) {
+                Devices.off("addon-status-updated", onUpdate);
+                deferred.resolve();
+              } else {
+                deferred.reject("Can't find item");
+              }
+            })
+          });
+          let li = doc.querySelector('[status="installed"][addon="adb"]');
+          li.querySelector(".uninstall-button").click();
+          return deferred.promise;
+        }
+
+        Task.spawn(function* () {
+
+          ok(!Devices.helperAddonInstalled, "Helper not installed");
+
+          let win = yield openWebIDE(true);
+
+          yield adbAddonsInstalled.promise;
+
+          ok(Devices.helperAddonInstalled, "Helper has been auto-installed");
+
+          yield nextTick();
+
+          let addons = yield GetAvailableAddons();
+
+          is(addons.simulators.length, 3, "3 simulator addons to install");
+
+          let sim10 = addons.simulators.filter(a => a.version == "1.0")[0];
+          sim10.install();
+
+          yield onSimulatorInstalled("1.0");
+
+          win.Cmds.showAddons();
+
+          let frame = win.document.querySelector("#deck-panel-addons");
+          let addonDoc = frame.contentWindow.document;
+          let lis;
+
+          lis = addonDoc.querySelectorAll("li");
+          is(lis.length, 4, "4 addons listed");
+
+          lis = addonDoc.querySelectorAll('li[status="installed"]');
+          is(lis.length, 2, "2 addons installed");
+
+          lis = addonDoc.querySelectorAll('li[status="uninstalled"]');
+          is(lis.length, 2, "2 addons uninstalled");
+
+          info("Uninstalling Simulator 2.0");
+
+          yield installSimulatorFromUI(addonDoc, "2.0");
+
+          info("Uninstalling Simulator 3.0");
+
+          yield installSimulatorFromUI(addonDoc, "3.0");
+
+          yield nextTick();
+
+          let panelNode = win.document.querySelector("#runtime-panel");
+          let items;
+
+          items = panelNode.querySelectorAll(".runtime-panel-item-usb");
+          is(items.length, 1, "Found one runtime button");
+
+          items = panelNode.querySelectorAll(".runtime-panel-item-simulator");
+          is(items.length, 3, "Found 3 simulators button");
+
+          yield uninstallSimulatorFromUI(addonDoc, "1.0");
+          yield uninstallSimulatorFromUI(addonDoc, "2.0");
+          yield uninstallSimulatorFromUI(addonDoc, "3.0");
+
+          items = panelNode.querySelectorAll(".runtime-panel-item-simulator");
+          is(items.length, 0, "No simulator listed");
+
+          let w = addonDoc.querySelector(".warning");
+          let display = addonDoc.defaultView.getComputedStyle(w).display
+          is(display, "none", "Warning about missing ADB hidden");
+
+          yield uninstallADBFromUI(addonDoc, "adb");
+
+          items = panelNode.querySelectorAll(".runtime-panel-item-usb");
+          is(items.length, 0, "No usb runtime listed");
+
+          display = addonDoc.defaultView.getComputedStyle(w).display
+          is(display, "block", "Warning about missing ADB present");
+
+          yield closeWebIDE(win);
+
+          SimpleTest.finish();
+
+        });
+      }
+
+
+    </script>
+  </body>
+</html>
--- a/browser/devtools/webide/test/test_basic.html
+++ b/browser/devtools/webide/test/test_basic.html
@@ -24,17 +24,17 @@
             ok(win, "Found a window");
             ok(win.AppManager, "App Manager accessible");
             let appmgr = win.AppManager;
             ok(appmgr.connection, "App Manager connection ready");
             ok(appmgr.runtimeList, "Runtime list ready");
             ok(appmgr.webAppsStore, "WebApps store ready");
 
             // test error reporting
-            let nbox = win.document.querySelector("#body");
+            let nbox = win.document.querySelector("#notificationbox");
             let notification =  nbox.getNotificationWithValue("webide:errornotification");
             ok(!notification, "No notification yet");
             let deferred = promise.defer();
             nextTick().then(() => {
               deferred.reject("BOOM!");
             });
             try {
               yield win.UI.busyUntil(deferred.promise, "xx");
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webide/test/test_deviceinfo.html
@@ -0,0 +1,126 @@
+<!DOCTYPE html>
+
+<html>
+
+  <head>
+    <meta charset="utf8">
+    <title></title>
+
+    <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+    <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
+    <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">
+      window.onload = function() {
+        SimpleTest.waitForExplicitFinish();
+
+        Task.spawn(function* () {
+          Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
+          DebuggerServer.init(function () { return true; });
+          DebuggerServer.addBrowserActors();
+
+          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");
+
+          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 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();
+            }
+          });
+
+          items[1].click();
+
+          yield deferred.promise;
+
+          yield nextTick();
+
+          let perm = win.document.querySelector("#cmd_showPermissionsTable");
+          let info = win.document.querySelector("#cmd_showRuntimeDetails");
+
+          ok(!perm.hasAttribute("disabled"), "perm cmd enabled");
+          ok(!info.hasAttribute("disabled"), "info cmd enabled");
+
+          let deck = win.document.querySelector("#deck");
+
+          win.Cmds.showRuntimeDetails();
+          is(deck.selectedPanel, infoIframe, "info iframe selected");
+
+          yield infoIframe.contentWindow.getRawPermissionsTablePromise;
+
+          yield nextTick();
+
+          // device info and permissions content is checked in other tests
+          // We just test one value to make sure we get something
+
+          let doc = infoIframe.contentWindow.document;
+          let trs = doc.querySelectorAll("tr");
+          let found = false;
+
+          for (let tr of trs) {
+            let [name,val] = tr.querySelectorAll("td");
+            if (name.textContent == "appid") {
+              found = true;
+              is(val.textContent, Services.appinfo.ID, "appid has the right value");
+            }
+          }
+          ok(found, "Found appid line");
+
+          win.Cmds.showPermissionsTable();
+          is(deck.selectedPanel, permIframe, "permission iframe selected");
+
+          yield infoIframe.contentWindow.getDescriptionPromise;
+
+          yield nextTick();
+
+          doc = permIframe.contentWindow.document;
+          trs = doc.querySelectorAll(".line");
+          found = false;
+          for (let tr of trs) {
+            let [name,v1,v2,v3] = tr.querySelectorAll("td");
+            if (name.textContent == "geolocation") {
+              found = true;
+              is(v1.className, "permprompt", "geolocation perm is valid");
+              is(v2.className, "permprompt", "geolocation perm is valid");
+              is(v3.className, "permprompt", "geolocation perm is valid");
+            }
+          }
+          ok(found, "Found geolocation line");
+
+          doc.querySelector("#close").click();
+
+          ok(!deck.selectedPanel, "No panel selected");
+
+          DebuggerServer.destroy();
+
+          yield closeWebIDE(win);
+
+          SimpleTest.finish();
+
+
+        }).then(null, e => {
+          ok(false, "Exception: " + e);
+          SimpleTest.finish();
+        });
+      }
+
+
+    </script>
+  </body>
+</html>
--- a/browser/devtools/webide/test/test_newapp.html
+++ b/browser/devtools/webide/test/test_newapp.html
@@ -13,18 +13,16 @@
   </head>
 
   <body>
 
     <script type="application/javascript;version=1.8">
       window.onload = function() {
         SimpleTest.waitForExplicitFinish();
 
-        Services.prefs.setCharPref("devtools.webide.templatesURL", TEST_BASE + "templates.json");
-
         Task.spawn(function* () {
             let win = yield openWebIDE();
             let tmpDir = FileUtils.getDir("TmpD", []);
             yield win.Cmds.newApp({
               index: 0,
               name: "webideTmpApp",
               folder: tmpDir
             });
@@ -32,17 +30,16 @@
             let project = win.AppManager.selectedProject;
             let tmpDir = FileUtils.getDir("TmpD", ["webidetmpapp"]);
             ok(tmpDir.isDirectory(), "Directory created");
             is(project.location, tmpDir.path, "Location is valid (and lowercase)");
             is(project.name, "webideTmpApp", "name field has been updated");
 
             // Clean up
             tmpDir.remove(true);
-            Services.prefs.clearUserPref("devtools.webide.templatesURL");
             yield closeWebIDE(win);
             yield removeAllProjects();
             SimpleTest.finish();
         });
       }
     </script>
   </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webide/themes/addons.css
@@ -0,0 +1,121 @@
+/* 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/. */
+
+@import url("chrome://browser/skin/in-content/common.css");
+
+html {
+  font: message-box;
+  font-size: 15px;
+  font-weight: normal;
+  margin: 0;
+  color: #737980;
+  background-image: linear-gradient(#fff, #ededed 100px);
+  height: 100%;
+}
+
+body {
+  padding: 20px;
+}
+
+h1 {
+  font-size: 2.5em;
+  font-weight: lighter;
+  line-height: 1.2;
+  margin: 0;
+  margin-bottom: .5em;
+}
+
+button {
+  line-height: 20px;
+  font-size: 1em;
+  height: 30px;
+  max-height: 30px;
+  min-width: 120px;
+  padding: 3px;
+  color: #737980;
+  border: 1px solid rgba(23,50,77,.4);
+  border-radius: 5px;
+  background-color: #f1f1f1;
+  background-image: linear-gradient(#fff, rgba(255,255,255,.1));
+  box-shadow: 0 1px 1px 0 #fff, inset 0 2px 2px 0 #fff;
+  text-shadow: 0 1px 1px #fefffe;
+  -moz-appearance: none;
+  -moz-border-top-colors: none !important;
+  -moz-border-right-colors: none !important;
+  -moz-border-bottom-colors: none !important;
+  -moz-border-left-colors: none !important;
+}
+
+button:hover {
+  background-image: linear-gradient(#fff, rgba(255,255,255,.6));
+  cursor: pointer;
+}
+
+button:hover:active {
+  background-image: linear-gradient(rgba(255,255,255,.1), rgba(255,255,255,.6));
+}
+
+progress {
+  height: 30px;
+  vertical-align: middle;
+  padding: 0;
+  width: 120px;
+}
+
+li {
+  margin: 20px 0;
+}
+
+.name {
+  display: inline-block;
+  min-width: 280px;
+}
+
+.status {
+  display: inline-block;
+  min-width: 120px;
+}
+
+.warning {
+  color: #F06;
+  margin: 0;
+  font-size: 0.9em;
+}
+
+
+#controls {
+  position: absolute;
+  top: 10px;
+  right: 10px;
+}
+
+#controls > a {
+  color: #4C9ED9;
+  font-size: small;
+  cursor: pointer;
+  border-bottom: 1px dotted;
+}
+
+#close {
+  margin-left: 10px;
+}
+
+li[status="unknown"],
+li > .uninstall-button,
+li > .install-button,
+li > progress {
+  display: none;
+}
+
+li[status="installed"] > .uninstall-button,
+li[status="uninstalled"] > .install-button,
+li[status="preparing"] > progress,
+li[status="downloading"] > progress,
+li[status="installing"] > progress {
+  display: inline;
+}
+
+li:not([status="uninstalled"]) > .warning {
+  display: none;
+}
--- a/browser/devtools/webide/themes/jar.mn
+++ b/browser/devtools/webide/themes/jar.mn
@@ -4,8 +4,10 @@
 
 webide.jar:
 % skin webide classic/1.0 %skin/
 * skin/webide.css         (webide.css)
   skin/icons.png          (icons.png)
   skin/details.css        (details.css)
   skin/newapp.css         (newapp.css)
   skin/throbber.svg       (throbber.svg)
+  skin/addons.css         (addons.css)
+  skin/tabledoc.css       (tabledoc.css)
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webide/themes/tabledoc.css
@@ -0,0 +1,54 @@
+body {
+  background: white;
+}
+
+#controls {
+  position: fixed;
+  top: 10px;
+  right: 10px;
+}
+
+#controls > a {
+  color: #4C9ED9;
+  font-size: small;
+  cursor: pointer;
+  border-bottom: 1px dotted;
+}
+
+#close {
+  margin-left: 10px;
+}
+
+table {
+  font-family: monospace;
+  border-collapse: collapse;
+}
+
+th, td {
+  padding: 5px;
+  border: 1px solid #EEE;
+}
+
+th {
+  min-width: 130px;
+}
+
+.permissionstable td {
+  text-align: center;
+}
+
+th:first-of-type, td:first-of-type {
+  text-align: left;
+}
+
+.permallow {
+  color: rgb(152,207,57);
+}
+
+.permprompt {
+  color: rgb(0,158,237);
+}
+
+.permdeny {
+  color: rgb(204,73,8);
+}
--- a/browser/devtools/webide/themes/webide.css
+++ b/browser/devtools/webide/themes/webide.css
@@ -35,17 +35,17 @@ window:not(.busy) #action-busy {
 
 .panel-button {
   -moz-box-align: center;
 }
 
 .panel-button-anchor {
   list-style-image: url('icons.png');
   -moz-image-region: rect(43px, 563px, 61px, 535px);
-  width: 12;
+  width: 12px;
   height: 7px;
   margin-bottom: -5px;
 }
 
 .panel-button:hover > .panel-button-anchor {
   -moz-image-region: rect(243px, 563px, 261px, 535px);
 }
 
@@ -113,23 +113,29 @@ panel > vbox {
   overflow-x: hidden;
 }
 
 panel > .panel-arrowcontainer > .panel-arrowcontent {
   padding: 12px 0;
   width: 180px;
 }
 
-.panel-item {
+.panel-item,
+.panel-item-help {
   padding: 3px 12px;
   margin: 0;
   -moz-appearance: none;
 }
 
-.panel-item:hover {
+.panel-item-help {
+  font-size: 0.9em;
+}
+
+.panel-item:hover,
+.panel-item-help:hover {
   background: #CBF0FE;
 }
 
 .panel-header {
   /* We can't use borders or vertical padding here because
    * panels don't take these into account when calculated the
    * height of the panel.
    */
@@ -146,17 +152,18 @@ panel > .panel-arrowcontainer > .panel-a
   font-weight: bold;
 }
 
 .panel-item > .toolbarbutton-icon {
   width: 18px;
   height: 18px;
 }
 
-.panel-item > .toolbarbutton-text {
+.panel-item > .toolbarbutton-text,
+.panel-item-help > .toolbarbutton-text {
   text-align: start;
 }
 
 /* project panel */
 
 .project-panel-item-newapp,
 .project-panel-item-openpackaged,
 .project-panel-item-openhosted {
@@ -209,17 +216,17 @@ panel > .panel-arrowcontainer > .panel-a
 }
 
 #runtime-actions > toolbarbutton:last-child {
   border-radius: 0 0 3px 3px;
 }
 
 /* Main view */
 
-#body {
+#deck {
   background-color: rgb(225, 225, 225);
   background-image: url('chrome://browser/skin/devtools/app-manager/rocket.svg'), url('chrome://browser/skin/devtools/app-manager/noise.png');
   background-repeat: no-repeat, repeat;
   background-size: 35%, auto;
   background-position: center center, top left;
 %ifndef XP_MACOSX
   border-top: 1px solid #AAA;
 %endif
--- a/browser/devtools/webide/webide-prefs.js
+++ b/browser/devtools/webide/webide-prefs.js
@@ -1,9 +1,15 @@
 # -*- Mode: JavaScript; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 # 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/.
 
 pref("devtools.webide.showProjectEditor", true);
-pref("devtools.webide.templatesURL", "http://people.mozilla.org/~prouget/webidetemplates/template.json"); // See bug 1021504
+pref("devtools.webide.templatesURL", "http://code.cdn.mozilla.net/templates/list.json");
+pref("devtools.webide.autoinstallADBHelper", true);
 pref("devtools.webide.lastprojectlocation", "");
 pref("devtools.webide.enableLocalRuntime", false);
+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");
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -1817,29 +1817,49 @@ richlistitem[type~="action"][actiontype=
 .tabs-newtab-button,
 #TabsToolbar > #new-tab-button ,
 #TabsToolbar > #wrapper-new-tab-button > #new-tab-button {
   list-style-image: url("moz-icon://stock/gtk-add?size=menu");
   -moz-image-region: auto;
 }
 
 /* Tabbrowser arrowscrollbox arrows */
+.tabbrowser-arrowscrollbox > .scrollbutton-up > .toolbarbutton-icon,
+.tabbrowser-arrowscrollbox > .scrollbutton-down > .toolbarbutton-icon {
+  -moz-appearance: none;
+}
+
 .tabbrowser-arrowscrollbox > .scrollbutton-up,
 .tabbrowser-arrowscrollbox > .scrollbutton-down {
   -moz-appearance: none;
+  list-style-image: url("chrome://browser/skin/tabbrowser/tab-arrow-left.png");
   margin: 0 0 @tabToolbarNavbarOverlap@;
 }
 
+#TabsToolbar[brighttext] > #tabbrowser-tabs > .tabbrowser-arrowscrollbox > .scrollbutton-up,
+#TabsToolbar[brighttext] > #tabbrowser-tabs > .tabbrowser-arrowscrollbox > .scrollbutton-down {
+  list-style-image: url(chrome://browser/skin/tabbrowser/tab-arrow-left-inverted.png);
+}
+
+.tabbrowser-arrowscrollbox > .scrollbutton-up[disabled],
+.tabbrowser-arrowscrollbox > .scrollbutton-down[disabled] {
+  opacity: .4;
+}
+
+.tabbrowser-arrowscrollbox > .scrollbutton-up:-moz-locale-dir(rtl),
+.tabbrowser-arrowscrollbox > .scrollbutton-down:-moz-locale-dir(ltr) {
+  transform: scaleX(-1);
+}
+
 .tabbrowser-arrowscrollbox > .scrollbutton-down {
-  transition: 1s box-shadow ease-out;
-  border-radius: 4px;
+  transition: 1s background-color ease-out;
 }
 
 .tabbrowser-arrowscrollbox > .scrollbutton-down[notifybgtab] {
-  box-shadow: 0 0 5px 5px Highlight inset;
+  background-color: Highlight;
   transition: none;
 }
 
 #TabsToolbar .toolbarbutton-1 {
   margin-bottom: @tabToolbarNavbarOverlap@;
 }
 
 #TabsToolbar .toolbarbutton-1 > .toolbarbutton-icon,
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -158,16 +158,18 @@ browser.jar:
   skin/classic/browser/social/share-button.png        (social/share-button.png)
   skin/classic/browser/social/share-button-active.png (social/share-button-active.png)
   skin/classic/browser/social/chat-icons.png          (social/chat-icons.png)
   skin/classic/browser/social/gear_default.png        (../shared/social/gear_default.png)
   skin/classic/browser/social/gear_clicked.png        (../shared/social/gear_clicked.png)
   skin/classic/browser/tabbrowser/connecting.png      (tabbrowser/connecting.png)
   skin/classic/browser/tabbrowser/loading.png         (tabbrowser/loading.png)
   skin/classic/browser/tabbrowser/tab-active-middle.png     (tabbrowser/tab-active-middle.png)
+  skin/classic/browser/tabbrowser/tab-arrow-left.png        (tabbrowser/tab-arrow-left.png)
+  skin/classic/browser/tabbrowser/tab-arrow-left-inverted.png (tabbrowser/tab-arrow-left-inverted.png)
   skin/classic/browser/tabbrowser/tab-background-end.png    (tabbrowser/tab-background-end.png)
   skin/classic/browser/tabbrowser/tab-background-middle.png (tabbrowser/tab-background-middle.png)
   skin/classic/browser/tabbrowser/tab-background-start.png  (tabbrowser/tab-background-start.png)
   skin/classic/browser/tabbrowser/tab-overflow-indicator.png (../shared/tabbrowser/tab-overflow-indicator.png)
 
 # NOTE: The following two files (tab-selected-end.svg, tab-selected-start.svg) get pre-processed in
 #       Makefile.in with a non-default marker of "%" and the result of that gets packaged.
   skin/classic/browser/tabbrowser/tab-selected-end.svg      (tab-selected-end.svg)
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..16cd7a2775ae1f56274035fcde4ed5e8c211a52c
GIT binary patch
literal 250
zc%17D@N?(olHy`uVBq!ia0vp^{6H+o!3HFmxV|j}Qj#UE5hcO-X(i=}MX3yqDfvmM
z3ZA)%>8U}fi7AzZCsS>JispE_IEGZ*O8WEvzdiHLhR#N&15IZVQ&NsBJI2|O+xWvk
z;JtI1rsC|sjW?bjY-YD~xFwk-z|`Q=l&B;1``+H_@1h#t<ueXsE9{qZwdFgjA?0Y&
z!M|zdgtQo`zlJeg%OuykInAoRVDe2l+vU2C#+IE85e&<9?9%_YbDUGTc2k5^;%K02
wp^~E^!>Wy<chlTbe3zv0xOsgtc4%N^kb38u%%>m}4Rj}ir>mdKI;Vst0OD<0yZ`_I
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..e0fb348d66f4001a50ca9488b9daddcf37bbc3ac
GIT binary patch
literal 368
zc%17D@N?(olHy`uVBq!ia0vp^{6H+o!3-o#udq!9QjEnx?oJHr&dIz4a+U`8gt*>$
z{+>wm^265`@4u*NXsXrEQfr!{)-q4q*h1gJ)_wl=!0;GTD?8Kv<(3mySWQ}K<Lu_-
z>Ejm?5ioyqP()17{LLZD_l7Rq8kvw1o0J}#oSu-Lm6Vmw*|v;ZNL-+Eg<#)0A!#`=
zIYqIVdn6`qkW<rES#Utd#7f`X#=y$n$ja_^()*tv*OvtO1v5B2yO9RsBze2LFm$lW
zdH^|`1s;*b3=G`DAk4@xYmNj^kiEpy*OmP?2Me2lnY^DX6Hv(9)5S4F;&O7r0wDnb
zho@Pd3kph~2JEQf(ui>u6*+5^_Gm@h>RBC&xK_+w#F-({y?|@Q$w^On88&`r?tb+}
Q0%R(Kr>mdKI;Vst0Co0^sQ>@~
--- a/mobile/android/base/GeckoAppShell.java
+++ b/mobile/android/base/GeckoAppShell.java
@@ -2701,30 +2701,34 @@ public class GeckoAppShell
     }
 
     @WrapElementForJNI(allowMultithread = true)
     static InputStream createInputStream(URLConnection connection) throws IOException {
         return connection.getInputStream();
     }
 
     @WrapElementForJNI(allowMultithread = true, narrowChars = true)
-    static URLConnection getConnection(String url) throws MalformedURLException, IOException {
-        String spec;
-        if (url.startsWith("android://")) {
-            spec = url.substring(10);
-        } else {
-            spec = url.substring(8);
+    static URLConnection getConnection(String url) {
+        try {
+            String spec;
+            if (url.startsWith("android://")) {
+                spec = url.substring(10);
+            } else {
+                spec = url.substring(8);
+            }
+
+            // if the colon got stripped, put it back
+            int colon = spec.indexOf(':');
+            if (colon == -1 || colon > spec.indexOf('/')) {
+                spec = spec.replaceFirst("/", ":/");
+            }
+        } catch(Exception ex) {
+            return null;
         }
-
-        // if the colon got stripped, put it back
-        int colon = spec.indexOf(':');
-        if (colon == -1 || colon > spec.indexOf('/')) {
-            spec = spec.replaceFirst("/", ":/");
-        }
-        return new URL(spec).openConnection();
+        return null;
     }
 
     @WrapElementForJNI(allowMultithread = true, narrowChars = true)
     static String connectionGetMimeType(URLConnection connection) {
         return connection.getContentType();
     }
 
 }
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -251,16 +251,17 @@ size. -->
 <!ENTITY save_as_pdf "Save as PDF">
 <!ENTITY find_in_page "Find in Page">
 <!ENTITY desktop_mode "Request Desktop Site">
 <!ENTITY page "Page">
 <!ENTITY tools "Tools">
 <!ENTITY new_tab "New Tab">
 <!ENTITY new_private_tab "New Private Tab">
 <!ENTITY close_all_tabs "Close All Tabs">
+<!ENTITY close_private_tabs "Close Private Tabs">
 <!ENTITY tabs_normal "Tabs">
 <!ENTITY tabs_private "Private">
 <!ENTITY tabs_synced "Synced">
 <!ENTITY set_image_fail "Unable to set image">
 <!ENTITY set_image_chooser_title "Set Image As">
 
 <!-- Localization note (find_text, find_prev, find_next, find_close) : These strings are used
      as alternate text for accessibility. They are not visible in the UI. -->
--- a/mobile/android/base/menu/MenuPopup.java
+++ b/mobile/android/base/menu/MenuPopup.java
@@ -15,28 +15,30 @@ import android.view.View;
 import android.view.ViewGroup;
 import android.widget.LinearLayout;
 import android.widget.PopupWindow;
 
 /**
  * A popup to show the inflated MenuPanel.
  */
 public class MenuPopup extends PopupWindow {
-    private LinearLayout mPanel;
+    private final LinearLayout mPanel;
 
-    private int mYOffset;
-    private int mPopupWidth;
+    private final int mYOffset;
+    private final int mPopupWidth;
+    private final int mPopupMinHeight;
 
     public MenuPopup(Context context) {
         super(context);
 
         setFocusable(true);
 
         mYOffset = context.getResources().getDimensionPixelSize(R.dimen.menu_popup_offset);
         mPopupWidth = context.getResources().getDimensionPixelSize(R.dimen.menu_popup_width);
+        mPopupMinHeight = context.getResources().getDimensionPixelSize(R.dimen.menu_item_row_height);
 
         // Setting a null background makes the popup to not close on touching outside.
         setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
         setWindowLayoutMode(View.MeasureSpec.makeMeasureSpec(mPopupWidth, View.MeasureSpec.AT_MOST),
                             ViewGroup.LayoutParams.WRAP_CONTENT);
 
         LayoutInflater inflater = LayoutInflater.from(context);
         mPanel = (LinearLayout) inflater.inflate(R.layout.menu_popup, null);
@@ -58,11 +60,17 @@ public class MenuPopup extends PopupWind
         mPanel.addView(view);
     }
 
     /**
      * A small little offset.
      */
     @Override
     public void showAsDropDown(View anchor) {
-        showAsDropDown(anchor, 0, -mYOffset);
+        // Set a height, so that the popup will not be displayed below the bottom of the screen.
+        setHeight(mPopupMinHeight);
+
+        // Attempt to align the center of the popup with the center of the anchor. If the anchor is
+        // near the edge of the screen, the popup will just align with the edge of the screen.
+        final int xOffset = anchor.getWidth()/2 - mPopupWidth/2;
+        showAsDropDown(anchor, xOffset, -mYOffset);
     }
 }
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..af08dc45169a1e4e9a7184783882eae006c0d70b
GIT binary patch
literal 136
zc%17D@N?(olHy`uVBq!ia0vp^Dj>|r3?y%#<GKx`SkfJR9T^xl_H+M9WCijWi-X*q
z7}lMWc?smO1^9%x0_p$%|1Z5c|1OZlR1)MD%rNu6!?PPeo{*=DV+hCf<b(q(XX-ta
c*%xy%h))xL`TUOTB%myVr>mdKI;Vst0O8#xv;Y7A
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..af08dc45169a1e4e9a7184783882eae006c0d70b
GIT binary patch
literal 136
zc%17D@N?(olHy`uVBq!ia0vp^Dj>|r3?y%#<GKx`SkfJR9T^xl_H+M9WCijWi-X*q
z7}lMWc?smO1^9%x0_p$%|1Z5c|1OZlR1)MD%rNu6!?PPeo{*=DV+hCf<b(q(XX-ta
c*%xy%h))xL`TUOTB%myVr>mdKI;Vst0O8#xv;Y7A
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..b0210e7cf2c17d485427671a7139536c115f2105
GIT binary patch
literal 136
zc%17D@N?(olHy`uVBq!ia0vp^5+KaT3?y&uT)!Jgv7|ftIx;Y9?C1WI$O_~$76-XI
zF|0c$^AgBm3-AeX1=9cj|6h7@{#_u8sU*lRm|^CBhi5l{JRwgP#}JO|$q5aOQaUq&
cEZNu?vbVFlaul6g43uT?boFyt=akR{0Of-vjsO4v
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..6fb5853084488393253ce3ade465eca2a33c79bf
GIT binary patch
literal 138
zc%17D@N?(olHy`uVBq!ia0vp^1|ZDH3?y^UWFG-iEa{HEjtmSN`?>!lvI6;x#X;^)
z4C~IxyaaOC0(?STf%O0X|CipJe;3GNDhcunW|;Zk;n@u!PsG#3F+}2Wa>4<=C-ni%
eN=!V)-3<N{6*jA#xMv5HXYh3Ob6Mw<&;$S~d?yzG
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..b0210e7cf2c17d485427671a7139536c115f2105
GIT binary patch
literal 136
zc%17D@N?(olHy`uVBq!ia0vp^5+KaT3?y&uT)!Jgv7|ftIx;Y9?C1WI$O_~$76-XI
zF|0c$^AgBm3-AeX1=9cj|6h7@{#_u8sU*lRm|^CBhi5l{JRwgP#}JO|$q5aOQaUq&
cEZNu?vbVFlaul6g43uT?boFyt=akR{0Of-vjsO4v
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..6fb5853084488393253ce3ade465eca2a33c79bf
GIT binary patch
literal 138
zc%17D@N?(olHy`uVBq!ia0vp^1|ZDH3?y^UWFG-iEa{HEjtmSN`?>!lvI6;x#X;^)
z4C~IxyaaOC0(?STf%O0X|CipJe;3GNDhcunW|;Zk;n@u!PsG#3F+}2Wa>4<=C-ni%
eN=!V)-3<N{6*jA#xMv5HXYh3Ob6Mw<&;$S~d?yzG
--- a/mobile/android/base/resources/layout-large-land-v11/tabs_panel_footer.xml
+++ b/mobile/android/base/resources/layout-large-land-v11/tabs_panel_footer.xml
@@ -1,17 +1,29 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- 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/. -->
 
 <merge xmlns:android="http://schemas.android.com/apk/res/android">
 
     <ImageButton android:id="@+id/add_tab"
-                 android:layout_width="fill_parent"
+                 android:layout_width="@dimen/browser_toolbar_height"
                  android:layout_height="@dimen/browser_toolbar_height"
                  android:padding="14dip"
                  android:src="@drawable/tab_new_level"
                  android:contentDescription="@string/new_tab"
-                 android:background="@drawable/action_bar_button_inverse"
-                 android:gravity="center"/>
+                 android:background="@drawable/action_bar_button_inverse"/>
+
+    <View android:layout_width="0dip"
+          android:layout_height="match_parent"
+          android:layout_weight="1.0"/>
+
+    <ImageButton android:id="@+id/menu"
+                 style="@style/UrlBar.ImageButton"
+                 android:layout_width="@dimen/browser_toolbar_height"
+                 android:layout_height="@dimen/browser_toolbar_height"
+                 android:padding="@dimen/browser_toolbar_button_padding"
+                 android:src="@drawable/menu_tabs"
+                 android:contentDescription="@string/menu"
+                 android:background="@drawable/action_bar_button"/>
 
 </merge>
--- a/mobile/android/base/resources/layout/tabs_panel_header.xml
+++ b/mobile/android/base/resources/layout/tabs_panel_header.xml
@@ -20,9 +20,18 @@
                  style="@style/UrlBar.ImageButton"
                  android:layout_width="@dimen/browser_toolbar_height"
                  android:layout_height="@dimen/browser_toolbar_height"
                  android:padding="@dimen/browser_toolbar_button_padding"
                  android:src="@drawable/tab_new_level"
                  android:contentDescription="@string/new_tab"
                  android:background="@drawable/action_bar_button_inverse"/>
 
+    <ImageButton android:id="@+id/menu"
+                 style="@style/UrlBar.ImageButton"
+                 android:layout_width="@dimen/browser_toolbar_height"
+                 android:layout_height="@dimen/browser_toolbar_height"
+                 android:padding="@dimen/browser_toolbar_button_padding"
+                 android:src="@drawable/menu_tabs"
+                 android:contentDescription="@string/menu"
+                 android:background="@drawable/action_bar_button"/>
+
 </merge>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/menu-v11/tabs_menu.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item android:id="@+id/new_tab"
+          android:title="@string/new_tab"/>
+
+    <item android:id="@+id/new_private_tab"
+          android:title="@string/new_private_tab"/>
+
+    <item android:id="@+id/close_all_tabs"
+          android:title="@string/close_all_tabs"/>
+
+    <item android:id="@+id/close_private_tabs"
+          android:title="@string/close_private_tabs"/>
+
+</menu>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/menu/tabs_menu.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item android:id="@+id/new_tab"
+          android:title="@string/new_tab"/>
+
+    <item android:id="@+id/new_private_tab"
+          android:title="@string/new_private_tab"/>
+
+    <item android:id="@+id/close_all_tabs"
+          android:title="@string/close_all_tabs"/>
+
+    <item android:id="@+id/close_private_tabs"
+          android:title="@string/close_private_tabs"/>
+
+</menu>
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -241,16 +241,17 @@
   <string name="num_tabs">&num_tabs2;</string>
   <string name="addons">&addons;</string>
   <string name="downloads">&downloads;</string>
   <string name="apps">&apps;</string>
   <string name="char_encoding">&char_encoding;</string>
   <string name="new_tab">&new_tab;</string>
   <string name="new_private_tab">&new_private_tab;</string>
   <string name="close_all_tabs">&close_all_tabs;</string>
+  <string name="close_private_tabs">&close_private_tabs;</string>
   <string name="tabs_normal">&tabs_normal;</string>
   <string name="tabs_private">&tabs_private;</string>
   <string name="tabs_synced">&tabs_synced;</string>
   <string name="edit_mode_cancel">&edit_mode_cancel;</string>
 
   <string name="site_settings_title">&site_settings_title3;</string>
   <string name="site_settings_cancel">&site_settings_cancel;</string>
   <string name="site_settings_clear">&site_settings_clear;</string>
--- a/mobile/android/base/tabspanel/TabsPanel.java
+++ b/mobile/android/base/tabspanel/TabsPanel.java
@@ -8,36 +8,43 @@ package org.mozilla.gecko.tabspanel;
 import org.mozilla.gecko.GeckoApp;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoAppShell.AppStateListener;
 import org.mozilla.gecko.GeckoApplication;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.LightweightTheme;
 import org.mozilla.gecko.LightweightThemeDrawable;
 import org.mozilla.gecko.R;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.animation.PropertyAnimator;
 import org.mozilla.gecko.animation.ViewHelper;
+import org.mozilla.gecko.widget.GeckoPopupMenu;
 import org.mozilla.gecko.widget.IconTabWidget;
 
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
 import android.view.View;
 import android.widget.Button;
 import android.widget.FrameLayout;
 import android.widget.ImageButton;
 import android.widget.LinearLayout;
 import android.widget.RelativeLayout;
 
 public class TabsPanel extends LinearLayout
-                       implements LightweightTheme.OnChangeListener,
+                       implements GeckoPopupMenu.OnMenuItemClickListener,
+                                  LightweightTheme.OnChangeListener,
                                   IconTabWidget.OnTabChangedListener {
     @SuppressWarnings("unused")
     private static final String LOGTAG = "Gecko" + TabsPanel.class.getSimpleName();
 
     public static enum Panel {
         NORMAL_TABS,
         PRIVATE_TABS,
         REMOTE_TABS
@@ -45,16 +52,20 @@ public class TabsPanel extends LinearLay
 
     public static interface PanelView {
         public void setTabsPanel(TabsPanel panel);
         public void show();
         public void hide();
         public boolean shouldExpand();
     }
 
+    public static interface CloseAllPanelView {
+        public void closeAll();
+    }
+
     public static interface TabsLayoutChangeListener {
         public void onTabsLayoutChange(int width, int height);
     }
 
     private Context mContext;
     private final GeckoApp mActivity;
     private final LightweightTheme mTheme;
     private RelativeLayout mHeader;
@@ -63,39 +74,46 @@ public class TabsPanel extends LinearLay
     private PanelView mPanelNormal;
     private PanelView mPanelPrivate;
     private PanelView mPanelRemote;
     private RelativeLayout mFooter;
     private TabsLayoutChangeListener mLayoutChangeListener;
     private AppStateListener mAppStateListener;
 
     private IconTabWidget mTabWidget;
+    private static ImageButton mMenuButton;
     private static ImageButton mAddTab;
 
     private Panel mCurrentPanel;
     private boolean mIsSideBar;
     private boolean mVisible;
     private boolean mHeaderVisible;
 
+    private GeckoPopupMenu mPopupMenu;
+
     public TabsPanel(Context context, AttributeSet attrs) {
         super(context, attrs);
         mContext = context;
         mActivity = (GeckoApp) context;
         mTheme = ((GeckoApplication) context.getApplicationContext()).getLightweightTheme();
 
         setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,
                                                       LinearLayout.LayoutParams.MATCH_PARENT));
         setOrientation(LinearLayout.VERTICAL);
 
         mCurrentPanel = Panel.NORMAL_TABS;
         mVisible = false;
         mHeaderVisible = false;
 
         mIsSideBar = false;
 
+        mPopupMenu = new GeckoPopupMenu(context);
+        mPopupMenu.inflate(R.menu.tabs_menu);
+        mPopupMenu.setOnMenuItemClickListener(this);
+
         LayoutInflater.from(context).inflate(R.layout.tabs_panel, this);
         initialize();
 
         mAppStateListener = new AppStateListener() {
             @Override
             public void onResume() {
                 if (mPanel == mPanelRemote) {
                     // Refresh the remote panel.
@@ -144,39 +162,91 @@ public class TabsPanel extends LinearLay
         if (!GeckoProfile.get(mContext).inGuestMode()) {
             // The initial icon is not the animated icon, because on Android
             // 4.4.2, the animation starts immediately (and can start at other
             // unpredictable times). See Bug 1015974.
             mTabWidget.addTab(R.drawable.tabs_synced, R.string.tabs_synced);
         }
 
         mTabWidget.setTabSelectionListener(this);
+
+        mMenuButton = (ImageButton) findViewById(R.id.menu);
+        mMenuButton.setOnClickListener(new Button.OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                final Menu menu = mPopupMenu.getMenu();
+                menu.findItem(R.id.close_all_tabs).setVisible(mCurrentPanel == Panel.NORMAL_TABS);
+                menu.findItem(R.id.close_private_tabs).setVisible(mCurrentPanel == Panel.PRIVATE_TABS);
+
+                mPopupMenu.show();
+            }
+        });
+        mPopupMenu.setAnchor(mMenuButton);
     }
 
-    public void addTab() {
+    private void addTab() {
         if (mCurrentPanel == Panel.NORMAL_TABS) {
-           mActivity.addTab();
+            Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.ACTIONBAR, "new_tab");
+            mActivity.addTab();
         } else {
-           mActivity.addPrivateTab();
+            Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.ACTIONBAR, "new_private_tab");
+            mActivity.addPrivateTab();
         }
 
         mActivity.autoHideTabs();
     }
 
     @Override
     public void onTabChanged(int index) {
         if (index == 0) {
             show(Panel.NORMAL_TABS);
         } else if (index == 1) {
             show(Panel.PRIVATE_TABS);
         } else {
             show(Panel.REMOTE_TABS);
         }
     }
 
+    @Override
+    public boolean onMenuItemClick(MenuItem item) {
+        final int itemId = item.getItemId();
+
+        if (itemId == R.id.close_all_tabs) {
+            if (mCurrentPanel == Panel.NORMAL_TABS) {
+                final String extras = getResources().getResourceEntryName(itemId);
+                Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, extras);
+
+                // Disable the menu button so that the menu won't interfere with the tab close animation.
+                mMenuButton.setEnabled(false);
+                ((CloseAllPanelView) mPanelNormal).closeAll();
+            } else {
+                Log.e(LOGTAG, "Close all tabs menu item should only be visible for normal tabs panel");
+            }
+            return true;
+        }
+
+        if (itemId == R.id.close_private_tabs) {
+            if (mCurrentPanel == Panel.PRIVATE_TABS) {
+                final String extras = getResources().getResourceEntryName(itemId);
+                Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, extras);
+
+                ((CloseAllPanelView) mPanelPrivate).closeAll();
+            } else {
+                Log.e(LOGTAG, "Close private tabs menu item should only be visible for private tabs panel");
+            }
+            return true;
+        }
+
+        if (itemId == R.id.new_tab || itemId == R.id.new_private_tab) {
+            hide();
+        }
+
+        return mActivity.onOptionsItemSelected(item);
+    }
+
     private static int getTabContainerHeight(TabsListContainer listContainer) {
         Resources resources = listContainer.getContext().getResources();
 
         PanelView panelView = listContainer.getCurrentPanelView();
         if (panelView != null && !panelView.shouldExpand()) {
             return resources.getDimensionPixelSize(R.dimen.tabs_tray_horizontal_height);
         }
 
@@ -345,22 +415,27 @@ public class TabsPanel extends LinearLay
         }
         mPanel.show();
 
         if (mCurrentPanel == Panel.REMOTE_TABS) {
             if (mFooter != null)
                 mFooter.setVisibility(View.GONE);
 
             mAddTab.setVisibility(View.INVISIBLE);
+
+            mMenuButton.setVisibility(View.INVISIBLE);
         } else {
             if (mFooter != null)
                 mFooter.setVisibility(View.VISIBLE);
 
             mAddTab.setVisibility(View.VISIBLE);
             mAddTab.setImageLevel(index);
+
+            mMenuButton.setVisibility(View.VISIBLE);
+            mMenuButton.setEnabled(true);
         }
 
         if (isSideBar()) {
             if (showAnimation)
                 dispatchLayoutChange(getWidth(), getHeight());
         } else {
             int actionBarHeight = mContext.getResources().getDimensionPixelSize(R.dimen.browser_toolbar_height);
             int height = actionBarHeight + getTabContainerHeight(mTabsContainer);
@@ -369,16 +444,17 @@ public class TabsPanel extends LinearLay
         mHeaderVisible = true;
     }
 
     public void hide() {
         mHeaderVisible = false;
 
         if (mVisible) {
             mVisible = false;
+            mPopupMenu.dismiss();
             dispatchLayoutChange(0, 0);
         }
     }
 
     public void refresh() {
         removeAllViews();
 
         LayoutInflater.from(mContext).inflate(R.layout.tabs_panel, this);
--- a/mobile/android/base/tabspanel/TabsTray.java
+++ b/mobile/android/base/tabspanel/TabsTray.java
@@ -12,16 +12,17 @@ import org.mozilla.gecko.AboutPages;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.animation.PropertyAnimator;
 import org.mozilla.gecko.animation.PropertyAnimator.Property;
 import org.mozilla.gecko.animation.ViewHelper;
+import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.widget.TwoWayView;
 import org.mozilla.gecko.widget.TabThumbnailWrapper;
 
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
@@ -33,48 +34,54 @@ import android.view.ViewConfiguration;
 import android.view.ViewGroup;
 import android.widget.BaseAdapter;
 import android.widget.Button;
 import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.TextView;
 
 class TabsTray extends TwoWayView
-    implements TabsPanel.PanelView {
+               implements TabsPanel.PanelView,
+                          TabsPanel.CloseAllPanelView {
     private static final String LOGTAG = "Gecko" + TabsTray.class.getSimpleName();
 
     private Context mContext;
     private TabsPanel mTabsPanel;
 
+    final private boolean mIsPrivate;
+
     private TabsAdapter mTabsAdapter;
 
     private List<View> mPendingClosedTabs;
-    private int mCloseAnimationCount;
+    private int mCloseAnimationCount = 0;
+    private int mCloseAllAnimationCount = 0;
 
     private TabSwipeGestureListener mSwipeListener;
 
     // Time to animate non-flinged tabs of screen, in milliseconds
     private static final int ANIMATION_DURATION = 250;
 
+    // Time between starting successive tab animations in closeAllTabs.
+    private static final int ANIMATION_CASCADE_DELAY = 75;
+
     private int mOriginalSize = 0;
 
     public TabsTray(Context context, AttributeSet attrs) {
         super(context, attrs);
         mContext = context;
 
-        mCloseAnimationCount = 0;
         mPendingClosedTabs = new ArrayList<View>();
 
         setItemsCanFocus(true);
 
         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabsTray);
-        boolean isPrivate = (a.getInt(R.styleable.TabsTray_tabs, 0x0) == 1);
+        mIsPrivate = (a.getInt(R.styleable.TabsTray_tabs, 0x0) == 1);
         a.recycle();
 
-        mTabsAdapter = new TabsAdapter(mContext, isPrivate);
+        mTabsAdapter = new TabsAdapter(mContext);
         setAdapter(mTabsAdapter);
 
         mSwipeListener = new TabSwipeGestureListener();
         setOnTouchListener(mSwipeListener);
         setOnScrollListener(mSwipeListener.makeScrollListener());
 
         setRecyclerListener(new RecyclerListener() {
             @Override
@@ -132,25 +139,23 @@ class TabsTray extends TwoWayView
             close = (ImageButton) view.findViewById(R.id.close);
             thumbnailWrapper = (TabThumbnailWrapper) view.findViewById(R.id.wrapper);
         }
     }
 
     // Adapter to bind tabs into a list
     private class TabsAdapter extends BaseAdapter implements Tabs.OnTabsChangedListener {
         private Context mContext;
-        private boolean mIsPrivate;
         private ArrayList<Tab> mTabs;
         private LayoutInflater mInflater;
         private Button.OnClickListener mOnCloseClickListener;
 
-        public TabsAdapter(Context context, boolean isPrivate) {
+        public TabsAdapter(Context context) {
             mContext = context;
             mInflater = LayoutInflater.from(mContext);
-            mIsPrivate = isPrivate;
 
             mOnCloseClickListener = new Button.OnClickListener() {
                 @Override
                 public void onClick(View v) {
                     TabRow tab = (TabRow) v.getTag();
                     final int pos = (isVertical() ? tab.info.getWidth() : 0 - tab.info.getHeight());
                     animateClose(tab.info, pos);
                 }
@@ -276,26 +281,31 @@ class TabsTray extends TwoWayView
                 row.thumbnailWrapper.setRecording(tab.isRecording());
             }
             row.title.setText(tab.getDisplayTitle());
             row.close.setTag(row);
         }
 
         private void resetTransforms(View view) {
             ViewHelper.setAlpha(view, 1);
-            if (mOriginalSize == 0)
-                return;
 
             if (isVertical()) {
-                ViewHelper.setHeight(view, mOriginalSize);
                 ViewHelper.setTranslationX(view, 0);
             } else {
-                ViewHelper.setWidth(view, mOriginalSize);
                 ViewHelper.setTranslationY(view, 0);
             }
+
+            // We only need to reset the height or width after individual tab close animations.
+            if (mOriginalSize != 0) {
+                if (isVertical()) {
+                    ViewHelper.setHeight(view, mOriginalSize);
+                } else {
+                    ViewHelper.setWidth(view, mOriginalSize);
+                }
+            }
         }
 
         @Override
         public View getView(int position, View convertView, ViewGroup parent) {
             TabRow row;
 
             if (convertView == null) {
                 convertView = mInflater.inflate(R.layout.tabs_row, null);
@@ -315,16 +325,85 @@ class TabsTray extends TwoWayView
             return convertView;
         }
     }
 
     private boolean isVertical() {
         return (getOrientation().compareTo(TwoWayView.Orientation.VERTICAL) == 0);
     }
 
+    @Override
+    public void closeAll() {
+        final int childCount = getChildCount();
+
+        // Just close the panel if there are no tabs to close.
+        if (childCount == 0) {
+            autoHidePanel();
+            return;
+        }
+
+        // Disable the view so that gestures won't interfere wth the tab close animation.
+        setEnabled(false);
+
+        // Delay starting each successive animation to create a cascade effect.
+        int cascadeDelay = 0;
+
+        for (int i = childCount - 1; i >= 0; i--) {
+            final View view = getChildAt(i);
+            final PropertyAnimator animator = new PropertyAnimator(ANIMATION_DURATION);
+            animator.attach(view, Property.ALPHA, 0);
+
+            if (isVertical()) {
+                animator.attach(view, Property.TRANSLATION_X, view.getWidth());
+            } else {
+                animator.attach(view, Property.TRANSLATION_Y, view.getHeight());
+            }
+
+            mCloseAllAnimationCount++;
+
+            animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
+                @Override
+                public void onPropertyAnimationStart() { }
+
+                @Override
+                public void onPropertyAnimationEnd() {
+                    mCloseAllAnimationCount--;
+                    if (mCloseAllAnimationCount > 0) {
+                        return;
+                    }
+
+                    // Hide the panel after the animation is done.
+                    autoHidePanel();
+
+                    // Re-enable the view after the animation is done.
+                    TabsTray.this.setEnabled(true);
+
+                    // Then actually close all the tabs.
+                    final Iterable<Tab> tabs = Tabs.getInstance().getTabsInOrder();
+                    for (Tab tab : tabs) {
+                        // In the normal panel we want to close all tabs (both private and normal),
+                        // but in the private panel we only want to close private tabs.
+                        if (!mIsPrivate || tab.isPrivate()) {
+                            Tabs.getInstance().closeTab(tab, false);
+                        }
+                    }
+                }
+            });
+
+            ThreadUtils.getUiHandler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    animator.start();
+                }
+            }, cascadeDelay);
+
+            cascadeDelay += ANIMATION_CASCADE_DELAY;
+        }
+    }
+
     private void animateClose(final View view, int pos) {
         PropertyAnimator animator = new PropertyAnimator(ANIMATION_DURATION);
         animator.attach(view, Property.ALPHA, 0);
 
         if (isVertical())
             animator.attach(view, Property.TRANSLATION_X, pos);
         else
             animator.attach(view, Property.TRANSLATION_Y, pos);
@@ -560,17 +639,17 @@ class TabsTray extends TwoWayView
                     mSwipeStartX = 0;
                     mSwipeStartY = 0;
                     mSwiping = false;
 
                     break;
                 }
 
                 case MotionEvent.ACTION_MOVE: {
-                    if (mSwipeView == null)
+                    if (mSwipeView == null || mVelocityTracker == null)
                         break;
 
                     mVelocityTracker.addMovement(e);
 
                     final boolean isVertical = isVertical();
 
                     float deltaX = e.getRawX() - mSwipeStartX;
                     float deltaY = e.getRawY() - mSwipeStartY;
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -195,8 +195,12 @@ user_pref('toolkit.telemetry.server', 'h
 
 // We don't want to hit the real Firefox Accounts server for tests.  We don't
 // actually need a functioning FxA server, so just set it to something that
 // resolves and accepts requests, even if they all fail.
 user_pref('identity.fxaccounts.auth.uri', 'https://%(server)s/fxa-dummy/');
 
 // Enable logging of APZ test data (see bug 961289).
 user_pref('apz.test.logging_enabled', true);
+
+// Make sure Translation won't hit the network.
+user_pref("browser.translation.bing.authURL", "http://%(server)s/browser/browser/components/translation/test/bing.sjs");
+user_pref("browser.translation.bing.translateArrayURL", "http://%(server)s/browser/browser/components/translation/test/bing.sjs");
--- a/toolkit/devtools/apps/Simulator.jsm
+++ b/toolkit/devtools/apps/Simulator.jsm
@@ -3,27 +3,34 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 Components.utils.import("resource://gre/modules/devtools/event-emitter.js");
 
 const EXPORTED_SYMBOLS = ["Simulator"];
 
+function getVersionNumber(fullVersion) {
+  return fullVersion.match(/(\d+\.\d+)/)[0];
+}
+
 const Simulator = {
   _simulators: {},
 
-  register: function (version, simulator) {
-    this._simulators[version] = simulator;
-    this.emit("register");
+  register: function (label, simulator) {
+    // simulators register themselves as "Firefox OS X.Y"
+    let versionNumber = getVersionNumber(label);
+    this._simulators[versionNumber] = simulator;
+    this.emit("register", versionNumber);
   },
 
-  unregister: function (version) {
-    delete this._simulators[version];
-    this.emit("unregister");
+  unregister: function (label) {
+    let versionNumber = getVersionNumber(label);
+    delete this._simulators[versionNumber];
+    this.emit("unregister", versionNumber);
   },
 
   availableVersions: function () {
     return Object.keys(this._simulators).sort();
   },
 
   getByVersion: function (version) {
     return this._simulators[version];