Bug 1325155 - 2. Convert text selection events to bundle events; r=sebastian
authorJim Chen <nchen@mozilla.com>
Wed, 28 Dec 2016 17:49:29 -0500
changeset 327510 2a43ca16d1c16f6b050fa40ebbdb6abc7a190212
parent 327509 58e189739172cf567fa33a7470ed5768f0448c38
child 327511 a9d1dbc6426dd1c8b89488f62ae859a3c8a7aa0c
push id35517
push userkwierso@gmail.com
push dateThu, 29 Dec 2016 20:22:54 +0000
treeherderautoland@3f2f8d77ad27 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssebastian
bugs1325155
milestone53.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1325155 - 2. Convert text selection events to bundle events; r=sebastian Convert the "TextSelection:*" events to use BundleEventListener / GeckoBundle, in both ActionBarTextSelection.java and FloatingToolbarTextSelection.java. UI events are used because the listeners require the UI thread. The "TextSelection:Update" listeners are removed because the event is no longer sent by ActionBarHandler.
mobile/android/base/java/org/mozilla/gecko/ActionBarTextSelection.java
mobile/android/base/java/org/mozilla/gecko/text/FloatingToolbarTextSelection.java
mobile/android/base/java/org/mozilla/gecko/text/TextAction.java
mobile/android/chrome/content/ActionBarHandler.js
--- a/mobile/android/base/java/org/mozilla/gecko/ActionBarTextSelection.java
+++ b/mobile/android/base/java/org/mozilla/gecko/ActionBarTextSelection.java
@@ -3,44 +3,46 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
 import org.mozilla.gecko.menu.GeckoMenu;
 import org.mozilla.gecko.menu.GeckoMenuItem;
 import org.mozilla.gecko.util.ResourceDrawableUtils;
 import org.mozilla.gecko.text.TextSelection;
-import org.mozilla.gecko.util.GeckoEventListener;
+import org.mozilla.gecko.util.BundleEventListener;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.ActionModeCompat.Callback;
 
 import android.content.Context;
 import android.graphics.drawable.Drawable;
 import android.view.MenuItem;
 
-import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
+import java.util.Arrays;
 import java.util.Timer;
 import java.util.TimerTask;
 
 import android.util.Log;
 
-class ActionBarTextSelection implements TextSelection, GeckoEventListener {
+class ActionBarTextSelection implements TextSelection, BundleEventListener {
     private static final String LOGTAG = "GeckoTextSelection";
     private static final int SHUTDOWN_DELAY_MS = 250;
 
     private final Context context;
 
     private boolean mDraggingHandles;
 
-    private String selectionID; // Unique ID provided for each selection action.
+    private int selectionID; // Unique ID provided for each selection action.
 
-    private String mCurrentItems;
+    private GeckoBundle[] mCurrentItems;
 
     private TextSelectionActionModeCallback mCallback;
 
     // These timers are used to avoid flicker caused by selection handles showing/hiding quickly.
     // For instance when moving between single handle caret mode and two handle selection mode.
     private final Timer mActionModeTimer = new Timer("actionMode");
     private class ActionModeTimerTask extends TimerTask {
         @Override
@@ -60,95 +62,79 @@ class ActionBarTextSelection implements 
     }
 
     @Override
     public void create() {
         // Only register listeners if we have valid start/middle/end handles
         if (context == null) {
             Log.e(LOGTAG, "Failed to initialize text selection because at least one context is null");
         } else {
-            GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
-                "TextSelection:ActionbarInit",
-                "TextSelection:ActionbarStatus",
-                "TextSelection:ActionbarUninit",
-                "TextSelection:Update");
+            GeckoApp.getEventDispatcher().registerUiThreadListener(this,
+                    "TextSelection:ActionbarInit",
+                    "TextSelection:ActionbarStatus",
+                    "TextSelection:ActionbarUninit");
         }
     }
 
     @Override
     public boolean dismiss() {
         // We do not call endActionMode() here because this is already handled by the activity.
         return false;
     }
 
     @Override
     public void destroy() {
         if (context == null) {
             Log.e(LOGTAG, "Do not unregister TextSelection:* listeners since context is null");
         } else {
-            GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
+            GeckoApp.getEventDispatcher().unregisterUiThreadListener(this,
                     "TextSelection:ActionbarInit",
                     "TextSelection:ActionbarStatus",
-                    "TextSelection:ActionbarUninit",
-                    "TextSelection:Update");
+                    "TextSelection:ActionbarUninit");
         }
     }
 
     @Override
