Bug 1416918 - 2. Make GeckoEditable/GeckoInputConnection work with TextInputController; r?esawin draft
authorJim Chen <nchen@mozilla.com>
Wed, 13 Dec 2017 22:57:21 -0500
changeset 711587 ea2df8f01714bdc2735356bfa1c4e60719910ea3
parent 711586 deaf501e7cfc2b2d3ee797701814fa5e9156eea2
child 711588 2b8574c6ea82995c1c85081b30cfac999ca5326f
push id93089
push userbmo:nchen@mozilla.com
push dateThu, 14 Dec 2017 03:58:24 +0000
reviewersesawin
bugs1416918
milestone59.0a1
Bug 1416918 - 2. Make GeckoEditable/GeckoInputConnection work with TextInputController; r?esawin Let GeckoEditable be created and managed by TextInputController, instead of being managed by native code. Let GeckoInputConnection also be managed by TextInputController, instead of being managed by GeckoEditable. Getting rid of native calls in GeckoEditable makes it easier to separate native code into a separate process down the road. MozReview-Commit-ID: HQI3qcAzOvT
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditable.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoInputConnection.java
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditable.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditable.java
@@ -32,23 +32,26 @@ import android.text.SpannableStringBuild
 import android.text.Spanned;
 import android.text.TextPaint;
 import android.text.TextUtils;
 import android.text.style.CharacterStyle;
 import android.util.Log;
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
 
-/*
-   GeckoEditable implements only some functions of Editable
-   The field mText contains the actual underlying
-   SpannableStringBuilder/Editable that contains our text.
-*/
-final class GeckoEditable extends IGeckoEditableParent.Stub
-        implements InvocationHandler, Editable, GeckoEditableClient {
+/**
+ * GeckoEditable implements only some functions of Editable
+ * The field mText contains the actual underlying
+ * SpannableStringBuilder/Editable that contains our text.
+ */
+/* package */ final class GeckoEditable
+    extends IGeckoEditableParent.Stub
+    implements InvocationHandler,
+               Editable,
+               GeckoEditableClient {
 
     private static final boolean DEBUG = false;
     private static final String LOGTAG = "GeckoEditable";
 
     // Filters to implement Editable's filtering functionality
     private InputFilter[] mFilters;
 
     private final AsyncText mText;
@@ -63,17 +66,16 @@ final class GeckoEditable extends IGecko
     private Handler mIcPostHandler;
 
     // Parent process child used as a default for key events.
     /* package */ IGeckoEditableChild mDefaultChild; // Used by IC thread.
     // Parent or content process child that has the focus.
     /* package */ IGeckoEditableChild mFocusedChild; // Used by IC thread.
     /* package */ IBinder mFocusedToken; // Used by Gecko/binder thread.
     /* package */ GeckoEditableListener mListener;
-    /* package */ GeckoView mView;
 
     /* package */ boolean mInBatchMode; // Used by IC thread
     /* package */ boolean mNeedSync; // Used by IC thread
     // Gecko side needs an updated composition from Java;
     private boolean mNeedUpdateComposition; // Used by IC thread
     private boolean mSuppressKeyUp; // Used by IC thread
 
     private boolean mIgnoreSelectionChange; // Used by Gecko thread
@@ -592,84 +594,57 @@ final class GeckoEditable extends IGecko
             if (DEBUG) {
                 Log.d(LOGTAG, "sending: " + event);
             }
             onKeyEvent(mFocusedChild, event, event.getAction(),
                        /* metaState */ 0, /* isSynthesizedImeKey */ true);
         }
     }
 
