Bug 1416918 - 1. Add TextInputController; r=esawin
authorJim Chen <nchen@mozilla.com>
Wed, 13 Dec 2017 22:57:21 -0500
changeset 396427 3e18c58d0320ef834bcf6b9f76dad2f8f1397315
parent 396365 57a108d1c90a9979e2b79dbf138e055ee07ed97b
child 396428 29a3c1e94980304151b3b9b58f03d83b4a70ad5b
push id98313
push usernbeleuzu@mozilla.com
push dateFri, 15 Dec 2017 01:48:15 +0000
treeherdermozilla-inbound@cebcea19cfb0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersesawin
bugs1416918
milestone59.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 1416918 - 1. Add TextInputController; r=esawin Add TextInputController and add a getter for it in GeckoSession. TextInputController is used to process key events and to interact with the input method manager. MozReview-Commit-ID: 1j2Moqukf8U
mobile/android/base/moz.build
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoSession.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/TextInputController.java
widget/android/nsWindow.cpp
widget/android/nsWindow.h
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -446,16 +446,17 @@ gvjar.sources += [geckoview_source_dir +
     'process/GeckoProcessManager.java',
     'process/GeckoServiceChildProcess.java',
     'ScreenManagerHelper.java',
     'ScreenOrientationDelegate.java',
     'sqlite/ByteBufferInputStream.java',
     'sqlite/MatrixBlobCursor.java',
     'sqlite/SQLiteBridge.java',
     'sqlite/SQLiteBridgeException.java',
+    'TextInputController.java',
     'TouchEventInterceptor.java',
     'WakeLockDelegate.java',
 ]]
 
 if CONFIG['MOZ_ANDROID_HLS_SUPPORT'] and CONFIG['MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE']:
     gvjar.sources += [geckoview_source_dir + 'java/org/mozilla/gecko/' + x for x in [
         'media/GeckoHlsAudioRenderer.java',
         'media/GeckoHlsPlayer.java',
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoSession.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoSession.java
@@ -11,28 +11,30 @@ import java.util.ArrayList;
 import java.util.Arrays;
 
 import org.mozilla.gecko.annotation.WrapForJNI;
 import org.mozilla.gecko.gfx.LayerSession;
 import org.mozilla.gecko.mozglue.JNIObject;
 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 android.content.ContentResolver;
 import android.content.Context;
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.IInterface;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.SystemClock;
+import android.support.annotation.NonNull;
 import android.util.AttributeSet;
 import android.util.DisplayMetrics;
 import android.util.Log;
 
 public class GeckoSession extends LayerSession
                           implements Parcelable {
     private static final String LOGTAG = "GeckoSession";
     private static final boolean DEBUG = false;
@@ -60,16 +62,18 @@ public class GeckoSession extends LayerS
     }
 
     private final NativeQueue mNativeQueue =
         new NativeQueue(State.INITIAL, State.READY);
 
     private final EventDispatcher mEventDispatcher =
         new EventDispatcher(mNativeQueue);
 
+    private final TextInputController mTextInput = new TextInputController(this);
+
     private final GeckoSessionHandler<ContentListener> mContentHandler =
         new GeckoSessionHandler<ContentListener>(
             "GeckoViewContent", this,
             new String[]{
                 "GeckoView:ContextMenu",
                 "GeckoView:DOMTitleChanged",
                 "GeckoView:FullScreenEnter",
                 "GeckoView:FullScreenExit"
@@ -323,16 +327,20 @@ public class GeckoSession extends LayerS
                 nativeQueue.setState(mNativeQueue.getState());
                 mNativeQueue = nativeQueue;
             }
         }
 
         @WrapForJNI(dispatchTo = "proxy")
         public native void attach(GeckoView view);
 
+        @WrapForJNI(dispatchTo = "proxy")
+        public native void attachEditable(IGeckoEditableParent parent,
+                                          GeckoEditableChild child);
+
         @WrapForJNI(calledFrom = "gecko")
         private synchronized void onReady() {
             if (mNativeQueue.checkAndSetState(State.INITIAL, State.READY)) {
                 Log.i(LOGTAG, "zerdatime " + SystemClock.elapsedRealtime() +
                       " - chrome startup finished");
             }
         }
     }
@@ -466,16 +474,18 @@ public class GeckoSession extends LayerS
         }
     }
 
     public boolean isOpen() {
         return mWindow != null;
     }
 
     public void openWindow(final Context appContext) {
+        ThreadUtils.assertOnUiThread();
+
         if (isOpen()) {
             throw new IllegalStateException("Session is open");
         }
 
         if (!GeckoThread.isLaunched()) {
             final boolean multiprocess =
                     mSettings.getBoolean(GeckoSessionSettings.USE_MULTIPROCESS);
             preload(appContext, /* geckoArgs */ null, multiprocess);
@@ -496,16 +506,20 @@ public class GeckoSession extends LayerS
                 Window.class, "open",
                 Window.class, mWindow,
                 Compositor.class, mCompositor,
                 EventDispatcher.class, mEventDispatcher,
                 GeckoBundle.class, mSettings.asBundle(),
                 String.class, chromeUri,
                 screenId, isPrivate);
         }
