Bug 696324 - Provide JS API for adding items to the Android menu [r=mfinkle]
authorFabrice Desré <fabrice@mozilla.com>
Fri, 21 Oct 2011 01:50:04 -0400
changeset 81616 50835832a726a66a9aeab0d3349138b6fb17847e
parent 81615 79809866caf35f004fb9fc2fdb35b89d959a908c
child 81617 0f3b96698f1743e6bb3c7461d87ebd3cb23712b5
push idunknown
push userunknown
push dateunknown
reviewersmfinkle
bugs696324
milestone10.0a1
Bug 696324 - Provide JS API for adding items to the Android menu [r=mfinkle]
embedding/android/GeckoApp.java
mobile/chrome/content/browser.js
--- a/embedding/android/GeckoApp.java
+++ b/embedding/android/GeckoApp.java
@@ -88,25 +88,39 @@ abstract public class GeckoApp
 
     private LinearLayout mMainLayout;
     private RelativeLayout mGeckoLayout;
     public static GeckoSurfaceView surfaceView;
     public static SurfaceView cameraView;
     public static GeckoApp mAppContext;
     public static boolean mFullscreen = false;
     public static File sGREDir = null;
+    public static Menu sMenu;
     public Handler mMainHandler;
     private IntentFilter mConnectivityFilter;
     private BroadcastReceiver mConnectivityReceiver;
     private BrowserToolbar mBrowserToolbar;
     private PopupWindow mTabsTray;
     private TabsAdapter mTabsAdapter;
     public DoorHanger mDoorHanger;
     private static boolean isTabsTrayShowing;
 
+    static class ExtraMenuItem implements MenuItem.OnMenuItemClickListener {
+        String label;
+        String icon;
+        int id;
+        public boolean onMenuItemClick(MenuItem item) {
+            Log.i("GeckoJSMenu", "menu item clicked");
+            GeckoAppShell.sendEventToGecko(new GeckoEvent("Menu:Clicked", Integer.toString(id)));
+            return true;
+        }
+    }
+
+    static Vector<ExtraMenuItem> sExtraMenuItems = new Vector<ExtraMenuItem>();
+
     enum LaunchState {Launching, WaitButton,
                       Launched, GeckoRunning, GeckoExiting};
     private static LaunchState sLaunchState = LaunchState.Launching;
     private static boolean sTryCatchAttached = false;
 
     private static final int FILE_PICKER_REQUEST = 1;
     private static final int AWESOMEBAR_REQUEST = 2;
     private static final int CAMERA_CAPTURE_REQUEST = 3;
@@ -377,24 +391,59 @@ abstract public class GeckoApp
         new GeckoTask().execute(intent);
 
         return true;
     }
 
     @Override
     public boolean onCreateOptionsMenu(Menu menu)
     {
-        final Activity self = this;
-
+        sMenu = menu;
         MenuInflater inflater = getMenuInflater();
         inflater.inflate(R.layout.gecko_menu, menu);
         return true;
     }
 
     @Override
