Bug 1416918 - 1. Add TextInputController; r?esawin draft
authorJim Chen <nchen@mozilla.com>
Wed, 13 Dec 2017 22:57:21 -0500
changeset 711586 deaf501e7cfc2b2d3ee797701814fa5e9156eea2
parent 711413 22d2831cc1f41e1b3e1ebac9be5a7aff33684843
child 711587 ea2df8f01714bdc2735356bfa1c4e60719910ea3
push id93089
push userbmo:nchen@mozilla.com
push dateThu, 14 Dec 2017 03:58:24 +0000
reviewersesawin
bugs1416918
milestone59.0a1
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();