+
+        if (mTextInput != null) {
+            mTextInput.onWindowReady(mNativeQueue, mWindow);
+        }
     }
 
     public void attachView(final GeckoView view) {
         if (view == null) {
             return;
         }
 
         if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
@@ -527,16 +541,26 @@ public class GeckoSession extends LayerS
             GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY,
                     mWindow, "disposeNative");
         }
 
         mWindow = null;
     }
 
     /**
+     * Get the TextInputController instance for this session.
+     *
+     * @return TextInputController instance.
+     */
+    public @NonNull TextInputController getTextInputController() {
+        // May be called on any thread.
+        return mTextInput;
+    }
+
+    /**
     * Load the given URI.
     * @param uri The URI of the resource to load.
     */
     public void loadUri(String uri) {
         final GeckoBundle msg = new GeckoBundle();
         msg.putString("uri", uri);
         mEventDispatcher.dispatch("GeckoView:LoadUri", msg);
     }
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/TextInputController.java
@@ -0,0 +1,213 @@
+/* -*- 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;
+
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.os.Handler;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+
+/**
+ * TextInputController handles text input for GeckoSession through key events or input
+ * methods. It is typically used to implement certain methods in View such as {@code
+ * onCreateInputConnection()}, by forwarding such calls to corresponding methods in
+ * TextInputController.
+ */
+public final class TextInputController {
+
+    /* package */ interface Delegate {
+        View getView();
+        Handler getHandler(Handler defHandler);
+        InputConnection onCreateInputConnection(EditorInfo attrs);
+        boolean onKeyPreIme(int keyCode, KeyEvent event);
+        boolean onKeyDown(int keyCode, KeyEvent event);
+        boolean onKeyUp(int keyCode, KeyEvent event);
+        boolean onKeyLongPress(int keyCode, KeyEvent event);
+        boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event);
+        boolean isInputActive();
+    }
+
+    private final GeckoSession mSession;
+    private final GeckoEditable mEditable = new GeckoEditable();
+    private final GeckoEditableChild mEditableChild = new GeckoEditableChild(mEditable);
+    private Delegate mInputConnection;
+
+    /* package */ TextInputController(final @NonNull GeckoSession session) {
+        mSession = session;
+        mEditable.setDefaultEditableChild(mEditableChild);
+    }
+
+    /* package */ void onWindowReady(final NativeQueue queue,
+                                     final GeckoSession.Window window) {
+        if (queue.isReady()) {
+            window.attachEditable(mEditable, mEditableChild);
+        } else {
+            queue.queueUntilReady(window, "attachEditable",
+                                  IGeckoEditableParent.class, mEditable,
+                                  GeckoEditableChild.class, mEditableChild);
+        }
+    }
+
+    /**
+     * Get a Handler for the background input method thread. In order to use a background
+     * thread for input method operations on systems prior to Nougat, first override
+     * {@code View.getHandler()} for the View returning the InputConnection instance, and
+     * then call this method from the overridden method.
+     *
+     * For example: <pre>{@code
+     * @Override
+     * public Handler getHandler() {
+     *     if (Build.VERSION.SDK_INT >= 24) {
+     *         return super.getHandler();
+     *     }
+     *     return getSession().getTextInputController().getHandler(super.getHandler());
+     * }
+     * }</pre>
+     *
+     * @param defHandler Handler returned by the system {@code getHandler} implementation.
+     * @return Handler to return to the system through {@code getHandler}.
+     */
+    public @NonNull Handler getHandler(final @NonNull Handler defHandler) {
+        // May be called on any thread.
+        if (mInputConnection != null) {
+            return mInputConnection.getHandler(defHandler);
+        }
+        return defHandler;
+    }
+
+    private synchronized void ensureInputConnection() {
+        if (mInputConnection == null) {
+            mInputConnection = GeckoInputConnection.create(mSession,
+                                                           /* view */ null,
+                                                           mEditable);
+            mEditable.setListener((GeckoEditableListener) mInputConnection);
+        }
+    }
+
+    /**
+     * Get the current View for text input.
+     *
+     * @return Current text input View or null if not set.
+     * @see #setView(View)
+     */
+    public @Nullable View getView() {
+        ThreadUtils.assertOnUiThread();
+        return mInputConnection != null ? mInputConnection.getView() : null;
+    }
+
+    /**
+     * Set the View for text input. The current View is used to interact with the system
+     * input method manager and to display certain text input UI elements.
+     *
+     * @param view Text input View or null to clear current View.
+     */
+    public synchronized void setView(final @Nullable View view) {
+        ThreadUtils.assertOnUiThread();
+
+        if (view == null) {
+            mInputConnection = null;
+        } else if (mInputConnection == null || mInputConnection.getView() != view) {
+            mInputConnection = GeckoInputConnection.create(mSession, view, mEditable);
+        }
+        mEditable.setListener((GeckoEditableListener) mInputConnection);
+    }
+
+    /**
+     * Get an InputConnection instance. For full functionality, call {@link
+     * #setView(View)} first before calling this method.
+     *
+     * @param outAttrs EditorInfo instance to be filled on return.
+     * @return InputConnection instance or null if input method is not active.
+     */
+    public synchronized @Nullable InputConnection onCreateInputConnection(
+            final @NonNull EditorInfo attrs) {
+        // May be called on any thread.
+        ensureInputConnection();
+        return mInputConnection.onCreateInputConnection(attrs);
+    }
+
+    /**
+     * Process a KeyEvent as a pre-IME event.
+     *
+     * @param keyCode Key code.
+     * @param event KeyEvent instance.
+     * @return True if the event was handled.
+     */
+    public boolean onKeyPreIme(final int keyCode, final @NonNull KeyEvent event) {
+        ThreadUtils.assertOnUiThread();
+        ensureInputConnection();
+        return mInputConnection.onKeyPreIme(keyCode, event);
+    }
+
+    /**
+     * Process a KeyEvent as a key-down event.
+     *
+     * @param keyCode Key code.
+     * @param event KeyEvent instance.
+     * @return True if the event was handled.
+     */
+    public boolean onKeyDown(final int keyCode, final @NonNull KeyEvent event) {
+        ThreadUtils.assertOnUiThread();
+        ensureInputConnection();
+        return mInputConnection.onKeyDown(keyCode, event);
+    }
+
+    /**
+     * Process a KeyEvent as a key-up event.
+     *
+     * @param keyCode Key code.
+     * @param event KeyEvent instance.
+     * @return True if the event was handled.
+     */
+    public boolean onKeyUp(final int keyCode, final @NonNull KeyEvent event) {
+        ThreadUtils.assertOnUiThread();
+        ensureInputConnection();
+        return mInputConnection.onKeyUp(keyCode, event);
+    }
+
+    /**
+     * Process a KeyEvent as a long-press event.
+     *
+     * @param keyCode Key code.
+     * @param event KeyEvent instance.
+     * @return True if the event was handled.
+     */
+    public boolean onKeyLongPress(final int keyCode, final @NonNull KeyEvent event) {
+        ThreadUtils.assertOnUiThread();
+        ensureInputConnection();
+        return mInputConnection.onKeyLongPress(keyCode, event);
+    }
+
+    /**
+     * Process a KeyEvent as a multiple-press event.
+     *
+     * @param keyCode Key code.
+     * @param event KeyEvent instance.
+     * @return True if the event was handled.
+     */
+    public boolean onKeyMultiple(final int keyCode, final int repeatCount,
+                                 final @NonNull KeyEvent event) {
+        ThreadUtils.assertOnUiThread();
+        ensureInputConnection();
+        return mInputConnection.onKeyMultiple(keyCode, repeatCount, event);
+    }
+
+    /**
+     * Return whether there is an active input connection, usually as a result of a
+     * focused input field.
+     *
+     * @return True if input is active.
+     */
+    public boolean isInputActive() {
+        ThreadUtils.assertOnUiThread();
+        return mInputConnection != null && mInputConnection.isInputActive();
+    }
+}
--- a/widget/android/nsWindow.cpp
+++ b/widget/android/nsWindow.cpp
@@ -296,16 +296,20 @@ public:
                   jni::Object::Param aCompositor,
                   jni::Object::Param aDispatcher,
                   jni::Object::Param aSettings);
 
     // Reattach this nsWindow to a new GeckoView.
     void Attach(const GeckoSession::Window::LocalRef& inst,
                 jni::Object::Param aView);
 
