diff --git a/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java b/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java index 843db6d28d30c..ffae361e76d42 100644 --- a/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java +++ b/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java @@ -51,6 +51,7 @@ class IInputMethodSessionWrapper extends IInputMethodSession.Stub private static final int DO_TOGGLE_SOFT_INPUT = 105; 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; HandlerCaller mCaller; InputMethodSession mInputMethodSession; @@ -129,6 +130,10 @@ class IInputMethodSessionWrapper extends IInputMethodSession.Stub mInputMethodSession.viewClicked(msg.arg1 == 1); return; } + case DO_NOTIFY_IME_HIDDEN: { + mInputMethodSession.notifyImeHidden(); + return; + } } Log.w(TAG, "Unhandled message code: " + msg.what); } @@ -171,6 +176,11 @@ class IInputMethodSessionWrapper extends IInputMethodSession.Stub mCaller.obtainMessageI(DO_VIEW_CLICKED, focusChanged ? 1 : 0)); } + @Override + public void notifyImeHidden() { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_NOTIFY_IME_HIDDEN)); + } + @Override public void updateCursor(Rect newCursor) { mCaller.executeOrSendMessage( diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 333cfbd400dd7..ab630fd7467bb 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -592,7 +592,6 @@ public class InputMethodService extends AbstractInputMethodService { final boolean wasVisible = mIsPreRendered ? mDecorViewVisible && mWindowVisible : isInputViewShown(); if (mIsPreRendered) { - // TODO: notify visibility to insets consumer. if (DEBUG) { Log.v(TAG, "Making IME window invisible"); } @@ -658,6 +657,11 @@ public class InputMethodService extends AbstractInputMethodService { } } + private void notifyImeHidden() { + setImeWindowStatus(IME_ACTIVE | IME_INVISIBLE, mBackDisposition); + onPreRenderedWindowVisibilityChanged(false /* setVisible */); + } + private void setImeWindowStatus(int visibilityFlags, int backDisposition) { mPrivOps.setImeWindowStatus(visibilityFlags, backDisposition); } @@ -760,6 +764,14 @@ public class InputMethodService extends AbstractInputMethodService { } InputMethodService.this.onUpdateCursorAnchorInfo(info); } + + /** + * Notify IME that window is hidden. + * @hide + */ + public final void notifyImeHidden() { + InputMethodService.this.notifyImeHidden(); + } } /** diff --git a/core/java/android/inputmethodservice/MultiClientInputMethodClientCallbackAdaptor.java b/core/java/android/inputmethodservice/MultiClientInputMethodClientCallbackAdaptor.java index b4b541dc5cd07..31c948a14698f 100644 --- a/core/java/android/inputmethodservice/MultiClientInputMethodClientCallbackAdaptor.java +++ b/core/java/android/inputmethodservice/MultiClientInputMethodClientCallbackAdaptor.java @@ -290,6 +290,12 @@ final class MultiClientInputMethodClientCallbackAdaptor { CallbackImpl::updateCursorAnchorInfo, mCallbackImpl, info)); } } + + @Override + public final void notifyImeHidden() { + // no-op for multi-session since IME is responsible controlling navigation bar buttons. + reportNotSupported(); + } } private static final class MultiClientInputMethodSessionImpl diff --git a/core/java/android/view/ImeInsetsSourceConsumer.java b/core/java/android/view/ImeInsetsSourceConsumer.java index 7026d2b1389c4..2ba1e016e03d2 100644 --- a/core/java/android/view/ImeInsetsSourceConsumer.java +++ b/core/java/android/view/ImeInsetsSourceConsumer.java @@ -18,10 +18,10 @@ package android.view; import static android.view.InsetsState.TYPE_IME; +import android.inputmethodservice.InputMethodService; import android.os.Parcel; import android.text.TextUtils; import android.view.SurfaceControl.Transaction; -import android.view.WindowInsets.Type; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; @@ -73,11 +73,7 @@ public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer { return; } - if (setVisible) { - mController.show(Type.IME); - } else { - mController.hide(Type.IME); - } + mController.applyImeVisibility(setVisible); } @Override @@ -91,6 +87,30 @@ public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer { mHasWindowFocus = false; } + /** + * Request {@link InputMethodManager} to show the IME. + * @return @see {@link android.view.InsetsSourceConsumer.ShowResult}. + */ + @Override + @ShowResult int requestShow(boolean fromIme) { + // TODO: ResultReceiver for IME. + // TODO: Set mShowOnNextImeRender to automatically show IME and guard it with a flag. + if (fromIme) { + return ShowResult.SHOW_IMMEDIATELY; + } + + return getImm().requestImeShow(null /* resultReceiver */) + ? ShowResult.SHOW_DELAYED : ShowResult.SHOW_FAILED; + } + + /** + * Notify {@link InputMethodService} that IME window is hidden. + */ + @Override + void notifyHidden() { + getImm().notifyImeHidden(); + } + private boolean isDummyOrEmptyEditor(EditorInfo info) { // TODO(b/123044812): Handle dummy input gracefully in IME Insets API return info == null || (info.fieldId <= 0 && info.inputType <= 0); diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index 8e773799d1881..3f8e6eccdc385 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -29,10 +29,13 @@ import android.graphics.Rect; import android.os.RemoteException; import android.util.ArraySet; import android.util.Log; +import android.util.Pair; import android.util.Property; import android.util.SparseArray; +import android.view.InsetsSourceConsumer.ShowResult; import android.view.InsetsState.InternalInsetType; import android.view.SurfaceControl.Transaction; +import android.view.WindowInsets.Type; import android.view.WindowInsets.Type.InsetType; import com.android.internal.annotations.VisibleForTesting; @@ -51,6 +54,7 @@ public class InsetsController implements WindowInsetsController { private static final int DIRECTION_NONE = 0; private static final int DIRECTION_SHOW = 1; private static final int DIRECTION_HIDE = 2; + @IntDef ({DIRECTION_NONE, DIRECTION_SHOW, DIRECTION_HIDE}) private @interface AnimationDirection{} @@ -103,6 +107,8 @@ public class InsetsController implements WindowInsetsController { private ObjectAnimator mAnimator; private @AnimationDirection int mAnimationDirection; + private int mPendingTypesToShow; + public InsetsController(ViewRootImpl viewRoot) { mViewRoot = viewRoot; mAnimCallback = () -> { @@ -193,6 +199,12 @@ public class InsetsController implements WindowInsetsController { @Override public void show(@InsetType int types) { + show(types, false /* fromIme */); + } + + private void show(@InsetType int types, boolean fromIme) { + // TODO: Support a ResultReceiver for IME. + // TODO(b/123718661): Make show() work for multi-session IME. int typesReady = 0; final ArraySet internalTypes = InsetsState.toInternalType(types); for (int i = internalTypes.size() - 1; i >= 0; i--) { @@ -201,15 +213,18 @@ public class InsetsController implements WindowInsetsController { // Only one animator (with multiple InsetType) can run at a time. // previous one should be cancelled for simplicity. cancelExistingAnimation(); - } else if (consumer.isVisible() || mAnimationDirection == DIRECTION_SHOW) { - // no-op: already shown or animating in. + } else if (consumer.isVisible() + && (mAnimationDirection == DIRECTION_NONE + || mAnimationDirection == DIRECTION_HIDE)) { + // no-op: already shown or animating in (because window visibility is + // applied before starting animation). // TODO: When we have more than one types: handle specific case when // show animation is going on, but the current type is not becoming visible. continue; } typesReady |= InsetsState.toPublicType(consumer.getType()); } - applyAnimation(typesReady, true /* show */); + applyAnimation(typesReady, true /* show */, fromIme); } @Override @@ -220,35 +235,114 @@ public class InsetsController implements WindowInsetsController { InsetsSourceConsumer consumer = getSourceConsumer(internalTypes.valueAt(i)); if (mAnimationDirection == DIRECTION_SHOW) { cancelExistingAnimation(); - } else if (!consumer.isVisible() || mAnimationDirection == DIRECTION_HIDE) { + } else if (!consumer.isVisible() + && (mAnimationDirection == DIRECTION_NONE + || mAnimationDirection == DIRECTION_HIDE)) { // no-op: already hidden or animating out. continue; } typesReady |= InsetsState.toPublicType(consumer.getType()); } - applyAnimation(typesReady, false /* show */); + applyAnimation(typesReady, false /* show */, false /* fromIme */); } @Override public void controlWindowInsetsAnimation(@InsetType int types, WindowInsetsAnimationControlListener listener) { + controlWindowInsetsAnimation(types, listener, false /* fromIme */); + } + + private void controlWindowInsetsAnimation(@InsetType int types, + WindowInsetsAnimationControlListener listener, boolean fromIme) { + if (types == 0) { + // nothing to animate. + return; + } // TODO: Check whether we already have a controller. final ArraySet internalTypes = mState.toInternalType(types); final SparseArray consumers = new SparseArray<>(); + + Pair typesReadyPair = collectConsumers(fromIme, internalTypes, consumers); + int typesReady = typesReadyPair.first; + boolean isReady = typesReadyPair.second; + if (!isReady) { + // IME isn't ready, all requested types would be shown once IME is ready. + mPendingTypesToShow = typesReady; + // TODO: listener for pending types. + return; + } + + // pending types from previous request. + typesReady = collectPendingConsumers(typesReady, consumers); + + if (typesReady == 0) { + listener.onCancelled(); + return; + } + + final InsetsAnimationControlImpl controller = new InsetsAnimationControlImpl(consumers, + mFrame, mState, listener, typesReady, + () -> new SyncRtSurfaceTransactionApplier(mViewRoot.mView), this); + mAnimationControls.add(controller); + } + + /** + * @return Pair of (types ready to animate, is ready to animate). + */ + private Pair collectConsumers(boolean fromIme, + ArraySet internalTypes, SparseArray consumers) { + int typesReady = 0; + boolean isReady = true; for (int i = internalTypes.size() - 1; i >= 0; i--) { InsetsSourceConsumer consumer = getSourceConsumer(internalTypes.valueAt(i)); if (consumer.getControl() != null) { + if (!consumer.isVisible()) { + // Show request + switch(consumer.requestShow(fromIme)) { + case ShowResult.SHOW_IMMEDIATELY: + typesReady |= InsetsState.toPublicType(TYPE_IME); + break; + case ShowResult.SHOW_DELAYED: + isReady = false; + break; + case ShowResult.SHOW_FAILED: + // IME cannot be shown (since it didn't have focus), proceed + // with animation of other types. + if (mPendingTypesToShow != 0) { + // remove IME from pending because view no longer has focus. + mPendingTypesToShow &= ~InsetsState.toPublicType(TYPE_IME); + } + break; + } + } else { + // Hide request + // TODO: Move notifyHidden() to beginning of the hide animation + // (when visibility actually changes using hideDirectly()). + consumer.notifyHidden(); + typesReady |= InsetsState.toPublicType(consumer.getType()); + } consumers.put(consumer.getType(), consumer); } else { // TODO: Let calling app know it's not possible, or wait // TODO: Remove it from types } } - final InsetsAnimationControlImpl controller = new InsetsAnimationControlImpl(consumers, - mFrame, mState, listener, types, - () -> new SyncRtSurfaceTransactionApplier(mViewRoot.mView), this); - mAnimationControls.add(controller); + return new Pair<>(typesReady, isReady); + } + + private int collectPendingConsumers(@InsetType int typesReady, + SparseArray consumers) { + if (mPendingTypesToShow != 0) { + typesReady |= mPendingTypesToShow; + final ArraySet internalTypes = mState.toInternalType(mPendingTypesToShow); + for (int i = internalTypes.size() - 1; i >= 0; i--) { + InsetsSourceConsumer consumer = getSourceConsumer(internalTypes.valueAt(i)); + consumers.put(consumer.getType(), consumer); + } + mPendingTypesToShow = 0; + } + return typesReady; } private void applyLocalVisibilityOverride() { @@ -293,6 +387,19 @@ public class InsetsController implements WindowInsetsController { return mViewRoot; } + /** + * Used by {@link ImeInsetsSourceConsumer} when IME decides to be shown/hidden. + * @hide + */ + @VisibleForTesting + public void applyImeVisibility(boolean setVisible) { + if (setVisible) { + show(Type.IME, true /* fromIme */); + } else { + hide(Type.IME); + } + } + private InsetsSourceConsumer createConsumerOfType(int type) { if (type == TYPE_IME) { return new ImeInsetsSourceConsumer(mState, Transaction::new, this); @@ -321,7 +428,7 @@ public class InsetsController implements WindowInsetsController { } } - private void applyAnimation(@InsetType final int types, boolean show) { + private void applyAnimation(@InsetType final int types, boolean show, boolean fromIme) { if (types == 0) { // nothing to animate. return; @@ -366,7 +473,7 @@ public class InsetsController implements WindowInsetsController { // TODO: Instead of clearing this here, properly wire up // InsetsAnimationControlImpl.finish() to remove this from mAnimationControls. mAnimationControls.clear(); - controlWindowInsetsAnimation(types, listener); + controlWindowInsetsAnimation(types, listener, fromIme); } private void hideDirectly(@InsetType int types) { diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java index cccfd870a3e45..eab83ce34708f 100644 --- a/core/java/android/view/InsetsSourceConsumer.java +++ b/core/java/android/view/InsetsSourceConsumer.java @@ -16,12 +16,15 @@ package android.view; +import android.annotation.IntDef; import android.annotation.Nullable; import android.view.InsetsState.InternalInsetType; import android.view.SurfaceControl.Transaction; import com.android.internal.annotations.VisibleForTesting; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.function.Supplier; /** @@ -30,6 +33,25 @@ import java.util.function.Supplier; */ public class InsetsSourceConsumer { + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = {ShowResult.SHOW_IMMEDIATELY, ShowResult.SHOW_DELAYED, ShowResult.SHOW_FAILED}) + @interface ShowResult { + /** + * Window type is ready to be shown, will be shown immidiately. + */ + int SHOW_IMMEDIATELY = 0; + /** + * Result will be delayed. Window needs to be prepared or request is not from controller. + * Request will be delegated to controller and may or may not be shown. + */ + int SHOW_DELAYED = 1; + /** + * Window will not be shown because one of the conditions couldn't be met. + * (e.g. in IME's case, when no editor is focused.) + */ + int SHOW_FAILED = 2; + } + protected final InsetsController mController; protected boolean mVisible; private final Supplier mTransactionSupplier; @@ -104,6 +126,25 @@ public class InsetsSourceConsumer { return mVisible; } + /** + * Request to show current window type. + * + * @param fromController {@code true} if request is coming from controller. + * (e.g. in IME case, controller is + * {@link android.inputmethodservice.InputMethodService}). + * @return @see {@link ShowResult}. + */ + @ShowResult int requestShow(boolean fromController) { + return ShowResult.SHOW_IMMEDIATELY; + } + + /** + * Notify listeners that window is now hidden. + */ + void notifyHidden() { + // no-op for types that always return ShowResult#SHOW_IMMEDIATELY. + } + private void setVisible(boolean visible) { if (mVisible == visible) { return; diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 7fee3ef29a092..ce94cb0644164 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -1886,6 +1886,36 @@ public final class InputMethodManager { } } + /** + * Call showSoftInput with currently focused view. + * @return {@code true} if IME can be shown. + * @hide + */ + public boolean requestImeShow(ResultReceiver resultReceiver) { + synchronized (mH) { + if (mServedView == null) { + return false; + } + showSoftInput(mServedView, 0 /* flags */, resultReceiver); + return true; + } + } + + /** + * Notify IME directly that it is no longer visible. + * @hide + */ + public void notifyImeHidden() { + synchronized (mH) { + try { + if (mCurMethod != null) { + mCurMethod.notifyImeHidden(); + } + } catch (RemoteException re) { + } + } + } + /** * Report the current selection range. * diff --git a/core/java/android/view/inputmethod/InputMethodSession.java b/core/java/android/view/inputmethod/InputMethodSession.java index de15f332d51db..eb81628f9e275 100644 --- a/core/java/android/view/inputmethod/InputMethodSession.java +++ b/core/java/android/view/inputmethod/InputMethodSession.java @@ -184,4 +184,11 @@ public interface InputMethodSession { * insertion point and composition string. */ public void updateCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo); + + /** + * Notifies {@link android.inputmethodservice.InputMethodService} that IME has been + * hidden from user. + * @hide + */ + public void notifyImeHidden(); } diff --git a/core/java/com/android/internal/view/IInputMethodSession.aidl b/core/java/com/android/internal/view/IInputMethodSession.aidl index 794238a3826e5..664643cc9b4d7 100644 --- a/core/java/com/android/internal/view/IInputMethodSession.aidl +++ b/core/java/com/android/internal/view/IInputMethodSession.aidl @@ -48,4 +48,6 @@ oneway interface IInputMethodSession { void finishSession(); void updateCursorAnchorInfo(in CursorAnchorInfo cursorAnchorInfo); + + void notifyImeHidden(); } diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java index c5800a380f5b6..6dad6a22f7ea2 100644 --- a/core/tests/coretests/src/android/view/InsetsControllerTest.java +++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java @@ -75,6 +75,7 @@ public class InsetsControllerTest { Insets.of(10, 10, 10, 10), rect, rect, rect, rect), rect, rect); }); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); } @Test @@ -95,6 +96,186 @@ public class InsetsControllerTest { @Test public void testAnimationEndState() { + InsetsSourceControl[] controls = prepareControls(); + InsetsSourceControl navBar = controls[0]; + InsetsSourceControl topBar = controls[1]; + InsetsSourceControl ime = controls[2]; + + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + mController.show(Type.all()); + // quickly jump to final state by cancelling it. + mController.cancelExistingAnimation(); + assertTrue(mController.getSourceConsumer(navBar.getType()).isVisible()); + assertTrue(mController.getSourceConsumer(topBar.getType()).isVisible()); + // no focused view, no IME. + assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + + mController.hide(Type.all()); + mController.cancelExistingAnimation(); + assertFalse(mController.getSourceConsumer(navBar.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(topBar.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + + mController.show(Type.ime()); + mController.cancelExistingAnimation(); + // no focused view, no IME. + assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + }); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } + + @Test + public void testApplyImeVisibility() { + final InsetsSourceControl ime = new InsetsSourceControl(TYPE_IME, mLeash, new Point()); + + InsetsSourceControl[] controls = new InsetsSourceControl[3]; + controls[0] = ime; + mController.onControlsChanged(controls); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + mController.applyImeVisibility(true); + mController.cancelExistingAnimation(); + assertTrue(mController.getSourceConsumer(ime.getType()).isVisible()); + mController.applyImeVisibility(false); + mController.cancelExistingAnimation(); + assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + }); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } + + @Test + public void testShowHideSelectively() { + InsetsSourceControl[] controls = prepareControls(); + InsetsSourceControl navBar = controls[0]; + InsetsSourceControl topBar = controls[1]; + InsetsSourceControl ime = controls[2]; + + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + int types = Type.sideBars() | Type.systemBars(); + // test show select types. + mController.show(types); + mController.cancelExistingAnimation(); + assertTrue(mController.getSourceConsumer(navBar.getType()).isVisible()); + assertTrue(mController.getSourceConsumer(topBar.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + + // test hide all + mController.hide(types); + mController.cancelExistingAnimation(); + assertFalse(mController.getSourceConsumer(navBar.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(topBar.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + }); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } + + @Test + public void testShowHideSingle() { + InsetsSourceControl[] controls = prepareControls(); + InsetsSourceControl navBar = controls[0]; + InsetsSourceControl topBar = controls[1]; + InsetsSourceControl ime = controls[2]; + + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + int types = Type.sideBars() | Type.systemBars(); + // test show select types. + mController.show(types); + mController.cancelExistingAnimation(); + assertTrue(mController.getSourceConsumer(navBar.getType()).isVisible()); + assertTrue(mController.getSourceConsumer(topBar.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + + // test hide all + mController.hide(Type.all()); + mController.cancelExistingAnimation(); + assertFalse(mController.getSourceConsumer(navBar.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(topBar.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + + // test single show + mController.show(Type.sideBars()); + mController.cancelExistingAnimation(); + assertTrue(mController.getSourceConsumer(navBar.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(topBar.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + + // test single hide + mController.hide(Type.sideBars()); + assertFalse(mController.getSourceConsumer(navBar.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(topBar.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + + }); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } + + @Test + public void testShowHideMultiple() { + InsetsSourceControl[] controls = prepareControls(); + InsetsSourceControl navBar = controls[0]; + InsetsSourceControl topBar = controls[1]; + InsetsSourceControl ime = controls[2]; + + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + // start two animations and see if previous is cancelled and final state is reached. + mController.show(Type.sideBars()); + mController.show(Type.systemBars()); + mController.cancelExistingAnimation(); + assertTrue(mController.getSourceConsumer(navBar.getType()).isVisible()); + assertTrue(mController.getSourceConsumer(topBar.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + + mController.hide(Type.sideBars()); + mController.hide(Type.systemBars()); + mController.cancelExistingAnimation(); + assertFalse(mController.getSourceConsumer(navBar.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(topBar.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + + int types = Type.sideBars() | Type.systemBars(); + // show two at a time and hide one by one. + mController.show(types); + mController.hide(Type.sideBars()); + mController.cancelExistingAnimation(); + assertFalse(mController.getSourceConsumer(navBar.getType()).isVisible()); + assertTrue(mController.getSourceConsumer(topBar.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + + mController.hide(Type.systemBars()); + mController.cancelExistingAnimation(); + assertFalse(mController.getSourceConsumer(navBar.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(topBar.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + }); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } + + @Test + public void testShowMultipleHideOneByOne() { + InsetsSourceControl[] controls = prepareControls(); + InsetsSourceControl navBar = controls[0]; + InsetsSourceControl topBar = controls[1]; + InsetsSourceControl ime = controls[2]; + + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + int types = Type.sideBars() | Type.systemBars(); + // show two at a time and hide one by one. + mController.show(types); + mController.hide(Type.sideBars()); + mController.cancelExistingAnimation(); + assertFalse(mController.getSourceConsumer(navBar.getType()).isVisible()); + assertTrue(mController.getSourceConsumer(topBar.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + + mController.hide(Type.systemBars()); + mController.cancelExistingAnimation(); + assertFalse(mController.getSourceConsumer(navBar.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(topBar.getType()).isVisible()); + assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); + }); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } + + private InsetsSourceControl[] prepareControls() { final InsetsSourceControl navBar = new InsetsSourceControl(TYPE_NAVIGATION_BAR, mLeash, new Point()); final InsetsSourceControl topBar = new InsetsSourceControl(TYPE_TOP_BAR, mLeash, @@ -106,27 +287,6 @@ public class InsetsControllerTest { controls[1] = topBar; controls[2] = ime; mController.onControlsChanged(controls); - InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { - mController.show(Type.all()); - // quickly jump to final state by cancelling it. - mController.cancelExistingAnimation(); - assertTrue(mController.getSourceConsumer(navBar.getType()).isVisible()); - assertTrue(mController.getSourceConsumer(topBar.getType()).isVisible()); - assertTrue(mController.getSourceConsumer(ime.getType()).isVisible()); - - mController.hide(Type.all()); - mController.cancelExistingAnimation(); - assertFalse(mController.getSourceConsumer(navBar.getType()).isVisible()); - assertFalse(mController.getSourceConsumer(topBar.getType()).isVisible()); - assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); - - mController.show(Type.ime()); - mController.cancelExistingAnimation(); - assertTrue(mController.getSourceConsumer(ime.getType()).isVisible()); - - mController.hide(Type.ime()); - mController.cancelExistingAnimation(); - assertFalse(mController.getSourceConsumer(ime.getType()).isVisible()); - }); + return controls; } }