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