From 1db132e5a75bdf62fde61d93092fc909feb3110a Mon Sep 17 00:00:00 2001 From: Nick Chameyev Date: Tue, 2 May 2023 17:12:14 +0000 Subject: [PATCH] Start unfold Shell transition from WM side only when unfolding Changes the conditions when we start the unfold shell transition only when we switch from folded state to unfolded or half folded. This fixes an issue when we start the transition e.g. in rear display device state where physical display switch happens but we don't play unfold transition. As we don't play the transition, the UnfoldTransitionHandler never finished it which led to 5 seconds of a frozen screen. Test: atest WmTests:PhysicalDisplaySwitchTransitionLauncherTest Test: manual fold/unfold Bug: 275343878 Change-Id: Iad0c2d62da8fa5d6205f57ffc100e83e92715ac2 --- ...ysicalDisplaySwitchTransitionLauncher.java | 58 +++- ...alDisplaySwitchTransitionLauncherTest.java | 273 ++++++++++++++++++ 2 files changed, 319 insertions(+), 12 deletions(-) create mode 100644 services/tests/wmtests/src/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncherTest.java diff --git a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java b/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java index 2edb082ab8df1..78522498987c6 100644 --- a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java +++ b/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java @@ -20,41 +20,73 @@ import static android.view.WindowManager.TRANSIT_CHANGE; import static com.android.internal.R.bool.config_unfoldTransitionEnabled; import static com.android.server.wm.ActivityTaskManagerService.POWER_MODE_REASON_CHANGE_DISPLAY; +import static com.android.server.wm.DeviceStateController.DeviceState.FOLDED; +import static com.android.server.wm.DeviceStateController.DeviceState.HALF_FOLDED; +import static com.android.server.wm.DeviceStateController.DeviceState.OPEN; import android.animation.ValueAnimator; import android.annotation.NonNull; import android.annotation.Nullable; +import android.content.Context; import android.graphics.Rect; import android.window.DisplayAreaInfo; import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.wm.DeviceStateController.DeviceState; + public class PhysicalDisplaySwitchTransitionLauncher { private final DisplayContent mDisplayContent; - private final WindowManagerService mService; + private final ActivityTaskManagerService mAtmService; + private final Context mContext; private final TransitionController mTransitionController; /** - * If on a foldable device represents whether the device is folded or not + * If on a foldable device represents whether we need to show unfold animation when receiving + * a physical display switch event */ - private boolean mIsFolded; + private boolean mShouldRequestTransitionOnDisplaySwitch = false; + /** + * Current device state from {@link android.hardware.devicestate.DeviceStateManager} + */ + private DeviceState mDeviceState = DeviceState.UNKNOWN; private Transition mTransition; public PhysicalDisplaySwitchTransitionLauncher(DisplayContent displayContent, TransitionController transitionController) { + this(displayContent, displayContent.mWmService.mAtmService, + displayContent.mWmService.mContext, transitionController); + } + + @VisibleForTesting + public PhysicalDisplaySwitchTransitionLauncher(DisplayContent displayContent, + ActivityTaskManagerService service, Context context, + TransitionController transitionController) { mDisplayContent = displayContent; - mService = displayContent.mWmService; + mAtmService = service; + mContext = context; mTransitionController = transitionController; } /** * Called by the DeviceStateManager callback when the state changes. */ - void foldStateChanged(DeviceStateController.DeviceState newDeviceState) { - // Ignore transitions to/from half-folded. - if (newDeviceState == DeviceStateController.DeviceState.HALF_FOLDED) return; - mIsFolded = newDeviceState == DeviceStateController.DeviceState.FOLDED; + void foldStateChanged(DeviceState newDeviceState) { + boolean isUnfolding = mDeviceState == FOLDED + && (newDeviceState == HALF_FOLDED || newDeviceState == OPEN); + + if (isUnfolding) { + // Request transition only if we are unfolding the device + mShouldRequestTransitionOnDisplaySwitch = true; + } else if (newDeviceState != HALF_FOLDED && newDeviceState != OPEN) { + // Cancel the transition request in case if we are folding or switching to back + // to the rear display before the displays got switched + mShouldRequestTransitionOnDisplaySwitch = false; + } + + mDeviceState = newDeviceState; } /** @@ -62,12 +94,12 @@ public class PhysicalDisplaySwitchTransitionLauncher { */ public void requestDisplaySwitchTransitionIfNeeded(int displayId, int oldDisplayWidth, int oldDisplayHeight, int newDisplayWidth, int newDisplayHeight) { + if (!mShouldRequestTransitionOnDisplaySwitch) return; if (!mTransitionController.isShellTransitionsEnabled()) return; if (!mDisplayContent.getLastHasContent()) return; - boolean shouldRequestUnfoldTransition = !mIsFolded - && mService.mContext.getResources().getBoolean(config_unfoldTransitionEnabled) - && ValueAnimator.areAnimatorsEnabled(); + boolean shouldRequestUnfoldTransition = mContext.getResources() + .getBoolean(config_unfoldTransitionEnabled) && ValueAnimator.areAnimatorsEnabled(); if (!shouldRequestUnfoldTransition) { return; @@ -91,6 +123,8 @@ public class PhysicalDisplaySwitchTransitionLauncher { mDisplayContent.mAtmService.startLaunchPowerMode(POWER_MODE_REASON_CHANGE_DISPLAY); mTransition = t; } + + mShouldRequestTransitionOnDisplaySwitch = false; } /** @@ -118,7 +152,7 @@ public class PhysicalDisplaySwitchTransitionLauncher { if (mTransition == null) return; if (transaction != null) { - mService.mAtmService.mWindowOrganizerController.applyTransaction(transaction); + mAtmService.mWindowOrganizerController.applyTransaction(transaction); } markTransitionAsReady(); diff --git a/services/tests/wmtests/src/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncherTest.java b/services/tests/wmtests/src/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncherTest.java new file mode 100644 index 0000000000000..cc8dab99c818d --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncherTest.java @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2023 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 com.android.server.wm; + +import static android.view.WindowManager.TRANSIT_CHANGE; + +import static com.android.server.wm.DeviceStateController.DeviceState.FOLDED; +import static com.android.server.wm.DeviceStateController.DeviceState.HALF_FOLDED; +import static com.android.server.wm.DeviceStateController.DeviceState.OPEN; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Rect; +import android.platform.test.annotations.Presubmit; +import android.window.TransitionRequestInfo.DisplayChange; + +import static com.android.internal.R.bool.config_unfoldTransitionEnabled; +import static com.android.server.wm.DeviceStateController.DeviceState.REAR; + +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for the {@link WindowToken} class. + * + * Build/Install/Run: + * atest WmTests:PhysicalDisplaySwitchTransitionLauncherTest + */ +@SmallTest +@Presubmit +@RunWith(WindowTestRunner.class) +public class PhysicalDisplaySwitchTransitionLauncherTest extends WindowTestsBase { + + @Mock + DisplayContent mDisplayContent; + @Mock + Context mContext; + @Mock + Resources mResources; + @Mock + ActivityTaskManagerService mActivityTaskManagerService; + @Mock + TransitionController mTransitionController; + + private PhysicalDisplaySwitchTransitionLauncher mTarget; + private float mOriginalAnimationScale; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mContext.getResources()).thenReturn(mResources); + mTarget = new PhysicalDisplaySwitchTransitionLauncher(mDisplayContent, + mActivityTaskManagerService, mContext, mTransitionController); + mOriginalAnimationScale = ValueAnimator.getDurationScale(); + } + + @After + public void after() { + ValueAnimator.setDurationScale(mOriginalAnimationScale); + } + + @Test + public void testDisplaySwitchAfterUnfoldToOpen_animationsEnabled_requestsTransition() { + givenAllAnimationsEnabled(); + mTarget.foldStateChanged(FOLDED); + + mTarget.foldStateChanged(OPEN); + mTarget.requestDisplaySwitchTransitionIfNeeded( + /* displayId= */ 123, + /* oldDisplayWidth= */ 100, + /* oldDisplayHeight= */ 150, + /* newDisplayWidth= */ 200, + /* newDisplayHeight= */ 250 + ); + + ArgumentCaptor displayChangeArgumentCaptor = + ArgumentCaptor.forClass(DisplayChange.class); + verify(mTransitionController).requestTransitionIfNeeded(eq(TRANSIT_CHANGE), /* flags= */ + eq(0), eq(mDisplayContent), eq(mDisplayContent), /* remoteTransition= */ isNull(), + displayChangeArgumentCaptor.capture()); + assertThat(displayChangeArgumentCaptor.getValue().getDisplayId()).isEqualTo(123); + assertThat(displayChangeArgumentCaptor.getValue().getStartAbsBounds()).isEqualTo( + new Rect(0, 0, 100, 150)); + assertThat(displayChangeArgumentCaptor.getValue().getEndAbsBounds()).isEqualTo( + new Rect(0, 0, 200, 250)); + } + + @Test + public void testDisplaySwitchAfterFolding_animationEnabled_doesNotRequestTransition() { + givenAllAnimationsEnabled(); + mTarget.foldStateChanged(OPEN); + + mTarget.foldStateChanged(FOLDED); + requestDisplaySwitch(); + + assertTransitionNotRequested(); + } + + @Test + public void testDisplaySwitchAfterUnfoldingToHalf_animationEnabled_requestsTransition() { + givenAllAnimationsEnabled(); + mTarget.foldStateChanged(FOLDED); + + mTarget.foldStateChanged(HALF_FOLDED); + requestDisplaySwitch(); + + assertTransitionRequested(); + } + + @Test + public void testDisplaySwitchSecondTimeAfterUnfolding_animationEnabled_noTransition() { + givenAllAnimationsEnabled(); + mTarget.foldStateChanged(FOLDED); + mTarget.foldStateChanged(OPEN); + requestDisplaySwitch(); + clearInvocations(mTransitionController); + + requestDisplaySwitch(); + + assertTransitionNotRequested(); + } + + + @Test + public void testDisplaySwitchAfterGoingToRearAndBack_animationEnabled_noTransition() { + givenAllAnimationsEnabled(); + mTarget.foldStateChanged(OPEN); + + mTarget.foldStateChanged(REAR); + mTarget.foldStateChanged(OPEN); + requestDisplaySwitch(); + + assertTransitionNotRequested(); + } + + @Test + public void testDisplaySwitchAfterUnfoldingAndFolding_animationEnabled_noTransition() { + givenAllAnimationsEnabled(); + mTarget.foldStateChanged(FOLDED); + mTarget.foldStateChanged(OPEN); + // No request display switch event (simulate very fast fold after unfold, even before + // the displays switched) + mTarget.foldStateChanged(FOLDED); + + requestDisplaySwitch(); + + assertTransitionNotRequested(); + } + + @Test + public void testDisplaySwitch_whenShellTransitionsNotEnabled_noTransition() { + givenAllAnimationsEnabled(); + givenShellTransitionsEnabled(false); + mTarget.foldStateChanged(FOLDED); + + mTarget.foldStateChanged(OPEN); + requestDisplaySwitch(); + + assertTransitionNotRequested(); + } + + @Test + public void testDisplaySwitch_whenAnimationsDisabled_noTransition() { + givenAllAnimationsEnabled(); + givenAnimationsEnabled(false); + mTarget.foldStateChanged(FOLDED); + + mTarget.foldStateChanged(OPEN); + requestDisplaySwitch(); + + assertTransitionNotRequested(); + } + + @Test + public void testDisplaySwitch_whenUnfoldAnimationDisabled_noTransition() { + givenAllAnimationsEnabled(); + givenUnfoldTransitionEnabled(false); + mTarget.foldStateChanged(FOLDED); + + mTarget.foldStateChanged(OPEN); + requestDisplaySwitch(); + + assertTransitionNotRequested(); + } + + @Test + public void testDisplaySwitch_whenNoContentInDisplayContent_noTransition() { + givenAllAnimationsEnabled(); + givenDisplayContentHasContent(false); + mTarget.foldStateChanged(FOLDED); + + mTarget.foldStateChanged(OPEN); + requestDisplaySwitch(); + + assertTransitionNotRequested(); + } + + private void assertTransitionRequested() { + verify(mTransitionController).requestTransitionIfNeeded(anyInt(), anyInt(), any(), any(), + any(), any()); + } + + private void assertTransitionNotRequested() { + verify(mTransitionController, never()).requestTransitionIfNeeded(anyInt(), anyInt(), any(), + any(), any(), any()); + } + + private void requestDisplaySwitch() { + mTarget.requestDisplaySwitchTransitionIfNeeded( + /* displayId= */ 123, + /* oldDisplayWidth= */ 100, + /* oldDisplayHeight= */ 150, + /* newDisplayWidth= */ 200, + /* newDisplayHeight= */ 250 + ); + } + + private void givenAllAnimationsEnabled() { + givenAnimationsEnabled(true); + givenUnfoldTransitionEnabled(true); + givenShellTransitionsEnabled(true); + givenDisplayContentHasContent(true); + } + + private void givenUnfoldTransitionEnabled(boolean enabled) { + when(mResources.getBoolean(config_unfoldTransitionEnabled)).thenReturn(enabled); + } + + private void givenAnimationsEnabled(boolean enabled) { + ValueAnimator.setDurationScale(enabled ? 1.0f : 0.0f); + } + + private void givenShellTransitionsEnabled(boolean enabled) { + when(mTransitionController.isShellTransitionsEnabled()).thenReturn(enabled); + } + + private void givenDisplayContentHasContent(boolean hasContent) { + when(mDisplayContent.getLastHasContent()).thenReturn(hasContent); + } +}