From 4203281253f89b91aab954a70a5b9bad8e6592ce Mon Sep 17 00:00:00 2001 From: Tiger Date: Fri, 30 Jun 2023 17:44:06 +0800 Subject: [PATCH] Play insets animation while forcibly showing system bars Before this CL, when a system bar is being forcibly shown, InsetsPolicy would set the control target to null, which just resets the leash without any animation. This CL sets the proper control target and uses the InsetsController to play insets animations. This CL also applies the same logic to the transient bars of immersive mode. Fix: 283738201 Test: InsetsPolicyTest Test: Open an app from kid space, and see if navigation bar is shown with animation. Test: Send a Chat message to a device which is in immersive mode, and see if status bar is shown with animation. Change-Id: I8178a9cabfa62a3942ca6ff51b31df22c0d570d3 --- core/java/android/view/InsetsController.java | 1 - .../com/android/server/wm/InsetsPolicy.java | 337 ++++++++---------- .../android/server/wm/InsetsPolicyTest.java | 13 - .../android/server/wm/TestDisplayContent.java | 4 + .../android/server/wm/WindowTestsBase.java | 13 + 5 files changed, 164 insertions(+), 204 deletions(-) diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index 0abad6c76c2e2..c1eacb535d57e 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -841,7 +841,6 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation return mLastDispatchedState; } - @VisibleForTesting public boolean onStateChanged(InsetsState state) { boolean stateChanged = false; if (!CAPTION_ON_SHELL) { diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java index 4620266c981e0..835c92d0a30de 100644 --- a/services/core/java/com/android/server/wm/InsetsPolicy.java +++ b/services/core/java/com/android/server/wm/InsetsPolicy.java @@ -21,12 +21,7 @@ import static android.app.StatusBarManager.WINDOW_STATE_SHOWING; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; -import static android.view.InsetsController.ANIMATION_TYPE_HIDE; -import static android.view.InsetsController.ANIMATION_TYPE_SHOW; -import static android.view.InsetsController.LAYOUT_INSETS_DURING_ANIMATION_HIDDEN; -import static android.view.InsetsController.LAYOUT_INSETS_DURING_ANIMATION_SHOWN; import static android.view.InsetsSource.ID_IME; -import static android.view.SyncRtSurfaceTransactionApplier.applyParams; import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION; @@ -39,16 +34,13 @@ import android.app.StatusBarManager; import android.app.WindowConfiguration; import android.content.ComponentName; import android.content.res.Resources; +import android.os.Handler; +import android.os.IBinder; import android.util.SparseArray; -import android.view.InsetsAnimationControlCallbacks; -import android.view.InsetsAnimationControlImpl; -import android.view.InsetsAnimationControlRunner; import android.view.InsetsController; import android.view.InsetsFrameProvider; import android.view.InsetsSource; -import android.view.InsetsSourceControl; import android.view.InsetsState; -import android.view.InternalInsetsAnimationController; import android.view.SurfaceControl; import android.view.SyncRtSurfaceTransactionApplier; import android.view.WindowInsets; @@ -56,15 +48,15 @@ import android.view.WindowInsets.Type; import android.view.WindowInsets.Type.InsetsType; import android.view.WindowInsetsAnimation; import android.view.WindowInsetsAnimation.Bounds; -import android.view.WindowInsetsAnimationControlListener; import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; -import com.android.server.DisplayThread; import com.android.server.statusbar.StatusBarManagerInternal; import java.io.PrintWriter; +import java.util.List; /** * Policy that implements who gets control over the windows generating insets. @@ -79,48 +71,19 @@ class InsetsPolicy { private final DisplayContent mDisplayContent; private final DisplayPolicy mPolicy; - /** For resetting visibilities of insets sources. */ - private final InsetsControlTarget mDummyControlTarget = new InsetsControlTarget() { + /** Used to show system bars transiently. This won't affect the layout. */ + private final InsetsControlTarget mTransientControlTarget; - @Override - public void notifyInsetsControlChanged() { - boolean hasLeash = false; - final InsetsSourceControl[] controls = - mStateController.getControlsForDispatch(this); - if (controls == null) { - return; - } - for (InsetsSourceControl control : controls) { - if (isTransient(control.getType())) { - // The visibilities of transient bars will be handled with animations. - continue; - } - final SurfaceControl leash = control.getLeash(); - if (leash != null) { - hasLeash = true; - - // We use alpha to control the visibility here which aligns the logic at - // SurfaceAnimator.createAnimationLeash - final boolean visible = - (control.getType() & WindowInsets.Type.defaultVisible()) != 0; - mDisplayContent.getPendingTransaction().setAlpha(leash, visible ? 1f : 0f); - } - } - if (hasLeash) { - mDisplayContent.scheduleAnimation(); - } - } - }; + /** Used to show system bars permanently. This will affect the layout. */ + private final InsetsControlTarget mPermanentControlTarget; private WindowState mFocusedWin; private final BarWindow mStatusBar = new BarWindow(StatusBarManager.WINDOW_STATUS_BAR); private final BarWindow mNavBar = new BarWindow(StatusBarManager.WINDOW_NAVIGATION_BAR); private @InsetsType int mShowingTransientTypes; private @InsetsType int mForcedShowingTypes; - private boolean mAnimatingShown; private final boolean mHideNavBarForKeyboard; - private final float[] mTmpFloat9 = new float[9]; InsetsPolicy(InsetsStateController stateController, DisplayContent displayContent) { mStateController = stateController; @@ -128,6 +91,10 @@ class InsetsPolicy { mPolicy = displayContent.getDisplayPolicy(); final Resources r = mPolicy.getContext().getResources(); mHideNavBarForKeyboard = r.getBoolean(R.bool.config_hideNavBarForKeyboard); + mTransientControlTarget = new ControlTarget( + stateController, displayContent.mWmService.mH, "TransientControlTarget"); + mPermanentControlTarget = new ControlTarget( + stateController, displayContent.mWmService.mH, "PermanentControlTarget"); } /** Updates the target which can control system bars. */ @@ -144,13 +111,13 @@ class InsetsPolicy { final WindowState topApp = mPolicy.getTopFullscreenOpaqueWindow(); mStateController.onBarControlTargetChanged( statusControlTarget, - statusControlTarget == mDummyControlTarget + statusControlTarget == mTransientControlTarget ? getStatusControlTarget(focusedWin, true /* fake */) : statusControlTarget == notificationShade ? getStatusControlTarget(topApp, true /* fake */) : null, navControlTarget, - navControlTarget == mDummyControlTarget + navControlTarget == mTransientControlTarget ? getNavControlTarget(focusedWin, true /* fake */) : navControlTarget == notificationShade ? getNavControlTarget(topApp, true /* fake */) @@ -200,19 +167,19 @@ class InsetsPolicy { mFocusedWin, (showingTransientTypes & (Type.statusBars() | Type.navigationBars())) != 0, isGestureOnSystemBar); - - // The leashes can be created while updating bar control target. The surface transaction - // of the new leashes might not be applied yet. The callback posted here ensures we can - // get the valid leashes because the surface transaction will be applied in the next - // animation frame which will be triggered if a new leash is created. - mDisplayContent.mWmService.mAnimator.getChoreographer().postFrameCallback(time -> { - synchronized (mDisplayContent.mWmService.mGlobalLock) { - startAnimation(true /* show */, null /* callback */); - } - }); } } + @VisibleForTesting + InsetsControlTarget getTransientControlTarget() { + return mTransientControlTarget; + } + + @VisibleForTesting + InsetsControlTarget getPermanentControlTarget() { + return mPermanentControlTarget; + } + void hideTransient() { if (mShowingTransientTypes == 0) { return; @@ -223,23 +190,8 @@ class InsetsPolicy { /* areVisible= */ false, /* wereRevealedFromSwipeOnSystemBar= */ false); - startAnimation(false /* show */, () -> { - synchronized (mDisplayContent.mWmService.mGlobalLock) { - final SparseArray providers = - mStateController.getSourceProviders(); - for (int i = providers.size() - 1; i >= 0; i--) { - final InsetsSourceProvider provider = providers.valueAt(i); - if (!isTransient(provider.getSource().getType())) { - continue; - } - // We are about to clear mShowingTransientTypes, we don't want the transient bar - // can cause insets on the client. Restore the client visibility. - provider.setClientVisible(false); - } - mShowingTransientTypes = 0; - updateBarControlTarget(mFocusedWin); - } - }); + mShowingTransientTypes = 0; + updateBarControlTarget(mFocusedWin); } boolean isTransient(@InsetsType int type) { @@ -502,7 +454,7 @@ class InsetsPolicy { private @Nullable InsetsControlTarget getStatusControlTarget(@Nullable WindowState focusedWin, boolean fake) { if (!fake && isTransient(Type.statusBars())) { - return mDummyControlTarget; + return mTransientControlTarget; } final WindowState notificationShade = mPolicy.getNotificationShade(); if (focusedWin == notificationShade) { @@ -519,13 +471,13 @@ class InsetsPolicy { if (areTypesForciblyShowing(Type.statusBars())) { // Status bar is forcibly shown. We don't want the client to control the status bar, and // we will dispatch the real visibility of status bar to the client. - return null; + return mPermanentControlTarget; } if (forceShowsStatusBarTransiently() && !fake) { // Status bar is forcibly shown transiently, and its new visibility won't be // dispatched to the client so that we can keep the layout stable. We will dispatch the // fake control to the client, so that it can re-show the bar during this scenario. - return mDummyControlTarget; + return mTransientControlTarget; } if (!canBeTopFullscreenOpaqueWindow(focusedWin) && mPolicy.topAppHidesSystemBar(Type.statusBars()) @@ -556,7 +508,7 @@ class InsetsPolicy { return null; } if (!fake && isTransient(Type.navigationBars())) { - return mDummyControlTarget; + return mTransientControlTarget; } if (focusedWin == mPolicy.getNotificationShade()) { // Notification shade has control anyways, no reason to force anything. @@ -579,13 +531,13 @@ class InsetsPolicy { if (areTypesForciblyShowing(Type.navigationBars())) { // Navigation bar is forcibly shown. We don't want the client to control the navigation // bar, and we will dispatch the real visibility of navigation bar to the client. - return null; + return mPermanentControlTarget; } if (forceShowsNavigationBarTransiently() && !fake) { // Navigation bar is forcibly shown transiently, and its new visibility won't be // dispatched to the client so that we can keep the layout stable. We will dispatch the // fake control to the client, so that it can re-show the bar during this scenario. - return mDummyControlTarget; + return mTransientControlTarget; } final WindowState notificationShade = mPolicy.getNotificationShade(); if (!canBeTopFullscreenOpaqueWindow(focusedWin) @@ -662,34 +614,6 @@ class InsetsPolicy { && (win.mAttrs.privateFlags & PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION) != 0; } - @VisibleForTesting - void startAnimation(boolean show, Runnable callback) { - @InsetsType int typesReady = 0; - final SparseArray controlsReady = new SparseArray<>(); - final InsetsSourceControl[] controls = - mStateController.getControlsForDispatch(mDummyControlTarget); - if (controls == null) { - if (callback != null) { - DisplayThread.getHandler().post(callback); - } - return; - } - for (InsetsSourceControl control : controls) { - if (isTransient(control.getType()) && control.getLeash() != null) { - typesReady |= control.getType(); - controlsReady.put(control.getId(), new InsetsSourceControl(control)); - } - } - controlAnimationUnchecked(typesReady, controlsReady, show, callback); - } - - private void controlAnimationUnchecked(int typesReady, - SparseArray controls, boolean show, Runnable callback) { - InsetsPolicyAnimationControlListener listener = - new InsetsPolicyAnimationControlListener(show, callback, typesReady); - listener.mControlCallbacks.controlAnimationUnchecked(typesReady, controls, show); - } - private void dispatchTransientSystemBarsVisibilityChanged( @Nullable WindowState focusedWindow, boolean areVisible, @@ -760,105 +684,138 @@ class InsetsPolicy { } } - private class InsetsPolicyAnimationControlListener extends - InsetsController.InternalAnimationControlListener { - Runnable mFinishCallback; - InsetsPolicyAnimationControlCallbacks mControlCallbacks; + private static class ControlTarget implements InsetsControlTarget { - InsetsPolicyAnimationControlListener(boolean show, Runnable finishCallback, int types) { - super(show, false /* hasCallbacks */, types, BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE, - false /* disable */, 0 /* floatingImeBottomInsets */, - null /* loggingListener */, null /* jankContext */); - mFinishCallback = finishCallback; - mControlCallbacks = new InsetsPolicyAnimationControlCallbacks(this); + private final InsetsState mState = new InsetsState(); + private final InsetsController mInsetsController; + private final InsetsStateController mStateController; + private final String mName; + + ControlTarget(InsetsStateController stateController, Handler handler, String name) { + mStateController = stateController; + mInsetsController = new InsetsController(new Host(handler, name)); + mName = name; } @Override - protected void onAnimationFinish() { - super.onAnimationFinish(); - if (mFinishCallback != null) { - DisplayThread.getHandler().post(mFinishCallback); - } + public void notifyInsetsControlChanged() { + mState.set(mStateController.getRawInsetsState(), true /* copySources */); + mInsetsController.onStateChanged(mState); + mInsetsController.onControlsChanged(mStateController.getControlsForDispatch(this)); } - private class InsetsPolicyAnimationControlCallbacks implements - InsetsAnimationControlCallbacks { - private InsetsAnimationControlImpl mAnimationControl = null; - private InsetsPolicyAnimationControlListener mListener; + @Override + public String toString() { + return mName; + } + } - InsetsPolicyAnimationControlCallbacks(InsetsPolicyAnimationControlListener listener) { - mListener = listener; + private static class Host implements InsetsController.Host { + + private final float[] mTmpFloat9 = new float[9]; + private final Handler mHandler; + private final String mName; + + Host(Handler handler, String name) { + mHandler = handler; + mName = name; + } + + @Override + public Handler getHandler() { + return mHandler; + } + + @Override + public void notifyInsetsChanged() { } + + @Override + public void dispatchWindowInsetsAnimationPrepare( + @NonNull WindowInsetsAnimation animation) { } + + @Override + public Bounds dispatchWindowInsetsAnimationStart( + @NonNull WindowInsetsAnimation animation, + @NonNull Bounds bounds) { + return bounds; + } + + @Override + public WindowInsets dispatchWindowInsetsAnimationProgress( + @NonNull WindowInsets insets, + @NonNull List runningAnimations) { + return insets; + } + + @Override + public void dispatchWindowInsetsAnimationEnd( + @NonNull WindowInsetsAnimation animation) { } + + @Override + public void applySurfaceParams(SyncRtSurfaceTransactionApplier.SurfaceParams... p) { + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + for (int i = p.length - 1; i >= 0; i--) { + SyncRtSurfaceTransactionApplier.applyParams(t, p[i], mTmpFloat9); } + t.apply(); + t.close(); + } - private void controlAnimationUnchecked(int typesReady, - SparseArray controls, boolean show) { - if (typesReady == 0) { - // nothing to animate. - return; - } - mAnimatingShown = show; + @Override + public void updateRequestedVisibleTypes(int types) { } - final InsetsState state = mFocusedWin.getInsetsState(); + @Override + public boolean hasAnimationCallbacks() { + return false; + } - // We are about to playing the default animation. Passing a null frame indicates - // the controlled types should be animated regardless of the frame. - mAnimationControl = new InsetsAnimationControlImpl(controls, - null /* frame */, state, mListener, typesReady, this, - mListener.getDurationMs(), getInsetsInterpolator(), - show ? ANIMATION_TYPE_SHOW : ANIMATION_TYPE_HIDE, show - ? LAYOUT_INSETS_DURING_ANIMATION_SHOWN - : LAYOUT_INSETS_DURING_ANIMATION_HIDDEN, - null /* translator */, null /* statsToken */); - SurfaceAnimationThread.getHandler().post( - () -> mListener.onReady(mAnimationControl, typesReady)); - } + @Override + public void setSystemBarsAppearance(int appearance, int mask) { } - /** Called on SurfaceAnimationThread without global WM lock held. */ - @Override - public void scheduleApplyChangeInsets(InsetsAnimationControlRunner runner) { - if (mAnimationControl.applyChangeInsets(null /* outState */)) { - mAnimationControl.finish(mAnimatingShown); - } - } + @Override + public int getSystemBarsAppearance() { + return 0; + } - @Override - public void notifyFinished(InsetsAnimationControlRunner runner, boolean shown) { - // Nothing's needed here. Finish steps is handled in the listener - // onAnimationFinished callback. - } + @Override + public void setSystemBarsBehavior(int behavior) { } - /** Called on SurfaceAnimationThread without global WM lock held. */ - @Override - public void applySurfaceParams( - final SyncRtSurfaceTransactionApplier.SurfaceParams... params) { - SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - for (int i = params.length - 1; i >= 0; i--) { - SyncRtSurfaceTransactionApplier.SurfaceParams surfaceParams = params[i]; - applyParams(t, surfaceParams, mTmpFloat9); - } - t.apply(); - t.close(); - } + @Override + public int getSystemBarsBehavior() { + return BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; + } - // Since we don't push applySurfaceParams to a Handler-queue we don't need - // to push release in this case. - @Override - public void releaseSurfaceControlFromRt(SurfaceControl sc) { - sc.release(); - } + @Override + public void releaseSurfaceControlFromRt(SurfaceControl surfaceControl) { + surfaceControl.release(); + } - @Override - public - void startAnimation(T runner, WindowInsetsAnimationControlListener listener, int types, - WindowInsetsAnimation animation, - Bounds bounds) { - } + @Override + public void addOnPreDrawRunnable(Runnable r) { } - @Override - public void reportPerceptible(int types, boolean perceptible) { - // No-op for now - only client windows report perceptibility for now, with policy - // controllers assumed to always be perceptible. - } + @Override + public void postInsetsAnimationCallback(Runnable r) { } + + @Override + public InputMethodManager getInputMethodManager() { + return null; + } + + @Nullable + @Override + public String getRootViewTitle() { + return mName; + } + + @Override + public int dipToPx(int dips) { + return 0; + } + + @Nullable + @Override + public IBinder getWindowToken() { + return null; } } } diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java index 204cbf79dba97..994dcf1e2ea50 100644 --- a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java @@ -31,17 +31,12 @@ import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.clearInvocations; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.verify; import android.app.StatusBarManager; @@ -268,8 +263,6 @@ public class InsetsPolicyTest extends WindowTestsBase { navBar.setHasSurface(true); navBarProvider.setServerVisible(true); final InsetsPolicy policy = mDisplayContent.getInsetsPolicy(); - spyOn(policy); - doNothing().when(policy).startAnimation(anyBoolean(), any()); // Make both system bars invisible. mAppWindow.setRequestedVisibleTypes( @@ -305,8 +298,6 @@ public class InsetsPolicyTest extends WindowTestsBase { addNavigationBar().getControllableInsetProvider().setServerVisible(true); final InsetsPolicy policy = mDisplayContent.getInsetsPolicy(); - spyOn(policy); - doNothing().when(policy).startAnimation(anyBoolean(), any()); policy.updateBarControlTarget(mAppWindow); policy.showTransient(navigationBars() | statusBars(), true /* isGestureOnSystemBar */); @@ -341,8 +332,6 @@ public class InsetsPolicyTest extends WindowTestsBase { mAppWindow.mAboveInsetsState.addSource(navBarSource); mAppWindow.mAboveInsetsState.addSource(statusBarSource); final InsetsPolicy policy = mDisplayContent.getInsetsPolicy(); - spyOn(policy); - doNothing().when(policy).startAnimation(anyBoolean(), any()); policy.updateBarControlTarget(mAppWindow); policy.showTransient(navigationBars() | statusBars(), true /* isGestureOnSystemBar */); @@ -390,8 +379,6 @@ public class InsetsPolicyTest extends WindowTestsBase { final WindowState app2 = addWindow(TYPE_APPLICATION, "app"); final InsetsPolicy policy = mDisplayContent.getInsetsPolicy(); - spyOn(policy); - doNothing().when(policy).startAnimation(anyBoolean(), any()); policy.updateBarControlTarget(app); policy.showTransient(navigationBars() | statusBars(), true /* isGestureOnSystemBar */); diff --git a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java index 4639ee0ae33d0..45ecc3f762ec8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java +++ b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java @@ -77,6 +77,10 @@ class TestDisplayContent extends DisplayContent { spyOn(inputMonitor); doNothing().when(inputMonitor).resumeDispatchingLw(any()); + final InsetsPolicy insetsPolicy = getInsetsPolicy(); + WindowTestsBase.suppressInsetsAnimation(insetsPolicy.getPermanentControlTarget()); + WindowTestsBase.suppressInsetsAnimation(insetsPolicy.getTransientControlTarget()); + // For devices that set the sysprop ro.bootanim.set_orientation_ // See DisplayRotation#readDefaultDisplayRotation for context. // Without that, meaning of height and width in context of the tests can be swapped if diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index be8ee7832a5dc..d5547ec692476 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -222,6 +222,10 @@ class WindowTestsBase extends SystemServiceTestsBase { displayPolicy.finishWindowsDrawn(); displayPolicy.finishScreenTurningOn(); + final InsetsPolicy insetsPolicy = mDefaultDisplay.getInsetsPolicy(); + suppressInsetsAnimation(insetsPolicy.getTransientControlTarget()); + suppressInsetsAnimation(insetsPolicy.getPermanentControlTarget()); + mTransaction = mSystemServicesTestRule.mTransaction; mMockSession = mock(Session.class); @@ -278,6 +282,15 @@ class WindowTestsBase extends SystemServiceTestsBase { checkDeviceSpecificOverridesNotApplied(); } + /** + * The test doesn't create real SurfaceControls, but mocked ones. This prevents the target from + * controlling them, or it will cause {@link NullPointerException}. + */ + static void suppressInsetsAnimation(InsetsControlTarget target) { + spyOn(target); + Mockito.doNothing().when(target).notifyInsetsControlChanged(); + } + @After public void tearDown() throws Exception { if (mUseFakeSettingsProvider) {