diff --git a/core/java/android/view/PendingInsetsController.java b/core/java/android/view/PendingInsetsController.java new file mode 100644 index 0000000000000..c0ed9359c613a --- /dev/null +++ b/core/java/android/view/PendingInsetsController.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.os.CancellationSignal; +import android.view.WindowInsets.Type.InsetsType; +import android.view.animation.Interpolator; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.ArrayList; + +/** + * An insets controller that keeps track of pending requests. This is such that an app can freely + * use {@link WindowInsetsController} before the view root is attached during activity startup. + * @hide + */ +public class PendingInsetsController implements WindowInsetsController { + + private static final int KEEP_BEHAVIOR = -1; + private final ArrayList mRequests = new ArrayList<>(); + private @Appearance int mAppearance; + private @Appearance int mAppearanceMask; + private @Behavior int mBehavior = KEEP_BEHAVIOR; + private final InsetsState mDummyState = new InsetsState(); + private InsetsController mReplayedInsetsController; + + @Override + public void show(int types) { + if (mReplayedInsetsController != null) { + mReplayedInsetsController.show(types); + } else { + mRequests.add(new ShowRequest(types)); + } + } + + @Override + public void hide(int types) { + if (mReplayedInsetsController != null) { + mReplayedInsetsController.hide(types); + } else { + mRequests.add(new HideRequest(types)); + } + } + + @Override + public CancellationSignal controlWindowInsetsAnimation(int types, long durationMillis, + Interpolator interpolator, + WindowInsetsAnimationControlListener listener) { + if (mReplayedInsetsController != null) { + return mReplayedInsetsController.controlWindowInsetsAnimation(types, durationMillis, + interpolator, listener); + } else { + listener.onCancelled(); + CancellationSignal cancellationSignal = new CancellationSignal(); + cancellationSignal.cancel(); + return cancellationSignal; + } + } + + @Override + public void setSystemBarsAppearance(int appearance, int mask) { + if (mReplayedInsetsController != null) { + mReplayedInsetsController.setSystemBarsAppearance(appearance, mask); + } else { + mAppearance = (mAppearance & ~mask) | (appearance & mask); + mAppearanceMask |= mask; + } + } + + @Override + public int getSystemBarsAppearance() { + if (mReplayedInsetsController != null) { + return mReplayedInsetsController.getSystemBarsAppearance(); + } + return mAppearance; + } + + @Override + public void setSystemBarsBehavior(int behavior) { + if (mReplayedInsetsController != null) { + mReplayedInsetsController.setSystemBarsBehavior(behavior); + } else { + mBehavior = behavior; + } + } + + @Override + public int getSystemBarsBehavior() { + if (mReplayedInsetsController != null) { + return mReplayedInsetsController.getSystemBarsBehavior(); + } + return mBehavior; + } + + @Override + public InsetsState getState() { + return mDummyState; + } + + /** + * Replays the commands on {@code controller} and attaches it to this instance such that any + * calls will be forwarded to the real instance in the future. + */ + @VisibleForTesting + public void replayAndAttach(InsetsController controller) { + if (mBehavior != KEEP_BEHAVIOR) { + controller.setSystemBarsBehavior(mBehavior); + } + if (mAppearanceMask != 0) { + controller.setSystemBarsAppearance(mAppearance, mAppearanceMask); + } + int size = mRequests.size(); + for (int i = 0; i < size; i++) { + mRequests.get(i).replay(controller); + } + + // Reset all state so it doesn't get applied twice just in case + mRequests.clear(); + mBehavior = KEEP_BEHAVIOR; + mAppearance = 0; + mAppearanceMask = 0; + + // After replaying, we forward everything directly to the replayed instance. + mReplayedInsetsController = controller; + } + + /** + * Detaches the controller to no longer forward calls to the real instance. + */ + @VisibleForTesting + public void detach() { + mReplayedInsetsController = null; + } + + private interface PendingRequest { + void replay(InsetsController controller); + } + + private static class ShowRequest implements PendingRequest { + + private final @InsetsType int mTypes; + + public ShowRequest(int types) { + mTypes = types; + } + + @Override + public void replay(InsetsController controller) { + controller.show(mTypes); + } + } + + private static class HideRequest implements PendingRequest { + + private final @InsetsType int mTypes; + + public HideRequest(int types) { + mTypes = types; + } + + @Override + public void replay(InsetsController controller) { + controller.hide(mTypes); + } + } +} diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 53e8b527e9285..6236e6ea31e90 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -11415,14 +11415,18 @@ public class View implements Drawable.Callback, KeyEvent.Callback, /** * Retrieves the single {@link WindowInsetsController} of the window this view is attached to. * - * @return The {@link WindowInsetsController} or {@code null} if the view isn't attached to a - * a window. + * @return The {@link WindowInsetsController} or {@code null} if the view is neither attached to + * a window nor a view tree with a decor. * @see Window#getInsetsController() */ public @Nullable WindowInsetsController getWindowInsetsController() { if (mAttachInfo != null) { return mAttachInfo.mViewRootImpl.getInsetsController(); } + ViewParent parent = getParent(); + if (parent instanceof View) { + return ((View) parent).getWindowInsetsController(); + } return null; } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 4de1c969057d9..229143a71d577 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -1117,6 +1117,14 @@ public final class ViewRootImpl implements ViewParent, mFirstInputStage = nativePreImeStage; mFirstPostImeInputStage = earlyPostImeStage; mPendingInputEventQueueLengthCounterName = "aq:pending:" + counterSuffix; + + if (mView instanceof RootViewSurfaceTaker) { + PendingInsetsController pendingInsetsController = + ((RootViewSurfaceTaker) mView).providePendingInsetsController(); + if (pendingInsetsController != null) { + pendingInsetsController.replayAndAttach(mInsetsController); + } + } } } } diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java index 36025e324203d..51b73fc674e79 100644 --- a/core/java/com/android/internal/policy/DecorView.java +++ b/core/java/com/android/internal/policy/DecorView.java @@ -78,6 +78,7 @@ import android.view.ContextThemeWrapper; import android.view.Gravity; import android.view.InputQueue; import android.view.InsetsState; +import android.view.InsetsController; import android.view.InsetsState.InternalInsetsType; import android.view.KeyEvent; import android.view.KeyboardShortcutGroup; @@ -85,6 +86,7 @@ import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; +import android.view.PendingInsetsController; import android.view.ThreadedRenderer; import android.view.View; import android.view.ViewGroup; @@ -286,6 +288,8 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind */ private boolean mUseNewInsetsApi; + private PendingInsetsController mPendingInsetsController = new PendingInsetsController(); + DecorView(Context context, int featureId, PhoneWindow window, WindowManager.LayoutParams params) { super(context); @@ -1780,6 +1784,8 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind getViewRootImpl().removeWindowCallbacks(this); mWindowResizeCallbacksAdded = false; } + + mPendingInsetsController.detach(); } @Override @@ -1819,6 +1825,11 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind updateColorViewTranslations(); } + @Override + public PendingInsetsController providePendingInsetsController() { + return mPendingInsetsController; + } + private ActionMode createActionMode( int type, ActionMode.Callback2 callback, View originatingView) { switch (type) { @@ -2539,6 +2550,15 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind return AccessibilityNodeInfo.ROOT_ITEM_ID; } + @Override + public WindowInsetsController getWindowInsetsController() { + if (isAttachedToWindow()) { + return super.getWindowInsetsController(); + } else { + return mPendingInsetsController; + } + } + @Override public String toString() { return "DecorView@" + Integer.toHexString(this.hashCode()) + "[" diff --git a/core/java/com/android/internal/view/RootViewSurfaceTaker.java b/core/java/com/android/internal/view/RootViewSurfaceTaker.java index 433ec730749ca..efd5fb2e1edf9 100644 --- a/core/java/com/android/internal/view/RootViewSurfaceTaker.java +++ b/core/java/com/android/internal/view/RootViewSurfaceTaker.java @@ -15,8 +15,11 @@ */ package com.android.internal.view; +import android.annotation.Nullable; import android.view.InputQueue; +import android.view.PendingInsetsController; import android.view.SurfaceHolder; +import android.view.WindowInsetsController; /** hahahah */ public interface RootViewSurfaceTaker { @@ -26,4 +29,5 @@ public interface RootViewSurfaceTaker { void setSurfaceKeepScreenOn(boolean keepOn); InputQueue.Callback willYouTakeTheInputQueue(); void onRootViewScrollYChanged(int scrollY); + @Nullable PendingInsetsController providePendingInsetsController(); } diff --git a/core/tests/coretests/src/android/view/PendingInsetsControllerTest.java b/core/tests/coretests/src/android/view/PendingInsetsControllerTest.java new file mode 100644 index 0000000000000..9787b77807020 --- /dev/null +++ b/core/tests/coretests/src/android/view/PendingInsetsControllerTest.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import static android.view.WindowInsets.Type.navigationBars; +import static android.view.WindowInsets.Type.systemBars; +import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; +import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import android.os.CancellationSignal; +import android.platform.test.annotations.Presubmit; +import android.view.animation.LinearInterpolator; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import androidx.test.runner.AndroidJUnit4; + +/** + * Tests for {@link PendingInsetsControllerTest}. + * + *

Build/Install/Run: + * atest FrameworksCoreTests:PendingInsetsControllerTest + * + *

This test class is a part of Window Manager Service tests and specified in + * {@link com.android.server.wm.test.filters.FrameworksTestsFilter}. + */ +@Presubmit +@RunWith(AndroidJUnit4.class) +public class PendingInsetsControllerTest { + + private PendingInsetsController mPendingInsetsController = new PendingInsetsController(); + private InsetsController mReplayedController; + + @Before + public void setUp() { + mPendingInsetsController = new PendingInsetsController(); + mReplayedController = mock(InsetsController.class); + } + + @Test + public void testShow() { + mPendingInsetsController.show(systemBars()); + mPendingInsetsController.replayAndAttach(mReplayedController); + verify(mReplayedController).show(eq(systemBars())); + } + + @Test + public void testShow_direct() { + mPendingInsetsController.replayAndAttach(mReplayedController); + mPendingInsetsController.show(systemBars()); + verify(mReplayedController).show(eq(systemBars())); + } + + @Test + public void testHide() { + mPendingInsetsController.hide(systemBars()); + mPendingInsetsController.replayAndAttach(mReplayedController); + verify(mReplayedController).hide(eq(systemBars())); + } + + @Test + public void testHide_direct() { + mPendingInsetsController.replayAndAttach(mReplayedController); + mPendingInsetsController.hide(systemBars()); + verify(mReplayedController).hide(eq(systemBars())); + } + + @Test + public void testControl() { + WindowInsetsAnimationControlListener listener = + mock(WindowInsetsAnimationControlListener.class); + CancellationSignal signal = mPendingInsetsController.controlWindowInsetsAnimation( + systemBars(), 0, new LinearInterpolator(), listener); + verify(listener).onCancelled(); + assertTrue(signal.isCanceled()); + } + + @Test + public void testControl_direct() { + WindowInsetsAnimationControlListener listener = + mock(WindowInsetsAnimationControlListener.class); + mPendingInsetsController.replayAndAttach(mReplayedController); + mPendingInsetsController.controlWindowInsetsAnimation( + systemBars(), 0L, new LinearInterpolator(), listener); + verify(mReplayedController).controlWindowInsetsAnimation(eq(systemBars()), eq(0L), any(), + eq(listener)); + } + + @Test + public void testBehavior() { + mPendingInsetsController.setSystemBarsBehavior(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + mPendingInsetsController.replayAndAttach(mReplayedController); + verify(mReplayedController).setSystemBarsBehavior( + eq(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE)); + } + + @Test + public void testBehavior_direct() { + mPendingInsetsController.replayAndAttach(mReplayedController); + mPendingInsetsController.setSystemBarsBehavior(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + verify(mReplayedController).setSystemBarsBehavior( + eq(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE)); + } + + @Test + public void testBehavior_direct_get() { + when(mReplayedController.getSystemBarsBehavior()) + .thenReturn(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + mPendingInsetsController.replayAndAttach(mReplayedController); + assertEquals(mPendingInsetsController.getSystemBarsBehavior(), + BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + } + + @Test + public void testAppearance() { + mPendingInsetsController.setSystemBarsAppearance( + APPEARANCE_LIGHT_STATUS_BARS, APPEARANCE_LIGHT_STATUS_BARS); + mPendingInsetsController.replayAndAttach(mReplayedController); + verify(mReplayedController).setSystemBarsAppearance(eq(APPEARANCE_LIGHT_STATUS_BARS), + eq(APPEARANCE_LIGHT_STATUS_BARS)); + } + + @Test + public void testAppearance_direct() { + mPendingInsetsController.replayAndAttach(mReplayedController); + mPendingInsetsController.setSystemBarsAppearance( + APPEARANCE_LIGHT_STATUS_BARS, APPEARANCE_LIGHT_STATUS_BARS); + verify(mReplayedController).setSystemBarsAppearance(eq(APPEARANCE_LIGHT_STATUS_BARS), + eq(APPEARANCE_LIGHT_STATUS_BARS)); + } + + @Test + public void testAppearance_direct_get() { + when(mReplayedController.getSystemBarsAppearance()) + .thenReturn(APPEARANCE_LIGHT_STATUS_BARS); + mPendingInsetsController.replayAndAttach(mReplayedController); + assertEquals(mPendingInsetsController.getSystemBarsAppearance(), + APPEARANCE_LIGHT_STATUS_BARS); + } + + @Test + public void testReplayTwice() { + mPendingInsetsController.show(systemBars()); + mPendingInsetsController.setSystemBarsBehavior(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + mPendingInsetsController.setSystemBarsAppearance(APPEARANCE_LIGHT_STATUS_BARS, + APPEARANCE_LIGHT_STATUS_BARS); + mPendingInsetsController.replayAndAttach(mReplayedController); + InsetsController secondController = mock(InsetsController.class); + mPendingInsetsController.replayAndAttach(secondController); + verify(mReplayedController).show(eq(systemBars())); + verifyZeroInteractions(secondController); + } + + @Test + public void testDetachReattach() { + mPendingInsetsController.show(systemBars()); + mPendingInsetsController.setSystemBarsBehavior(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + mPendingInsetsController.setSystemBarsAppearance(APPEARANCE_LIGHT_STATUS_BARS, + APPEARANCE_LIGHT_STATUS_BARS); + mPendingInsetsController.replayAndAttach(mReplayedController); + mPendingInsetsController.detach(); + mPendingInsetsController.show(navigationBars()); + InsetsController secondController = mock(InsetsController.class); + mPendingInsetsController.replayAndAttach(secondController); + + verify(mReplayedController).show(eq(systemBars())); + verify(secondController).show(eq(navigationBars())); + } +} diff --git a/tests/utils/testutils/java/com/android/server/wm/test/filters/FrameworksTestsFilter.java b/tests/utils/testutils/java/com/android/server/wm/test/filters/FrameworksTestsFilter.java index aed62d0468961..3026e0b511336 100644 --- a/tests/utils/testutils/java/com/android/server/wm/test/filters/FrameworksTestsFilter.java +++ b/tests/utils/testutils/java/com/android/server/wm/test/filters/FrameworksTestsFilter.java @@ -46,7 +46,8 @@ public final class FrameworksTestsFilter extends SelectTest { "android.view.InsetsSourceTest", "android.view.InsetsSourceConsumerTest", "android.view.InsetsStateTest", - "android.view.WindowMetricsTest" + "android.view.WindowMetricsTest", + "android.view.PendingInsetsControllerTest" }; public FrameworksTestsFilter(Bundle testArgs) {