summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionTextInput.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionTextInput.java')
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionTextInput.java463
1 files changed, 463 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionTextInput.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionTextInput.java
new file mode 100644
index 0000000000..f5e6c6976c
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionTextInput.java
@@ -0,0 +1,463 @@
+/* -*- 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.geckoview;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.RectF;
+import android.os.Handler;
+import android.text.Editable;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+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;
+import androidx.annotation.AnyThread;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import org.mozilla.gecko.IGeckoEditableParent;
+import org.mozilla.gecko.InputMethods;
+import org.mozilla.gecko.NativeQueue;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.util.ThreadUtils;
+
+/**
+ * {@code SessionTextInput} handles text input for {@code GeckoSession} through key events or input
+ * methods. It is typically used to implement certain methods in {@link android.view.View} such as
+ * {@link android.view.View#onCreateInputConnection}, by forwarding such calls to corresponding
+ * methods in {@code SessionTextInput}.
+ *
+ * <p>For full functionality, {@code SessionTextInput} requires a {@link android.view.View} to be
+ * set first through {@link #setView}. When a {@link android.view.View} is not set or set to null,
+ * {@code SessionTextInput} will operate in a reduced functionality mode. See {@link
+ * #onCreateInputConnection} and methods in {@link GeckoSession.TextInputDelegate} for changes in
+ * behavior in this viewless mode.
+ */
+public final class SessionTextInput {
+ /* package */ static final String LOGTAG = "GeckoSessionTextInput";
+ private static final boolean DEBUG = false;
+
+ // Interface to access GeckoInputConnection from SessionTextInput.
+ /* package */ interface InputConnectionClient {
+ View getView();
+
+ Handler getHandler(Handler defHandler);
+
+ InputConnection onCreateInputConnection(EditorInfo attrs);
+ }
+
+ // Interface to access GeckoEditable from GeckoInputConnection.
+ /* package */ interface EditableClient {
+ // The following value is used by requestCursorUpdates
+ // ONE_SHOT calls updateCompositionRects() after getting current composing
+ // character rects.
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({ONE_SHOT, START_MONITOR, END_MONITOR})
+ /* package */ @interface CursorMonitorMode {}
+
+ @WrapForJNI static final int ONE_SHOT = 1;
+ // START_MONITOR start the monitor for composing character rects. If is is
+ // updaed, call updateCompositionRects()
+ @WrapForJNI static final int START_MONITOR = 2;
+ // ENDT_MONITOR stops the monitor for composing character rects.
+ @WrapForJNI static final int END_MONITOR = 3;
+
+ void sendKeyEvent(@Nullable View view, int action, @NonNull KeyEvent event);
+
+ Editable getEditable();
+
+ void setBatchMode(boolean isBatchMode);
+
+ Handler setInputConnectionHandler(@NonNull Handler handler);
+
+ void postToInputConnection(@NonNull Runnable runnable);
+
+ void requestCursorUpdates(@CursorMonitorMode int requestMode);
+
+ void insertImage(@NonNull byte[] data, @NonNull String mimeType);
+ }
+
+ // Interface to access GeckoInputConnection from GeckoEditable.
+ /* package */ interface EditableListener {
+ // IME notification type for notifyIME(), corresponding to NotificationToIME enum.
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ NOTIFY_IME_OF_TOKEN,
+ NOTIFY_IME_OPEN_VKB,
+ NOTIFY_IME_REPLY_EVENT,
+ NOTIFY_IME_OF_FOCUS,
+ NOTIFY_IME_OF_BLUR,
+ NOTIFY_IME_TO_COMMIT_COMPOSITION,
+ NOTIFY_IME_TO_CANCEL_COMPOSITION
+ })
+ /* package */ @interface IMENotificationType {}
+
+ @WrapForJNI static final int NOTIFY_IME_OF_TOKEN = -3;
+ @WrapForJNI static final int NOTIFY_IME_OPEN_VKB = -2;
+ @WrapForJNI static final int NOTIFY_IME_REPLY_EVENT = -1;
+ @WrapForJNI static final int NOTIFY_IME_OF_FOCUS = 1;
+ @WrapForJNI static final int NOTIFY_IME_OF_BLUR = 2;
+ @WrapForJNI static final int NOTIFY_IME_TO_COMMIT_COMPOSITION = 8;
+ @WrapForJNI static final int NOTIFY_IME_TO_CANCEL_COMPOSITION = 9;
+
+ // IME enabled state for notifyIMEContext().
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({IME_STATE_UNKNOWN, IME_STATE_DISABLED, IME_STATE_ENABLED, IME_STATE_PASSWORD})
+ /* package */ @interface IMEState {}
+
+ static final int IME_STATE_UNKNOWN = -1;
+ static final int IME_STATE_DISABLED = 0;
+ static final int IME_STATE_ENABLED = 1;
+ static final int IME_STATE_PASSWORD = 2;
+
+ // Flags for notifyIMEContext().
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {IME_FLAG_PRIVATE_BROWSING, IME_FLAG_USER_ACTION, IME_FOCUS_NOT_CHANGED})
+ /* package */ @interface IMEContextFlags {}
+
+ @WrapForJNI static final int IME_FLAG_PRIVATE_BROWSING = 1 << 0;
+ @WrapForJNI static final int IME_FLAG_USER_ACTION = 1 << 1;
+ @WrapForJNI static final int IME_FOCUS_NOT_CHANGED = 1 << 2;
+
+ void notifyIME(@IMENotificationType int type);
+
+ void notifyIMEContext(
+ @IMEState int state,
+ String typeHint,
+ String modeHint,
+ String actionHint,
+ @IMEContextFlags int flag);
+
+ void onSelectionChange();
+
+ void onTextChange();
+
+ void onDiscardComposition();
+
+ void onDefaultKeyEvent(KeyEvent event);
+
+ void updateCompositionRects(final RectF[] aRects, final RectF aCaretRect);
+ }
+
+ private static final class DefaultDelegate implements GeckoSession.TextInputDelegate {
+ public static final DefaultDelegate INSTANCE = new DefaultDelegate();
+
+ private InputMethodManager getInputMethodManager(@Nullable final View view) {
+ if (view == null) {
+ return null;
+ }
+ return (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ }
+
+ @Override
+ public void restartInput(@NonNull final GeckoSession session, final int reason) {
+ ThreadUtils.assertOnUiThread();
+ final View view = session.getTextInput().getView();
+
+ final InputMethodManager imm = getInputMethodManager(view);
+ if (imm == null) {
+ return;
+ }
+
+ // InputMethodManager has internal logic to detect if we are restarting input
+ // in an already focused View, which is the case here because all content text
+ // fields are inside one LayerView. When this happens, InputMethodManager will
+ // tell the input method to soft reset instead of hard reset. Stock latin IME
+ // on Android 4.2+ has a quirk that when it soft resets, it does not clear the
+ // composition. The following workaround tricks the IME into clearing the
+ // composition when soft resetting.
+ if (InputMethods.needsSoftResetWorkaround(
+ InputMethods.getCurrentInputMethod(view.getContext()))) {
+ // Fake a selection change, because the IME clears the composition when
+ // the selection changes, even if soft-resetting. Offsets here must be
+ // different from the previous selection offsets, and -1 seems to be a
+ // reasonable, deterministic value
+ imm.updateSelection(view, -1, -1, -1, -1);
+ }
+
+ try {
+ imm.restartInput(view);
+ } catch (final RuntimeException e) {
+ Log.e(LOGTAG, "Error restarting input", e);
+ }
+ }
+
+ @Override
+ public void showSoftInput(@NonNull final GeckoSession session) {
+ ThreadUtils.assertOnUiThread();
+ final View view = session.getTextInput().getView();
+ final InputMethodManager imm = getInputMethodManager(view);
+ if (imm != null) {
+ if (view.hasFocus() && !imm.isActive(view)) {
+ // Marshmallow workaround: The view has focus but it is not the active
+ // view for the input method. (Bug 1211848)
+ view.clearFocus();
+ view.requestFocus();
+ }
+ imm.showSoftInput(view, 0);
+ }
+ }
+
+ @Override
+ public void hideSoftInput(@NonNull final GeckoSession session) {
+ ThreadUtils.assertOnUiThread();
+ final View view = session.getTextInput().getView();
+ final InputMethodManager imm = getInputMethodManager(view);
+ if (imm != null) {
+ imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
+ }
+ }
+
+ @Override
+ public void updateSelection(
+ @NonNull final GeckoSession session,
+ final int selStart,
+ final int selEnd,
+ final int compositionStart,
+ final int compositionEnd) {
+ ThreadUtils.assertOnUiThread();
+ final View view = session.getTextInput().getView();
+ final InputMethodManager imm = getInputMethodManager(view);
+ if (imm != null) {
+ // When composition start and end is -1,
+ // InputMethodManager.updateSelection will remove composition
+ // on most IMEs. If not working, we have to add a workaround
+ // to EditableListener.onDiscardComposition.
+ imm.updateSelection(view, selStart, selEnd, compositionStart, compositionEnd);
+ }
+ }
+
+ @Override
+ public void updateExtractedText(
+ @NonNull final GeckoSession session,
+ @NonNull final ExtractedTextRequest request,
+ @NonNull final ExtractedText text) {
+ ThreadUtils.assertOnUiThread();
+ final View view = session.getTextInput().getView();
+ final InputMethodManager imm = getInputMethodManager(view);
+ if (imm != null) {
+ imm.updateExtractedText(view, request.token, text);
+ }
+ }
+
+ @TargetApi(21)
+ @Override
+ public void updateCursorAnchorInfo(
+ @NonNull final GeckoSession session, @NonNull final CursorAnchorInfo info) {
+ ThreadUtils.assertOnUiThread();
+ final View view = session.getTextInput().getView();
+ final InputMethodManager imm = getInputMethodManager(view);
+ if (imm != null) {
+ imm.updateCursorAnchorInfo(view, info);
+ }
+ }
+ }
+
+ private final GeckoSession mSession;
+ private final NativeQueue mQueue;
+ private final GeckoEditable mEditable;
+ private InputConnectionClient mInputConnection;
+ private GeckoSession.TextInputDelegate mDelegate;
+
+ /* package */ SessionTextInput(
+ final @NonNull GeckoSession session, final @NonNull NativeQueue queue) {
+ mSession = session;
+ mQueue = queue;
+ mEditable = new GeckoEditable(session);
+ }
+
+ /* package */ void onWindowChanged(final GeckoSession.Window window) {
+ if (mQueue.isReady()) {
+ window.attachEditable(mEditable);
+ } else {
+ mQueue.queueUntilReady(window, "attachEditable", IGeckoEditableParent.class, mEditable);
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * <p>For example:
+ *
+ * <pre>
+ * &#64;Override
+ * public Handler getHandler() {
+ * if (Build.VERSION.SDK_INT &gt;= 24) {
+ * return super.getHandler();
+ * }
+ * return getSession().getTextInput().getHandler(super.getHandler());
+ * }</pre>
+ *
+ * @param defHandler Handler returned by the system {@code getHandler} implementation.
+ * @return Handler to return to the system through {@code getHandler}.
+ */
+ @AnyThread
+ public synchronized @NonNull Handler getHandler(final @NonNull Handler defHandler) {
+ // May be called on any thread.
+ if (mInputConnection != null) {
+ return mInputConnection.getHandler(defHandler);
+ }
+ return defHandler;
+ }
+
+ /**
+ * Get the current {@link android.view.View} for text input.
+ *
+ * @return Current text input View or null if not set.
+ * @see #setView(View)
+ */
+ @UiThread
+ public @Nullable View getView() {
+ ThreadUtils.assertOnUiThread();
+ return mInputConnection != null ? mInputConnection.getView() : null;
+ }
+
+ /**
+ * Set the current {@link android.view.View} for text input. The {@link android.view.View} is used
+ * to interact with the system input method manager and to display certain text input UI elements.
+ * See the {@code SessionTextInput} class documentation for information on viewless mode, when the
+ * current {@link android.view.View} is not set or set to null.
+ *
+ * @param view Text input View or null to clear current View.
+ * @see #getView()
+ */
+ @UiThread
+ 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((EditableListener) mInputConnection);
+ }
+
+ /**
+ * Get an {@link android.view.inputmethod.InputConnection} instance. In viewless mode, this method
+ * still fills out the {@link android.view.inputmethod.EditorInfo} object, but the return value
+ * will always be null.
+ *
+ * @param attrs EditorInfo instance to be filled on return.
+ * @return InputConnection instance, or null if there is no active input (or if in viewless mode).
+ */
+ @AnyThread
+ public synchronized @Nullable InputConnection onCreateInputConnection(
+ final @NonNull EditorInfo attrs) {
+ // May be called on any thread.
+ mEditable.onCreateInputConnection(attrs);
+
+ if (!mQueue.isReady() || mInputConnection == null) {
+ return null;
+ }
+ 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.
+ */
+ @UiThread
+ public boolean onKeyPreIme(final int keyCode, final @NonNull KeyEvent event) {
+ ThreadUtils.assertOnUiThread();
+ return mEditable.onKeyPreIme(getView(), 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.
+ */
+ @UiThread
+ public boolean onKeyDown(final int keyCode, final @NonNull KeyEvent event) {
+ ThreadUtils.assertOnUiThread();
+ return mEditable.onKeyDown(getView(), 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.
+ */
+ @UiThread
+ public boolean onKeyUp(final int keyCode, final @NonNull KeyEvent event) {
+ ThreadUtils.assertOnUiThread();
+ return mEditable.onKeyUp(getView(), 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.
+ */
+ @UiThread
+ public boolean onKeyLongPress(final int keyCode, final @NonNull KeyEvent event) {
+ ThreadUtils.assertOnUiThread();
+ return mEditable.onKeyLongPress(getView(), keyCode, event);
+ }
+
+ /**
+ * Process a KeyEvent as a multiple-press event.
+ *
+ * @param keyCode Key code.
+ * @param repeatCount Key repeat count.
+ * @param event KeyEvent instance.
+ * @return True if the event was handled.
+ */
+ @UiThread
+ public boolean onKeyMultiple(
+ final int keyCode, final int repeatCount, final @NonNull KeyEvent event) {
+ ThreadUtils.assertOnUiThread();
+ return mEditable.onKeyMultiple(getView(), keyCode, repeatCount, event);
+ }
+
+ /**
+ * Set the current text input delegate.
+ *
+ * @param delegate TextInputDelegate instance or null to restore to default.
+ */
+ @UiThread
+ public void setDelegate(@Nullable final GeckoSession.TextInputDelegate delegate) {
+ ThreadUtils.assertOnUiThread();
+ mDelegate = delegate;
+ }
+
+ /**
+ * Get the current text input delegate.
+ *
+ * @return TextInputDelegate instance or a default instance if no delegate has been set.
+ */
+ @UiThread
+ public @NonNull GeckoSession.TextInputDelegate getDelegate() {
+ ThreadUtils.assertOnUiThread();
+ if (mDelegate == null) {
+ mDelegate = DefaultDelegate.INSTANCE;
+ }
+ return mDelegate;
+ }
+}