Fix WindowInsetsController lifecycle

Provide a recording insets controller before the window gets
created, and replay the commands once a view gets attached. This
allows the client to use the controller in Activity.onCreate.

Test: WindowInsetsControllerTests
Bug: 118118435
Change-Id: I1a825ecc4367c02b27f2d08cd5442325315d4f89
This commit is contained in:
Jorim Jaggi
2019-01-22 19:01:48 +01:00
parent 920105c38b
commit 57157ac5d8
7 changed files with 414 additions and 3 deletions

View File

@@ -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<PendingRequest> 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);
}
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}
}
}
}

View File

@@ -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()) + "["

View File

@@ -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();
}

View File

@@ -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}.
*
* <p>Build/Install/Run:
* atest FrameworksCoreTests:PendingInsetsControllerTest
*
* <p>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()));
}
}

View File

@@ -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) {