Bug 605494 - UI for restartless addons [r=mfinkle]
authorWes Johnston <wjohnston@mozilla.com>
Tue, 04 Jan 2011 17:20:30 -0800
changeset 67208 b8f518466e04450543aed427eaa545a06e3ffd11
parent 67207 a74f648d9d9f086cd95aaeafee9ca107969cbd5b
child 67209 e6bc890dd616def8b9fb1c5a260dd450afef9a12
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmfinkle
bugs605494
Bug 605494 - UI for restartless addons [r=mfinkle]
mobile/chrome/content/browser.js
mobile/chrome/content/extensions.js
--- a/mobile/chrome/content/browser.js
+++ b/mobile/chrome/content/browser.js
@@ -685,17 +685,17 @@ var Browser = {
   getTabFromChrome: function getTabFromChrome(chromeTab) {
     for (var t = 0; t < this._tabs.length; t++) {
       if (this._tabs[t].chromeTab == chromeTab)
         return this._tabs[t];
     }
     return null;
   },
 
-  addTab: function(aURI, aBringFront, aOwner, aParams) {
+  addTab: function browser_addTab(aURI, aBringFront, aOwner, aParams) {
     let params = aParams || {};
     let newTab = new Tab(aURI, params);
     newTab.owner = aOwner || null;
     this._tabs.push(newTab);
 
     if (aBringFront)
       this.selectedTab = newTab;
 
@@ -1364,22 +1364,22 @@ nsBrowserAccess.prototype = {
       }
       browser.focus();
     } catch(e) { }
 
     BrowserUI.closeAutoComplete();
     return browser;
   },
 
-  openURI: function(aURI, aOpener, aWhere, aContext) {
+  openURI: function browser_openURI(aURI, aOpener, aWhere, aContext) {
     let browser = this._getBrowser(aURI, aOpener, aWhere, aContext);
     return browser ? browser.contentWindow : null;
   },
 
-  openURIInFrame: function(aURI, aOpener, aWhere, aContext) {
+  openURIInFrame: function browser_openURIInFrame(aURI, aOpener, aWhere, aContext) {
     let browser = this._getBrowser(aURI, aOpener, aWhere, aContext);
     return browser ? browser.QueryInterface(Ci.nsIFrameLoaderOwner) : null;
   },
 
   isTabContentWindow: function(aWindow) {
     return Browser.browsers.some(function (browser) browser.contentWindow == aWindow);
   }
 };
--- a/mobile/chrome/content/extensions.js
+++ b/mobile/chrome/content/extensions.js
@@ -273,59 +273,65 @@ var ExtensionsView = {
   },
 
   hideOnSelect: function ev_handleEvent(aEvent) {
     // When list selection changes, be sure to close up any open options sections
     if (aEvent.target == this._list)
       this.hideOptions();
   },
 
+  _createLocalAddon: function ev__createLocalAddon(aAddon) {
+    let strings = Elements.browserBundle;
+
+    let appManaged = (aAddon.scope == AddonManager.SCOPE_APPLICATION);
+    let opType = this._getOpTypeForOperations(aAddon.pendingOperations);
+    let updateable = (aAddon.permissions & AddonManager.PERM_CAN_UPGRADE) > 0;
+    let uninstallable = (aAddon.permissions & AddonManager.PERM_CAN_UNINSTALL) > 0;
+
+    let blocked = "";
+    switch(aAddon.blocklistState) {
+      case Ci.nsIBlocklistService.STATE_BLOCKED:
+        blocked = strings.getString("addonBlocked.blocked")
+        break;
+      case Ci.nsIBlocklistService.STATE_SOFTBLOCKED:
+        blocked = strings.getString("addonBlocked.softBlocked");
+        break;
+      case Ci.nsIBlocklistService.STATE_OUTDATED:
+        blocked = srings.getString("addonBlocked.outdated");
+        break;
+    }            
+
+    let listitem = this._createItem(aAddon, "local");
+    listitem.setAttribute("isDisabled", !aAddon.isActive);
+    listitem.setAttribute("appDisabled", aAddon.appDisabled);
+    listitem.setAttribute("appManaged", appManaged);
+    listitem.setAttribute("description", aAddon.description);
+    listitem.setAttribute("optionsURL", aAddon.optionsURL);
+    listitem.setAttribute("opType", opType);
+    listitem.setAttribute("updateable", updateable);
+    listitem.setAttribute("isReadonly", !uninstallable);
+    if (blocked)
+      listitem.setAttribute("blockedStatus", blocked);
+    listitem.addon = aAddon;
+    return listitem;
+  },
+
   getAddonsFromLocal: function ev_getAddonsFromLocal() {
     this.clearSection("local");
 
     let self = this;
     AddonManager.getAddonsByTypes(["extension", "theme", "locale"], function(items) {
       let strings = Strings.browser;
       let anyUpdateable = false;
       for (let i = 0; i < items.length; i++) {
-        let addon = items[i];
-        let appManaged = (addon.scope == AddonManager.SCOPE_APPLICATION);
-        let opType = self._getOpTypeForOperations(addon.pendingOperations);
-        let updateable = (addon.permissions & AddonManager.PERM_CAN_UPGRADE) > 0;
-        let uninstallable = (addon.permissions & AddonManager.PERM_CAN_UNINSTALL) > 0;
-
-        let blocked = "";
-        switch(addon.blocklistState) {
-          case Ci.nsIBlocklistService.STATE_BLOCKED:
-            blocked = strings.GetStringFromName("addonBlocked.blocked")
-            break;
-          case Ci.nsIBlocklistService.STATE_SOFTBLOCKED:
-            blocked = strings.GetStringFromName("addonBlocked.softBlocked");
-            break;
-          case Ci.nsIBlocklistService.STATE_OUTDATED:
-            blocked = srings.GetStringFromName("addonBlocked.outdated");
-            break;
-        }            
-
-        if (updateable)
+        let listitem = self._createLocalAddon(items[i]);
+        if ((items[i].permissions & AddonManager.PERM_CAN_UPGRADE) > 0)
           anyUpdateable = true;
 
-        let listitem = self._createItem(addon, "local");
-        listitem.setAttribute("isDisabled", !addon.isActive);
-        listitem.setAttribute("appDisabled", addon.appDisabled);
-        listitem.setAttribute("appManaged", appManaged);
-        listitem.setAttribute("description", addon.description);
-        listitem.setAttribute("optionsURL", addon.optionsURL ? addon.optionsURL : "");
-        listitem.setAttribute("opType", opType);
-        listitem.setAttribute("updateable", updateable);
-        listitem.setAttribute("isReadonly", !uninstallable);
-        if (blocked)
-          listitem.setAttribute("blockedStatus", blocked);
-        listitem.addon = addon;
-        self._list.insertBefore(listitem, self._repoItem);
+        self.addItem(listitem);
       }
 
       // Load the search engines
       let defaults = Services.search.getDefaultEngines({ }).map(function (e) e.name);
       function isDefault(aEngine)
         defaults.indexOf(aEngine.name) != -1
 
       let defaultDescription = strings.GetStringFromName("addonsSearchEngine.description");
@@ -344,27 +350,40 @@ var ExtensionsView = {
         listitem._engine = engine;
         listitem.setAttribute("isDisabled", engine.hidden ? "true" : "false");
         listitem.setAttribute("appDisabled", "false");
         listitem.setAttribute("appManaged", isDefault(engine));
         listitem.setAttribute("description", engine.description || defaultDescription);
         listitem.setAttribute("optionsURL", "");
         listitem.setAttribute("opType", engine.hidden ? "needs-disable" : "");
         listitem.setAttribute("updateable", "false");
-        self._list.insertBefore(listitem, self._repoItem);
+        self.addItem(listitem);
       }
 
       if (engines.length + items.length == 0)
         self.displaySectionMessage("local", strings.GetStringFromName("addonsLocalNone.label"), null, true);
 
       if (!anyUpdateable)
         document.getElementById("addons-update-all").disabled = true;
     });
   },
 
+  addItem : function ev_addItem(aItem, aPosition) {
+    if (aPosition == "repo")
+      return this._list.appendChild(aItem);
+    else if (aPosition == "local")
+      return this._list.insertBefore(aItem, this._localItem.nextSibling);
+    else
+      return this._list.insertBefore(aItem, this._repoItem);
+  },
+
+  removeItem : function ev_moveItem(aItem) {
+    this._list.removeChild(aItem);
+  },
+
   enable: function ev_enable(aItem) {
     let opType;
     if (aItem.getAttribute("type") == "search") {
       aItem.setAttribute("isDisabled", false);
       aItem._engine.hidden = false;
       opType = "needs-enable";
     } else if (aItem.getAttribute("type") == "theme") {
       // we can have only one theme enabled, so disable the current one if any
@@ -381,20 +400,23 @@ var ExtensionsView = {
         this.disable(theme);
 
       aItem.addon.userDisabled = false;
       aItem.setAttribute("isDisabled", false);
     } else {
       aItem.addon.userDisabled = false;
       opType = this._getOpTypeForOperations(aItem.addon.pendingOperations);
 
-      if (opType == "needs-enable")
+      if (aItem.addon.pendingOperations & AddonManager.PENDING_ENABLE) {
         this.showRestart();
-      else
-        this.hideRestart();
+      } else {
+        aItem.removeAttribute("isDisabled");
+        if (aItem.getAttribute("opType") == "needs-disable")
+          this.hideRestart();
+      };
     }
 
     aItem.setAttribute("opType", opType);
   },
 
   disable: function ev_disable(aItem) {
     let opType;
     if (aItem.getAttribute("type") == "search") {
@@ -403,47 +425,58 @@ var ExtensionsView = {
       opType = "needs-disable";
     } else if (aItem.getAttribute("type") == "theme") {
       aItem.addon.userDisabled = true;
       aItem.setAttribute("isDisabled", true);
     } else {
       aItem.addon.userDisabled = true;
       opType = this._getOpTypeForOperations(aItem.addon.pendingOperations);
 
-      if (opType == "needs-disable")
+      if (aItem.addon.pendingOperations & AddonManager.PENDING_DISABLE) {
         this.showRestart();
-      else
-        this.hideRestart();
+      } else {
+        aItem.setAttribute("isDisabled", !aItem.addon.isActive);
+        if (aItem.getAttribute("opType") == "needs-enable")
+          this.hideRestart();
+      }
     }
 
     aItem.setAttribute("opType", opType);
   },
 
   uninstall: function ev_uninstall(aItem) {
     let opType;
     if (aItem.getAttribute("type") == "search") {
       // Make sure the engine isn't hidden before removing it, to make sure it's
       // visible if the user later re-adds it (works around bug 341833)
       aItem._engine.hidden = false;
       Services.search.removeEngine(aItem._engine);
       // the search-engine-modified observer in browser.js will take care of
       // updating the list
     } else {
+      if (!aItem.addon) {
+        this._list.removeChild(aItem);
+        return;
+      }
+
       aItem.addon.uninstall();
       opType = this._getOpTypeForOperations(aItem.addon.pendingOperations);
 
-      if (opType == "needs-uninstall")
+      if (aItem.addon.pendingOperations & AddonManager.PENDING_UNINSTALL) {
         this.showRestart();
 
-      // A disabled addon doesn't need a restart so it has no pending ops and
-      // can't be cancelled
-      if (!aItem.addon.isActive && opType == "")
-        opType = "needs-uninstall";
-
-      aItem.setAttribute("opType", opType);
+        // A disabled addon doesn't need a restart so it has no pending ops and
+        // can't be cancelled
+        if (!aItem.addon.isActive && opType == "")
+          opType = "needs-uninstall";
+  
+        aItem.setAttribute("opType", opType);
+      } else {
+        this._list.removeChild(aItem);
+      }
     }
   },
 
   cancelUninstall: function ev_cancelUninstall(aItem) {
     aItem.addon.cancelUninstall();
 
     this.hideRestart();
 
@@ -477,20 +510,17 @@ var ExtensionsView = {
     item.setAttribute("typeName", "message");
     item.setAttribute("message", aMessage);
     if (aButtonLabel)
       item.setAttribute("button", aButtonLabel);
     else
       item.setAttribute("hidebutton", "true");
     item.setAttribute("hidethrobber", aHideThrobber);
 
-    if (aSection == "repo")
-      this._list.appendChild(item);
-    else
-      this._list.insertBefore(item, this._repoItem);
+    this.addItem(item, aSection);
 
     return item;
   },
 
   getAddonsFromRepo: function ev_getAddonsFromRepo(aTerms, aSelectFirstResult) {
     this.clearSection("repo");
 
     // Make sure we're online before attempting to load
@@ -539,17 +569,17 @@ var ExtensionsView = {
       listitem.setAttribute("description", addon.description);
       if (addon.homepageURL)
         listitem.setAttribute("homepageURL", addon.homepageURL);
       listitem.install = addon.install;
       listitem.setAttribute("sourceURL", addon.install.sourceURI.spec);
       if (aShowRating)
         listitem.setAttribute("rating", addon.averageRating);
 
-      let item = this._list.appendChild(listitem);
+      let item = this.addItem(listitem, "repo");
 
       // Hide any overflow add-ons. The user can see them later by pressing the
       // "See More" button
       aShowCount--;
       if (aShowCount < 0)
         item.hidden = true;
     }
   },
@@ -589,17 +619,17 @@ var ExtensionsView = {
     whatare.setAttribute("label", strings.GetStringFromName("addonsWhatAre.label"));
 
     let desc = strings.GetStringFromName("addonsWhatAre.description");
     desc = desc.replace(/#1/g, brandShortName);
     whatare.setAttribute("description", desc);
 
     whatare.setAttribute("button", strings.GetStringFromName("addonsWhatAre.button"));
     whatare.setAttribute("onbuttoncommand", "BrowserUI.newTab('" + browseURL + "');");
-    this._list.appendChild(whatare);
+    this.addItem(whatare, "repo");
 
     if (aRecommendedAddons.length == 0 && aBrowseAddons.length == 0) {
       let msg = strings.GetStringFromName("addonsSearchNone.recommended");
       let button = strings.GetStringFromName("addonsSearchNone.button");
       let item = this.displaySectionMessage("repo", msg, button, true);
 
       this._list.scrollBoxObject.scrollToElement(item);
       return;
@@ -626,17 +656,17 @@ var ExtensionsView = {
 
     let showmore = document.createElement("richlistitem");
     showmore.setAttribute("typeName", "showmore");
     showmore.setAttribute("pagelabel", strings.GetStringFromName("addonsBrowseAll.seeMore"));
     showmore.setAttribute("onpagecommand", "ExtensionsView.showMoreSearchResults();");
     showmore.setAttribute("hidepage", totalAddons > kAddonPageSize ? "false" : "true");
     showmore.setAttribute("sitelabel", strings.GetStringFromName("addonsBrowseAll.browseSite"));
     showmore.setAttribute("onsitecommand", "ExtensionsView.showMoreResults('" + browseURL + "');");
-    this._list.appendChild(showmore);
+    this.addItem(showmore, "repo");
 
     let evt = document.createEvent("Events");
     evt.initEvent("ViewChanged", true, false);
     this._list.dispatchEvent(evt);
   },
 
   displaySearchResults: function ev_displaySearchResults(aAddons, aTotalResults, aSelectFirstResult) {
     this.clearSection("repo");
@@ -669,17 +699,17 @@ var ExtensionsView = {
       let label = PluralForm.get(aTotalResults, labelBase).replace("#1", aTotalResults);
 
       showmore.setAttribute("sitelabel", label);
 
       let url = Services.prefs.getCharPref("extensions.getAddons.search.browseURL");
       url = url.replace(/%TERMS%/g, encodeURIComponent(this.searchBox.value));
       url = formatter.formatURL(url);
       showmore.setAttribute("onsitecommand", "ExtensionsView.showMoreResults('" + url + "');");
-      this._list.appendChild(showmore);
+      this.addItem(showmore, "repo");
     }
 
     this.displaySectionMessage("repo", null, strings.GetStringFromName("addonsSearchSuccess2.button"), true);
   },
 
   showPage: function ev_showPage(aItem) {
     let uri = aItem.getAttribute("homepageURL");
     if (uri)
@@ -849,34 +879,63 @@ var AddonSearchResults = {
 ///////////////////////////////////////////////////////////////////////////////
 // XPInstall download helper
 function AddonInstallListener() {
 }
 
 AddonInstallListener.prototype = {
 
   onInstallEnded: function(aInstall, aAddon) {
-    if (aInstall.existingAddon && (aInstall.existingAddon.pendingOperations & AddonManager.PENDING_UPGRADE))
-      ExtensionsView.showRestart("update");
-    else if (aAddon.pendingOperations & AddonManager.PENDING_INSTALL)
-      ExtensionsView.showRestart("normal");
+    let needsRestart = false;
+    let mode = "";
+    if (aInstall.existingAddon && (aInstall.existingAddon.pendingOperations & AddonManager.PENDING_UPGRADE)) {
+      needsRestart = true;
+      mode = "update";
+    } else if (aAddon.pendingOperations & AddonManager.PENDING_INSTALL) {
+      needsRestart = true;
+      mode = "normal";
+    }
+
+    // if we already have a mode, then we need to show a restart notification
+    // otherwise, we are likely a bootstrapped addon
+    if (needsRestart)
+      ExtensionsView.showRestart(mode);
+    this._showInstallCompleteAlert(true, needsRestart);
+
+    // only do this if the view has already been inited
+    if (!ExtensionsView._list)
+      return;
 
     let element = ExtensionsView.getElementForAddon(aAddon.id);
-    if (element) {  
+    if (!element) {
+      element = ExtensionsView._createLocalAddon(aAddon);
+      ExtensionsView.addItem(element, "local");
+    }
+
+    if (needsRestart) {
       element.setAttribute("opType", "needs-restart");
-      element.setAttribute("status", "success");
-  
-      // If we are updating an add-on, change the status
-      if (element.hasAttribute("updating")) {
-        let strings = Strings.browser;
-        element.setAttribute("updateStatus", strings.formatStringFromName("addonUpdate.updated", [aAddon.version], 1));
-        element.removeAttribute("updating");
+    } else {
+      if (element.getAttribute("typeName") == "search") {
+        if (aAddon.permissions & AddonManager.PERM_CAN_UPGRADE)
+          document.getElementById("addons-update-all").disabled = false;
+
+        ExtensionsView.removeItem(element);
+        element = ExtensionsView._createLocalAddon(aAddon);
+        ExtensionsView.addItem(element, "local");
       }
     }
-    this._showInstallCompleteAlert(true);
+
+    element.setAttribute("status", "success");
+
+    // If we are updating an add-on, change the status
+    if (element.hasAttribute("updating")) {
+      let strings = Elements.browserBundle;
+      element.setAttribute("updateStatus", strings.getFormattedString("addonUpdate.updated", [aAddon.version]));
+      element.removeAttribute("updating");
+    }
   },
 
   onInstallFailed: function(aInstall, aError) {
     this._showInstallCompleteAlert(false);
 
     if (ExtensionsView.visible) {
       let element = ExtensionsView.getElementForAddon(aInstall.sourceURI.spec);
       if (!element)
@@ -925,45 +984,49 @@ AddonInstallListener.prototype = {
     let progress = Math.round((aInstall.progress / aInstall.maxProgress) * 100);
     element.setAttribute("progress", progress);
   },
 
   onDownloadFailed: function(aInstall, aError) {
     this.onInstallFailed(aInstall, aError);
   },
 
-  onDownloadCancelled: function(aInstall, aAddon) {
+  onDownloadCancelled: function(aInstall) {
     let strings = Strings.browser;
     let brandBundle = Strings.brand;
     let brandShortName = brandBundle.GetStringFromName("brandShortName");
     let host = (aInstall.originatingURI instanceof Ci.nsIStandardURL) && aInstall.originatingURI.host;
     if (!host)
       host = (aInstall.sourceURI instanceof Ci.nsIStandardURL) && aInstall.sourceURI.host;
 
     let error = (host || aInstall.error == 0) ? "addonError" : "addonLocalError";
     if (aInstall.error != 0)
       error += aInstall.error;
-    else if (aInstall.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED)
+    else if (aInstall.addon && aInstall.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED)
       error += "Blocklisted";
-    else if (!aInstall.addon.isCompatible || !aInstall.addon.isPlatformCompatible)
+    else if (aInstall.addon && (!aInstall.addon.isCompatible || !aInstall.addon.isPlatformCompatible))
       error += "Incompatible";
     else {
       ExtensionsView.hideAlerts();
       return; // no need to show anything in this case
     }
 
     let messageString = strings.GetStringFromName(error);
     messageString = messageString.replace("#1", aInstall.name);
     if (host)
       messageString = messageString.replace("#2", host);
     messageString = messageString.replace("#3", brandShortName);
     messageString = messageString.replace("#4", Services.appinfo.version);
     
     ExtensionsView.showAlert(messageString);
   },
 
-  _showInstallCompleteAlert: function xpidm_showAlert(aSucceeded) {
+  _showInstallCompleteAlert: function xpidm_showAlert(aSucceeded, aNeedsRestart) {
     let strings = Strings.browser;
-    let msg = aSucceeded ? strings.GetStringFromName("alertAddonsInstalled") :
-                           strings.GetStringFromName("alertAddonsFail");
-    ExtensionsView.showAlert(msg);
+    let stringName = "alertAddonsFail";
+    if (aSucceeded) {
+      stringName = "alertAddonsInstalled";
+      if (!aNeedsRestart)
+        stringName += "NoRestart";
+    }
+    ExtensionsView.showAlert(strings.GetStringFromName(stringName));
   },
 };