tor-browser

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

GeckoEditableChild.java (14866B)


      1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.gecko;
      7 
      8 import android.graphics.RectF;
      9 import android.os.IBinder;
     10 import android.os.RemoteException;
     11 import android.util.Log;
     12 import android.view.KeyEvent;
     13 import androidx.annotation.Nullable;
     14 import org.mozilla.gecko.annotation.WrapForJNI;
     15 import org.mozilla.gecko.mozglue.JNIObject;
     16 import org.mozilla.gecko.util.ThreadUtils;
     17 
     18 /**
     19 * GeckoEditableChild implements the Gecko-facing side of IME operation. Each nsWindow in the main
     20 * process and each PuppetWidget in each child content process has an instance of
     21 * GeckoEditableChild, which communicates with the GeckoEditableParent instance in the main process.
     22 */
     23 public final class GeckoEditableChild extends JNIObject implements IGeckoEditableChild {
     24 
     25  private static final boolean DEBUG = false;
     26  private static final String LOGTAG = "GeckoEditableChild";
     27 
     28  private static final int NOTIFY_IME_TO_CANCEL_COMPOSITION = 9;
     29 
     30  private final class RemoteChild extends IGeckoEditableChild.Stub {
     31    @Override // IGeckoEditableChild
     32    public void transferParent(final IGeckoEditableParent editableParent) {
     33      GeckoEditableChild.this.transferParent(editableParent);
     34    }
     35 
     36    @Override // IGeckoEditableChild
     37    public void onKeyEvent(
     38        final int action,
     39        final int keyCode,
     40        final int scanCode,
     41        final int metaState,
     42        final int keyPressMetaState,
     43        final long time,
     44        final int domPrintableKeyValue,
     45        final int repeatCount,
     46        final int flags,
     47        final boolean isSynthesizedImeKey,
     48        final boolean waitingReply,
     49        final KeyEvent event) {
     50      GeckoEditableChild.this.onKeyEvent(
     51          action,
     52          keyCode,
     53          scanCode,
     54          metaState,
     55          keyPressMetaState,
     56          time,
     57          domPrintableKeyValue,
     58          repeatCount,
     59          flags,
     60          isSynthesizedImeKey,
     61          waitingReply,
     62          event);
     63    }
     64 
     65    @Override // IGeckoEditableChild
     66    public void onImeSynchronize() {
     67      GeckoEditableChild.this.onImeSynchronize();
     68    }
     69 
     70    @Override // IGeckoEditableChild
     71    public void onImeReplaceText(final int start, final int end, final String text) {
     72      GeckoEditableChild.this.onImeReplaceText(start, end, text);
     73    }
     74 
     75    @Override // IGeckoEditableChild
     76    public void onImeInsertImage(final byte[] data, final String mimeType) {
     77      GeckoEditableChild.this.onImeInsertImage(data, mimeType);
     78    }
     79 
     80    @Override // IGeckoEditableChild
     81    public void onImeAddCompositionRange(
     82        final int start,
     83        final int end,
     84        final int rangeType,
     85        final int rangeStyles,
     86        final int rangeLineStyle,
     87        final boolean rangeBoldLine,
     88        final int rangeForeColor,
     89        final int rangeBackColor,
     90        final int rangeLineColor) {
     91      GeckoEditableChild.this.onImeAddCompositionRange(
     92          start,
     93          end,
     94          rangeType,
     95          rangeStyles,
     96          rangeLineStyle,
     97          rangeBoldLine,
     98          rangeForeColor,
     99          rangeBackColor,
    100          rangeLineColor);
    101    }
    102 
    103    @Override // IGeckoEditableChild
    104    public void onImeUpdateComposition(final int start, final int end, final int flags) {
    105      GeckoEditableChild.this.onImeUpdateComposition(start, end, flags);
    106    }
    107 
    108    @Override // IGeckoEditableChild
    109    public void onImeRequestCursorUpdates(final int requestMode) {
    110      GeckoEditableChild.this.onImeRequestCursorUpdates(requestMode);
    111    }
    112 
    113    @Override // IGeckoEditableChild
    114    public void onImeRequestCommit() {
    115      GeckoEditableChild.this.onImeRequestCommit();
    116    }
    117  }
    118 
    119  private final IGeckoEditableChild mEditableChild;
    120  private final boolean mIsDefault;
    121 
    122  private IGeckoEditableParent mEditableParent;
    123  private int mCurrentTextLength; // Used by Gecko thread
    124 
    125  @WrapForJNI(calledFrom = "gecko")
    126  private GeckoEditableChild(
    127      @Nullable final IGeckoEditableParent editableParent, final boolean isDefault) {
    128    mIsDefault = isDefault;
    129 
    130    if (editableParent != null
    131        && editableParent.asBinder().queryLocalInterface(IGeckoEditableParent.class.getName())
    132            != null) {
    133      // IGeckoEditableParent is local; i.e. we're in the main process.
    134      mEditableChild = this;
    135    } else {
    136      // IGeckoEditableParent is remote; i.e. we're in a content process.
    137      mEditableChild = new RemoteChild();
    138    }
    139 
    140    if (editableParent != null) {
    141      setParent(editableParent);
    142    }
    143  }
    144 
    145  @WrapForJNI(calledFrom = "gecko")
    146  private void setParent(final IGeckoEditableParent editableParent) {
    147    mEditableParent = editableParent;
    148 
    149    if (mIsDefault) {
    150      // Tell the parent we're the default child.
    151      try {
    152        editableParent.setDefaultChild(mEditableChild);
    153      } catch (final RemoteException e) {
    154        Log.e(LOGTAG, "Failed to set default child", e);
    155      }
    156    }
    157  }
    158 
    159  @WrapForJNI(dispatchTo = "proxy")
    160  @Override // IGeckoEditableChild
    161  public native void transferParent(IGeckoEditableParent editableParent);
    162 
    163  @WrapForJNI(dispatchTo = "proxy")
    164  @Override // IGeckoEditableChild
    165  public native void onKeyEvent(
    166      int action,
    167      int keyCode,
    168      int scanCode,
    169      int metaState,
    170      int keyPressMetaState,
    171      long time,
    172      int domPrintableKeyValue,
    173      int repeatCount,
    174      int flags,
    175      boolean isSynthesizedImeKey,
    176      boolean waitingReply,
    177      KeyEvent event);
    178 
    179  @WrapForJNI(dispatchTo = "proxy")
    180  @Override // IGeckoEditableChild
    181  public native void onImeSynchronize();
    182 
    183  @WrapForJNI(dispatchTo = "proxy")
    184  @Override // IGeckoEditableChild
    185  public native void onImeReplaceText(int start, int end, String text);
    186 
    187  @WrapForJNI(dispatchTo = "proxy")
    188  @Override // IGeckoEditableChild
    189  public native void onImeAddCompositionRange(
    190      int start,
    191      int end,
    192      int rangeType,
    193      int rangeStyles,
    194      int rangeLineStyle,
    195      boolean rangeBoldLine,
    196      int rangeForeColor,
    197      int rangeBackColor,
    198      int rangeLineColor);
    199 
    200  // Don't update to the new composition if it's different than the current composition.
    201  @WrapForJNI public static final int FLAG_KEEP_CURRENT_COMPOSITION = 1;
    202 
    203  @WrapForJNI(dispatchTo = "proxy")
    204  @Override // IGeckoEditableChild
    205  public native void onImeUpdateComposition(int start, int end, int flags);
    206 
    207  @WrapForJNI(dispatchTo = "proxy")
    208  @Override // IGeckoEditableChild
    209  public native void onImeRequestCursorUpdates(int requestMode);
    210 
    211  @WrapForJNI(dispatchTo = "proxy")
    212  @Override // IGeckoEditableChild
    213  public native void onImeRequestCommit();
    214 
    215  @WrapForJNI(dispatchTo = "proxy")
    216  @Override // IGeckoEditableChild
    217  public native void onImeInsertImage(byte[] data, String mimeType);
    218 
    219  @Override // JNIObject
    220  protected void disposeNative() {
    221    // Disposal happens in native code.
    222    throw new UnsupportedOperationException();
    223  }
    224 
    225  @WrapForJNI(calledFrom = "gecko")
    226  private boolean hasEditableParent() {
    227    if (mEditableParent != null) {
    228      return true;
    229    }
    230    Log.w(LOGTAG, "No editable parent");
    231    return false;
    232  }
    233 
    234  @Override // IInterface
    235  public IBinder asBinder() {
    236    // Return the GeckoEditableParent's binder as fallback for comparison purposes.
    237    return mEditableChild != this
    238        ? mEditableChild.asBinder()
    239        : hasEditableParent() ? mEditableParent.asBinder() : null;
    240  }
    241 
    242  @WrapForJNI(calledFrom = "gecko")
    243  private void notifyIME(final int type) {
    244    if (DEBUG) {
    245      ThreadUtils.assertOnGeckoThread();
    246      Log.d(LOGTAG, "notifyIME(" + type + ")");
    247    }
    248    if (!hasEditableParent()) {
    249      return;
    250    }
    251    if (type == NOTIFY_IME_TO_CANCEL_COMPOSITION) {
    252      // Composition should have been canceled on the parent side through text
    253      // update notifications. We cannot verify that here because we don't
    254      // keep track of spans on the child side, but it's simple to add the
    255      // check to the parent side if ever needed.
    256      return;
    257    }
    258 
    259    try {
    260      mEditableParent.notifyIME(mEditableChild, type);
    261    } catch (final RemoteException e) {
    262      Log.e(LOGTAG, "Remote call failed", e);
    263      return;
    264    }
    265  }
    266 
    267  @WrapForJNI(calledFrom = "gecko")
    268  private void notifyIMEContext(
    269      final int state,
    270      final String typeHint,
    271      final String modeHint,
    272      final String actionHint,
    273      final String autocapitalize,
    274      final boolean autocorrect,
    275      final int flags) {
    276    if (DEBUG) {
    277      ThreadUtils.assertOnGeckoThread();
    278      final StringBuilder sb = new StringBuilder("notifyIMEContext(");
    279      sb.append(state)
    280          .append(", \"")
    281          .append(typeHint)
    282          .append("\", \"")
    283          .append(modeHint)
    284          .append("\", \"")
    285          .append(actionHint)
    286          .append("\", \"")
    287          .append(autocapitalize)
    288          .append("\", ")
    289          .append(autocorrect)
    290          .append(", 0x")
    291          .append(Integer.toHexString(flags))
    292          .append(")");
    293      Log.d(LOGTAG, sb.toString());
    294    }
    295    if (!hasEditableParent()) {
    296      return;
    297    }
    298 
    299    try {
    300      mEditableParent.notifyIMEContext(
    301          mEditableChild.asBinder(),
    302          state,
    303          typeHint,
    304          modeHint,
    305          actionHint,
    306          autocapitalize,
    307          autocorrect,
    308          flags);
    309    } catch (final RemoteException e) {
    310      Log.e(LOGTAG, "Remote call failed", e);
    311    }
    312  }
    313 
    314  @WrapForJNI(calledFrom = "gecko", exceptionMode = "ignore")
    315  private void onSelectionChange(
    316      final int start, final int end, final boolean causedOnlyByComposition)
    317      throws RemoteException {
    318    if (DEBUG) {
    319      ThreadUtils.assertOnGeckoThread();
    320      final StringBuilder sb = new StringBuilder("onSelectionChange(");
    321      sb.append(start)
    322          .append(", ")
    323          .append(end)
    324          .append(", ")
    325          .append(causedOnlyByComposition)
    326          .append(")");
    327      Log.d(LOGTAG, sb.toString());
    328    }
    329    if (!hasEditableParent()) {
    330      return;
    331    }
    332 
    333    final int currentLength = mCurrentTextLength;
    334    if (start < 0 || start > currentLength || end < 0 || end > currentLength) {
    335      Log.e(
    336          LOGTAG,
    337          "invalid selection notification range: "
    338              + start
    339              + " to "
    340              + end
    341              + ", length: "
    342              + currentLength);
    343      throw new IllegalArgumentException("invalid selection notification range");
    344    }
    345 
    346    mEditableParent.onSelectionChange(
    347        mEditableChild.asBinder(), start, end, causedOnlyByComposition);
    348  }
    349 
    350  @WrapForJNI(calledFrom = "gecko", exceptionMode = "ignore")
    351  private void onTextChange(
    352      final CharSequence text,
    353      final int start,
    354      final int unboundedOldEnd,
    355      final int unboundedNewEnd,
    356      final boolean causedOnlyByComposition)
    357      throws RemoteException {
    358    if (DEBUG) {
    359      ThreadUtils.assertOnGeckoThread();
    360      final StringBuilder sb = new StringBuilder("onTextChange(");
    361      sb.append(text)
    362          .append(", ")
    363          .append(start)
    364          .append(", ")
    365          .append(unboundedOldEnd)
    366          .append(", ")
    367          .append(unboundedNewEnd)
    368          .append(", ")
    369          .append(causedOnlyByComposition)
    370          .append(")");
    371      Log.d(LOGTAG, sb.toString());
    372    }
    373    if (!hasEditableParent()) {
    374      return;
    375    }
    376 
    377    if (start < 0 || start > unboundedOldEnd) {
    378      Log.e(LOGTAG, "invalid text notification range: " + start + " to " + unboundedOldEnd);
    379      throw new IllegalArgumentException("invalid text notification range");
    380    }
    381 
    382    /* For the "end" parameters, Gecko can pass in a large
    383    number to denote "end of the text". Fix that here */
    384    final int currentLength = mCurrentTextLength;
    385    final int oldEnd = unboundedOldEnd > currentLength ? currentLength : unboundedOldEnd;
    386    // new end should always match text
    387    if (unboundedOldEnd <= currentLength && unboundedNewEnd != (start + text.length())) {
    388      Log.e(
    389          LOGTAG,
    390          "newEnd does not match text: " + unboundedNewEnd + " vs " + (start + text.length()));
    391      throw new IllegalArgumentException("newEnd does not match text");
    392    }
    393 
    394    mCurrentTextLength += start + text.length() - oldEnd;
    395    // Need unboundedOldEnd so GeckoEditable can distinguish changed text vs cleared text.
    396    if (text.length() == 0) {
    397      // Remove text in range.
    398      mEditableParent.onTextChange(
    399          mEditableChild.asBinder(), text, start, unboundedOldEnd, causedOnlyByComposition);
    400      return;
    401    }
    402    // Using large text causes TransactionTooLargeException, so split text data.
    403    int offset = 0;
    404    int newUnboundedOldEnd = unboundedOldEnd;
    405    while (offset < text.length()) {
    406      final int end = Math.min(offset + 1024 * 64 /* 64KB */, text.length());
    407      mEditableParent.onTextChange(
    408          mEditableChild.asBinder(),
    409          text.subSequence(offset, end),
    410          start + offset,
    411          newUnboundedOldEnd,
    412          causedOnlyByComposition);
    413      offset = end;
    414      newUnboundedOldEnd = start + offset;
    415    }
    416  }
    417 
    418  @WrapForJNI(calledFrom = "gecko")
    419  private void onDefaultKeyEvent(final KeyEvent event) {
    420    if (DEBUG) {
    421      // GeckoEditableListener methods should all be called from the Gecko thread
    422      ThreadUtils.assertOnGeckoThread();
    423      final StringBuilder sb = new StringBuilder("onDefaultKeyEvent(");
    424      sb.append("action=")
    425          .append(event.getAction())
    426          .append(", ")
    427          .append("keyCode=")
    428          .append(event.getKeyCode())
    429          .append(", ")
    430          .append("metaState=")
    431          .append(event.getMetaState())
    432          .append(", ")
    433          .append("time=")
    434          .append(event.getEventTime())
    435          .append(", ")
    436          .append("repeatCount=")
    437          .append(event.getRepeatCount())
    438          .append(")");
    439      Log.d(LOGTAG, sb.toString());
    440    }
    441    if (!hasEditableParent()) {
    442      return;
    443    }
    444 
    445    try {
    446      mEditableParent.onDefaultKeyEvent(mEditableChild.asBinder(), event);
    447    } catch (final RemoteException e) {
    448      Log.e(LOGTAG, "Remote call failed", e);
    449    }
    450  }
    451 
    452  @WrapForJNI(calledFrom = "gecko")
    453  private void updateCompositionRects(final RectF[] rects, final RectF caretRect) {
    454    if (DEBUG) {
    455      // GeckoEditableListener methods should all be called from the Gecko thread
    456      ThreadUtils.assertOnGeckoThread();
    457      Log.d(LOGTAG, "updateCompositionRects(rects.length = " + rects.length + ")");
    458    }
    459    if (!hasEditableParent()) {
    460      return;
    461    }
    462 
    463    try {
    464      mEditableParent.updateCompositionRects(mEditableChild.asBinder(), rects, caretRect);
    465    } catch (final RemoteException e) {
    466      Log.e(LOGTAG, "Remote call failed", e);
    467    }
    468  }
    469 }