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
--- 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();