-    @WrapForJNI(calledFrom = "gecko")
-    private GeckoEditable() {
+    /* package */ GeckoEditable() {
         if (DEBUG) {
-            // Called by nsWindow.
-            ThreadUtils.assertOnGeckoThread();
+            // Called by TextInputController.
+            ThreadUtils.assertOnUiThread();
         }
 
         mText = new AsyncText();
         mActions = new ConcurrentLinkedQueue<Action>();
 
         final Class<?>[] PROXY_INTERFACES = { Editable.class };
-        mProxy = (Editable)Proxy.newProxyInstance(
-                Editable.class.getClassLoader(),
-                PROXY_INTERFACES, this);
+        mProxy = (Editable) Proxy.newProxyInstance(Editable.class.getClassLoader(),
+                                                   PROXY_INTERFACES, this);
 
         mIcRunHandler = mIcPostHandler = ThreadUtils.getUiHandler();
     }
 
-    @WrapForJNI(calledFrom = "gecko")
-    private void setDefaultEditableChild(final IGeckoEditableChild child) {
+    /* package */ void setDefaultEditableChild(final IGeckoEditableChild child) {
+        if (DEBUG) {
+            // Called by TextInputController.
+            ThreadUtils.assertOnUiThread();
+            Log.d(LOGTAG, "setDefaultEditableChild " + child);
+        }
         mDefaultChild = child;
     }
 
-    @WrapForJNI(calledFrom = "gecko")
-    private void onViewChange(final GeckoView v) {
+    /* package */ void setListener(final GeckoEditableListener newListener) {
         if (DEBUG) {
-            // Called by nsWindow.
-            ThreadUtils.assertOnGeckoThread();
-            Log.d(LOGTAG, "onViewChange(" + v + ")");
+            // Called by TextInputController.
+            ThreadUtils.assertOnUiThread();
+            Log.d(LOGTAG, "setListener " + newListener);
         }
 
-        final GeckoEditableListener newListener =
-            v != null ? GeckoInputConnection.create(v, this) : null;
-
-        final Runnable setListenerRunnable = new Runnable() {
+        mIcPostHandler.post(new Runnable() {
             @Override
             public void run() {
                 if (DEBUG) {
                     Log.d(LOGTAG, "onViewChange (set listener)");
                 }
 
                 mListener = newListener;
             }
-        };
-
-        // Post to UI thread first to make sure any code that is using the old input
-        // connection has finished running, before we switch to a new input connection or
-        // before we clear the input connection on destruction.
-        final Handler icHandler = mIcPostHandler;
-        ThreadUtils.postToUiThread(new Runnable() {
-            @Override
-            public void run() {
-                if (DEBUG) {
-                    Log.d(LOGTAG, "onViewChange (set IC)");
-                }
-
-                if (mView != null) {
-                    // Detach the previous view.
-                    mView.setInputConnectionListener(null);
-                }
-                if (v != null) {
-                    // And attach the new view.
-                    v.setInputConnectionListener((InputConnectionListener) newListener);
-                }
-
-                mView = v;
-                icHandler.post(setListenerRunnable);
-            }
         });
     }
 
     private boolean onIcThread() {
         return mIcRunHandler.getLooper() == Looper.myLooper();
     }
 
     private void assertOnIcThread() {
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoInputConnection.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoInputConnection.java
@@ -39,19 +39,20 @@ import android.view.View;
 import android.view.inputmethod.BaseInputConnection;
 import android.view.inputmethod.CursorAnchorInfo;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.ExtractedText;
 import android.view.inputmethod.ExtractedTextRequest;
 import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputMethodManager;
 
-class GeckoInputConnection
+/* package */ class GeckoInputConnection
     extends BaseInputConnection
-    implements InputConnectionListener, GeckoEditableListener {
+    implements TextInputController.Delegate,
+               GeckoEditableListener {
 
     private static final boolean DEBUG = false;
     protected static final String LOGTAG = "GeckoInputConnection";
 
     private static final String CUSTOM_HANDLER_TEST_METHOD = "testInputConnection";
     private static final String CUSTOM_HANDLER_TEST_CLASS =
         "org.mozilla.gecko.tests.components.GeckoViewComponent$TextInput";
 
@@ -65,38 +66,43 @@ class GeckoInputConnection
     private String mIMEModeHint = "";
     private String mIMEActionHint = "";
     private boolean mInPrivateBrowsing;
     private boolean mIsUserAction;
     private boolean mFocused;
 
     private String mCurrentInputMethod = "";
 
-    private final GeckoView mView;
+    private final GeckoSession mSession;
+    private final View mView;
     private final GeckoEditableClient mEditableClient;
     protected int mBatchEditCount;
     private ExtractedTextRequest mUpdateRequest;
     private final ExtractedText mUpdateExtract = new ExtractedText();
     private final InputConnection mKeyInputConnection;
     private CursorAnchorInfo.Builder mCursorAnchorInfoBuilder;
 
     // Prevent showSoftInput and hideSoftInput from causing reentrant calls on some devices.
     private volatile boolean mSoftInputReentrancyGuard;
 
-    public static GeckoEditableListener create(GeckoView targetView,
-                                               GeckoEditableClient editable) {
-        if (DEBUG)
-            return DebugGeckoInputConnection.create(targetView, editable);
-        else
-            return new GeckoInputConnection(targetView, editable);
+    public static TextInputController.Delegate create(GeckoSession session,
+                                                      View targetView,
+                                                      GeckoEditableClient editable) {
+        if (DEBUG) {
+            return DebugGeckoInputConnection.create(session, targetView, editable);
+        } else {
+            return new GeckoInputConnection(session, targetView, editable);
+        }
     }
 
-    protected GeckoInputConnection(GeckoView targetView,
+    protected GeckoInputConnection(GeckoSession session,
+                                   View targetView,
                                    GeckoEditableClient editable) {
         super(targetView, true);
+        mSession = session;
         mView = targetView;
         mEditableClient = editable;
         mIMEState = IME_STATE_DISABLED;
         // InputConnection that sends keys for plugins, which don't have full editors
         mKeyInputConnection = new BaseInputConnection(targetView, false);
     }
 
     @Override
@@ -197,17 +203,18 @@ class GeckoInputConnection
         if ((req.flags & GET_TEXT_WITH_STYLES) != 0) {
             extract.text = new SpannableString(editable);
         } else {
             extract.text = editable.toString();
         }
         return extract;
     }
 
-    private GeckoView getView() {
+    @Override // TextInputController.Delegate
+    public View getView() {
         return mView;
     }
 
     private InputMethodManager getInputMethodManager() {
         View view = getView();
         if (view == null) {
             return null;
         }
@@ -229,23 +236,22 @@ class GeckoInputConnection
             @Override
             public void run() {
                 if (v.hasFocus() && !imm.isActive(v)) {
                     // Marshmallow workaround: The view has focus but it is not the active
                     // view for the input method. (Bug 1211848)
                     v.clearFocus();
                     v.requestFocus();
                 }
-                final GeckoView view = getView();
-                if (view != null && view.getSession() != null) {
-                    if (showToolbar) {
-                        view.getDynamicToolbarAnimator().showToolbar(/*immediately*/ true);
-                    }
-                    view.getEventDispatcher().dispatch("GeckoView:ZoomToInput", null);
+
+                if (showToolbar) {
+                    mSession.getDynamicToolbarAnimator().showToolbar(/* immediately */ true);
                 }
+                mSession.getEventDispatcher().dispatch("GeckoView:ZoomToInput", null);
+
                 mSoftInputReentrancyGuard = true;
                 imm.showSoftInput(v, 0);
                 mSoftInputReentrancyGuard = false;
             }
         });
     }
 
     private void hideSoftInput() {
@@ -352,17 +358,17 @@ class GeckoInputConnection
 
     @TargetApi(21)
     @Override
     public void updateCompositionRects(final RectF[] rects) {
         if (!(Build.VERSION.SDK_INT >= 21)) {
             return;
         }
 
-        final GeckoView view = getView();
+        final View view = getView();
         if (view == null) {
             return;
         }
 
         final Editable content = getEditable();
         if (content == null) {
             return;
         }
@@ -382,30 +388,26 @@ class GeckoInputConnection
             @Override
             public void run() {
                 updateCompositionRectsOnUi(view, rects, composition);
             }
         });
     }
 
     @TargetApi(21)
-    /* package */ void updateCompositionRectsOnUi(final GeckoView view,
+    /* package */ void updateCompositionRectsOnUi(final View view,
                                                   final RectF[] rects,
                                                   final CharSequence composition) {
-        if (view.getSession() == null) {
-            return;
-        }
-
         if (mCursorAnchorInfoBuilder == null) {
             mCursorAnchorInfoBuilder = new CursorAnchorInfo.Builder();
         }
         mCursorAnchorInfoBuilder.reset();
 
         final Matrix matrix = new Matrix();
-        view.getSession().getClientToScreenMatrix(matrix);
+        mSession.getClientToScreenMatrix(matrix);
         mCursorAnchorInfoBuilder.setMatrix(matrix);
 
         for (int i = 0; i < rects.length; i++) {
             mCursorAnchorInfoBuilder.addCharacterBounds(
                     i, rects[i].left, rects[i].top, rects[i].right, rects[i].bottom,
                     CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION);
         }
 
@@ -472,31 +474,31 @@ class GeckoInputConnection
                 // wait for new thread to set sBackgroundHandler
                 GeckoInputConnection.class.wait();
             } catch (InterruptedException e) {
             }
         }
         return sBackgroundHandler;
     }
 
-    private boolean canReturnCustomHandler() {
+    private synchronized boolean canReturnCustomHandler() {
         if (mIMEState == IME_STATE_DISABLED) {
             return false;
         }
         for (StackTraceElement frame : Thread.currentThread().getStackTrace()) {
             // We only return our custom Handler to InputMethodManager's InputConnection
             // proxy. For all other purposes, we return the regular Handler.
             // InputMethodManager retrieves the Handler for its InputConnection proxy
             // inside its method startInputInner(), so we check for that here. This is
             // valid from Android 2.2 to at least Android 4.2. If this situation ever
             // changes, we gracefully fall back to using the regular Handler.
             if ("startInputInner".equals(frame.getMethodName()) &&
                 "android.view.inputmethod.InputMethodManager".equals(frame.getClassName())) {
-                // only return our own Handler to InputMethodManager
-                return true;
+                // Only return our own Handler to InputMethodManager and only prior to 24.
+                return Build.VERSION.SDK_INT < 24;
             }
             if (CUSTOM_HANDLER_TEST_METHOD.equals(frame.getMethodName()) &&
                 CUSTOM_HANDLER_TEST_CLASS.equals(frame.getClassName())) {
                 // InputConnection tests should also run on the custom handler
                 return true;
             }
         }
         return false;
@@ -512,34 +514,36 @@ class GeckoInputConnection
     }
 
     // Android N: @Override // InputConnection
     // We need to suppress lint complaining about the lack override here in the meantime: it wants us to build
     // against sdk 24, even though we're using 23, and therefore complains about the lack of override.
     // Once we update to 24, we can use the actual override annotation and remove the lint suppression.
     @SuppressLint("Override")
     public Handler getHandler() {
+        final Handler handler;
         if (isPhysicalKeyboardPresent()) {
-            return ThreadUtils.getUiHandler();
+            handler = ThreadUtils.getUiHandler();
+        } else {
+            handler = getBackgroundHandler();
         }
-
-        return getBackgroundHandler();
+        return mEditableClient.setInputConnectionHandler(handler);
     }
 
-    @Override // InputConnectionListener
+    @Override // TextInputController.Delegate
     public Handler getHandler(Handler defHandler) {
         if (!canReturnCustomHandler()) {
             return defHandler;
         }
 
-        return mEditableClient.setInputConnectionHandler(getHandler());
+        return getHandler();
     }
 
-    @Override
-    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+    @Override // TextInputController.Delegate
+    public synchronized InputConnection onCreateInputConnection(EditorInfo outAttrs) {
         // Some keyboards require us to fill out outAttrs even if we return null.
         outAttrs.inputType = InputType.TYPE_CLASS_TEXT;
         outAttrs.imeOptions = EditorInfo.IME_ACTION_NONE;
         outAttrs.actionLabel = null;
 
         if (mIMEState == IME_STATE_DISABLED) {
             hideSoftInput();
             return null;
@@ -741,17 +745,17 @@ class GeckoInputConnection
     }
 
     @Override
     public boolean sendKeyEvent(KeyEvent event) {
         sendKeyEvent(event.getAction(), event);
         return false; // seems to always return false
     }
 
-    @Override
+    @Override // TextInputController.Delegate
     public boolean onKeyPreIme(int keyCode, KeyEvent event) {
         return false;
     }
 
     private boolean shouldProcessKey(int keyCode, KeyEvent event) {
         switch (keyCode) {
             case KeyEvent.KEYCODE_MENU:
             case KeyEvent.KEYCODE_BACK:
@@ -842,22 +846,22 @@ class GeckoInputConnection
             @Override
             public void run() {
                 sendKeyEvent(action, event);
             }
         });
         return true;
     }
 
-    @Override
+    @Override // TextInputController.Delegate
     public boolean onKeyDown(int keyCode, KeyEvent event) {
         return processKey(KeyEvent.ACTION_DOWN, keyCode, event);
     }
 
-    @Override
+    @Override // TextInputController.Delegate
     public boolean onKeyUp(int keyCode, KeyEvent event) {
         return processKey(KeyEvent.ACTION_UP, keyCode, event);
     }
 
     /**
      * Get a key that represents a given character.
      */
     private KeyEvent getCharKeyEvent(final char c) {
@@ -871,17 +875,17 @@ class GeckoInputConnection
 
             @Override
             public int getUnicodeChar(int metaState) {
                 return c;
             }
         };
     }
 
-    @Override
+    @Override // TextInputController.Delegate
     public boolean onKeyMultiple(int keyCode, int repeatCount, final KeyEvent event) {
         if (keyCode == KeyEvent.KEYCODE_UNKNOWN) {
             // KEYCODE_UNKNOWN means the characters are in KeyEvent.getCharacters()
             final String str = event.getCharacters();
             for (int i = 0; i < str.length(); i++) {
                 final KeyEvent charEvent = getCharKeyEvent(str.charAt(i));
                 if (!processKey(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_UNKNOWN, charEvent) ||
                     !processKey(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_UNKNOWN, charEvent)) {
@@ -895,33 +899,33 @@ class GeckoInputConnection
             if (!processKey(KeyEvent.ACTION_DOWN, keyCode, event) ||
                 !processKey(KeyEvent.ACTION_UP, keyCode, event)) {
                 return false;
             }
         }
         return true;
     }
 
-    @Override
+    @Override // TextInputController.Delegate
     public boolean onKeyLongPress(int keyCode, KeyEvent event) {
         View v = getView();
         switch (keyCode) {
             case KeyEvent.KEYCODE_MENU:
                 InputMethodManager imm = getInputMethodManager();
                 imm.toggleSoftInputFromWindow(v.getWindowToken(),
                                               InputMethodManager.SHOW_FORCED, 0);
                 return true;
             default:
                 break;
         }
         return false;
     }
 
-    @Override
-    public boolean isIMEEnabled() {
+    @Override // TextInputController.Delegate
+    public synchronized boolean isInputActive() {
         // make sure this picks up PASSWORD and PLUGIN states as well
         return mIMEState != IME_STATE_DISABLED;
     }
 
     @Override
     public void notifyIME(int type) {
         switch (type) {
 
@@ -968,18 +972,19 @@ class GeckoInputConnection
                 if (DEBUG) {
                     throw new IllegalArgumentException("Unexpected NOTIFY_IME=" + type);
                 }
                 break;
         }
     }
 
     @Override
-    public void notifyIMEContext(int state, String typeHint, String modeHint, String actionHint,
-                                 boolean inPrivateBrowsing, boolean isUserAction) {
+    public synchronized void notifyIMEContext(int state, String typeHint, String modeHint,
+                                              String actionHint, boolean inPrivateBrowsing,
+                                              boolean isUserAction) {
         // For some input type we will use a widget to display the ui, for those we must not
         // display the ime. We can display a widget for date and time types and, if the sdk version
         // is 11 or greater, for datetime/month/week as well.
         if (typeHint != null &&
             (typeHint.equalsIgnoreCase("date") ||
              typeHint.equalsIgnoreCase("time") ||
              typeHint.equalsIgnoreCase("datetime") ||
              typeHint.equalsIgnoreCase("month") ||
@@ -1030,33 +1035,35 @@ class GeckoInputConnection
 
 final class DebugGeckoInputConnection
         extends GeckoInputConnection
         implements InvocationHandler {
 
     private InputConnection mProxy;
     private final StringBuilder mCallLevel;
 
-    private DebugGeckoInputConnection(GeckoView targetView,
+    private DebugGeckoInputConnection(GeckoSession session,
+                                      View targetView,
                                       GeckoEditableClient editable) {
-        super(targetView, editable);
+        super(session, targetView, editable);
         mCallLevel = new StringBuilder();
     }
 
-    public static GeckoEditableListener create(GeckoView targetView,
-                                               GeckoEditableClient editable) {
+    public static TextInputController.Delegate create(GeckoSession session,
+                                                      View targetView,
+                                                      GeckoEditableClient editable) {
         final Class<?>[] PROXY_INTERFACES = { InputConnection.class,
-                InputConnectionListener.class,
+                TextInputController.Delegate.class,
                 GeckoEditableListener.class };
         DebugGeckoInputConnection dgic =
-                new DebugGeckoInputConnection(targetView, editable);
-        dgic.mProxy = (InputConnection)Proxy.newProxyInstance(
+                new DebugGeckoInputConnection(session, targetView, editable);
+        dgic.mProxy = (InputConnection) Proxy.newProxyInstance(
                 GeckoInputConnection.class.getClassLoader(),
                 PROXY_INTERFACES, dgic);
-        return (GeckoEditableListener)dgic.mProxy;
+        return (TextInputController.Delegate) dgic.mProxy;
     }
 
     @Override
     public Object invoke(Object proxy, Method method, Object[] args)
             throws Throwable {
 
         StringBuilder log = new StringBuilder(mCallLevel);
         log.append("> ").append(method.getName()).append("(");