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
This commit is contained in:
Nick Chameyev
2023-05-02 17:12:14 +00:00
parent a262e87a4a
commit 1db132e5a7
2 changed files with 319 additions and 12 deletions

View File

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

View File

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