/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.view.inputmethod; import static com.android.internal.inputmethod.InputConnectionProtoDumper.buildGetCursorCapsModeProto; import static com.android.internal.inputmethod.InputConnectionProtoDumper.buildGetExtractedTextProto; import static com.android.internal.inputmethod.InputConnectionProtoDumper.buildGetSelectedTextProto; import static com.android.internal.inputmethod.InputConnectionProtoDumper.buildGetSurroundingTextProto; import static com.android.internal.inputmethod.InputConnectionProtoDumper.buildGetTextAfterCursorProto; import static com.android.internal.inputmethod.InputConnectionProtoDumper.buildGetTextBeforeCursorProto; import static java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.AnyThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UiThread; import android.app.UriGrantsManager; import android.content.ContentProvider; import android.content.Intent; import android.graphics.RectF; import android.net.Uri; import android.os.Binder; import android.os.Bundle; import android.os.CancellationSignal; import android.os.CancellationSignalBeamer; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.ResultReceiver; import android.os.Trace; import android.os.UserHandle; import android.util.Log; import android.util.proto.ProtoOutputStream; import android.view.KeyEvent; import android.view.View; import android.view.ViewRootImpl; import com.android.internal.infra.AndroidFuture; import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection; import com.android.internal.inputmethod.IRemoteInputConnection; import com.android.internal.inputmethod.ImeTracing; import com.android.internal.inputmethod.InputConnectionCommandHeader; import java.lang.annotation.Retention; import java.lang.ref.WeakReference; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.function.Supplier; /** * Takes care of remote method invocations of {@link InputConnection} in the IME client side. * *

{@link android.inputmethodservice.RemoteInputConnection} code is executed in the IME process. * It makes {@link IRemoteInputConnection} binder calls under the hood. * {@link RemoteInputConnectionImpl} receives {@link IRemoteInputConnection} binder calls in the IME * client (editor app) process, and forwards them to {@link InputConnection} that the IME client * provided, on the {@link Looper} associated to the {@link InputConnection}.

* *

{@link com.android.internal.inputmethod.RemoteAccessibilityInputConnection} code is executed * in the {@link android.accessibilityservice.AccessibilityService} process. It makes * {@link com.android.internal.inputmethod.IRemoteAccessibilityInputConnection} binder calls under * the hood. {@link #mAccessibilityInputConnection} receives the binder calls in the IME client * (editor app) process, and forwards them to {@link InputConnection} that the IME client provided, * on the {@link Looper} associated to the {@link InputConnection}.

*/ final class RemoteInputConnectionImpl extends IRemoteInputConnection.Stub { private static final String TAG = "RemoteInputConnectionImpl"; private static final boolean DEBUG = false; /** * An upper limit of calling {@link InputConnection#endBatchEdit()}. * *

This is a safeguard against broken {@link InputConnection#endBatchEdit()} implementations, * which are real as we've seen in Bug 208941904. If the retry count reaches to the number * defined here, we fall back into {@link InputMethodManager#restartInput(View)} as a * workaround.

*/ private static final int MAX_END_BATCH_EDIT_RETRY = 16; /** * A lightweight per-process type cache to remember classes that never returns {@code false} * from {@link InputConnection#endBatchEdit()}. The implementation is optimized for simplicity * and speed with accepting false-negatives in {@link #contains(Class)}. */ private static final class KnownAlwaysTrueEndBatchEditCache { @Nullable private static volatile Class sElement; @Nullable private static volatile Class[] sArray; /** * Query if the specified {@link InputConnection} implementation is known to be broken, with * allowing false-negative results. * * @param klass An implementation class of {@link InputConnection} to be tested. * @return {@code true} if the specified type was passed to {@link #add(Class)}. * Note that there is a chance that you still receive {@code false} even if you * called {@link #add(Class)} (false-negative). */ @AnyThread static boolean contains(@NonNull Class klass) { if (klass == sElement) { return true; } final Class[] array = sArray; if (array == null) { return false; } for (Class item : array) { if (item == klass) { return true; } } return false; } /** * Try to remember the specified {@link InputConnection} implementation as a known bad. * *

There is a chance that calling this method can accidentally overwrite existing * cache entries. See the document of {@link #contains(Class)} for details.

* * @param klass The implementation class of {@link InputConnection} to be remembered. */ @AnyThread static void add(@NonNull Class klass) { if (sElement == null) { // OK to accidentally overwrite an existing element that was set by another thread. sElement = klass; return; } final Class[] array = sArray; final int arraySize = array != null ? array.length : 0; final Class[] newArray = new Class[arraySize + 1]; for (int i = 0; i < arraySize; ++i) { newArray[i] = array[i]; } newArray[arraySize] = klass; // OK to accidentally overwrite an existing array that was set by another thread. sArray = newArray; } } @Retention(SOURCE) private @interface Dispatching { boolean cancellable(); } @NonNull private final AtomicReference mInputConnectionRef; @NonNull private final AtomicBoolean mDeactivateRequested = new AtomicBoolean(false); @NonNull private final Looper mLooper; private final Handler mH; private final InputMethodManager mParentInputMethodManager; private final WeakReference mServedView; private final AtomicInteger mCurrentSessionId = new AtomicInteger(0); private final AtomicBoolean mHasPendingInvalidation = new AtomicBoolean(); private final AtomicBoolean mIsCursorAnchorInfoMonitoring = new AtomicBoolean(false); private final AtomicBoolean mHasPendingImmediateCursorAnchorInfoUpdate = new AtomicBoolean(false); private CancellationSignalBeamer.Receiver mBeamer; RemoteInputConnectionImpl(@NonNull Looper looper, @NonNull InputConnection inputConnection, @NonNull InputMethodManager inputMethodManager, @Nullable View servedView) { mInputConnectionRef = new AtomicReference<>(inputConnection); mLooper = looper; mH = new Handler(mLooper); mParentInputMethodManager = inputMethodManager; mServedView = new WeakReference<>(servedView); } /** * @return {@link InputConnection} to which incoming IPCs will be dispatched. */ @Nullable public InputConnection getInputConnection() { return mInputConnectionRef.get(); } /** * @return {@code true} if there is a pending {@link InputMethodManager#invalidateInput(View)} * call. */ public boolean hasPendingInvalidation() { return mHasPendingInvalidation.get(); } /** * @return {@code true} until the target {@link InputConnection} receives * {@link InputConnection#closeConnection()} as a result of {@link #deactivate()}. */ private boolean isFinished() { return mInputConnectionRef.get() == null; } private View getServedView() { return mServedView.get(); } /** * Queries if the given {@link View} is associated with this {@link RemoteInputConnectionImpl} * or not. * * @param view {@link View}. * @return {@code true} if the given {@link View} is not null and is associated with this * {@link RemoteInputConnectionImpl}. */ @AnyThread public boolean isAssociatedWith(@Nullable View view) { if (view == null) { return false; } return mServedView.refersTo(view); } /** * Gets and resets {@link #mHasPendingImmediateCursorAnchorInfoUpdate}. * *

Calling this method resets {@link #mHasPendingImmediateCursorAnchorInfoUpdate}. This * means that the second call of this method returns {@code false} unless the IME requests * {@link android.view.inputmethod.CursorAnchorInfo} again with * {@link InputConnection#CURSOR_UPDATE_IMMEDIATE} flag.

* * @return {@code true} if there is any pending request for * {@link android.view.inputmethod.CursorAnchorInfo} with * {@link InputConnection#CURSOR_UPDATE_IMMEDIATE} flag. */ @AnyThread public boolean resetHasPendingImmediateCursorAnchorInfoUpdate() { return mHasPendingImmediateCursorAnchorInfoUpdate.getAndSet(false); } /** * @return {@code true} if there is any active request for * {@link android.view.inputmethod.CursorAnchorInfo} with * {@link InputConnection#CURSOR_UPDATE_MONITOR} flag. */ @AnyThread public boolean isCursorAnchorInfoMonitoring() { return mIsCursorAnchorInfoMonitoring.get(); } /** * Schedule a task to execute * {@link InputMethodManager#doInvalidateInput(RemoteInputConnectionImpl, TextSnapshot, int)} * on the associated Handler if not yet scheduled. * *

By calling {@link InputConnection#takeSnapshot()} directly from the message loop, we can * make sure that application code is not modifying text context in a reentrant manner.

*/ public void scheduleInvalidateInput() { if (mHasPendingInvalidation.compareAndSet(false, true)) { final int nextSessionId = mCurrentSessionId.incrementAndGet(); // By calling InputConnection#takeSnapshot() directly from the message loop, we can make // sure that application code is not modifying text context in a reentrant manner. // e.g. We may see methods like EditText#setText() in the callstack here. mH.post(() -> { try { if (isFinished()) { // This is a stale request, which can happen. No need to show a warning // because this situation itself is not an error. return; } final InputConnection ic = getInputConnection(); if (ic == null) { // This is a stale request, which can happen. No need to show a warning // because this situation itself is not an error. return; } final View view = getServedView(); if (view == null) { // This is a stale request, which can happen. No need to show a warning // because this situation itself is not an error. return; } final Class icClass = ic.getClass(); boolean alwaysTrueEndBatchEditDetected = KnownAlwaysTrueEndBatchEditCache.contains(icClass); if (!alwaysTrueEndBatchEditDetected) { // Clean up composing text and batch edit. final boolean supportsBatchEdit = ic.beginBatchEdit(); ic.finishComposingText(); if (supportsBatchEdit) { // Also clean up batch edit. int retryCount = 0; while (true) { if (!ic.endBatchEdit()) { break; } ++retryCount; if (retryCount > MAX_END_BATCH_EDIT_RETRY) { Log.e(TAG, icClass.getTypeName() + "#endBatchEdit() still" + " returns true even after retrying " + MAX_END_BATCH_EDIT_RETRY + " times. Falling back to" + " InputMethodManager#restartInput(View)"); alwaysTrueEndBatchEditDetected = true; KnownAlwaysTrueEndBatchEditCache.add(icClass); break; } } } } if (!alwaysTrueEndBatchEditDetected) { final TextSnapshot textSnapshot = ic.takeSnapshot(); if (textSnapshot != null && mParentInputMethodManager.doInvalidateInput( this, textSnapshot, nextSessionId)) { return; } } mParentInputMethodManager.restartInput(view); } finally { mHasPendingInvalidation.set(false); } }); } } /** * Called when this object needs to be permanently deactivated. * *

Multiple invocations will be simply ignored.

*/ @Dispatching(cancellable = false) public void deactivate() { if (mDeactivateRequested.getAndSet(true)) { // This is a small performance optimization. Still only the 1st call of // deactivate() will take effect. return; } dispatch(() -> { Trace.traceBegin(Trace.TRACE_TAG_INPUT, "InputConnection#closeConnection"); try { InputConnection ic = getInputConnection(); if (ic == null) { return; } try { ic.closeConnection(); } catch (AbstractMethodError ignored) { // TODO(b/199934664): See if we can remove this by providing a default impl. } } finally { mInputConnectionRef.set(null); Trace.traceEnd(Trace.TRACE_TAG_INPUT); } // Notify the app that the InputConnection was closed. final View servedView = mServedView.get(); if (servedView != null) { final Handler handler = servedView.getHandler(); // The handler is null if the view is already detached. When that's the case, for // now, we simply don't dispatch this callback. if (handler != null) { if (DEBUG) { Log.v(TAG, "Calling View.onInputConnectionClosed: view=" + servedView); } if (handler.getLooper().isCurrentThread()) { servedView.onInputConnectionClosedInternal(); } else { handler.post(servedView::onInputConnectionClosedInternal); } } final ViewRootImpl viewRoot = servedView.getViewRootImpl(); if (viewRoot != null) { viewRoot.getHandwritingInitiator().onInputConnectionClosed(servedView); } } }); } @Dispatching(cancellable = false) @Override public void cancelCancellationSignal(IBinder token) { if (mBeamer == null) { return; } dispatch(() -> { mBeamer.cancel(token); }); } @Override public void forgetCancellationSignal(IBinder token) { if (mBeamer == null) { return; } dispatch(() -> { mBeamer.forget(token); }); } @Override public String toString() { return "RemoteInputConnectionImpl{" + "connection=" + getInputConnection() + " mDeactivateRequested=" + mDeactivateRequested.get() + " mServedView=" + mServedView.get() + "}"; } /** * Called by {@link InputMethodManager} to dump the editor state. * * @param proto {@link ProtoOutputStream} to which the editor state should be dumped. * @param fieldId the ID to be passed to * {@link DumpableInputConnection#dumpDebug(ProtoOutputStream, long)}. */ public void dumpDebug(ProtoOutputStream proto, long fieldId) { final InputConnection ic = mInputConnectionRef.get(); // Check that the call is initiated in the target thread of the current InputConnection // {@link InputConnection#getHandler} since the messages to IInputConnectionWrapper are // executed on this thread. Otherwise the messages are dispatched to the correct thread // in IInputConnectionWrapper, but this is not wanted while dumping, for performance // reasons. if ((ic instanceof DumpableInputConnection) && mLooper.isCurrentThread()) { ((DumpableInputConnection) ic).dumpDebug(proto, fieldId); } } /** * Invoke {@link InputConnection#reportFullscreenMode(boolean)} or schedule it on the target * thread associated with {@link InputConnection#getHandler()}. * * @param enabled the parameter to be passed to * {@link InputConnection#reportFullscreenMode(boolean)}. */ @Dispatching(cancellable = false) public void dispatchReportFullscreenMode(boolean enabled) { dispatch(() -> { final InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { return; } ic.reportFullscreenMode(enabled); }); } @Dispatching(cancellable = true) @Override public void getTextAfterCursor(InputConnectionCommandHeader header, int length, int flags, AndroidFuture future /* T=CharSequence */) { dispatchWithTracing("getTextAfterCursor", future, () -> { if (header.mSessionId != mCurrentSessionId.get()) { return null; // cancelled } final InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "getTextAfterCursor on inactive InputConnection"); return null; } if (length < 0) { Log.i(TAG, "Returning null to getTextAfterCursor due to an invalid length=" + length); return null; } return ic.getTextAfterCursor(length, flags); }, useImeTracing() ? result -> buildGetTextAfterCursorProto(length, flags, result) : null); } @Dispatching(cancellable = true) @Override public void getTextBeforeCursor(InputConnectionCommandHeader header, int length, int flags, AndroidFuture future /* T=CharSequence */) { dispatchWithTracing("getTextBeforeCursor", future, () -> { if (header.mSessionId != mCurrentSessionId.get()) { return null; // cancelled } final InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "getTextBeforeCursor on inactive InputConnection"); return null; } if (length < 0) { Log.i(TAG, "Returning null to getTextBeforeCursor due to an invalid length=" + length); return null; } return ic.getTextBeforeCursor(length, flags); }, useImeTracing() ? result -> buildGetTextBeforeCursorProto(length, flags, result) : null); } @Dispatching(cancellable = true) @Override public void getSelectedText(InputConnectionCommandHeader header, int flags, AndroidFuture future /* T=CharSequence */) { dispatchWithTracing("getSelectedText", future, () -> { if (header.mSessionId != mCurrentSessionId.get()) { return null; // cancelled } final InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "getSelectedText on inactive InputConnection"); return null; } try { return ic.getSelectedText(flags); } catch (AbstractMethodError ignored) { // TODO(b/199934664): See if we can remove this by providing a default impl. return null; } }, useImeTracing() ? result -> buildGetSelectedTextProto(flags, result) : null); } @Dispatching(cancellable = true) @Override public void getSurroundingText(InputConnectionCommandHeader header, int beforeLength, int afterLength, int flags, AndroidFuture future /* T=SurroundingText */) { dispatchWithTracing("getSurroundingText", future, () -> { if (header.mSessionId != mCurrentSessionId.get()) { return null; // cancelled } final InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "getSurroundingText on inactive InputConnection"); return null; } if (beforeLength < 0) { Log.i(TAG, "Returning null to getSurroundingText due to an invalid" + " beforeLength=" + beforeLength); return null; } if (afterLength < 0) { Log.i(TAG, "Returning null to getSurroundingText due to an invalid" + " afterLength=" + afterLength); return null; } return ic.getSurroundingText(beforeLength, afterLength, flags); }, useImeTracing() ? result -> buildGetSurroundingTextProto( beforeLength, afterLength, flags, result) : null); } @Dispatching(cancellable = true) @Override public void getCursorCapsMode(InputConnectionCommandHeader header, int reqModes, AndroidFuture future /* T=Integer */) { dispatchWithTracing("getCursorCapsMode", future, () -> { if (header.mSessionId != mCurrentSessionId.get()) { return 0; // cancelled } final InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "getCursorCapsMode on inactive InputConnection"); return 0; } return ic.getCursorCapsMode(reqModes); }, useImeTracing() ? result -> buildGetCursorCapsModeProto(reqModes, result) : null); } @Dispatching(cancellable = true) @Override public void getExtractedText(InputConnectionCommandHeader header, ExtractedTextRequest request, int flags, AndroidFuture future /* T=ExtractedText */) { dispatchWithTracing("getExtractedText", future, () -> { if (header.mSessionId != mCurrentSessionId.get()) { return null; // cancelled } final InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "getExtractedText on inactive InputConnection"); return null; } return ic.getExtractedText(request, flags); }, useImeTracing() ? result -> buildGetExtractedTextProto(request, flags, result) : null); } @Dispatching(cancellable = true) @Override public void commitText(InputConnectionCommandHeader header, CharSequence text, int newCursorPosition) { dispatchWithTracing("commitText", () -> { if (header.mSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "commitText on inactive InputConnection"); return; } ic.commitText(text, newCursorPosition); }); } @Dispatching(cancellable = true) @Override public void commitTextWithTextAttribute(InputConnectionCommandHeader header, CharSequence text, int newCursorPosition, @Nullable TextAttribute textAttribute) { dispatchWithTracing("commitTextWithTextAttribute", () -> { if (header.mSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "commitText on inactive InputConnection"); return; } ic.commitText(text, newCursorPosition, textAttribute); }); } @Dispatching(cancellable = true) @Override public void commitCompletion(InputConnectionCommandHeader header, CompletionInfo text) { dispatchWithTracing("commitCompletion", () -> { if (header.mSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "commitCompletion on inactive InputConnection"); return; } ic.commitCompletion(text); }); } @Dispatching(cancellable = true) @Override public void commitCorrection(InputConnectionCommandHeader header, CorrectionInfo info) { dispatchWithTracing("commitCorrection", () -> { if (header.mSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "commitCorrection on inactive InputConnection"); return; } try { ic.commitCorrection(info); } catch (AbstractMethodError ignored) { // TODO(b/199934664): See if we can remove this by providing a default impl. } }); } @Dispatching(cancellable = true) @Override public void setSelection(InputConnectionCommandHeader header, int start, int end) { dispatchWithTracing("setSelection", () -> { if (header.mSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "setSelection on inactive InputConnection"); return; } ic.setSelection(start, end); }); } @Dispatching(cancellable = true) @Override public void performEditorAction(InputConnectionCommandHeader header, int id) { dispatchWithTracing("performEditorAction", () -> { if (header.mSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "performEditorAction on inactive InputConnection"); return; } ic.performEditorAction(id); }); } @Dispatching(cancellable = true) @Override public void performContextMenuAction(InputConnectionCommandHeader header, int id) { dispatchWithTracing("performContextMenuAction", () -> { if (header.mSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "performContextMenuAction on inactive InputConnection"); return; } ic.performContextMenuAction(id); }); } @Dispatching(cancellable = true) @Override public void setComposingRegion(InputConnectionCommandHeader header, int start, int end) { dispatchWithTracing("setComposingRegion", () -> { if (header.mSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "setComposingRegion on inactive InputConnection"); return; } try { ic.setComposingRegion(start, end); } catch (AbstractMethodError ignored) { // TODO(b/199934664): See if we can remove this by providing a default impl. } }); } @Dispatching(cancellable = true) @Override public void setComposingRegionWithTextAttribute(InputConnectionCommandHeader header, int start, int end, @Nullable TextAttribute textAttribute) { dispatchWithTracing("setComposingRegionWithTextAttribute", () -> { if (header.mSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "setComposingRegion on inactive InputConnection"); return; } ic.setComposingRegion(start, end, textAttribute); }); } @Dispatching(cancellable = true) @Override public void setComposingText(InputConnectionCommandHeader header, CharSequence text, int newCursorPosition) { dispatchWithTracing("setComposingText", () -> { if (header.mSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "setComposingText on inactive InputConnection"); return; } ic.setComposingText(text, newCursorPosition); }); } @Dispatching(cancellable = true) @Override public void setComposingTextWithTextAttribute(InputConnectionCommandHeader header, CharSequence text, int newCursorPosition, @Nullable TextAttribute textAttribute) { dispatchWithTracing("setComposingTextWithTextAttribute", () -> { if (header.mSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "setComposingText on inactive InputConnection"); return; } ic.setComposingText(text, newCursorPosition, textAttribute); }); } /** * Dispatches {@link InputConnection#finishComposingText()}. * *

This method is intended to be called only from {@link InputMethodManager}.

*/ @Dispatching(cancellable = true) public void finishComposingTextFromImm() { final int currentSessionId = mCurrentSessionId.get(); dispatchWithTracing("finishComposingTextFromImm", () -> { if (isFinished()) { // In this case, #finishComposingText() is guaranteed to be called already. // There should be no negative impact if we ignore this call silently. if (DEBUG) { Log.w(TAG, "Bug 35301295: Redundant finishComposingTextFromImm."); } return; } if (currentSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "finishComposingTextFromImm on inactive InputConnection"); return; } ic.finishComposingText(); }); } @Dispatching(cancellable = true) @Override public void finishComposingText(InputConnectionCommandHeader header) { dispatchWithTracing("finishComposingText", () -> { if (isFinished()) { // In this case, #finishComposingText() is guaranteed to be called already. // There should be no negative impact if we ignore this call silently. if (DEBUG) { Log.w(TAG, "Bug 35301295: Redundant finishComposingText."); } return; } if (header.mSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null && mDeactivateRequested.get()) { Log.w(TAG, "finishComposingText on inactive InputConnection"); return; } ic.finishComposingText(); }); } @Dispatching(cancellable = true) @Override public void sendKeyEvent(InputConnectionCommandHeader header, KeyEvent event) { dispatchWithTracing("sendKeyEvent", () -> { if (header.mSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "sendKeyEvent on inactive InputConnection"); return; } ic.sendKeyEvent(event); }); } @Dispatching(cancellable = true) @Override public void clearMetaKeyStates(InputConnectionCommandHeader header, int states) { dispatchWithTracing("clearMetaKeyStates", () -> { if (header.mSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "clearMetaKeyStates on inactive InputConnection"); return; } ic.clearMetaKeyStates(states); }); } @Dispatching(cancellable = true) @Override public void deleteSurroundingText(InputConnectionCommandHeader header, int beforeLength, int afterLength) { dispatchWithTracing("deleteSurroundingText", () -> { if (header.mSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "deleteSurroundingText on inactive InputConnection"); return; } ic.deleteSurroundingText(beforeLength, afterLength); }); } @Dispatching(cancellable = true) @Override public void deleteSurroundingTextInCodePoints(InputConnectionCommandHeader header, int beforeLength, int afterLength) { dispatchWithTracing("deleteSurroundingTextInCodePoints", () -> { if (header.mSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "deleteSurroundingTextInCodePoints on inactive InputConnection"); return; } try { ic.deleteSurroundingTextInCodePoints(beforeLength, afterLength); } catch (AbstractMethodError ignored) { // TODO(b/199934664): See if we can remove this by providing a default impl. } }); } @Dispatching(cancellable = true) @Override public void beginBatchEdit(InputConnectionCommandHeader header) { dispatchWithTracing("beginBatchEdit", () -> { if (header.mSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "beginBatchEdit on inactive InputConnection"); return; } ic.beginBatchEdit(); }); } @Dispatching(cancellable = true) @Override public void endBatchEdit(InputConnectionCommandHeader header) { dispatchWithTracing("endBatchEdit", () -> { if (header.mSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "endBatchEdit on inactive InputConnection"); return; } ic.endBatchEdit(); }); } @Dispatching(cancellable = true) @Override public void performSpellCheck(InputConnectionCommandHeader header) { dispatchWithTracing("performSpellCheck", () -> { if (header.mSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "performSpellCheck on inactive InputConnection"); return; } ic.performSpellCheck(); }); } @Dispatching(cancellable = true) @Override public void performPrivateCommand(InputConnectionCommandHeader header, String action, Bundle data) { dispatchWithTracing("performPrivateCommand", () -> { if (header.mSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "performPrivateCommand on inactive InputConnection"); return; } ic.performPrivateCommand(action, data); }); } @Dispatching(cancellable = true) @Override public void performHandwritingGesture( InputConnectionCommandHeader header, ParcelableHandwritingGesture gestureContainer, ResultReceiver resultReceiver) { final HandwritingGesture gesture = gestureContainer.get(); if (gesture instanceof CancellableHandwritingGesture) { // For cancellable gestures, unbeam and save the CancellationSignal. CancellableHandwritingGesture cancellableGesture = (CancellableHandwritingGesture) gesture; cancellableGesture.unbeamCancellationSignal(getCancellationSignalBeamer()); if (cancellableGesture.getCancellationSignal() != null && cancellableGesture.getCancellationSignal().isCanceled()) { // Send result for canceled operations. if (resultReceiver != null) { resultReceiver.send( InputConnection.HANDWRITING_GESTURE_RESULT_CANCELLED, null); } return; } } dispatchWithTracing("performHandwritingGesture", () -> { if (header.mSessionId != mCurrentSessionId.get()) { if (resultReceiver != null) { resultReceiver.send( InputConnection.HANDWRITING_GESTURE_RESULT_CANCELLED, null); } return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "performHandwritingGesture on inactive InputConnection"); if (resultReceiver != null) { resultReceiver.send( InputConnection.HANDWRITING_GESTURE_RESULT_CANCELLED, null); } return; } // TODO(210039666): implement Cleaner to return HANDWRITING_GESTURE_RESULT_UNKNOWN if // editor doesn't return any type. ic.performHandwritingGesture( gesture, resultReceiver != null ? Runnable::run : null, resultReceiver != null ? (resultCode) -> resultReceiver.send(resultCode, null /* resultData */) : null); }); } @Dispatching(cancellable = true) @Override public void previewHandwritingGesture( InputConnectionCommandHeader header, ParcelableHandwritingGesture gestureContainer, IBinder cancellationSignalToken) { final CancellationSignal cancellationSignal = cancellationSignalToken != null ? getCancellationSignalBeamer().unbeam(cancellationSignalToken) : null; // Previews always use PreviewableHandwritingGesture but if incorrectly wrong class is // passed, ClassCastException will be sent back to caller. final PreviewableHandwritingGesture gesture = (PreviewableHandwritingGesture) gestureContainer.get(); dispatchWithTracing("previewHandwritingGesture", () -> { if (header.mSessionId != mCurrentSessionId.get() || (cancellationSignal != null && cancellationSignal.isCanceled())) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "previewHandwritingGesture on inactive InputConnection"); return; // cancelled } ic.previewHandwritingGesture(gesture, cancellationSignal); }); } private CancellationSignalBeamer.Receiver getCancellationSignalBeamer() { if (mBeamer != null) { return mBeamer; } mBeamer = new CancellationSignalBeamer.Receiver(true /* cancelOnSenderDeath */); return mBeamer; } @Dispatching(cancellable = true) @Override public void requestCursorUpdates(InputConnectionCommandHeader header, int cursorUpdateMode, int imeDisplayId, AndroidFuture future /* T=Boolean */) { dispatchWithTracing("requestCursorUpdates", future, () -> { if (header.mSessionId != mCurrentSessionId.get()) { return false; // cancelled } return requestCursorUpdatesInternal( cursorUpdateMode, 0 /* cursorUpdateFilter */, imeDisplayId); }); } @Dispatching(cancellable = true) @Override public void requestCursorUpdatesWithFilter(InputConnectionCommandHeader header, int cursorUpdateMode, int cursorUpdateFilter, int imeDisplayId, AndroidFuture future /* T=Boolean */) { dispatchWithTracing("requestCursorUpdates", future, () -> { if (header.mSessionId != mCurrentSessionId.get()) { return false; // cancelled } return requestCursorUpdatesInternal( cursorUpdateMode, cursorUpdateFilter, imeDisplayId); }); } private boolean requestCursorUpdatesInternal( @InputConnection.CursorUpdateMode int cursorUpdateMode, @InputConnection.CursorUpdateFilter int cursorUpdateFilter, int imeDisplayId) { final InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "requestCursorUpdates on inactive InputConnection"); return false; } if (mParentInputMethodManager.mRequestCursorUpdateDisplayIdCheck.get() && mParentInputMethodManager.getDisplayId() != imeDisplayId) { // requestCursorUpdates() is not currently supported across displays. return false; } final boolean hasImmediate = (cursorUpdateMode & InputConnection.CURSOR_UPDATE_IMMEDIATE) != 0; final boolean hasMonitoring = (cursorUpdateMode & InputConnection.CURSOR_UPDATE_MONITOR) != 0; boolean result = false; try { result = ic.requestCursorUpdates(cursorUpdateMode, cursorUpdateFilter); return result; } catch (AbstractMethodError ignored) { // TODO(b/199934664): See if we can remove this by providing a default impl. return false; } finally { mHasPendingImmediateCursorAnchorInfoUpdate.set(result && hasImmediate); mIsCursorAnchorInfoMonitoring.set(result && hasMonitoring); } } @Dispatching(cancellable = true) @Override public void requestTextBoundsInfo( InputConnectionCommandHeader header, RectF bounds, @NonNull ResultReceiver resultReceiver) { dispatchWithTracing("requestTextBoundsInfo", () -> { if (header.mSessionId != mCurrentSessionId.get()) { resultReceiver.send(TextBoundsInfoResult.CODE_CANCELLED, null); return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "requestTextBoundsInfo on inactive InputConnection"); resultReceiver.send(TextBoundsInfoResult.CODE_CANCELLED, null); return; } ic.requestTextBoundsInfo( bounds, Runnable::run, (textBoundsInfoResult) -> { final int resultCode = textBoundsInfoResult.getResultCode(); final TextBoundsInfo textBoundsInfo = textBoundsInfoResult.getTextBoundsInfo(); resultReceiver.send(resultCode, textBoundsInfo == null ? null : textBoundsInfo.toBundle()); }); }); } @Dispatching(cancellable = true) @Override public void commitContent(InputConnectionCommandHeader header, InputContentInfo inputContentInfo, int flags, Bundle opts, AndroidFuture future /* T=Boolean */) { final int imeUid = Binder.getCallingUid(); dispatchWithTracing("commitContent", future, () -> { // Check if the originator IME has the right permissions try { final int contentUriOwnerUserId = ContentProvider.getUserIdFromUri( inputContentInfo.getContentUri(), UserHandle.getUserId(imeUid)); final Uri contentUriWithoutUserId = ContentProvider.getUriWithoutUserId( inputContentInfo.getContentUri()); UriGrantsManager.getService().checkGrantUriPermission_ignoreNonSystem(imeUid, null, contentUriWithoutUserId, Intent.FLAG_GRANT_READ_URI_PERMISSION, contentUriOwnerUserId); } catch (Exception e) { Log.w(TAG, "commitContent with invalid Uri permission from IME:", e); return false; } if (header.mSessionId != mCurrentSessionId.get()) { return false; // cancelled } final InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "commitContent on inactive InputConnection"); return false; } if (inputContentInfo == null || !inputContentInfo.validate()) { Log.w(TAG, "commitContent with invalid inputContentInfo=" + inputContentInfo); return false; } try { return ic.commitContent(inputContentInfo, flags, opts); } catch (AbstractMethodError ignored) { // TODO(b/199934664): See if we can remove this by providing a default impl. return false; } }); } @Dispatching(cancellable = true) @Override public void setImeConsumesInput(InputConnectionCommandHeader header, boolean imeConsumesInput) { dispatchWithTracing("setImeConsumesInput", () -> { if (header.mSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "setImeConsumesInput on inactive InputConnection"); return; } ic.setImeConsumesInput(imeConsumesInput); }); } @Dispatching(cancellable = true) @Override public void replaceText( InputConnectionCommandHeader header, int start, int end, @NonNull CharSequence text, int newCursorPosition, @Nullable TextAttribute textAttribute) { dispatchWithTracing( "replaceText", () -> { if (header.mSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "replaceText on inactive InputConnection"); return; } ic.replaceText(start, end, text, newCursorPosition, textAttribute); }); } private final IRemoteAccessibilityInputConnection mAccessibilityInputConnection = new IRemoteAccessibilityInputConnection.Stub() { @Dispatching(cancellable = true) @Override public void commitText(InputConnectionCommandHeader header, CharSequence text, int newCursorPosition, @Nullable TextAttribute textAttribute) { dispatchWithTracing("commitTextFromA11yIme", () -> { if (header.mSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "commitText on inactive InputConnection"); return; } // A11yIME's commitText() also triggers finishComposingText() automatically. ic.beginBatchEdit(); ic.finishComposingText(); ic.commitText(text, newCursorPosition, textAttribute); ic.endBatchEdit(); }); } @Dispatching(cancellable = true) @Override public void setSelection(InputConnectionCommandHeader header, int start, int end) { dispatchWithTracing("setSelectionFromA11yIme", () -> { if (header.mSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "setSelection on inactive InputConnection"); return; } ic.setSelection(start, end); }); } @Dispatching(cancellable = true) @Override public void getSurroundingText(InputConnectionCommandHeader header, int beforeLength, int afterLength, int flags, AndroidFuture future /* T=SurroundingText */) { dispatchWithTracing("getSurroundingTextFromA11yIme", future, () -> { if (header.mSessionId != mCurrentSessionId.get()) { return null; // cancelled } final InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "getSurroundingText on inactive InputConnection"); return null; } if (beforeLength < 0) { Log.i(TAG, "Returning null to getSurroundingText due to an invalid" + " beforeLength=" + beforeLength); return null; } if (afterLength < 0) { Log.i(TAG, "Returning null to getSurroundingText due to an invalid" + " afterLength=" + afterLength); return null; } return ic.getSurroundingText(beforeLength, afterLength, flags); }, useImeTracing() ? result -> buildGetSurroundingTextProto( beforeLength, afterLength, flags, result) : null); } @Dispatching(cancellable = true) @Override public void deleteSurroundingText(InputConnectionCommandHeader header, int beforeLength, int afterLength) { dispatchWithTracing("deleteSurroundingTextFromA11yIme", () -> { if (header.mSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "deleteSurroundingText on inactive InputConnection"); return; } ic.deleteSurroundingText(beforeLength, afterLength); }); } @Dispatching(cancellable = true) @Override public void sendKeyEvent(InputConnectionCommandHeader header, KeyEvent event) { dispatchWithTracing("sendKeyEventFromA11yIme", () -> { if (header.mSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "sendKeyEvent on inactive InputConnection"); return; } ic.sendKeyEvent(event); }); } @Dispatching(cancellable = true) @Override public void performEditorAction(InputConnectionCommandHeader header, int id) { dispatchWithTracing("performEditorActionFromA11yIme", () -> { if (header.mSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "performEditorAction on inactive InputConnection"); return; } ic.performEditorAction(id); }); } @Dispatching(cancellable = true) @Override public void performContextMenuAction(InputConnectionCommandHeader header, int id) { dispatchWithTracing("performContextMenuActionFromA11yIme", () -> { if (header.mSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "performContextMenuAction on inactive InputConnection"); return; } ic.performContextMenuAction(id); }); } @Dispatching(cancellable = true) @Override public void getCursorCapsMode(InputConnectionCommandHeader header, int reqModes, AndroidFuture future /* T=Integer */) { dispatchWithTracing("getCursorCapsModeFromA11yIme", future, () -> { if (header.mSessionId != mCurrentSessionId.get()) { return 0; // cancelled } final InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "getCursorCapsMode on inactive InputConnection"); return 0; } return ic.getCursorCapsMode(reqModes); }, useImeTracing() ? result -> buildGetCursorCapsModeProto(reqModes, result) : null); } @Dispatching(cancellable = true) @Override public void clearMetaKeyStates(InputConnectionCommandHeader header, int states) { dispatchWithTracing("clearMetaKeyStatesFromA11yIme", () -> { if (header.mSessionId != mCurrentSessionId.get()) { return; // cancelled } InputConnection ic = getInputConnection(); if (ic == null || mDeactivateRequested.get()) { Log.w(TAG, "clearMetaKeyStates on inactive InputConnection"); return; } ic.clearMetaKeyStates(states); }); } }; /** * @return {@link IRemoteAccessibilityInputConnection} associated with this object. */ public IRemoteAccessibilityInputConnection asIRemoteAccessibilityInputConnection() { return mAccessibilityInputConnection; } private void dispatch(@NonNull Runnable runnable) { // If we are calling this from the target thread, then we can call right through. // Otherwise, we need to send the message to the target thread. if (mLooper.isCurrentThread()) { runnable.run(); return; } mH.post(runnable); } private void dispatchWithTracing(@NonNull String methodName, @NonNull Runnable runnable) { final Runnable actualRunnable; if (Trace.isTagEnabled(Trace.TRACE_TAG_INPUT)) { actualRunnable = () -> { Trace.traceBegin(Trace.TRACE_TAG_INPUT, "InputConnection#" + methodName); try { runnable.run(); } finally { Trace.traceEnd(Trace.TRACE_TAG_INPUT); } }; } else { actualRunnable = runnable; } dispatch(actualRunnable); } private void dispatchWithTracing(@NonNull String methodName, @NonNull AndroidFuture untypedFuture, @NonNull Supplier supplier) { dispatchWithTracing(methodName, untypedFuture, supplier, null /* dumpProtoProvider */); } private void dispatchWithTracing(@NonNull String methodName, @NonNull AndroidFuture untypedFuture, @NonNull Supplier supplier, @Nullable Function dumpProtoProvider) { @SuppressWarnings("unchecked") final AndroidFuture future = untypedFuture; dispatchWithTracing(methodName, () -> { final T result; try { result = supplier.get(); } catch (Throwable throwable) { future.completeExceptionally(throwable); throw throwable; } future.complete(result); if (dumpProtoProvider != null) { final byte[] icProto = dumpProtoProvider.apply(result); ImeTracing.getInstance().triggerClientDump( TAG + "#" + methodName, mParentInputMethodManager, icProto); } }); } private static boolean useImeTracing() { return ImeTracing.getInstance().isEnabled(); } }