summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableChild.java
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableChild.java
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableChild.java')
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableChild.java456
1 files changed, 456 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableChild.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableChild.java
new file mode 100644
index 0000000000..8a76548c1d
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableChild.java
@@ -0,0 +1,456 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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 android.graphics.RectF;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.KeyEvent;
+import androidx.annotation.Nullable;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.mozglue.JNIObject;
+import org.mozilla.gecko.util.ThreadUtils;
+
+/**
+ * GeckoEditableChild implements the Gecko-facing side of IME operation. Each nsWindow in the main
+ * process and each PuppetWidget in each child content process has an instance of
+ * GeckoEditableChild, which communicates with the GeckoEditableParent instance in the main process.
+ */
+public final class GeckoEditableChild extends JNIObject implements IGeckoEditableChild {
+
+ private static final boolean DEBUG = false;
+ private static final String LOGTAG = "GeckoEditableChild";
+
+ private static final int NOTIFY_IME_TO_CANCEL_COMPOSITION = 9;
+
+ private final class RemoteChild extends IGeckoEditableChild.Stub {
+ @Override // IGeckoEditableChild
+ public void transferParent(final IGeckoEditableParent editableParent) {
+ GeckoEditableChild.this.transferParent(editableParent);
+ }
+
+ @Override // IGeckoEditableChild
+ public void onKeyEvent(
+ final int action,
+ final int keyCode,
+ final int scanCode,
+ final int metaState,
+ final int keyPressMetaState,
+ final long time,
+ final int domPrintableKeyValue,
+ final int repeatCount,
+ final int flags,
+ final boolean isSynthesizedImeKey,
+ final KeyEvent event) {
+ GeckoEditableChild.this.onKeyEvent(
+ action,
+ keyCode,
+ scanCode,
+ metaState,
+ keyPressMetaState,
+ time,
+ domPrintableKeyValue,
+ repeatCount,
+ flags,
+ isSynthesizedImeKey,
+ event);
+ }
+
+ @Override // IGeckoEditableChild
+ public void onImeSynchronize() {
+ GeckoEditableChild.this.onImeSynchronize();
+ }
+
+ @Override // IGeckoEditableChild
+ public void onImeReplaceText(final int start, final int end, final String text) {
+ GeckoEditableChild.this.onImeReplaceText(start, end, text);
+ }
+
+ @Override // IGeckoEditableChild
+ public void onImeInsertImage(final byte[] data, final String mimeType) {
+ GeckoEditableChild.this.onImeInsertImage(data, mimeType);
+ }
+
+ @Override // IGeckoEditableChild
+ public void onImeAddCompositionRange(
+ final int start,
+ final int end,
+ final int rangeType,
+ final int rangeStyles,
+ final int rangeLineStyle,
+ final boolean rangeBoldLine,
+ final int rangeForeColor,
+ final int rangeBackColor,
+ final int rangeLineColor) {
+ GeckoEditableChild.this.onImeAddCompositionRange(
+ start,
+ end,
+ rangeType,
+ rangeStyles,
+ rangeLineStyle,
+ rangeBoldLine,
+ rangeForeColor,
+ rangeBackColor,
+ rangeLineColor);
+ }
+
+ @Override // IGeckoEditableChild
+ public void onImeUpdateComposition(final int start, final int end, final int flags) {
+ GeckoEditableChild.this.onImeUpdateComposition(start, end, flags);
+ }
+
+ @Override // IGeckoEditableChild
+ public void onImeRequestCursorUpdates(final int requestMode) {
+ GeckoEditableChild.this.onImeRequestCursorUpdates(requestMode);
+ }
+
+ @Override // IGeckoEditableChild
+ public void onImeRequestCommit() {
+ GeckoEditableChild.this.onImeRequestCommit();
+ }
+ }
+
+ private final IGeckoEditableChild mEditableChild;
+ private final boolean mIsDefault;
+
+ private IGeckoEditableParent mEditableParent;
+ private int mCurrentTextLength; // Used by Gecko thread
+
+ @WrapForJNI(calledFrom = "gecko")
+ private GeckoEditableChild(
+ @Nullable final IGeckoEditableParent editableParent, final boolean isDefault) {
+ mIsDefault = isDefault;
+
+ if (editableParent != null
+ && editableParent.asBinder().queryLocalInterface(IGeckoEditableParent.class.getName())
+ != null) {
+ // IGeckoEditableParent is local; i.e. we're in the main process.
+ mEditableChild = this;
+ } else {
+ // IGeckoEditableParent is remote; i.e. we're in a content process.
+ mEditableChild = new RemoteChild();
+ }
+
+ if (editableParent != null) {
+ setParent(editableParent);
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void setParent(final IGeckoEditableParent editableParent) {
+ mEditableParent = editableParent;
+
+ if (mIsDefault) {
+ // Tell the parent we're the default child.
+ try {
+ editableParent.setDefaultChild(mEditableChild);
+ } catch (final RemoteException e) {
+ Log.e(LOGTAG, "Failed to set default child", e);
+ }
+ }
+ }
+
+ @WrapForJNI(dispatchTo = "proxy")
+ @Override // IGeckoEditableChild
+ public native void transferParent(IGeckoEditableParent editableParent);
+
+ @WrapForJNI(dispatchTo = "proxy")
+ @Override // IGeckoEditableChild
+ public native void onKeyEvent(
+ int action,
+ int keyCode,
+ int scanCode,
+ int metaState,
+ int keyPressMetaState,
+ long time,
+ int domPrintableKeyValue,
+ int repeatCount,
+ int flags,
+ boolean isSynthesizedImeKey,
+ KeyEvent event);
+
+ @WrapForJNI(dispatchTo = "proxy")
+ @Override // IGeckoEditableChild
+ public native void onImeSynchronize();
+
+ @WrapForJNI(dispatchTo = "proxy")
+ @Override // IGeckoEditableChild
+ public native void onImeReplaceText(int start, int end, String text);
+
+ @WrapForJNI(dispatchTo = "proxy")
+ @Override // IGeckoEditableChild
+ public native void onImeAddCompositionRange(
+ int start,
+ int end,
+ int rangeType,
+ int rangeStyles,
+ int rangeLineStyle,
+ boolean rangeBoldLine,
+ int rangeForeColor,
+ int rangeBackColor,
+ int rangeLineColor);
+
+ // Don't update to the new composition if it's different than the current composition.
+ @WrapForJNI public static final int FLAG_KEEP_CURRENT_COMPOSITION = 1;
+
+ @WrapForJNI(dispatchTo = "proxy")
+ @Override // IGeckoEditableChild
+ public native void onImeUpdateComposition(int start, int end, int flags);
+
+ @WrapForJNI(dispatchTo = "proxy")
+ @Override // IGeckoEditableChild
+ public native void onImeRequestCursorUpdates(int requestMode);
+
+ @WrapForJNI(dispatchTo = "proxy")
+ @Override // IGeckoEditableChild
+ public native void onImeRequestCommit();
+
+ @WrapForJNI(dispatchTo = "proxy")
+ @Override // IGeckoEditableChild
+ public native void onImeInsertImage(byte[] data, String mimeType);
+
+ @Override // JNIObject
+ protected void disposeNative() {
+ // Disposal happens in native code.
+ throw new UnsupportedOperationException();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private boolean hasEditableParent() {
+ if (mEditableParent != null) {
+ return true;
+ }
+ Log.w(LOGTAG, "No editable parent");
+ return false;
+ }
+
+ @Override // IInterface
+ public IBinder asBinder() {
+ // Return the GeckoEditableParent's binder as fallback for comparison purposes.
+ return mEditableChild != this
+ ? mEditableChild.asBinder()
+ : hasEditableParent() ? mEditableParent.asBinder() : null;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void notifyIME(final int type) {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ Log.d(LOGTAG, "notifyIME(" + type + ")");
+ }
+ if (!hasEditableParent()) {
+ return;
+ }
+ if (type == NOTIFY_IME_TO_CANCEL_COMPOSITION) {
+ // Composition should have been canceled on the parent side through text
+ // update notifications. We cannot verify that here because we don't
+ // keep track of spans on the child side, but it's simple to add the
+ // check to the parent side if ever needed.
+ return;
+ }
+
+ try {
+ mEditableParent.notifyIME(mEditableChild, type);
+ } catch (final RemoteException e) {
+ Log.e(LOGTAG, "Remote call failed", e);
+ return;
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void notifyIMEContext(
+ final int state,
+ final String typeHint,
+ final String modeHint,
+ final String actionHint,
+ final String autocapitalize,
+ final int flags) {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ final StringBuilder sb = new StringBuilder("notifyIMEContext(");
+ sb.append(state)
+ .append(", \"")
+ .append(typeHint)
+ .append("\", \"")
+ .append(modeHint)
+ .append("\", \"")
+ .append(actionHint)
+ .append("\", \"")
+ .append(autocapitalize)
+ .append("\", 0x")
+ .append(Integer.toHexString(flags))
+ .append(")");
+ Log.d(LOGTAG, sb.toString());
+ }
+ if (!hasEditableParent()) {
+ return;
+ }
+
+ try {
+ mEditableParent.notifyIMEContext(
+ mEditableChild.asBinder(), state, typeHint, modeHint, actionHint, autocapitalize, flags);
+ } catch (final RemoteException e) {
+ Log.e(LOGTAG, "Remote call failed", e);
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko", exceptionMode = "ignore")
+ private void onSelectionChange(
+ final int start, final int end, final boolean causedOnlyByComposition)
+ throws RemoteException {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ final StringBuilder sb = new StringBuilder("onSelectionChange(");
+ sb.append(start)
+ .append(", ")
+ .append(end)
+ .append(", ")
+ .append(causedOnlyByComposition)
+ .append(")");
+ Log.d(LOGTAG, sb.toString());
+ }
+ if (!hasEditableParent()) {
+ return;
+ }
+
+ final int currentLength = mCurrentTextLength;
+ if (start < 0 || start > currentLength || end < 0 || end > currentLength) {
+ Log.e(
+ LOGTAG,
+ "invalid selection notification range: "
+ + start
+ + " to "
+ + end
+ + ", length: "
+ + currentLength);
+ throw new IllegalArgumentException("invalid selection notification range");
+ }
+
+ mEditableParent.onSelectionChange(
+ mEditableChild.asBinder(), start, end, causedOnlyByComposition);
+ }
+
+ @WrapForJNI(calledFrom = "gecko", exceptionMode = "ignore")
+ private void onTextChange(
+ final CharSequence text,
+ final int start,
+ final int unboundedOldEnd,
+ final int unboundedNewEnd,
+ final boolean causedOnlyByComposition)
+ throws RemoteException {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ final StringBuilder sb = new StringBuilder("onTextChange(");
+ sb.append(text)
+ .append(", ")
+ .append(start)
+ .append(", ")
+ .append(unboundedOldEnd)
+ .append(", ")
+ .append(unboundedNewEnd)
+ .append(", ")
+ .append(causedOnlyByComposition)
+ .append(")");
+ Log.d(LOGTAG, sb.toString());
+ }
+ if (!hasEditableParent()) {
+ return;
+ }
+
+ if (start < 0 || start > unboundedOldEnd) {
+ Log.e(LOGTAG, "invalid text notification range: " + start + " to " + unboundedOldEnd);
+ throw new IllegalArgumentException("invalid text notification range");
+ }
+
+ /* For the "end" parameters, Gecko can pass in a large
+ number to denote "end of the text". Fix that here */
+ final int currentLength = mCurrentTextLength;
+ final int oldEnd = unboundedOldEnd > currentLength ? currentLength : unboundedOldEnd;
+ // new end should always match text
+ if (unboundedOldEnd <= currentLength && unboundedNewEnd != (start + text.length())) {
+ Log.e(
+ LOGTAG,
+ "newEnd does not match text: " + unboundedNewEnd + " vs " + (start + text.length()));
+ throw new IllegalArgumentException("newEnd does not match text");
+ }
+
+ mCurrentTextLength += start + text.length() - oldEnd;
+ // Need unboundedOldEnd so GeckoEditable can distinguish changed text vs cleared text.
+ if (text.length() == 0) {
+ // Remove text in range.
+ mEditableParent.onTextChange(
+ mEditableChild.asBinder(), text, start, unboundedOldEnd, causedOnlyByComposition);
+ return;
+ }
+ // Using large text causes TransactionTooLargeException, so split text data.
+ int offset = 0;
+ int newUnboundedOldEnd = unboundedOldEnd;
+ while (offset < text.length()) {
+ final int end = Math.min(offset + 1024 * 64 /* 64KB */, text.length());
+ mEditableParent.onTextChange(
+ mEditableChild.asBinder(),
+ text.subSequence(offset, end),
+ start + offset,
+ newUnboundedOldEnd,
+ causedOnlyByComposition);
+ offset = end;
+ newUnboundedOldEnd = start + offset;
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void onDefaultKeyEvent(final KeyEvent event) {
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ final StringBuilder sb = new StringBuilder("onDefaultKeyEvent(");
+ sb.append("action=")
+ .append(event.getAction())
+ .append(", ")
+ .append("keyCode=")
+ .append(event.getKeyCode())
+ .append(", ")
+ .append("metaState=")
+ .append(event.getMetaState())
+ .append(", ")
+ .append("time=")
+ .append(event.getEventTime())
+ .append(", ")
+ .append("repeatCount=")
+ .append(event.getRepeatCount())
+ .append(")");
+ Log.d(LOGTAG, sb.toString());
+ }
+ if (!hasEditableParent()) {
+ return;
+ }
+
+ try {
+ mEditableParent.onDefaultKeyEvent(mEditableChild.asBinder(), event);
+ } catch (final RemoteException e) {
+ Log.e(LOGTAG, "Remote call failed", e);
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void updateCompositionRects(final RectF[] rects, final RectF caretRect) {
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ Log.d(LOGTAG, "updateCompositionRects(rects.length = " + rects.length + ")");
+ }
+ if (!hasEditableParent()) {
+ return;
+ }
+
+ try {
+ mEditableParent.updateCompositionRects(mEditableChild.asBinder(), rects, caretRect);
+ } catch (final RemoteException e) {
+ Log.e(LOGTAG, "Remote call failed", e);
+ }
+ }
+}