Bug 978991 - Add hooks to let an add-on know when a panel is installed/uninstalled. r=liuche a=lsblakk
authorMargaret Leibovic <margaret.leibovic@gmail.com>
Wed, 02 Apr 2014 11:26:18 -0700
changeset 192709 9ee4156b0686a9952ccda813b748da0c83d7431a
parent 192708 cad2ea18d984e73b2582858f301225ad1f371b77
child 192710 a3e167bd143ba73ddcf5895e72f5752e17f67ca2
push id474
push userasasaki@mozilla.com
push dateMon, 02 Jun 2014 21:01:02 +0000
treeherdermozilla-release@967f4cf1b31c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersliuche, lsblakk
bugs978991
milestone30.0a2
Bug 978991 - Add hooks to let an add-on know when a panel is installed/uninstalled. r=liuche a=lsblakk
mobile/android/base/home/HomeConfig.java
mobile/android/chrome/content/browser.js
mobile/android/modules/Home.jsm
--- a/mobile/android/base/home/HomeConfig.java
+++ b/mobile/android/base/home/HomeConfig.java
@@ -1,15 +1,17 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * 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/. */
 
 package org.mozilla.gecko.home;
 
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import android.content.Context;
@@ -18,16 +20,17 @@ import android.os.Parcelable;
 import android.text.TextUtils;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
