tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

SessionTextInput.java (15835B)


      1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
      2 * This Source Code Form is subject to the terms of the Mozilla Public
      3 * License, v. 2.0. If a copy of the MPL was not distributed with this
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      5 
      6 package org.mozilla.geckoview;
      7 
      8 import android.content.Context;
      9 import android.graphics.RectF;
     10 import android.os.Handler;
     11 import android.text.Editable;
     12 import android.util.Log;
     13 import android.view.KeyEvent;
     14 import android.view.View;
     15 import android.view.inputmethod.CursorAnchorInfo;
     16 import android.view.inputmethod.EditorInfo;
     17 import android.view.inputmethod.ExtractedText;
     18 import android.view.inputmethod.ExtractedTextRequest;
     19 import android.view.inputmethod.InputConnection;
     20 import android.view.inputmethod.InputMethodManager;
     21 import androidx.annotation.AnyThread;
     22 import androidx.annotation.IntDef;
     23 import androidx.annotation.NonNull;
     24 import androidx.annotation.Nullable;
     25 import androidx.annotation.UiThread;
     26 import java.lang.annotation.Retention;
     27 import java.lang.annotation.RetentionPolicy;
     28 import org.mozilla.gecko.IGeckoEditableParent;
     29 import org.mozilla.gecko.InputMethods;
     30 import org.mozilla.gecko.NativeQueue;
     31 import org.mozilla.gecko.annotation.WrapForJNI;
     32 import org.mozilla.gecko.util.ThreadUtils;
     33 
     34 /**
     35 * {@code SessionTextInput} handles text input for {@code GeckoSession} through key events or input
     36 * methods. It is typically used to implement certain methods in {@link android.view.View} such as
     37 * {@link android.view.View#onCreateInputConnection}, by forwarding such calls to corresponding
     38 * methods in {@code SessionTextInput}.
     39 *
     40 * <p>For full functionality, {@code SessionTextInput} requires a {@link android.view.View} to be
     41 * set first through {@link #setView}. When a {@link android.view.View} is not set or set to null,
     42 * {@code SessionTextInput} will operate in a reduced functionality mode. See {@link
     43 * #onCreateInputConnection} and methods in {@link GeckoSession.TextInputDelegate} for changes in
     44 * behavior in this viewless mode.
     45 */
     46 public final class SessionTextInput {
     47  /* package */ static final String LOGTAG = "GeckoSessionTextInput";
     48  private static final boolean DEBUG = false;
     49 
     50  // Interface to access GeckoInputConnection from SessionTextInput.
     51  /* package */ interface InputConnectionClient {
     52    View getView();
     53 
     54    Handler getHandler(Handler defHandler);
     55 
     56    InputConnection onCreateInputConnection(EditorInfo attrs);
     57  }
     58 
     59  // Interface to access GeckoEditable from GeckoInputConnection.
     60  /* package */ interface EditableClient {
     61    // The following value is used by requestCursorUpdates
     62    // ONE_SHOT calls updateCompositionRects() after getting current composing
     63    // character rects.
     64    @Retention(RetentionPolicy.SOURCE)
     65    @IntDef({ONE_SHOT, START_MONITOR, END_MONITOR})
     66    /* package */ @interface CursorMonitorMode {}
     67 
     68    @WrapForJNI int ONE_SHOT = 1;
     69    // START_MONITOR start the monitor for composing character rects.  If is is
     70    // updaed,  call updateCompositionRects()
     71    @WrapForJNI int START_MONITOR = 2;
     72    // ENDT_MONITOR stops the monitor for composing character rects.
     73    @WrapForJNI int END_MONITOR = 3;
     74 
     75    void sendKeyEvent(@Nullable View view, int action, @NonNull KeyEvent event);
     76 
     77    Editable getEditable();
     78 
     79    void setBatchMode(boolean isBatchMode);
     80 
     81    Handler setInputConnectionHandler(@NonNull Handler handler);
     82 
     83    void postToInputConnection(@NonNull Runnable runnable);
     84 
     85    void requestCursorUpdates(@CursorMonitorMode int requestMode);
     86 
     87    void insertImage(@NonNull byte[] data, @NonNull String mimeType);
     88  }
     89 
     90  // Interface to access GeckoInputConnection from GeckoEditable.
     91  /* package */ interface EditableListener {
     92    // IME notification type for notifyIME(), corresponding to NotificationToIME enum.
     93    @Retention(RetentionPolicy.SOURCE)
     94    @IntDef({
     95      NOTIFY_IME_OF_TOKEN,
     96      NOTIFY_IME_OPEN_VKB,
     97      NOTIFY_IME_REPLY_EVENT,
     98      NOTIFY_IME_OF_FOCUS,
     99      NOTIFY_IME_OF_BLUR,
    100      NOTIFY_IME_TO_COMMIT_COMPOSITION,
    101      NOTIFY_IME_TO_CANCEL_COMPOSITION
    102    })
    103    /* package */ @interface IMENotificationType {}
    104 
    105    @WrapForJNI int NOTIFY_IME_OF_TOKEN = -3;
    106    @WrapForJNI int NOTIFY_IME_OPEN_VKB = -2;
    107    @WrapForJNI int NOTIFY_IME_REPLY_EVENT = -1;
    108    @WrapForJNI int NOTIFY_IME_OF_FOCUS = 1;
    109    @WrapForJNI int NOTIFY_IME_OF_BLUR = 2;
    110    @WrapForJNI int NOTIFY_IME_TO_COMMIT_COMPOSITION = 8;
    111    @WrapForJNI int NOTIFY_IME_TO_CANCEL_COMPOSITION = 9;
    112 
    113    // IME enabled state for notifyIMEContext().
    114    @Retention(RetentionPolicy.SOURCE)
    115    @IntDef({IME_STATE_UNKNOWN, IME_STATE_DISABLED, IME_STATE_ENABLED, IME_STATE_PASSWORD})
    116    /* package */ @interface IMEState {}
    117 
    118    int IME_STATE_UNKNOWN = -1;
    119    int IME_STATE_DISABLED = 0;
    120    int IME_STATE_ENABLED = 1;
    121    int IME_STATE_PASSWORD = 2;
    122 
    123    // Flags for notifyIMEContext().
    124    @Retention(RetentionPolicy.SOURCE)
    125    @IntDef(
    126        flag = true,
    127        value = {IME_FLAG_PRIVATE_BROWSING, IME_FLAG_USER_ACTION, IME_FOCUS_NOT_CHANGED})
    128    /* package */ @interface IMEContextFlags {}
    129 
    130    @WrapForJNI int IME_FLAG_PRIVATE_BROWSING = 1 << 0;
    131    @WrapForJNI int IME_FLAG_USER_ACTION = 1 << 1;
    132    @WrapForJNI int IME_FOCUS_NOT_CHANGED = 1 << 2;
    133 
    134    void notifyIME(@IMENotificationType int type);
    135 
    136    void notifyIMEContext(
    137        @IMEState int state,
    138        String typeHint,
    139        String modeHint,
    140        String actionHint,
    141        @IMEContextFlags int flag);
    142 
    143    void onSelectionChange();
    144 
    145    void onTextChange();
    146 
    147    void onDiscardComposition();
    148 
    149    void onDefaultKeyEvent(KeyEvent event);
    150 
    151    void updateCompositionRects(final RectF[] aRects, final RectF aCaretRect);
    152  }
    153 
    154  private static final class DefaultDelegate implements GeckoSession.TextInputDelegate {
    155    public static final DefaultDelegate INSTANCE = new DefaultDelegate();
    156 
    157    private InputMethodManager getInputMethodManager(@Nullable final View view) {
    158      if (view == null) {
    159        return null;
    160      }
    161      return (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
    162    }
    163 
    164    @Override
    165    public void restartInput(@NonNull final GeckoSession session, final int reason) {
    166      ThreadUtils.assertOnUiThread();
    167      final View view = session.getTextInput().getView();
    168 
    169      final InputMethodManager imm = getInputMethodManager(view);
    170      if (imm == null) {
    171        return;
    172      }
    173 
    174      // InputMethodManager has internal logic to detect if we are restarting input
    175      // in an already focused View, which is the case here because all content text
    176      // fields are inside one LayerView. When this happens, InputMethodManager will
    177      // tell the input method to soft reset instead of hard reset. Stock latin IME
    178      // on Android 4.2+ has a quirk that when it soft resets, it does not clear the
    179      // composition. The following workaround tricks the IME into clearing the
    180      // composition when soft resetting.
    181      if (InputMethods.needsSoftResetWorkaround(
    182          InputMethods.getCurrentInputMethod(view.getContext()))) {
    183        // Fake a selection change, because the IME clears the composition when
    184        // the selection changes, even if soft-resetting. Offsets here must be
    185        // different from the previous selection offsets, and -1 seems to be a
    186        // reasonable, deterministic value
    187        imm.updateSelection(view, -1, -1, -1, -1);
    188      }
    189 
    190      try {
    191        imm.restartInput(view);
    192      } catch (final RuntimeException e) {
    193        Log.e(LOGTAG, "Error restarting input", e);
    194      }
    195    }
    196 
    197    @Override
    198    public void showSoftInput(@NonNull final GeckoSession session) {
    199      ThreadUtils.assertOnUiThread();
    200      final View view = session.getTextInput().getView();
    201      final InputMethodManager imm = getInputMethodManager(view);
    202      if (imm != null) {
    203        if (view.hasFocus() && !imm.isActive(view)) {
    204          // Marshmallow workaround: The view has focus but it is not the active
    205          // view for the input method. (Bug 1211848)
    206          view.clearFocus();
    207          view.requestFocus();
    208        }
    209        imm.showSoftInput(view, 0);
    210      }
    211    }
    212 
    213    @Override
    214    public void hideSoftInput(@NonNull final GeckoSession session) {
    215      ThreadUtils.assertOnUiThread();
    216      final View view = session.getTextInput().getView();
    217      final InputMethodManager imm = getInputMethodManager(view);
    218      if (imm != null) {
    219        imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
    220      }
    221    }
    222 
    223    @Override
    224    public void updateSelection(
    225        @NonNull final GeckoSession session,
    226        final int selStart,
    227        final int selEnd,
    228        final int compositionStart,
    229        final int compositionEnd) {
    230      ThreadUtils.assertOnUiThread();
    231      final View view = session.getTextInput().getView();
    232      final InputMethodManager imm = getInputMethodManager(view);
    233      if (imm != null) {
    234        // When composition start and end is -1,
    235        // InputMethodManager.updateSelection will remove composition
    236        // on most IMEs. If not working, we have to add a workaround
    237        // to EditableListener.onDiscardComposition.
    238        imm.updateSelection(view, selStart, selEnd, compositionStart, compositionEnd);
    239      }
    240    }
    241 
    242    @Override
    243    public void updateExtractedText(
    244        @NonNull final GeckoSession session,
    245        @NonNull final ExtractedTextRequest request,
    246        @NonNull final ExtractedText text) {
    247      ThreadUtils.assertOnUiThread();
    248      final View view = session.getTextInput().getView();
    249      final InputMethodManager imm = getInputMethodManager(view);
    250      if (imm != null) {
    251        imm.updateExtractedText(view, request.token, text);
    252      }
    253    }
    254 
    255    @Override
    256    public void updateCursorAnchorInfo(
    257        @NonNull final GeckoSession session, @NonNull final CursorAnchorInfo info) {
    258      ThreadUtils.assertOnUiThread();
    259      final View view = session.getTextInput().getView();
    260      final InputMethodManager imm = getInputMethodManager(view);
    261      if (imm != null) {
    262        imm.updateCursorAnchorInfo(view, info);
    263      }
    264    }
    265  }
    266 
    267  private final GeckoSession mSession;
    268  private final NativeQueue mQueue;
    269  private final GeckoEditable mEditable;
    270  private InputConnectionClient mInputConnection;
    271  private GeckoSession.TextInputDelegate mDelegate;
    272 
    273  /* package */ SessionTextInput(
    274      final @NonNull GeckoSession session, final @NonNull NativeQueue queue) {
    275    mSession = session;
    276    mQueue = queue;
    277    mEditable = new GeckoEditable(session);
    278  }
    279 
    280  /* package */ void onWindowChanged(final GeckoSession.Window window) {
    281    if (mQueue.isReady()) {
    282      window.attachEditable(mEditable);
    283    } else {
    284      mQueue.queueUntilReady(window, "attachEditable", IGeckoEditableParent.class, mEditable);
    285    }
    286  }
    287 
    288  /**
    289   * Get a Handler for the background input method thread.
    290   *
    291   * @param defHandler Handler returned by the system {@code getHandler} implementation.
    292   * @return Handler to return to the system through {@code getHandler}.
    293   */
    294  @AnyThread
    295  public synchronized @NonNull Handler getHandler(final @NonNull Handler defHandler) {
    296    // May be called on any thread.
    297    if (mInputConnection != null) {
    298      return mInputConnection.getHandler(defHandler);
    299    }
    300    return defHandler;
    301  }
    302 
    303  /**
    304   * Get the current {@link android.view.View} for text input.
    305   *
    306   * @return Current text input View or null if not set.
    307   * @see #setView(View)
    308   */
    309  @UiThread
    310  public @Nullable View getView() {
    311    ThreadUtils.assertOnUiThread();
    312    return mInputConnection != null ? mInputConnection.getView() : null;
    313  }
    314 
    315  /**
    316   * Set the current {@link android.view.View} for text input. The {@link android.view.View} is used
    317   * to interact with the system input method manager and to display certain text input UI elements.
    318   * See the {@code SessionTextInput} class documentation for information on viewless mode, when the
    319   * current {@link android.view.View} is not set or set to null.
    320   *
    321   * @param view Text input View or null to clear current View.
    322   * @see #getView()
    323   */
    324  @UiThread
    325  public synchronized void setView(final @Nullable View view) {
    326    ThreadUtils.assertOnUiThread();
    327 
    328    if (view == null) {
    329      mInputConnection = null;
    330    } else if (mInputConnection == null || mInputConnection.getView() != view) {
    331      mInputConnection = GeckoInputConnection.create(mSession, view, mEditable);
    332    }
    333    mEditable.setListener((EditableListener) mInputConnection);
    334  }
    335 
    336  /**
    337   * Get an {@link android.view.inputmethod.InputConnection} instance. In viewless mode, this method
    338   * still fills out the {@link android.view.inputmethod.EditorInfo} object, but the return value
    339   * will always be null.
    340   *
    341   * @param attrs EditorInfo instance to be filled on return.
    342   * @return InputConnection instance, or null if there is no active input (or if in viewless mode).
    343   */
    344  @AnyThread
    345  public synchronized @Nullable InputConnection onCreateInputConnection(
    346      final @NonNull EditorInfo attrs) {
    347    // May be called on any thread.
    348    mEditable.onCreateInputConnection(attrs);
    349 
    350    if (!mQueue.isReady() || mInputConnection == null) {
    351      return null;
    352    }
    353    return mInputConnection.onCreateInputConnection(attrs);
    354  }
    355 
    356  /**
    357   * Process a KeyEvent as a pre-IME event.
    358   *
    359   * @param keyCode Key code.
    360   * @param event KeyEvent instance.
    361   * @return True if the event was handled.
    362   */
    363  @UiThread
    364  public boolean onKeyPreIme(final int keyCode, final @NonNull KeyEvent event) {
    365    ThreadUtils.assertOnUiThread();
    366    return mEditable.onKeyPreIme(getView(), keyCode, event);
    367  }
    368 
    369  /**
    370   * Process a KeyEvent as a key-down event.
    371   *
    372   * @param keyCode Key code.
    373   * @param event KeyEvent instance.
    374   * @return True if the event was handled.
    375   */
    376  @UiThread
    377  public boolean onKeyDown(final int keyCode, final @NonNull KeyEvent event) {
    378    ThreadUtils.assertOnUiThread();
    379    return mEditable.onKeyDown(getView(), keyCode, event);
    380  }
    381 
    382  /**
    383   * Process a KeyEvent as a key-up event.
    384   *
    385   * @param keyCode Key code.
    386   * @param event KeyEvent instance.
    387   * @return True if the event was handled.
    388   */
    389  @UiThread
    390  public boolean onKeyUp(final int keyCode, final @NonNull KeyEvent event) {
    391    ThreadUtils.assertOnUiThread();
    392    return mEditable.onKeyUp(getView(), keyCode, event);
    393  }
    394 
    395  /**
    396   * Process a KeyEvent as a long-press event.
    397   *
    398   * @param keyCode Key code.
    399   * @param event KeyEvent instance.
    400   * @return True if the event was handled.
    401   */
    402  @UiThread
    403  public boolean onKeyLongPress(final int keyCode, final @NonNull KeyEvent event) {
    404    ThreadUtils.assertOnUiThread();
    405    return mEditable.onKeyLongPress(getView(), keyCode, event);
    406  }
    407 
    408  /**
    409   * Process a KeyEvent as a multiple-press event.
    410   *
    411   * @param keyCode Key code.
    412   * @param repeatCount Key repeat count.
    413   * @param event KeyEvent instance.
    414   * @return True if the event was handled.
    415   */
    416  @UiThread
    417  public boolean onKeyMultiple(
    418      final int keyCode, final int repeatCount, final @NonNull KeyEvent event) {
    419    ThreadUtils.assertOnUiThread();
    420    return mEditable.onKeyMultiple(getView(), keyCode, repeatCount, event);
    421  }
    422 
    423  /**
    424   * Set the current text input delegate.
    425   *
    426   * @param delegate TextInputDelegate instance or null to restore to default.
    427   */
    428  @UiThread
    429  public void setDelegate(@Nullable final GeckoSession.TextInputDelegate delegate) {
    430    ThreadUtils.assertOnUiThread();
    431    mDelegate = delegate;
    432  }
    433 
    434  /**
    435   * Get the current text input delegate.
    436   *
    437   * @return TextInputDelegate instance or a default instance if no delegate has been set.
    438   */
    439  @UiThread
    440  public @NonNull GeckoSession.TextInputDelegate getDelegate() {
    441    ThreadUtils.assertOnUiThread();
    442    if (mDelegate == null) {
    443      mDelegate = DefaultDelegate.INSTANCE;
    444    }
    445    return mDelegate;
    446  }
    447 }