diff --git a/core/api/current.txt b/core/api/current.txt index f6d0d9dea096a..56d000e28ede9 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -50616,6 +50616,7 @@ package android.view.inputmethod { method public boolean sendKeyEvent(android.view.KeyEvent); method public boolean setComposingRegion(int, int); method public boolean setComposingText(CharSequence, int); + method public default boolean setImeTemporarilyConsumesInput(boolean); method public boolean setSelection(int, int); field public static final int CURSOR_UPDATE_IMMEDIATE = 1; // 0x1 field public static final int CURSOR_UPDATE_MONITOR = 2; // 0x2 diff --git a/core/java/android/view/inputmethod/BaseInputConnection.java b/core/java/android/view/inputmethod/BaseInputConnection.java index f057c1239e52f..415b3a766d16a 100644 --- a/core/java/android/view/inputmethod/BaseInputConnection.java +++ b/core/java/android/view/inputmethod/BaseInputConnection.java @@ -163,11 +163,13 @@ public class BaseInputConnection implements InputConnection { } /** - * Default implementation calls {@link #finishComposingText()}. + * Default implementation calls {@link #finishComposingText()} and + * {@code setImeTemporarilyConsumesInput(false)}. */ @CallSuper public void closeConnection() { finishComposingText(); + setImeTemporarilyConsumesInput(false); } /** diff --git a/core/java/android/view/inputmethod/InputConnection.java b/core/java/android/view/inputmethod/InputConnection.java index 8c81143e4cdc9..a76d46d1c00f1 100644 --- a/core/java/android/view/inputmethod/InputConnection.java +++ b/core/java/android/view/inputmethod/InputConnection.java @@ -1002,4 +1002,22 @@ public interface InputConnection { */ boolean commitContent(@NonNull InputContentInfo inputContentInfo, int flags, @Nullable Bundle opts); + + /** + * Called by the input method to indicate that it temporarily consumes all input for itself, + * or no longer does so. + * + *

Editors should reflect that they are temporarily not receiving input by hiding the + * cursor if {@code imeTemporarilyConsumesInput} is {@code true}, and resume showing the + * cursor if it is {@code false}. + * + * @param imeTemporarilyConsumesInput {@code true} when the IME is temporarily consuming input + * and the cursor should be hidden, {@code false} when input to the editor resumes and the + * cursor should be shown again. + * @return {@code true} on success, {@code false} if the input connection is no longer valid, or + * the protocol is not supported. + */ + default boolean setImeTemporarilyConsumesInput(boolean imeTemporarilyConsumesInput) { + return false; + } } diff --git a/core/java/android/view/inputmethod/InputConnectionWrapper.java b/core/java/android/view/inputmethod/InputConnectionWrapper.java index ca853485d4f39..b29149fc1fa03 100644 --- a/core/java/android/view/inputmethod/InputConnectionWrapper.java +++ b/core/java/android/view/inputmethod/InputConnectionWrapper.java @@ -335,4 +335,13 @@ public class InputConnectionWrapper implements InputConnection { public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) { return mTarget.commitContent(inputContentInfo, flags, opts); } + + /** + * {@inheritDoc} + * @throws NullPointerException if the target is {@code null}. + */ + @Override + public boolean setImeTemporarilyConsumesInput(boolean imeTemporarilyConsumesInput) { + return mTarget.setImeTemporarilyConsumesInput(imeTemporarilyConsumesInput); + } } diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 977a0e89a143d..8be9021a44f2b 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -494,6 +494,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private TextUtils.TruncateAt mEllipsize; + // A flag to indicate the cursor was hidden by IME. + private boolean mImeTemporarilyConsumesInput; + + // Whether cursor is visible without regard to {@link mImeTemporarilyConsumesInput}. + // {code true} is the default value. + private boolean mCursorVisibleFromAttr = true; + static class Drawables { static final int LEFT = 0; static final int TOP = 1; @@ -10496,7 +10503,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** * Set whether the cursor is visible. The default is true. Note that this property only - * makes sense for editable TextView. + * makes sense for editable TextView. If IME is temporarily consuming the input, the cursor will + * be always invisible, visibility will be updated as the last state when IME does not consume + * the input anymore. * * @see #isCursorVisible() * @@ -10504,6 +10513,25 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ @android.view.RemotableViewMethod public void setCursorVisible(boolean visible) { + mCursorVisibleFromAttr = visible; + updateCursorVisibleInternal(); + } + + /** + * Sets the IME is temporarily consuming the input and make the cursor invisible if + * {@code imeTemporarilyConsumesInput} is {@code true}. Otherwise, make the cursor visible. + * + * @param imeTemporarilyConsumesInput {@code true} if IME is temporarily consuming the input + * + * @hide + */ + public void setImeTemporarilyConsumesInput(boolean imeTemporarilyConsumesInput) { + mImeTemporarilyConsumesInput = imeTemporarilyConsumesInput; + updateCursorVisibleInternal(); + } + + private void updateCursorVisibleInternal() { + boolean visible = mCursorVisibleFromAttr && !mImeTemporarilyConsumesInput; if (visible && mEditor == null) return; // visible is the default value with no edit data createEditorIfNeeded(); if (mEditor.mCursorVisible != visible) { @@ -10518,7 +10546,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** - * @return whether or not the cursor is visible (assuming this TextView is editable) + * @return whether or not the cursor is visible (assuming this TextView is editable). This + * method may return {@code false} when the IME is temporarily consuming the input even if the + * {@code mEditor.mCursorVisible} attribute is {@code true} or {@code #setCursorVisible(true)} + * is called. * * @see #setCursorVisible(boolean) * diff --git a/core/java/com/android/internal/view/IInputConnectionWrapper.java b/core/java/com/android/internal/view/IInputConnectionWrapper.java index 3a7e66ce76f08..e7b7bf4a5b524 100644 --- a/core/java/com/android/internal/view/IInputConnectionWrapper.java +++ b/core/java/com/android/internal/view/IInputConnectionWrapper.java @@ -79,6 +79,7 @@ public abstract class IInputConnectionWrapper extends IInputContext.Stub { private static final int DO_CLOSE_CONNECTION = 150; private static final int DO_COMMIT_CONTENT = 160; private static final int DO_GET_SURROUNDING_TEXT = 41; + private static final int DO_SET_IME_TEMPORARILY_CONSUMES_INPUT = 170; @GuardedBy("mLock") @@ -266,6 +267,16 @@ public abstract class IInputConnectionWrapper extends IInputContext.Stub { dispatchMessage(mH.obtainMessage(DO_COMMIT_CONTENT, flags, 0 /* unused */, args)); } + /** + * Dispatches the request for setting ime temporarily consumes input. + * + *