+import java.util.LinkedList;
 import java.util.List;
 
 public final class HomeConfig {
     /**
      * Used to determine what type of HomeFragment subclass to use when creating
      * a given panel. With the exception of DYNAMIC, all of these types correspond
      * to a default set of built-in panels. The DYNAMIC panel type is used by
      * third-party services to create panels with varying types of content.
@@ -747,25 +750,27 @@ public final class HomeConfig {
      *
      * {@code Editor} is *not* thread-safe. You can only make calls on it
      * from the thread where it was originally created. It will throw an
      * exception if you don't follow this invariant.
      */
     public static class Editor implements Iterable<PanelConfig> {
         private final HomeConfig mHomeConfig;
         private final HashMap<String, PanelConfig> mConfigMap;
+        private final List<GeckoEvent> mEventQueue;
         private final Thread mOriginalThread;
 
         private PanelConfig mDefaultPanel;
         private int mEnabledCount;
 
         private Editor(HomeConfig homeConfig, State configState) {
             mHomeConfig = homeConfig;
             mOriginalThread = Thread.currentThread();
             mConfigMap = new LinkedHashMap<String, PanelConfig>();
+            mEventQueue = new LinkedList<GeckoEvent>();
             mEnabledCount = 0;
 
             initFromState(configState);
         }
 
         /**
          * Initialize the initial state of the editor from the given
          * {@sode State}. A LinkedHashMap is used to represent the list of
@@ -941,16 +946,19 @@ public final class HomeConfig {
                 mConfigMap.put(id, panelConfig);
 
                 mEnabledCount++;
                 if (mEnabledCount == 1 || panelConfig.isDefault()) {
                     setDefault(panelConfig.getId());
                 }
 
                 installed = true;
+
+                // Add an event to the queue if a new panel is sucessfully installed.
+                mEventQueue.add(GeckoEvent.createBroadcastEvent("HomePanels:Installed", panelConfig.getId()));
             }
 
             return installed;
         }
 
         /**
          * Removes an existing panel.
          *
@@ -973,16 +981,19 @@ public final class HomeConfig {
             if (!panelConfig.isDisabled()) {
                 mEnabledCount--;
             }
 
             if (isCurrentDefaultPanel(panelConfig)) {
                 findNewDefault();
             }
 
+            // Add an event to the queue if a panel is succesfully uninstalled.
+            mEventQueue.add(GeckoEvent.createBroadcastEvent("HomePanels:Uninstalled", panelId));
+
             return true;
         }
 
         /**
          * Replaces an existing panel with a new {@code PanelConfig} instance.
          *
          * @return true if the item has been updated.
          */
@@ -1019,20 +1030,28 @@ public final class HomeConfig {
         public State apply() {
             ThreadUtils.assertOnThread(mOriginalThread);
 
             // We're about to save the current state in the background thread
             // so we should use a deep copy of the PanelConfig instances to
             // avoid saving corrupted state.
             final State newConfigState = new State(mHomeConfig, makeDeepCopy());
 
+            // Copy the event queue to a new list, so that we only modify mEventQueue on
+            // the original thread where it was created.
+            final LinkedList<GeckoEvent> eventQueueCopy = new LinkedList<GeckoEvent>(mEventQueue);
+            mEventQueue.clear();
+
             ThreadUtils.getBackgroundHandler().post(new Runnable() {
                 @Override
                 public void run() {
                     mHomeConfig.save(newConfigState);
+
+                    // Send pending events after the new config is saved.
+                    sendEventsToGecko(eventQueueCopy);
                 }
             });
 
             return newConfigState;
         }
 
         /**
          * Saves the current {@code Editor} state synchronously in the
@@ -1045,23 +1064,33 @@ public final class HomeConfig {
 
             final State newConfigState =
                     new State(mHomeConfig, new ArrayList<PanelConfig>(mConfigMap.values()));
 
             // This is a synchronous blocking operation, hence no
             // need to deep copy the current PanelConfig instances.
             mHomeConfig.save(newConfigState);
 
+            // Send pending events after the new config is saved.
+            sendEventsToGecko(mEventQueue);
+            mEventQueue.clear();
+
             return newConfigState;
         }
 
         public boolean isEmpty() {
             return mConfigMap.isEmpty();
         }
 
+        private void sendEventsToGecko(List<GeckoEvent> events) {
+            for (GeckoEvent e : events) {
+                GeckoAppShell.sendEventToGecko(e);
+            }
+        }
+
         @Override
         public Iterator<PanelConfig> iterator() {
             ThreadUtils.assertOnThread(mOriginalThread);
 
             return mConfigMap.values().iterator();
         }
     }
 
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -126,17 +126,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
     Services.obs.addObserver(function(s, t, d) {
         window[name].observe(s, t, d)
     }, aNotification, false);
   });
 });
 
 // Lazily-loaded JS modules that use observer notifications
 [
-  ["Home", ["HomePanels:Get"], "resource://gre/modules/Home.jsm"],
+  ["Home", ["HomePanels:Get", "HomePanels:Installed", "HomePanels:Uninstalled"], "resource://gre/modules/Home.jsm"],
 ].forEach(module => {
   let [name, notifications, resource] = module;
   XPCOMUtils.defineLazyModuleGetter(this, name, resource);
   notifications.forEach(notification => {
     Services.obs.addObserver((s,t,d) => {
       this[name].observe(s,t,d)
     }, notification, false);
   });
--- a/mobile/android/modules/Home.jsm
+++ b/mobile/android/modules/Home.jsm
@@ -160,21 +160,72 @@ function Panel(id, options) {
 
   if ("layout" in options)
     this.layout = options.layout;
 
   if ("views" in options)
     this.views = options.views;
 }
 
-// We need this function to have access to the HomePanels
+// We need this object to have access to the HomePanels
 // private members without leaking it outside Home.jsm.
-let handlePanelsGet;
+let HomePanelsMessageHandlers;
 
 let HomePanels = (function () {
+  // Functions used to handle messages sent from Java.
+  HomePanelsMessageHandlers = {
+
+    "HomePanels:Get": function handlePanelsGet(data) {
+      data = JSON.parse(data);
+
+      let requestId = data.requestId;
+      let ids = data.ids || null;
+
+      let panels = [];
+      for (let id in _registeredPanels) {
+        // Null ids means we want to fetch all available panels
+        if (ids == null || ids.indexOf(id) >= 0) {
+          try {
+            panels.push(_generatePanel(id));
+          } catch(e) {
+            Cu.reportError("Home.panels: Invalid options, panel.id = " + id + ": " + e);
+          }
+        }
+      }
+
+      sendMessageToJava({
+        type: "HomePanels:Data",
+        panels: panels,
+        requestId: requestId
+      });
+    },
+
+    "HomePanels:Installed": function handlePanelsInstalled(id) {
+      let options = _registeredPanels[id]();
+      if (!options.oninstall) {
+        return;
+      }
+      if (typeof options.oninstall !== "function") {
+        throw "Home.panels: Invalid oninstall function: panel.id = " + this.id;
+      }
+      options.oninstall();
+    },
+
+    "HomePanels:Uninstalled": function handlePanelsUninstalled(id) {
+      let options = _registeredPanels[id]();
+      if (!options.onuninstall) {
+        return;
+      }
+      if (typeof options.onuninstall !== "function") {
+        throw "Home.panels: Invalid onuninstall function: panel.id = " + this.id;
+      }
+      options.onuninstall();
+    }
+  };
+
   // Holds the current set of registered panels that can be
   // installed, updated, uninstalled, or unregistered. It maps
   // panel ids with the functions that dynamically generate
   // their respective panel options. This is used to retrieve
   // the current list of available panels in the system.
   // See HomePanels:Get handler.
   let _registeredPanels = {};
 
@@ -242,39 +293,16 @@ let HomePanels = (function () {
       if (!view.dataset) {
         throw "Home.panels: No dataset provided for view: panel.id = " + panel.id + ", view.type = " + view.type;
       }
     }
 
     return panel;
   };
 
-  handlePanelsGet = function(data) {
-    let requestId = data.requestId;
-    let ids = data.ids || null;
-
-    let panels = [];
-    for (let id in _registeredPanels) {
-      // Null ids means we want to fetch all available panels
-      if (ids == null || ids.indexOf(id) >= 0) {
-        try {
-          panels.push(_generatePanel(id));
-        } catch(e) {
-          Cu.reportError("Home.panels: Invalid options, panel.id = " + id + ": " + e);
-        }
-      }
-    }
-
-    sendMessageToJava({
-      type: "HomePanels:Data",
-      panels: panels,
-      requestId: requestId
-    });
-  };
-
   // Helper function used to see if a value is in an object.
   let _valueExists = function(obj, value) {
     for (let key in obj) {
       if (obj[key] == value) {
         return true;
       }
     }
     return false;
@@ -342,15 +370,15 @@ let HomePanels = (function () {
 
 // Public API
 this.Home = Object.freeze({
   banner: HomeBanner,
   panels: HomePanels,
 
   // Lazy notification observer registered in browser.js
   observe: function(subject, topic, data) {
-    switch(topic) {
-      case "HomePanels:Get":
-        handlePanelsGet(JSON.parse(data));
-        break;
+    if (topic in HomePanelsMessageHandlers) {
+      HomePanelsMessageHandlers[topic](data);
+    } else {
+      Cu.reportError("Home.observe: message handler not found for topic: " + topic);
     }
   }
 });