+    public boolean onPrepareOptionsMenu(Menu aMenu)
+    {
+        Iterator<ExtraMenuItem> i = sExtraMenuItems.iterator();
+        while (i.hasNext()) {
+            final ExtraMenuItem item = i.next();
+            if (aMenu.findItem(item.id) == null) {
+                final MenuItem mi = aMenu.add(aMenu.NONE, item.id, aMenu.NONE, item.label);
+                if (item.icon != null) {
+                    if (item.icon.startsWith("data")) {
+                        byte[] raw = Base64.decode(item.icon.substring(22), Base64.DEFAULT);
+                        Bitmap bitmap = BitmapFactory.decodeByteArray(raw, 0, raw.length);
+                        BitmapDrawable drawable = new BitmapDrawable(bitmap);
+                        mi.setIcon(drawable);
+                    }
+                    else if (item.icon.startsWith("jar:") || item.icon.startsWith("file://")) {
+                        GeckoAppShell.getHandler().post(new Runnable() {
+                            public void run() {
+                                try {
+                                    URL url = new URL(item.icon);
+                                    InputStream is = (InputStream) url.getContent();
+                                    Drawable drawable = Drawable.createFromStream(is, "src");
+                                    mi.setIcon(drawable);
+                                } catch(Exception e) {
+                                    Log.e("Gecko", "onPrepareOptionsMenu: Unable to set icon", e);
+                                }
+                            }
+                        });
+                    }
+                }
+                mi.setOnMenuItemClickListener(item);
+            }
+        }
+        return true;
+    }
+
+    @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         Tab tab = null;
         Tab.HistoryEntry he = null;
         switch (item.getItemId()) {
            case R.id.quit:
                quit();
                return true;
            case R.id.bookmarks:
@@ -541,18 +590,42 @@ abstract public class GeckoApp
         } else {
             mTabsAdapter = new TabsAdapter(mAppContext, tabs);
             ListView list = (ListView) mTabsTray.getContentView().findViewById(R.id.list);
             list.setAdapter(mTabsAdapter);
         }
     }
 
     public void handleMessage(String event, JSONObject message) {
+        Log.i("Gecko", "Got message: " + event);
         try {
-            if (event.equals("DOMContentLoaded")) {
+            if (event.equals("Menu:Add")) {
+                String name = message.getString("name");
+                ExtraMenuItem item = new ExtraMenuItem();
+                item.label = message.getString("name");
+                item.id = message.getInt("id");
+                try { // icon is optional
+                    item.icon = message.getString("icon");
+                } catch (Exception ex) { }
+                sExtraMenuItems.add(item);
+            } else if (event.equals("Menu:Remove")) {
+                // remove it from the menu and from our vector
+                Iterator<ExtraMenuItem> i = sExtraMenuItems.iterator();
+                int id = message.getInt("id");
+                while (i.hasNext()) {
+                    ExtraMenuItem item = i.next();
+                    if (item.id == id) {
+                        sExtraMenuItems.remove(item);
+                        MenuItem menu = sMenu.findItem(id);
+                        if (menu != null)
+                            sMenu.removeItem(id);
+                        return;
+                    }
+                }
+            } else if (event.equals("DOMContentLoaded")) {
                 final int tabId = message.getInt("tabID");
                 final String uri = message.getString("uri");
                 final String title = message.getString("title");
                 final CharSequence titleText = title;
                 handleContentLoaded(tabId, uri, title);
                 Log.i(LOG_FILE_NAME, "URI - " + uri + ", title - " + title);
             } else if (event.equals("DOMTitleChanged")) {
                 final int tabId = message.getInt("tabID");
@@ -991,16 +1064,18 @@ abstract public class GeckoApp
         GeckoAppShell.registerGeckoEventListener("onLocationChange", GeckoApp.mAppContext);
         GeckoAppShell.registerGeckoEventListener("onStateChange", GeckoApp.mAppContext);
         GeckoAppShell.registerGeckoEventListener("onProgressChange", GeckoApp.mAppContext);
         GeckoAppShell.registerGeckoEventListener("onCameraCapture", GeckoApp.mAppContext);
         GeckoAppShell.registerGeckoEventListener("Tab:Added", GeckoApp.mAppContext);
         GeckoAppShell.registerGeckoEventListener("Tab:Closed", GeckoApp.mAppContext);
         GeckoAppShell.registerGeckoEventListener("Tab:Selected", GeckoApp.mAppContext);
         GeckoAppShell.registerGeckoEventListener("Doorhanger:Add", GeckoApp.mAppContext);
+        GeckoAppShell.registerGeckoEventListener("Menu:Add", GeckoApp.mAppContext);
+        GeckoAppShell.registerGeckoEventListener("Menu:Remove", GeckoApp.mAppContext);
 
         mConnectivityFilter = new IntentFilter();
         mConnectivityFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
         mConnectivityReceiver = new GeckoConnectivityReceiver();
 
         final GeckoApp self = this;
  
         mMainHandler.postDelayed(new Runnable() {
@@ -1184,16 +1259,18 @@ abstract public class GeckoApp
         GeckoAppShell.unregisterGeckoEventListener("onLocationChange", GeckoApp.mAppContext);
         GeckoAppShell.unregisterGeckoEventListener("onStateChange", GeckoApp.mAppContext);
         GeckoAppShell.unregisterGeckoEventListener("onProgressChange", GeckoApp.mAppContext);
         GeckoAppShell.unregisterGeckoEventListener("onCameraCapture", GeckoApp.mAppContext);
         GeckoAppShell.unregisterGeckoEventListener("Tab:Added", GeckoApp.mAppContext);
         GeckoAppShell.unregisterGeckoEventListener("Tab:Closed", GeckoApp.mAppContext);
         GeckoAppShell.unregisterGeckoEventListener("Tab:Selected", GeckoApp.mAppContext);
         GeckoAppShell.unregisterGeckoEventListener("Doorhanger:Add", GeckoApp.mAppContext);
+        GeckoAppShell.unregisterGeckoEventListener("Menu:Add", GeckoApp.mAppContext);
+        GeckoAppShell.unregisterGeckoEventListener("Menu:Remove", GeckoApp.mAppContext);
 
         super.onDestroy();
     }
 
     @Override
     public void onConfigurationChanged(android.content.res.Configuration newConfig)
     {
         Log.i(LOG_FILE_NAME, "configuration changed");
--- a/mobile/chrome/content/browser.js
+++ b/mobile/chrome/content/browser.js
@@ -69,16 +69,18 @@ var BrowserApp = {
 
     Services.obs.addObserver(this, "Tab:Add", false);
     Services.obs.addObserver(this, "Tab:Load", false);
     Services.obs.addObserver(this, "Tab:Select", false);
     Services.obs.addObserver(this, "Tab:Close", false);
     Services.obs.addObserver(this, "session-back", false);
     Services.obs.addObserver(this, "session-reload", false);
 
+    NativeWindow.init();
+
     let uri = "about:support";
     if ("arguments" in window && window.arguments[0])
       uri = window.arguments[0];
 
     // XXX maybe we don't do this if the launch was kicked off from external
     Services.io.offline = false;
     let newTab = this.addTab(uri);
     newTab.active = true;
@@ -87,16 +89,17 @@ var BrowserApp = {
 
     // Broadcast a UIReady message so add-ons know we are finished with startup
     let event = document.createEvent("Events");
     event.initEvent("UIReady", true, false);
     window.dispatchEvent(event);
   },
 
   shutdown: function shutdown() {
+    NativeWindow.uninit();
   },
 
   get tabs() {
     return this._tabs;
   },
 
   get selectedTab() {
     return this._selectedTab;
@@ -183,17 +186,17 @@ var BrowserApp = {
       this.selectedTab = aTab;
       aTab.active = true;
       let message = {
         gecko: {
           type: "Tab:Selected",
           tabID: aTab.id
         }
       };
-    
+
       sendMessageToJava(message);
     }
   },
 
   observe: function(aSubject, aTopic, aData) {
     let browser = this.selectedBrowser;
     if (!browser)
       return;
@@ -209,16 +212,46 @@ var BrowserApp = {
       browser.loadURI(aData);
     else if (aTopic == "Tab:Select") 
       this.selectTab(this.getTabForId(parseInt(aData)));
     else if (aTopic == "Tab:Close")
       this.closeTab(this.getTabForId(parseInt(aData)));
   }
 }
 
+var NativeWindow = {
+  init: function() {
+    Services.obs.addObserver(this, "Menu:Clicked", false);
+  },
+
+  uninit: function() {
+    Services.obs.removeObserver(this, "Menu:Clicked");
+  },
+
+  menu: {
+    _callbacks: [],
+    _menuId: 0,
+    add: function(aName, aIcon, aCallback) {
+      sendMessageToJava({ gecko: {type: "Menu:Add", name: aName, icon: aIcon, id: this._menuId }});
+      this._callbacks[this._menuId] = aCallback;
+      this._menuId++;
+      return this._menuId - 1;
+    },
+
+    remove: function(aId) {
+      sendMessageToJava({ gecko: {type: "Menu:Remove", id: aId }});
+    }
+  },
+
+  observe: function(aSubject, aTopic, aData) {
+    if (this.menu._callbacks[aData])
+      this.menu._callbacks[aData]();
+  }
+};
+
 
 function nsBrowserAccess() {
 }
 
 nsBrowserAccess.prototype = {
   openURI: function browser_openURI(aURI, aOpener, aWhere, aContext) {
     dump("nsBrowserAccess::openURI");
     let browser = BrowserApp.selectedBrowser;
@@ -294,17 +327,17 @@ Tab.prototype = {
     BrowserApp.deck.removeChild(this.browser);
     this.browser = null;
     let message = {
       gecko: {
         type: "Tab:Closed",
         tabID: this.id
       }
     };
-    
+
     sendMessageToJava(message);
   },
 
   set active(aActive) {
     if (!this.browser)
       return;
 
     if (aActive) {
@@ -451,17 +484,17 @@ var BrowserEventHandler = {
         });
         break;
       }
 
       case "DOMLinkAdded": {
         let target = aEvent.originalTarget;
         if (!target.href || target.disabled)
           return;
-        
+
         let browser = BrowserApp.getBrowserForDocument(target.ownerDocument); 
         let tabID = BrowserApp.getTabForBrowser(browser).id;
 
         let json = {
           type: "DOMLinkAdded",
           tabID: tabID,
           windowId: target.ownerDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID,
           href: resolveGeckoURI(target.href),