See {@link InputConnection#setImeTemporarilyConsumesInput(boolean)}. + */ + public void setImeTemporarilyConsumesInput(boolean imeTemporarilyConsumesInput) { + dispatchMessage(obtainMessageB(DO_SET_IME_TEMPORARILY_CONSUMES_INPUT, + imeTemporarilyConsumesInput)); + } + void dispatchMessage(Message msg) { // If we are calling this from the main thread, then we can call // right through. Otherwise, we need to send the message to the @@ -811,6 +822,22 @@ public abstract class IInputConnectionWrapper extends IInputContext.Stub { } return; } + case DO_SET_IME_TEMPORARILY_CONSUMES_INPUT: { + Trace.traceBegin(Trace.TRACE_TAG_INPUT, + "InputConnection#setImeTemporarilyConsumesInput"); + try { + InputConnection ic = getInputConnection(); + if (ic == null || !isActive()) { + Log.w(TAG, + "setImeTemporarilyConsumesInput on inactive InputConnection"); + return; + } + ic.setImeTemporarilyConsumesInput(msg.arg1 == 1); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_INPUT); + } + return; + } } Log.w(TAG, "Unhandled message code: " + msg.what); } @@ -837,4 +864,8 @@ public abstract class IInputConnectionWrapper extends IInputContext.Stub { args.arg2 = arg2; return mH.obtainMessage(what, 0, 0, args); } + + Message obtainMessageB(int what, boolean arg1) { + return mH.obtainMessage(what, arg1 ? 1 : 0, 0); + } } diff --git a/core/java/com/android/internal/view/IInputContext.aidl b/core/java/com/android/internal/view/IInputContext.aidl index 53cbf961fff4b..586404c53f188 100644 --- a/core/java/com/android/internal/view/IInputContext.aidl +++ b/core/java/com/android/internal/view/IInputContext.aidl @@ -85,4 +85,6 @@ import com.android.internal.inputmethod.ISurroundingTextResultCallback; void getSurroundingText(int beforeLength, int afterLength, int flags, ISurroundingTextResultCallback callback); + + void setImeTemporarilyConsumesInput(boolean imeTemporarilyConsumesInput); } diff --git a/core/java/com/android/internal/view/InputConnectionWrapper.java b/core/java/com/android/internal/view/InputConnectionWrapper.java index af9c0124078a2..84c92ca83f364 100644 --- a/core/java/com/android/internal/view/InputConnectionWrapper.java +++ b/core/java/com/android/internal/view/InputConnectionWrapper.java @@ -524,6 +524,19 @@ public class InputConnectionWrapper implements InputConnection { value, TAG, "commitContent()", mCancellationGroup, MAX_WAIT_TIME_MILLIS) != 0; } + /** + * See {@link InputConnection#setImeTemporarilyConsumesInput(boolean)}. + */ + @AnyThread + public boolean setImeTemporarilyConsumesInput(boolean imeTemporarilyConsumesInput) { + try { + mIInputContext.setImeTemporarilyConsumesInput(imeTemporarilyConsumesInput); + return true; + } catch (RemoteException e) { + return false; + } + } + @AnyThread private boolean isMethodMissing(@MissingMethodFlags final int methodFlag) { return (mMissingMethods & methodFlag) == methodFlag; diff --git a/core/java/com/android/internal/widget/EditableInputConnection.java b/core/java/com/android/internal/widget/EditableInputConnection.java index 767ad42efbf3c..4ccf9ce91f270 100644 --- a/core/java/com/android/internal/widget/EditableInputConnection.java +++ b/core/java/com/android/internal/widget/EditableInputConnection.java @@ -244,6 +244,15 @@ public class EditableInputConnection extends BaseInputConnection return true; } + @Override + public boolean setImeTemporarilyConsumesInput(boolean imeTemporarilyConsumesInput) { + if (mTextView == null) { + return super.setImeTemporarilyConsumesInput(imeTemporarilyConsumesInput); + } + mTextView.setImeTemporarilyConsumesInput(imeTemporarilyConsumesInput); + return true; + } + @Override public void dumpDebug(ProtoOutputStream proto, long fieldId) { final long token = proto.start(fieldId); diff --git a/core/tests/coretests/src/android/widget/TextViewTest.java b/core/tests/coretests/src/android/widget/TextViewTest.java index 66fdfff990f8e..a4284a04310e1 100644 --- a/core/tests/coretests/src/android/widget/TextViewTest.java +++ b/core/tests/coretests/src/android/widget/TextViewTest.java @@ -276,6 +276,34 @@ public class TextViewTest { 0, mTextView.getImeOptions() & EditorInfo.IME_FLAG_NO_FULLSCREEN); } + @Test + @UiThreadTest + public void setSetImeTemporarilyConsumesInput_recoveryToVisible() { + mTextView = new TextView(mActivity); + mTextView.setCursorVisible(true); + assertTrue(mTextView.isCursorVisible()); + + mTextView.setImeTemporarilyConsumesInput(true); + assertFalse(mTextView.isCursorVisible()); + + mTextView.setImeTemporarilyConsumesInput(false); + assertTrue(mTextView.isCursorVisible()); + } + + @Test + @UiThreadTest + public void setSetImeTemporarilyConsumesInput_recoveryToInvisible() { + mTextView = new TextView(mActivity); + mTextView.setCursorVisible(false); + assertFalse(mTextView.isCursorVisible()); + + mTextView.setImeTemporarilyConsumesInput(true); + assertFalse(mTextView.isCursorVisible()); + + mTextView.setImeTemporarilyConsumesInput(false); + assertFalse(mTextView.isCursorVisible()); + } + private String createLongText() { int size = 600 * 1000; final StringBuilder builder = new StringBuilder(size);