+    void AttachEditable(const GeckoSession::Window::LocalRef& inst,
+                        jni::Object::Param aEditableParent,
+                        jni::Object::Param aEditableChild);
+
     void EnableEventDispatcher();
 };
 
 /**
  * NativePanZoomController handles its native calls on the UI thread, so make
  * it separate from GeckoViewSupport.
  */
 class nsWindow::NPZCSupport final
@@ -1222,20 +1226,20 @@ public:
 
 ANativeWindow* nsWindow::PMPMSupport::sWindow;
 EGLSurface nsWindow::PMPMSupport::sSurface;
 
 
 nsWindow::GeckoViewSupport::~GeckoViewSupport()
 {
     // Disassociate our GeckoEditable instance with our native object.
-    MOZ_ASSERT(window.mEditableSupport && window.mEditable);
-    window.mEditableSupport.Detach();
-    window.mEditable->OnViewChange(nullptr);
-    window.mEditable = nullptr;
+    if (window.mEditableSupport) {
+        window.mEditableSupport.Detach();
+        window.mEditableParent = nullptr;
+    }
 
     if (window.mNPZCSupport) {
         window.mNPZCSupport.Detach();
     }
 
     if (window.mLayerViewSupport) {
         window.mLayerViewSupport.Detach();
     }
@@ -1297,23 +1301,16 @@ nsWindow::GeckoViewSupport::Open(const j
             mozilla::MakeUnique<GeckoViewSupport>(window, sessionWindow);
     window->mGeckoViewSupport->mDOMWindow = pdomWindow;
     window->mAndroidView = androidView;
 
     // Attach other session support objects.
     window->mGeckoViewSupport->Transfer(
             sessionWindow, aCompositor, aDispatcher, aSettings);
 
-    // Attach a new GeckoEditable support object to the new window.
-    auto editable = GeckoEditable::New();
-    auto editableChild = GeckoEditableChild::New(editable);
-    editable->SetDefaultEditableChild(editableChild);
-    window->mEditable = editable;
-    window->mEditableSupport.Attach(editableChild, window, editableChild);
-
     if (window->mWidgetListener) {
         nsCOMPtr<nsIXULWindow> xulWindow(
                 window->mWidgetListener->GetXULWindow());
         if (xulWindow) {
             // Our window is not intrinsically sized, so tell nsXULWindow to
             // not set a size for us.
             xulWindow->SetIntrinsicallySized(false);
         }
@@ -1373,19 +1370,29 @@ nsWindow::GeckoViewSupport::Transfer(con
         bridge->SendForceIsFirstPaint();
     }
 }
 
 void
 nsWindow::GeckoViewSupport::Attach(const GeckoSession::Window::LocalRef& inst,
                                    jni::Object::Param aView)
 {
-    // Associate our previous GeckoEditable with the new GeckoView.
-    MOZ_ASSERT(window.mEditable);
-    window.mEditable->OnViewChange(aView);
+}
+
+void
+nsWindow::GeckoViewSupport::AttachEditable(const GeckoSession::Window::LocalRef& inst,
+                                           jni::Object::Param aEditableParent,
+                                           jni::Object::Param aEditableChild)
+{
+    java::GeckoEditableChild::LocalRef editableChild(inst.Env());
+    editableChild = java::GeckoEditableChild::Ref::From(aEditableChild);
+
+    MOZ_ASSERT(!window.mEditableSupport);
+    window.mEditableSupport.Attach(editableChild, &window, editableChild);
+    window.mEditableParent = aEditableParent;
 }
 
 void
 nsWindow::InitNatives()
 {
     nsWindow::GeckoViewSupport::Base::Init();
     nsWindow::LayerViewSupport::Init();
     nsWindow::NPZCSupport::Init();
--- a/widget/android/nsWindow.h
+++ b/widget/android/nsWindow.h
@@ -179,17 +179,17 @@ private:
     class NPZCSupport;
     // Object that implements native NativePanZoomController calls.
     // Owned by the Java NativePanZoomController instance.
     NativePtr<NPZCSupport> mNPZCSupport;
 
     // Object that implements native GeckoEditable calls.
     // Strong referenced by the Java instance.
     NativePtr<mozilla::widget::GeckoEditableSupport> mEditableSupport;
-    mozilla::java::GeckoEditable::GlobalRef mEditable;
+    mozilla::jni::Object::GlobalRef mEditableParent;
 
     class GeckoViewSupport;
     // Object that implements native GeckoView calls and associated states.
     // nullptr for nsWindows that were not opened from GeckoView.
     // Because other objects get destroyed in the mGeckOViewSupport destructor,
     // keep it last in the list, so its destructor is called first.
     mozilla::UniquePtr<GeckoViewSupport> mGeckoViewSupport;
 
@@ -304,17 +304,17 @@ public:
 
     void SetContentDocumentDisplayed(bool aDisplayed);
     bool IsContentDocumentDisplayed();
 
     // Call this function when the users activity is the direct cause of an
     // event (like a keypress or mouse click).
     void UserActivity();
 
-    mozilla::java::GeckoEditable::Ref& GetEditableParent() { return mEditable; }
+    mozilla::jni::Object::Ref& GetEditableParent() { return mEditableParent; }
 
     void RecvToolbarAnimatorMessageFromCompositor(int32_t aMessage) override;
     void UpdateRootFrameMetrics(const ScreenPoint& aScrollOffset, const CSSToScreenScale& aZoom) override;
     void RecvScreenPixels(mozilla::ipc::Shmem&& aMem, const ScreenIntSize& aSize) override;
 protected:
     void BringToFront();
     nsWindow *FindTopLevel();
     bool IsTopLevel();