diff options
9 files changed, 177 insertions, 63 deletions
diff --git a/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java b/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java index eccbb403b306..9a7ccc64efcd 100644 --- a/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java +++ b/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java @@ -53,7 +53,6 @@ class IInputMethodSessionWrapper extends IInputMethodSession.Stub private static final int DO_APP_PRIVATE_COMMAND = 100; private static final int DO_FINISH_SESSION = 110; private static final int DO_VIEW_CLICKED = 115; - 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; @@ -133,10 +132,6 @@ class IInputMethodSessionWrapper extends IInputMethodSession.Stub mInputMethodSession.viewClicked(msg.arg1 == 1); return; } - case DO_NOTIFY_IME_HIDDEN: { - mInputMethodSession.notifyImeHidden(); - return; - } case DO_REMOVE_IME_SURFACE: { mInputMethodSession.removeImeSurface(); return; @@ -198,11 +193,6 @@ class IInputMethodSessionWrapper extends IInputMethodSession.Stub } @Override - public void notifyImeHidden() { - mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_NOTIFY_IME_HIDDEN)); - } - - @Override public void removeImeSurface() { mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_REMOVE_IME_SURFACE)); } diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 4fdd53425328..a6ed42348af6 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -1058,10 +1058,6 @@ public class InputMethodService extends AbstractInputMethodService { return viewRoot == null ? null : viewRoot.getInputToken(); } - private void notifyImeHidden() { - requestHideSelf(0); - } - private void scheduleImeSurfaceRemoval() { if (mShowInputRequested || mWindowVisible || mWindow == null || mImeSurfaceScheduledForRemoval) { @@ -1225,14 +1221,6 @@ public class InputMethodService extends AbstractInputMethodService { } /** - * Notify IME that window is hidden. - * @hide - */ - public final void notifyImeHidden() { - InputMethodService.this.notifyImeHidden(); - } - - /** * Notify IME that surface can be now removed. * @hide */ diff --git a/core/java/android/view/ImeInsetsSourceConsumer.java b/core/java/android/view/ImeInsetsSourceConsumer.java index d609fb8eb234..4fdea3b006dc 100644 --- a/core/java/android/view/ImeInsetsSourceConsumer.java +++ b/core/java/android/view/ImeInsetsSourceConsumer.java @@ -18,12 +18,13 @@ package android.view; import static android.os.Trace.TRACE_TAG_VIEW; import static android.view.ImeInsetsSourceConsumerProto.INSETS_SOURCE_CONSUMER; +import static android.view.ImeInsetsSourceConsumerProto.IS_HIDE_ANIMATION_RUNNING; import static android.view.ImeInsetsSourceConsumerProto.IS_REQUESTED_VISIBLE_AWAITING_CONTROL; +import static android.view.ImeInsetsSourceConsumerProto.IS_SHOW_REQUESTED_DURING_HIDE_ANIMATION; import static android.view.InsetsController.AnimationType; import static android.view.InsetsState.ITYPE_IME; import android.annotation.Nullable; -import android.inputmethodservice.InputMethodService; import android.os.IBinder; import android.os.Trace; import android.util.proto.ProtoOutputStream; @@ -44,6 +45,16 @@ public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer { */ private boolean mIsRequestedVisibleAwaitingControl; + private boolean mIsHideAnimationRunning; + + /** + * Tracks whether {@link WindowInsetsController#show(int)} or + * {@link InputMethodManager#showSoftInput(View, int)} is called during IME hide animation. + * If it was called, we should not call {@link InputMethodManager#notifyImeHidden(IBinder)}, + * because the IME is being shown. + */ + private boolean mIsShowRequestedDuringHideAnimation; + public ImeInsetsSourceConsumer( InsetsState state, Supplier<Transaction> transactionSupplier, InsetsController controller) { @@ -64,6 +75,12 @@ public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer { } @Override + public void show(boolean fromIme) { + super.show(fromIme); + onShowRequested(); + } + + @Override public void hide() { super.hide(); mIsRequestedVisibleAwaitingControl = false; @@ -74,10 +91,20 @@ public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer { hide(); if (animationFinished) { - // remove IME surface as IME has finished hide animation. - notifyHidden(); - removeSurface(); + // Remove IME surface as IME has finished hide animation, if there is no pending + // show request. + if (!mIsShowRequestedDuringHideAnimation) { + notifyHidden(); + removeSurface(); + } } + // This method is called + // (1) before the hide animation starts. + // (2) after the hide animation ends. + // (3) if the IME is not controllable (animationFinished == true in this case). + // We should reset mIsShowRequestedDuringHideAnimation in all cases. + mIsHideAnimationRunning = !animationFinished; + mIsShowRequestedDuringHideAnimation = false; } /** @@ -104,7 +131,8 @@ public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer { } /** - * Notify {@link InputMethodService} that IME window is hidden. + * Notify {@link com.android.server.inputmethod.InputMethodManagerService} that + * IME insets are hidden. */ @Override void notifyHidden() { @@ -157,9 +185,20 @@ public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer { final long token = proto.start(fieldId); super.dumpDebug(proto, INSETS_SOURCE_CONSUMER); proto.write(IS_REQUESTED_VISIBLE_AWAITING_CONTROL, mIsRequestedVisibleAwaitingControl); + proto.write(IS_HIDE_ANIMATION_RUNNING, mIsHideAnimationRunning); + proto.write(IS_SHOW_REQUESTED_DURING_HIDE_ANIMATION, mIsShowRequestedDuringHideAnimation); proto.end(token); } + /** + * Called when {@link #show} or {@link InputMethodManager#showSoftInput(View, int)} is called. + */ + public void onShowRequested() { + if (mIsHideAnimationRunning) { + mIsShowRequestedDuringHideAnimation = true; + } + } + private InputMethodManager getImm() { return mController.getHost().getInputMethodManager(); } diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 84f13930e03a..850256871b15 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -543,6 +543,7 @@ public final class InputMethodManager { static final int MSG_BIND_ACCESSIBILITY_SERVICE = 11; static final int MSG_UNBIND_ACCESSIBILITY_SERVICE = 12; static final int MSG_UPDATE_VIRTUAL_DISPLAY_TO_SCREEN_MATRIX = 30; + static final int MSG_ON_SHOW_REQUESTED = 31; private static boolean isAutofillUIShowing(View servedView) { AutofillManager afm = servedView.getContext().getSystemService(AutofillManager.class); @@ -1117,6 +1118,14 @@ public final class InputMethodManager { } return; } + case MSG_ON_SHOW_REQUESTED: { + synchronized (mH) { + if (mImeInsetsConsumer != null) { + mImeInsetsConsumer.onShowRequested(); + } + } + return; + } } } } @@ -1834,6 +1843,9 @@ public final class InputMethodManager { return false; } + // Makes sure to call ImeInsetsSourceConsumer#onShowRequested on the UI thread. + // TODO(b/229426865): call WindowInsetsController#show instead. + mH.executeOrSendMessage(Message.obtain(mH, MSG_ON_SHOW_REQUESTED)); try { Log.d(TAG, "showSoftInput() view=" + view + " flags=" + flags + " reason=" + InputMethodDebug.softInputDisplayReasonToString(reason)); @@ -1869,6 +1881,9 @@ public final class InputMethodManager { Log.w(TAG, "No current root view, ignoring showSoftInputUnchecked()"); return; } + // Makes sure to call ImeInsetsSourceConsumer#onShowRequested on the UI thread. + // TODO(b/229426865): call WindowInsetsController#show instead. + mH.executeOrSendMessage(Message.obtain(mH, MSG_ON_SHOW_REQUESTED)); mService.showSoftInput( mClient, mCurRootView.getView().getWindowToken(), @@ -2521,7 +2536,7 @@ public final class InputMethodManager { } /** - * Notify IME directly that it is no longer visible. + * Notify IMMS that IME insets are no longer visible. * * @param windowToken the window from which this request originates. If this doesn't match the * currently served view, the request is ignored. @@ -2533,7 +2548,13 @@ public final class InputMethodManager { synchronized (mH) { if (mCurrentInputMethodSession != null && mCurRootView != null && mCurRootView.getWindowToken() == windowToken) { - mCurrentInputMethodSession.notifyImeHidden(); + try { + mService.hideSoftInput(mClient, windowToken, 0 /* flags */, + null /* resultReceiver */, + SoftInputShowHideReason.HIDE_SOFT_INPUT); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } } } diff --git a/core/java/android/view/inputmethod/InputMethodSession.java b/core/java/android/view/inputmethod/InputMethodSession.java index a178ee8ee866..28c44507e43c 100644 --- a/core/java/android/view/inputmethod/InputMethodSession.java +++ b/core/java/android/view/inputmethod/InputMethodSession.java @@ -195,13 +195,6 @@ public interface InputMethodSession { public void updateCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo); /** - * Notifies {@link android.inputmethodservice.InputMethodService} that IME has been - * hidden from user. - * @hide - */ - public void notifyImeHidden(); - - /** * Notify IME directly to remove surface as it is no longer visible. * @hide */ diff --git a/core/java/android/view/inputmethod/InputMethodSessionWrapper.java b/core/java/android/view/inputmethod/InputMethodSessionWrapper.java index a1995202485f..ee22b6508549 100644 --- a/core/java/android/view/inputmethod/InputMethodSessionWrapper.java +++ b/core/java/android/view/inputmethod/InputMethodSessionWrapper.java @@ -106,15 +106,6 @@ final class InputMethodSessionWrapper { } @AnyThread - void notifyImeHidden() { - try { - mSession.notifyImeHidden(); - } catch (RemoteException e) { - Log.w(TAG, "IME died", e); - } - } - - @AnyThread void viewClicked(boolean focusChanged) { try { mSession.viewClicked(focusChanged); diff --git a/core/java/com/android/internal/view/IInputMethodSession.aidl b/core/java/com/android/internal/view/IInputMethodSession.aidl index b9eb997b851b..d505c1995def 100644 --- a/core/java/com/android/internal/view/IInputMethodSession.aidl +++ b/core/java/com/android/internal/view/IInputMethodSession.aidl @@ -50,8 +50,6 @@ oneway interface IInputMethodSession { void updateCursorAnchorInfo(in CursorAnchorInfo cursorAnchorInfo); - void notifyImeHidden(); - void removeImeSurface(); void finishInput(); diff --git a/core/proto/android/view/imeinsetssourceconsumer.proto b/core/proto/android/view/imeinsetssourceconsumer.proto index b1ed365309c4..6e007fad5f8d 100644 --- a/core/proto/android/view/imeinsetssourceconsumer.proto +++ b/core/proto/android/view/imeinsetssourceconsumer.proto @@ -29,4 +29,6 @@ message ImeInsetsSourceConsumerProto { optional InsetsSourceConsumerProto insets_source_consumer = 1; reserved 2; // focused_editor = 2 optional bool is_requested_visible_awaiting_control = 3; + optional bool is_hide_animation_running = 4; + optional bool is_show_requested_during_hide_animation = 5; }
\ No newline at end of file diff --git a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeOpenCloseStressTest.java b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeOpenCloseStressTest.java index 1c957d4d78cf..8419276f4406 100644 --- a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeOpenCloseStressTest.java +++ b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeOpenCloseStressTest.java @@ -30,6 +30,7 @@ import android.os.Bundle; import android.os.SystemClock; import android.platform.test.annotations.RootPermissionTest; import android.platform.test.rule.UnlockScreenRule; +import android.util.Log; import android.view.WindowInsets; import android.view.WindowInsetsAnimation; import android.view.inputmethod.InputMethodManager; @@ -40,16 +41,19 @@ import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.ArrayList; import java.util.List; @RootPermissionTest @RunWith(AndroidJUnit4.class) public final class ImeOpenCloseStressTest { + private static final String TAG = "ImeOpenCloseStressTest"; private static final int NUM_TEST_ITERATIONS = 10; @Rule @@ -58,32 +62,103 @@ public final class ImeOpenCloseStressTest { @Rule public ScreenCaptureRule mScreenCaptureRule = new ScreenCaptureRule("/sdcard/InputMethodStressTest"); + private Instrumentation mInstrumentation; + + @Before + public void setUp() { + mInstrumentation = InstrumentationRegistry.getInstrumentation(); + } + + @Test + public void testShowHide_waitingVisibilityChange() { + TestActivity activity = TestActivity.start(); + EditText editText = activity.getEditText(); + waitOnMainUntil("activity should gain focus", editText::hasWindowFocus); + for (int i = 0; i < NUM_TEST_ITERATIONS; i++) { + String msgPrefix = "Iteration #" + i + " "; + Log.i(TAG, msgPrefix + "start"); + mInstrumentation.runOnMainSync(activity::showIme); + waitOnMainUntil(msgPrefix + "IME should be visible", () -> isImeShown(editText)); + mInstrumentation.runOnMainSync(activity::hideIme); + waitOnMainUntil(msgPrefix + "IME should be hidden", () -> !isImeShown(editText)); + } + } @Test - public void test() { - Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); - Intent intent = new Intent() - .setAction(Intent.ACTION_MAIN) - .setClass(instrumentation.getContext(), TestActivity.class) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - TestActivity activity = (TestActivity) instrumentation.startActivitySync(intent); + public void testShowHide_waitingAnimationEnd() { + TestActivity activity = TestActivity.start(); + activity.enableAnimationMonitoring(); EditText editText = activity.getEditText(); waitOnMainUntil("activity should gain focus", editText::hasWindowFocus); for (int i = 0; i < NUM_TEST_ITERATIONS; i++) { String msgPrefix = "Iteration #" + i + " "; - instrumentation.runOnMainSync(activity::showIme); + Log.i(TAG, msgPrefix + "start"); + mInstrumentation.runOnMainSync(activity::showIme); waitOnMainUntil(msgPrefix + "IME should be visible", () -> !activity.isAnimating() && isImeShown(editText)); - instrumentation.runOnMainSync(activity::hideIme); + mInstrumentation.runOnMainSync(activity::hideIme); waitOnMainUntil(msgPrefix + "IME should be hidden", () -> !activity.isAnimating() && !isImeShown(editText)); - // b/b/221483132, wait until IMS and IMMS handles IMM#notifyImeHidden. - // There is no good signal, so we just wait a second. - SystemClock.sleep(1000); } } + @Test + public void testShowHide_intervalAfterHide() { + // Regression test for b/221483132 + TestActivity activity = TestActivity.start(); + EditText editText = activity.getEditText(); + // Intervals = 10, 20, 30, ..., 100, 150, 200, ... + List<Integer> intervals = new ArrayList<>(); + for (int i = 10; i < 100; i += 10) intervals.add(i); + for (int i = 100; i < 1000; i += 50) intervals.add(i); + waitOnMainUntil("activity should gain focus", editText::hasWindowFocus); + for (int intervalMillis : intervals) { + String msgPrefix = "Interval = " + intervalMillis + " "; + Log.i(TAG, msgPrefix + " start"); + mInstrumentation.runOnMainSync(activity::hideIme); + SystemClock.sleep(intervalMillis); + mInstrumentation.runOnMainSync(activity::showIme); + waitOnMainUntil(msgPrefix + "IME should be visible", + () -> isImeShown(editText)); + } + } + + @Test + public void testShowHideInSameFrame() { + TestActivity activity = TestActivity.start(); + activity.enableAnimationMonitoring(); + EditText editText = activity.getEditText(); + waitOnMainUntil("activity should gain focus", editText::hasWindowFocus); + + // hidden -> show -> hide + mInstrumentation.runOnMainSync(() -> { + Log.i(TAG, "Calling showIme() and hideIme()"); + activity.showIme(); + activity.hideIme(); + }); + // Wait until IMMS / IMS handles messages. + SystemClock.sleep(1000); + mInstrumentation.waitForIdleSync(); + waitOnMainUntil("IME should be invisible after show/hide", () -> !isImeShown(editText)); + + mInstrumentation.runOnMainSync(activity::showIme); + waitOnMainUntil("IME should be visible", + () -> !activity.isAnimating() && isImeShown(editText)); + mInstrumentation.waitForIdleSync(); + + // shown -> hide -> show + mInstrumentation.runOnMainSync(() -> { + Log.i(TAG, "Calling hideIme() and showIme()"); + activity.hideIme(); + activity.showIme(); + }); + // Wait until IMMS / IMS handles messages. + SystemClock.sleep(1000); + mInstrumentation.waitForIdleSync(); + waitOnMainUntil("IME should be visible after hide/show", + () -> !activity.isAnimating() && isImeShown(editText)); + } + public static class TestActivity extends Activity { private EditText mEditText; @@ -111,6 +186,15 @@ public final class ImeOpenCloseStressTest { } }; + public static TestActivity start() { + Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); + Intent intent = new Intent() + .setAction(Intent.ACTION_MAIN) + .setClass(instrumentation.getContext(), TestActivity.class) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + return (TestActivity) instrumentation.startActivitySync(intent); + } @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -120,9 +204,6 @@ public final class ImeOpenCloseStressTest { mEditText = new EditText(this); rootView.addView(mEditText, new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)); setContentView(rootView); - // Enable WindowInsetsAnimation. - getWindow().setDecorFitsSystemWindows(false); - mEditText.setWindowInsetsAnimationCallback(mWindowInsetsAnimationCallback); } public EditText getEditText() { @@ -130,16 +211,27 @@ public final class ImeOpenCloseStressTest { } public void showIme() { + Log.i(TAG, "TestActivity.showIme"); mEditText.requestFocus(); InputMethodManager imm = getSystemService(InputMethodManager.class); imm.showSoftInput(mEditText, 0); } public void hideIme() { + Log.i(TAG, "TestActivity.hideIme"); InputMethodManager imm = getSystemService(InputMethodManager.class); imm.hideSoftInputFromWindow(mEditText.getWindowToken(), 0); } + public void enableAnimationMonitoring() { + // Enable WindowInsetsAnimation. + // Note that this has a side effect of disabling InsetsAnimationThreadControlRunner. + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + getWindow().setDecorFitsSystemWindows(false); + mEditText.setWindowInsetsAnimationCallback(mWindowInsetsAnimationCallback); + }); + } + public boolean isAnimating() { return mIsAnimating; } |