-    public void handleMessage(final String event, final JSONObject message) {
-        ThreadUtils.postToUiThread(new Runnable() {
-            @Override
-            public void run() {
-                try {
-                    if (event.equals("TextSelection:Update")) {
-                        if (mActionModeTimerTask != null)
-                            mActionModeTimerTask.cancel();
-                        showActionMode(message.getJSONArray("actions"));
-                    } else if (event.equals("TextSelection:ActionbarInit")) {
-                        // Init / Open the action bar. Note the current selectionID,
-                        // cancel any pending actionBar close.
-                        Telemetry.sendUIEvent(TelemetryContract.Event.SHOW,
-                            TelemetryContract.Method.CONTENT, "text_selection");
+    public void handleMessage(final String event, final GeckoBundle message,
+                              final EventCallback callback) {
+        if ("TextSelection:ActionbarInit".equals(event)) {
+            // Init / Open the action bar. Note the current selectionID,
+            // cancel any pending actionBar close.
+            Telemetry.sendUIEvent(TelemetryContract.Event.SHOW,
+                TelemetryContract.Method.CONTENT, "text_selection");
 
-                        selectionID = message.getString("selectionID");
-                        mCurrentItems = null;
-                        if (mActionModeTimerTask != null) {
-                            mActionModeTimerTask.cancel();
-                        }
+            selectionID = message.getInt("selectionID");
+            mCurrentItems = null;
+            if (mActionModeTimerTask != null) {
+                mActionModeTimerTask.cancel();
+            }
 
-                    } else if (event.equals("TextSelection:ActionbarStatus")) {
-                        // Ensure async updates from SearchService for example are valid.
-                        if (selectionID != message.optString("selectionID")) {
-                            return;
-                        }
-
-                        // Update the actionBar actions as provided by Gecko.
-                        showActionMode(message.getJSONArray("actions"));
+        } else if ("TextSelection:ActionbarStatus".equals(event)) {
+            // Ensure async updates from SearchService for example are valid.
+            if (selectionID != message.getInt("selectionID")) {
+                return;
+            }
 
-                    } else if (event.equals("TextSelection:ActionbarUninit")) {
-                        // Uninit the actionbar. Schedule a cancellable close
-                        // action to avoid UI jank. (During SelectionAll for ex).
-                        mCurrentItems = null;
-                        mActionModeTimerTask = new ActionModeTimerTask();
-                        mActionModeTimer.schedule(mActionModeTimerTask, SHUTDOWN_DELAY_MS);
-                    }
+            // Update the actionBar actions as provided by Gecko.
+            showActionMode(message.getBundleArray("actions"));
 
-                } catch (JSONException e) {
-                    Log.e(LOGTAG, "JSON exception", e);
-                }
-            }
-        });
+        } else if ("TextSelection:ActionbarUninit".equals(event)) {
+            // Uninit the actionbar. Schedule a cancellable close
+            // action to avoid UI jank. (During SelectionAll for ex).
+            mCurrentItems = null;
+            mActionModeTimerTask = new ActionModeTimerTask();
+            mActionModeTimer.schedule(mActionModeTimerTask, SHUTDOWN_DELAY_MS);
+        }
     }
 
-    private void showActionMode(final JSONArray items) {
-        String itemsString = items.toString();
-        if (itemsString.equals(mCurrentItems)) {
+    private void showActionMode(final GeckoBundle[] items) {
+        if (Arrays.equals(items, mCurrentItems)) {
             return;
         }
-        mCurrentItems = itemsString;
+        mCurrentItems = items;
 
         if (mCallback != null) {
             mCallback.updateItems(items);
             return;
         }
 
         if (context instanceof ActionModeCompat.Presenter) {
             final ActionModeCompat.Presenter presenter = (ActionModeCompat.Presenter) context;
@@ -162,84 +148,78 @@ class ActionBarTextSelection implements 
         if (context instanceof ActionModeCompat.Presenter) {
             final ActionModeCompat.Presenter presenter = (ActionModeCompat.Presenter) context;
             presenter.endActionModeCompat();
         }
         mCurrentItems = null;
     }
 
     private class TextSelectionActionModeCallback implements Callback {
-        private JSONArray mItems;
+        private GeckoBundle[] mItems;
         private ActionModeCompat mActionMode;
 
-        public TextSelectionActionModeCallback(JSONArray items) {
+        public TextSelectionActionModeCallback(final GeckoBundle[] items) {
             mItems = items;
         }
 
-        public void updateItems(JSONArray items) {
+        public void updateItems(final GeckoBundle[] items) {
             mItems = items;
             if (mActionMode != null) {
                 mActionMode.invalidate();
             }
         }
 
         public void animateIn() {
             if (mActionMode != null) {
                 mActionMode.animateIn();
             }
         }
 
         @Override
         public boolean onPrepareActionMode(final ActionModeCompat mode, final GeckoMenu menu) {
-            // Android would normally expect us to only update the state of menu items here
-            // To make the js-java interaction a bit simpler, we just wipe out the menu here and recreate all
-            // the javascript menu items in onPrepare instead. This will be called any time invalidate() is called on the
-            // action mode.
+            // Android would normally expect us to only update the state of menu items
+            // here To make the js-java interaction a bit simpler, we just wipe out the
+            // menu here and recreate all the javascript menu items in onPrepare instead.
+            // This will be called any time invalidate() is called on the action mode.
             menu.clear();
 
-            int length = mItems.length();
+            final int length = mItems.length;
             for (int i = 0; i < length; i++) {
-                try {
-                    final JSONObject obj = mItems.getJSONObject(i);
-                    final GeckoMenuItem menuitem = (GeckoMenuItem) menu.add(0, i, 0, obj.optString("label"));
-                    final int actionEnum = obj.optBoolean("showAsAction") ? GeckoMenuItem.SHOW_AS_ACTION_ALWAYS : GeckoMenuItem.SHOW_AS_ACTION_NEVER;
-                    menuitem.setShowAsAction(actionEnum, R.attr.menuItemActionModeStyle);
+                final GeckoBundle obj = mItems[i];
+                final GeckoMenuItem menuitem = (GeckoMenuItem)
+                        menu.add(0, i, 0, obj.getString("label"));
+                final int actionEnum = obj.getBoolean("showAsAction") ?
+                        GeckoMenuItem.SHOW_AS_ACTION_ALWAYS : GeckoMenuItem.SHOW_AS_ACTION_NEVER;
+                menuitem.setShowAsAction(actionEnum, R.attr.menuItemActionModeStyle);
 
-                    final String iconString = obj.optString("icon");
-                    ResourceDrawableUtils.getDrawable(context, iconString, new ResourceDrawableUtils.BitmapLoader() {
-                        @Override
-                        public void onBitmapFound(Drawable d) {
-                            if (d != null) {
-                                menuitem.setIcon(d);
-                            }
+                final String iconString = obj.getString("icon");
+                ResourceDrawableUtils.getDrawable(context, iconString,
+                        new ResourceDrawableUtils.BitmapLoader() {
+                    @Override
+                    public void onBitmapFound(Drawable d) {
+                        if (d != null) {
+                            menuitem.setIcon(d);
                         }
-                    });
-                } catch (Exception ex) {
-                    Log.i(LOGTAG, "Exception building menu", ex);
-                }
+                    }
+                });
             }
             return true;
         }
 
         @Override
         public boolean onCreateActionMode(ActionModeCompat mode, GeckoMenu unused) {
             mActionMode = mode;
             return true;
         }
 
         @Override
         public boolean onActionItemClicked(ActionModeCompat mode, MenuItem item) {
-            try {
-                final JSONObject obj = mItems.getJSONObject(item.getItemId());
-                GeckoAppShell.notifyObservers("TextSelection:Action", obj.optString("id"));
-                return true;
-            } catch (Exception ex) {
-                Log.i(LOGTAG, "Exception calling action", ex);
-            }
-            return false;
+            final GeckoBundle obj = mItems[item.getItemId()];
+            GeckoAppShell.notifyObservers("TextSelection:Action", obj.getString("id"));
+            return true;
         }
 
         // Called when the user exits the action mode
         @Override
         public void onDestroyActionMode(ActionModeCompat mode) {
             mActionMode = null;
             mCallback = null;
             final JSONObject args = new JSONObject();
--- a/mobile/android/base/java/org/mozilla/gecko/text/FloatingToolbarTextSelection.java
+++ b/mobile/android/base/java/org/mozilla/gecko/text/FloatingToolbarTextSelection.java
@@ -16,42 +16,42 @@ import org.json.JSONException;
 import org.json.JSONObject;
 
 import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.GeckoApp;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.gfx.LayerView;
-import org.mozilla.gecko.util.GeckoEventListener;
+import org.mozilla.gecko.util.BundleEventListener;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import java.util.List;
 
-import ch.boye.httpclientandroidlib.util.TextUtils;
-
 /**
  * Floating toolbar for text selection actions. Only on Android 6+.
  */
 @TargetApi(Build.VERSION_CODES.M)
-public class FloatingToolbarTextSelection implements TextSelection, GeckoEventListener {
+public class FloatingToolbarTextSelection implements TextSelection, BundleEventListener {
     private static final String LOGTAG = "GeckoFloatTextSelection";
 
     // This is an additional offset we add to the height of the selection. This will avoid that the
     // floating toolbar overlays the bottom handle(s).
     private static final int HANDLES_OFFSET_DP = 20;
 
     private final Activity activity;
     private final LayerView layerView;
     private final int[] locationInWindow;
     private final float handlesOffset;
 
     private ActionMode actionMode;
     private FloatingActionModeCallback actionModeCallback;
-    private String selectionID;
+    private int selectionID;
     /* package-private */ Rect contentRect;
 
     public FloatingToolbarTextSelection(Activity activity, LayerView layerView) {
         this.activity = activity;
         this.layerView = layerView;
         this.locationInWindow = new int[2];
 
         this.handlesOffset = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
@@ -64,17 +64,17 @@ public class FloatingToolbarTextSelectio
             endTextSelection();
             return true;
         }
 
         return false;
     }
 
     private void endTextSelection() {
-        if (TextUtils.isEmpty(selectionID)) {
+        if (selectionID == 0) {
             return;
         }
 
         final JSONObject args = new JSONObject();
         try {
             args.put("selectionID", selectionID);
         } catch (JSONException e) {
             Log.e(LOGTAG, "Error building JSON arguments for TextSelection:End", e);
@@ -90,66 +90,57 @@ public class FloatingToolbarTextSelectio
     }
 
     @Override
     public void destroy() {
         unregisterFromEvents();
     }
 
     private void registerForEvents() {
-        GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
+        GeckoApp.getEventDispatcher().registerUiThreadListener(this,
                 "TextSelection:ActionbarInit",
                 "TextSelection:ActionbarStatus",
                 "TextSelection:ActionbarUninit",
-                "TextSelection:Update",
                 "TextSelection:Visibility");
     }
 
     private void unregisterFromEvents() {
-        GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
+        GeckoApp.getEventDispatcher().unregisterUiThreadListener(this,
                 "TextSelection:ActionbarInit",
                 "TextSelection:ActionbarStatus",
                 "TextSelection:ActionbarUninit",
-                "TextSelection:Update",
                 "TextSelection:Visibility");
     }
 
-    @Override
-    public void handleMessage(final String event, final JSONObject message) {
-        ThreadUtils.postToUiThread(new Runnable() {
-            @Override
-            public void run() {
-                handleOnMainThread(event, message);
-            }
-        });
-    }
-
-    private void handleOnMainThread(final String event, final JSONObject message) {
+    @Override // BundleEventListener
+    public void handleMessage(final String event, final GeckoBundle message,
+                              final EventCallback callback) {
         if ("TextSelection:ActionbarInit".equals(event)) {
             Telemetry.sendUIEvent(TelemetryContract.Event.SHOW,
                 TelemetryContract.Method.CONTENT, "text_selection");
 
-            selectionID = message.optString("selectionID");
+            selectionID = message.getInt("selectionID");
+
         } else if ("TextSelection:ActionbarStatus".equals(event)) {
             // Ensure async updates from SearchService for example are valid.
-            if (selectionID != message.optString("selectionID")) {
+            if (selectionID != message.getInt("selectionID")) {
                 return;
             }
 
             updateRect(message);
 
             if (!isRectVisible()) {
                 finishActionMode();
             } else {
                 startActionMode(TextAction.fromEventMessage(message));
             }
+
         } else if ("TextSelection:ActionbarUninit".equals(event)) {
             finishActionMode();
-        } else if ("TextSelection:Update".equals(event)) {
-            startActionMode(TextAction.fromEventMessage(message));
+
         } else if ("TextSelection:Visibility".equals(event)) {
             finishActionMode();
         }
     }
 
     private void startActionMode(List<TextAction> actions) {
         if (actionMode != null) {
             actionModeCallback.updateActions(actions);
@@ -178,29 +169,25 @@ public class FloatingToolbarTextSelectio
      */
     private boolean isRectVisible() {
         // There's another case of an empty rect where just left == right but not top == bottom.
         // That's the rect for a collapsed selection. While technically this rect isn't visible too
         // we are not interested in this case because we do not want to hide the toolbar.
         return contentRect.left != contentRect.right || contentRect.top != contentRect.bottom;
     }
 
-    private void updateRect(JSONObject message) {
-        try {
-            final double x = message.getDouble("x");
-            final double y = (int) message.getDouble("y");
-            final double width = (int) message.getDouble("width");
-            final double height = (int) message.getDouble("height");
-
-            final float zoomFactor = layerView.getZoomFactor();
-            layerView.getLocationInWindow(locationInWindow);
+    private void updateRect(final GeckoBundle message) {
+        final double x = message.getDouble("x");
+        final double y = (int) message.getDouble("y");
+        final double width = (int) message.getDouble("width");
+        final double height = (int) message.getDouble("height");
 
-            contentRect = new Rect(
-                    (int) (x * zoomFactor + locationInWindow[0]),
-                    (int) (y * zoomFactor + locationInWindow[1]),
-                    (int) ((x + width) * zoomFactor + locationInWindow[0]),
-                    (int) ((y + height) * zoomFactor + locationInWindow[1] +
-                           (height > 0 ? handlesOffset : 0)));
-        } catch (JSONException e) {
-            Log.w(LOGTAG, "Could not calculate content rect", e);
-        }
+        final float zoomFactor = layerView.getZoomFactor();
+        layerView.getLocationInWindow(locationInWindow);
+
+        contentRect = new Rect(
+                (int) (x * zoomFactor + locationInWindow[0]),
+                (int) (y * zoomFactor + locationInWindow[1]),
+                (int) ((x + width) * zoomFactor + locationInWindow[0]),
+                (int) ((y + height) * zoomFactor + locationInWindow[1] +
+                       (height > 0 ? handlesOffset : 0)));
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/text/TextAction.java
+++ b/mobile/android/base/java/org/mozilla/gecko/text/TextAction.java
@@ -5,51 +5,48 @@
 package org.mozilla.gecko.text;
 
 import android.util.Log;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
+import org.mozilla.gecko.util.GeckoBundle;
+
 import java.util.ArrayList;
 import java.util.List;
 
 /**
  * Text selection action like "copy", "paste", ..
  */
 public class TextAction {
     private static final String LOGTAG = "GeckoTextAction";
 
     private String id;
     private String label;
     private int order;
     private int floatingOrder;
 
     private TextAction() {}
 
-    public static List<TextAction> fromEventMessage(JSONObject message) {
+    public static List<TextAction> fromEventMessage(final GeckoBundle message) {
         final List<TextAction> actions = new ArrayList<>();
-
-        try {
-            final JSONArray array = message.getJSONArray("actions");
-
-            for (int i = 0; i < array.length(); i++) {
-                final JSONObject object = array.getJSONObject(i);
+        final GeckoBundle[] array = message.getBundleArray("actions");
 
-                final TextAction action = new TextAction();
-                action.id = object.getString("id");
-                action.label = object.getString("label");
-                action.order = object.getInt("order");
-                action.floatingOrder = object.optInt("floatingOrder", i);
+        for (int i = 0; i < array.length; i++) {
+            final GeckoBundle object = array[i];
+            final TextAction action = new TextAction();
 
-                actions.add(action);
-            }
-        } catch (JSONException e) {
-            Log.w(LOGTAG, "Could not parse text actions", e);
+            action.id = object.getString("id");
+            action.label = object.getString("label");
+            action.order = object.getInt("order");
+            action.floatingOrder = object.getInt("floatingOrder", i);
+
+            actions.add(action);
         }
 
         return actions;
     }
 
     public String getId() {
         return id;
     }
--- a/mobile/android/chrome/content/ActionBarHandler.js
+++ b/mobile/android/chrome/content/ActionBarHandler.js
@@ -138,30 +138,30 @@ var ActionBarHandler = {
     }
 
     // Hold the ActionBar ID provided by Gecko.
     this._selectionID = this._nextSelectionID++;
     [this._targetElement, this._contentWindow] = [element, win];
     this._boundingClientRect = boundingClientRect;
 
     // Open the ActionBar, send it's actions list.
-    Messaging.sendRequest({
+    WindowEventDispatcher.sendRequest({
       type: "TextSelection:ActionbarInit",
       selectionID: this._selectionID,
     });
     this._sendActionBarActions(true);
 
     return this.START_TOUCH_ERROR.NONE;
   },
 
   /**
    * Called when content is scrolled and handles are hidden.
    */
   _updateVisibility: function() {
-    Messaging.sendRequest({
+    WindowEventDispatcher.sendRequest({
       type: "TextSelection:Visibility",
       selectionID: this._selectionID,
     });
   },
 
   /**
    * Determines the window containing the selection, and its
    * editable element if present.
@@ -202,17 +202,17 @@ var ActionBarHandler = {
    */
   _uninit: function(clearSelection = true) {
     // Bail if there's no active selection.
     if (!this._selectionID) {
       return;
     }
 
     // Close the ActionBar.
-    Messaging.sendRequest({
+    WindowEventDispatcher.sendRequest({
       type: "TextSelection:ActionbarUninit",
     });
 
     // Clear the selection ID to complete the uninit(), but leave our reference
     // to selectionTargets (_targetElement, _contentWindow) in case we need
     // a final clearSelection().
     this._selectionID = null;
     this._boundingClientRect = null;
@@ -261,17 +261,17 @@ var ActionBarHandler = {
     let actionCountUnchanged = this._actionBarActions &&
       actions.length === this._actionBarActions.length;
     let actionsMatch = actionCountUnchanged &&
       this._actionBarActions.every((e,i) => {
         return e.id === actions[i].id;
       });
 
     if (sendAlways || !actionsMatch) {
-      Messaging.sendRequest({
+      WindowEventDispatcher.sendRequest({
         type: "TextSelection:ActionbarStatus",
         selectionID: this._selectionID,
         actions: actions,
         x: this._boundingClientRect.x,
         y: this._boundingClientRect.y,
         width: this._boundingClientRect.width,
         height: this._boundingClientRect.height
       });