diff --git a/core/api/current.txt b/core/api/current.txt index d7b17b0ff038d..aefe7751557d1 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -52442,6 +52442,7 @@ package android.view.inputmethod { method public boolean hideSoftInputFromWindow(android.os.IBinder, int); method public boolean hideSoftInputFromWindow(android.os.IBinder, int, android.os.ResultReceiver); method @Deprecated public void hideStatusIcon(android.os.IBinder); + method public void invalidateInput(@NonNull android.view.View); method public boolean isAcceptingText(); method public boolean isActive(android.view.View); method public boolean isActive(); diff --git a/core/java/android/inputmethodservice/AbstractInputMethodService.java b/core/java/android/inputmethodservice/AbstractInputMethodService.java index 4ecd15e002efd..75beacfb12f32 100644 --- a/core/java/android/inputmethodservice/AbstractInputMethodService.java +++ b/core/java/android/inputmethodservice/AbstractInputMethodService.java @@ -67,6 +67,16 @@ public abstract class AbstractInputMethodService extends WindowProviderService implements KeyEvent.Callback { private InputMethod mInputMethod; + /** + * @return {@link InputMethod} instance returned from {@link #onCreateInputMethodInterface()}. + * {@code null} if {@link #onCreateInputMethodInterface()} is not yet called. + * @hide + */ + @Nullable + protected final InputMethod getInputMethodInternal() { + return mInputMethod; + } + /** * Keep the strong reference to {@link InputMethodServiceInternal} to ensure that it will not be * garbage-collected until {@link AbstractInputMethodService} gets garbage-collected. diff --git a/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java b/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java index 5a517ee0b0eb6..eccbb403b3068 100644 --- a/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java +++ b/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java @@ -32,11 +32,13 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.CursorAnchorInfo; +import android.view.inputmethod.EditorInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.InputMethodSession; import com.android.internal.os.HandlerCaller; import com.android.internal.os.SomeArgs; +import com.android.internal.view.IInputContext; import com.android.internal.view.IInputMethodSession; class IInputMethodSessionWrapper extends IInputMethodSession.Stub @@ -54,6 +56,7 @@ class IInputMethodSessionWrapper extends IInputMethodSession.Stub private static final int DO_NOTIFY_IME_HIDDEN = 120; private static final int DO_REMOVE_IME_SURFACE = 130; private static final int DO_FINISH_INPUT = 140; + private static final int DO_INVALIDATE_INPUT = 150; @UnsupportedAppUsage @@ -142,6 +145,16 @@ class IInputMethodSessionWrapper extends IInputMethodSession.Stub mInputMethodSession.finishInput(); return; } + case DO_INVALIDATE_INPUT: { + final SomeArgs args = (SomeArgs) msg.obj; + try { + mInputMethodSession.invalidateInputInternal((EditorInfo) args.arg1, + (IInputContext) args.arg2, msg.arg1); + } finally { + args.recycle(); + } + return; + } } Log.w(TAG, "Unhandled message code: " + msg.what); } @@ -217,6 +230,12 @@ class IInputMethodSessionWrapper extends IInputMethodSession.Stub mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_FINISH_SESSION)); } + @Override + public void invalidateInput(EditorInfo editorInfo, IInputContext inputContext, int sessionId) { + mCaller.executeOrSendMessage(mCaller.obtainMessageIOO( + DO_INVALIDATE_INPUT, sessionId, editorInfo, inputContext)); + } + @Override public void finishInput() { mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_FINISH_INPUT)); diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index aa3c3611fc7da..e02a01e5887e8 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -135,6 +135,7 @@ import com.android.internal.inputmethod.ImeTracing; import com.android.internal.inputmethod.InputMethodPrivilegedOperations; import com.android.internal.inputmethod.InputMethodPrivilegedOperationsRegistry; import com.android.internal.view.IInlineSuggestionsRequestCallback; +import com.android.internal.view.IInputContext; import com.android.internal.view.InlineSuggestionsRequestInfo; import java.io.FileDescriptor; @@ -1063,8 +1064,30 @@ public class InputMethodService extends AbstractInputMethodService { public final void removeImeSurface() { InputMethodService.this.scheduleImeSurfaceRemoval(); } + + /** + * {@inheritDoc} + * @hide + */ + @Override + public final void invalidateInputInternal(@NonNull EditorInfo editorInfo, + @NonNull IInputContext inputContext, int sessionId) { + if (mStartedInputConnection instanceof RemoteInputConnection) { + final RemoteInputConnection ric = (RemoteInputConnection) mStartedInputConnection; + if (!ric.isSameConnection(inputContext)) { + // This is not an error, and can be safely ignored. + if (DEBUG) { + Log.d(TAG, "ignoring invalidateInput() due to context mismatch."); + } + return; + } + editorInfo.makeCompatible(getApplicationInfo().targetSdkVersion); + getInputMethodInternal().restartInput(new RemoteInputConnection(ric, sessionId), + editorInfo); + } + } } - + /** * Information about where interesting parts of the input method UI appear. */ diff --git a/core/java/android/inputmethodservice/RemoteInputConnection.java b/core/java/android/inputmethodservice/RemoteInputConnection.java index ed617afab96e9..9ef2579d04e20 100644 --- a/core/java/android/inputmethodservice/RemoteInputConnection.java +++ b/core/java/android/inputmethodservice/RemoteInputConnection.java @@ -103,6 +103,17 @@ final class RemoteInputConnection implements InputConnection { mCancellationGroup = cancellationGroup; } + @AnyThread + public boolean isSameConnection(@NonNull IInputContext inputContext) { + return mInvoker.isSameConnection(inputContext); + } + + RemoteInputConnection(@NonNull RemoteInputConnection original, int sessionId) { + mImsInternal = original.mImsInternal; + mInvoker = original.mInvoker.cloneWithSessionId(sessionId); + mCancellationGroup = original.mCancellationGroup; + } + /** * See {@link InputConnection#getTextAfterCursor(int, int)}. */ diff --git a/core/java/android/view/inputmethod/EditorInfo.java b/core/java/android/view/inputmethod/EditorInfo.java index 35e60b8b023df..74ca913766214 100644 --- a/core/java/android/view/inputmethod/EditorInfo.java +++ b/core/java/android/view/inputmethod/EditorInfo.java @@ -44,6 +44,7 @@ import android.view.View; import android.view.autofill.AutofillId; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.ArrayUtils; import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; @@ -584,6 +585,16 @@ public class EditorInfo implements InputType, Parcelable { setInitialSurroundingSubText(sourceText, /* subTextStart = */ 0); } + /** + * An internal variant of {@link #setInitialSurroundingText(CharSequence)}. + * + * @param surroundingText {@link SurroundingText} to be set. + * @hide + */ + public final void setInitialSurroundingTextInternal(@NonNull SurroundingText surroundingText) { + mInitialSurroundingText = surroundingText; + } + /** * Editors may use this method to provide initial input text to IMEs. As the surrounding text * could be used to provide various input assistance, we recommend editors to provide the @@ -971,6 +982,35 @@ public class EditorInfo implements InputType, Parcelable { } } + /** + * @return A deep copy of {@link EditorInfo}. + * @hide + */ + @NonNull + public final EditorInfo createCopyInternal() { + final EditorInfo newEditorInfo = new EditorInfo(); + newEditorInfo.inputType = inputType; + newEditorInfo.imeOptions = imeOptions; + newEditorInfo.privateImeOptions = privateImeOptions; + newEditorInfo.internalImeOptions = internalImeOptions; + newEditorInfo.actionLabel = TextUtils.stringOrSpannedString(actionLabel); + newEditorInfo.actionId = actionId; + newEditorInfo.initialSelStart = initialSelStart; + newEditorInfo.initialSelEnd = initialSelEnd; + newEditorInfo.initialCapsMode = initialCapsMode; + newEditorInfo.hintText = TextUtils.stringOrSpannedString(hintText); + newEditorInfo.label = TextUtils.stringOrSpannedString(label); + newEditorInfo.packageName = packageName; + newEditorInfo.autofillId = autofillId; + newEditorInfo.fieldId = fieldId; + newEditorInfo.fieldName = fieldName; + newEditorInfo.extras = extras != null ? extras.deepCopy() : null; + newEditorInfo.mInitialSurroundingText = mInitialSurroundingText; + newEditorInfo.hintLocales = hintLocales; + newEditorInfo.contentMimeTypes = ArrayUtils.cloneOrNull(contentMimeTypes); + return newEditorInfo; + } + /** * Used to package this object into a {@link Parcel}. * diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index e023ed5a82696..7566ceab334d5 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -1837,6 +1837,69 @@ public final class InputMethodManager { startInputInner(StartInputReason.APP_CALLED_RESTART_INPUT_API, null, 0, 0, 0); } + /** + * Sends an async signal to the IME to reset the currently served {@link InputConnection}. + * + * @param inputConnection the connection to be invalidated. + * @param textSnapshot {@link TextSnapshot} to be used to update {@link EditorInfo}. + * @param sessionId the session ID to be sent. + * @hide + */ + public void doInvalidateInput(@NonNull RemoteInputConnectionImpl inputConnection, + @NonNull TextSnapshot textSnapshot, int sessionId) { + synchronized (mH) { + if (mServedInputConnection != inputConnection || mCurrentTextBoxAttribute == null) { + // OK to ignore because the calling InputConnection is already abandoned. + return; + } + final EditorInfo editorInfo = mCurrentTextBoxAttribute.createCopyInternal(); + editorInfo.initialSelStart = mCursorSelStart = textSnapshot.getSelectionStart(); + editorInfo.initialSelEnd = mCursorSelEnd = textSnapshot.getSelectionEnd(); + mCursorCandStart = textSnapshot.getCompositionStart(); + mCursorCandEnd = textSnapshot.getCompositionEnd(); + editorInfo.initialCapsMode = textSnapshot.getCursorCapsMode(); + editorInfo.setInitialSurroundingTextInternal(textSnapshot.getSurroundingText()); + mCurrentInputMethodSession.invalidateInput(editorInfo, mServedInputConnection, + sessionId); + } + } + + /** + * Gives a hint to the system that the text associated with {@code view} is updated by something + * that is not an input method editor (IME), so that the system can cancel any pending text + * editing requests from the IME until it receives the new editing context such as surrounding + * text provided by {@link InputConnection#takeSnapshot()}. + * + *
When {@code view} does not support {@link InputConnection#takeSnapshot()} protocol, + * calling this method may trigger {@link View#onCreateInputConnection(EditorInfo)}.
+ * + *Unlike {@link #restartInput(View)}, this API does not immediately interact with + * {@link InputConnection}. Instead, the application may later receive + * {@link InputConnection#takeSnapshot()} as needed so that the system can capture new editing + * context for the IME. For instance, successive invocations of this API can be coerced into a + * single (or zero) callback of {@link InputConnection#takeSnapshot()}.
+ * + * @param view The view whose text has changed. + * @see #restartInput(View) + */ + public void invalidateInput(@NonNull View view) { + Objects.requireNonNull(view); + + // Re-dispatch if there is a context mismatch. + final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view); + if (fallbackImm != null) { + fallbackImm.invalidateInput(view); + return; + } + + synchronized (mH) { + if (mServedInputConnection == null || getServedViewLocked() != view) { + return; + } + mServedInputConnection.scheduleInvalidateInput(); + } + } + /** * Called when {@link DelegateImpl#startInput}, {@link #restartInput(View)}, * {@link #MSG_BIND} or {@link #MSG_UNBIND}. @@ -1929,7 +1992,7 @@ public final class InputMethodManager { } // Hook 'em up and let 'er rip. - mCurrentTextBoxAttribute = tba; + mCurrentTextBoxAttribute = tba.createCopyInternal(); mServedConnecting = false; if (mServedInputConnection != null) { @@ -2202,6 +2265,10 @@ public final class InputMethodManager { return; } + if (mServedInputConnection != null && mServedInputConnection.hasPendingInvalidation()) { + return; + } + if (mCursorSelStart != selStart || mCursorSelEnd != selEnd || mCursorCandStart != candidatesStart || mCursorCandEnd != candidatesEnd) { diff --git a/core/java/android/view/inputmethod/InputMethodSession.java b/core/java/android/view/inputmethod/InputMethodSession.java index 52c1cd4f628a2..a178ee8ee866e 100644 --- a/core/java/android/view/inputmethod/InputMethodSession.java +++ b/core/java/android/view/inputmethod/InputMethodSession.java @@ -18,11 +18,12 @@ package android.view.inputmethod; import android.graphics.Rect; import android.inputmethodservice.InputMethodService; -import android.os.Build; import android.os.Bundle; import android.view.KeyEvent; import android.view.MotionEvent; +import com.android.internal.view.IInputContext; + /** * The InputMethodSession interface provides the per-client functionality * of {@link InputMethod} that is safe to expose to applications. @@ -175,8 +176,8 @@ public interface InputMethodSession { * 0 or have the {@link InputMethodManager#HIDE_IMPLICIT_ONLY}, * {@link InputMethodManager#HIDE_NOT_ALWAYS} bit set. * - * @deprecated Starting in {@link Build.VERSION_CODES#S} the system no longer invokes this - * method, instead it explicitly shows or hides the IME. An {@code InputMethodService} + * @deprecated Starting in {@link android.os.Build.VERSION_CODES#S} the system no longer invokes + * this method, instead it explicitly shows or hides the IME. An {@code InputMethodService} * wishing to toggle its own visibility should instead invoke {@link * InputMethodService#requestShowSelf} or {@link InputMethodService#requestHideSelf} */ @@ -205,4 +206,20 @@ public interface InputMethodSession { * @hide */ public void removeImeSurface(); + + /** + * Called when {@code inputContext} is about to be reset with {@code sessionId}. + * + *The actual implementation should ignore if {@code inputContext} is no longer the current + * {@link InputConnection} due to a stale callback.
+ * + * @param editorInfo {@link EditorInfo} to be used + * @param inputContext specifies which {@link InputConnection} is being updated. + * @param sessionId the ID to be specified to + * {@link com.android.internal.inputmethod.InputConnectionCommandHeader}. + * @hide + */ + default void invalidateInputInternal(EditorInfo editorInfo, IInputContext inputContext, + int sessionId) { + } } diff --git a/core/java/android/view/inputmethod/InputMethodSessionWrapper.java b/core/java/android/view/inputmethod/InputMethodSessionWrapper.java index ef1814b7bdf2c..a1995202485fc 100644 --- a/core/java/android/view/inputmethod/InputMethodSessionWrapper.java +++ b/core/java/android/view/inputmethod/InputMethodSessionWrapper.java @@ -24,6 +24,7 @@ import android.os.Bundle; import android.os.RemoteException; import android.util.Log; +import com.android.internal.view.IInputContext; import com.android.internal.view.IInputMethodSession; /** @@ -142,6 +143,15 @@ final class InputMethodSessionWrapper { } } + @AnyThread + void invalidateInput(EditorInfo editorInfo, IInputContext inputContext, int sessionId) { + try { + mSession.invalidateInput(editorInfo, inputContext, sessionId); + } catch (RemoteException e) { + Log.w(TAG, "IME died", e); + } + } + /** * @return {@link IInputMethodSession#toString()} as a debug string. */ diff --git a/core/java/com/android/internal/inputmethod/IInputContextInvoker.java b/core/java/com/android/internal/inputmethod/IInputContextInvoker.java index 4dbd941b3ae07..e6fafe0ac56a4 100644 --- a/core/java/com/android/internal/inputmethod/IInputContextInvoker.java +++ b/core/java/com/android/internal/inputmethod/IInputContextInvoker.java @@ -43,9 +43,11 @@ public final class IInputContextInvoker { @NonNull private final IInputContext mIInputContext; + private final int mSessionId; - private IInputContextInvoker(@NonNull IInputContext inputContext) { + private IInputContextInvoker(@NonNull IInputContext inputContext, int sessionId) { mIInputContext = inputContext; + mSessionId = sessionId; } /** @@ -56,13 +58,36 @@ public final class IInputContextInvoker { */ public static IInputContextInvoker create(@NonNull IInputContext inputContext) { Objects.requireNonNull(inputContext); - return new IInputContextInvoker(inputContext); + return new IInputContextInvoker(inputContext, 0); + } + + /** + * Creates a new instance of {@link IInputContextInvoker} with the given {@code sessionId}. + * + * @param sessionId the new session ID to be used. + * @return A new instance of {@link IInputContextInvoker}. + */ + @NonNull + public IInputContextInvoker cloneWithSessionId(int sessionId) { + return new IInputContextInvoker(mIInputContext, sessionId); + } + + /** + * @param inputContext {@code IInputContext} to be compared with + * @return {@code true} if the underlying {@code IInputContext} is the same. {@code false} if + * {@code inputContext} is {@code null}. + */ + @AnyThread + public boolean isSameConnection(@NonNull IInputContext inputContext) { + if (inputContext == null) { + return false; + } + return mIInputContext.asBinder() == inputContext.asBinder(); } @NonNull InputConnectionCommandHeader createHeader() { - // TODO(b/203086369): Propagate session ID for interruption - return new InputConnectionCommandHeader(0 /* sessionId */); + return new InputConnectionCommandHeader(mSessionId); } /** diff --git a/core/java/com/android/internal/inputmethod/RemoteInputConnectionImpl.java b/core/java/com/android/internal/inputmethod/RemoteInputConnectionImpl.java index 550322627794a..6c2b330611f37 100644 --- a/core/java/com/android/internal/inputmethod/RemoteInputConnectionImpl.java +++ b/core/java/com/android/internal/inputmethod/RemoteInputConnectionImpl.java @@ -43,6 +43,7 @@ import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputContentInfo; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.TextAttribute; +import android.view.inputmethod.TextSnapshot; import com.android.internal.annotations.GuardedBy; import com.android.internal.infra.AndroidFuture; @@ -50,6 +51,7 @@ import com.android.internal.view.IInputContext; 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.function.Function; import java.util.function.Supplier; @@ -87,8 +89,8 @@ public final class RemoteInputConnectionImpl extends IInputContext.Stub { private final InputMethodManager mParentInputMethodManager; private final WeakReferenceBy 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()) { + return; + } + final InputConnection ic = getInputConnection(); + if (ic == null) { + return; + } + + // Clean up composing text and batch edit. + ic.finishComposingText(); + // Also clean up batch edit. + while (true) { + if (!ic.endBatchEdit()) { + break; + } + } + + final TextSnapshot textSnapshot = ic.takeSnapshot(); + if (textSnapshot == null) { + final View view = getServedView(); + if (view == null) { + return; + } + mParentInputMethodManager.restartInput(view); + return; + } + mParentInputMethodManager.doInvalidateInput(this, textSnapshot, nextSessionId); + } finally { + mHasPendingInvalidation.set(false); + } + }); + } + } + /** * Called when this object needs to be permanently deactivated. * diff --git a/core/java/com/android/internal/view/IInputMethodSession.aidl b/core/java/com/android/internal/view/IInputMethodSession.aidl index acb4754886aba..b9eb997b851b9 100644 --- a/core/java/com/android/internal/view/IInputMethodSession.aidl +++ b/core/java/com/android/internal/view/IInputMethodSession.aidl @@ -22,8 +22,11 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.CursorAnchorInfo; +import android.view.inputmethod.EditorInfo; import android.view.inputmethod.ExtractedText; +import com.android.internal.view.IInputContext; + /** * Sub-interface of IInputMethod which is safe to give to client applications. * {@hide} @@ -52,4 +55,6 @@ oneway interface IInputMethodSession { void removeImeSurface(); void finishInput(); + + void invalidateInput(in EditorInfo editorInfo, in IInputContext inputContext, int sessionId); }