diff options
| author | 2022-10-13 08:47:42 +0000 | |
|---|---|---|
| committer | 2022-10-13 08:47:42 +0000 | |
| commit | 6d1c1b57b45cfd3a38c5ae632ef80cd8caa0051d (patch) | |
| tree | ab5caa2faf465b512f8c3e55548dc6fbd17c0feb | |
| parent | 7e64429d94a480a845b4c01cc192b03ae3b43e97 (diff) | |
| parent | 9723cfbc38ba5dc47208584def2711924184a77e (diff) | |
Merge "Allow rotation when display is half-folded" into tm-qpr-dev
10 files changed, 470 insertions, 35 deletions
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 3408f75f0f72..84280605862f 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -658,6 +658,20 @@ --> </integer-array> + <!-- The device states (supplied by DeviceStateManager) that should be treated as half-folded by + the display fold controller. Default is empty. --> + <integer-array name="config_halfFoldedDeviceStates"> + <!-- Example: + <item>0</item> + <item>1</item> + <item>2</item> + --> + </integer-array> + + <!-- Indicates whether the window manager reacts to half-fold device states by overriding + rotation. --> + <bool name="config_windowManagerHalfFoldAutoRotateOverride">false</bool> + <!-- When a device enters any of these states, it should be woken up. States are defined in device_state_configuration.xml. --> <integer-array name="config_deviceStatesOnWhichToWakeUp"> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 16798b8b0a24..97105d80bf79 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -4005,6 +4005,8 @@ <!-- For Foldables --> <java-symbol type="array" name="config_foldedDeviceStates" /> + <java-symbol type="array" name="config_halfFoldedDeviceStates" /> + <java-symbol type="bool" name="config_windowManagerHalfFoldAutoRotateOverride" /> <java-symbol type="array" name="config_deviceStatesOnWhichToWakeUp" /> <java-symbol type="array" name="config_deviceStatesOnWhichToSleep" /> <java-symbol type="string" name="config_foldedArea" /> diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index b1ecb43f9a23..31e2abe0bb85 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -1087,6 +1087,12 @@ "group": "WM_DEBUG_FOCUS", "at": "com\/android\/server\/wm\/WindowState.java" }, + "-1043981272": { + "message": "Reverting orientation. Rotating to %s from %s rather than %s.", + "level": "VERBOSE", + "group": "WM_DEBUG_ORIENTATION", + "at": "com\/android\/server\/wm\/DisplayRotation.java" + }, "-1042574499": { "message": "Attempted to add Accessibility overlay window with unknown token %s. Aborting.", "level": "WARN", @@ -4285,6 +4291,12 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "2066210760": { + "message": "foldStateChanged: displayId %d, halfFoldStateChanged %s, saved rotation: %d, mUserRotation: %d, mLastSensorRotation: %d, mLastOrientation: %d, mRotation: %d", + "level": "VERBOSE", + "group": "WM_DEBUG_ORIENTATION", + "at": "com\/android\/server\/wm\/DisplayRotation.java" + }, "2070726247": { "message": "InsetsSource updateVisibility for %s, serverVisible: %s clientVisible: %s", "level": "DEBUG", diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java index 22bffda33918..6087655615a6 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java @@ -132,8 +132,11 @@ public class RotationButtonController { mMainThreadHandler.postAtFrontOfQueue(() -> { // If the screen rotation changes while locked, potentially update lock to flow with // new screen rotation and hide any showing suggestions. - if (isRotationLocked()) { - if (shouldOverrideUserLockPrefs(rotation)) { + boolean rotationLocked = isRotationLocked(); + // The isVisible check makes the rotation button disappear when we are not locked + // (e.g. for tabletop auto-rotate). + if (rotationLocked || mRotationButton.isVisible()) { + if (shouldOverrideUserLockPrefs(rotation) && rotationLocked) { setRotationLockedAtAngle(rotation); } setRotateSuggestionButtonState(false /* visible */, true /* forced */); diff --git a/services/core/java/com/android/server/wm/DeviceStateController.java b/services/core/java/com/android/server/wm/DeviceStateController.java new file mode 100644 index 000000000000..a6f855755192 --- /dev/null +++ b/services/core/java/com/android/server/wm/DeviceStateController.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2022 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 android.annotation.Nullable; +import android.content.Context; +import android.hardware.devicestate.DeviceStateManager; +import android.os.Handler; +import android.os.HandlerExecutor; + +import com.android.internal.util.ArrayUtils; + +import java.util.function.Consumer; + +/** + * Class that registers callbacks with the {@link DeviceStateManager} and + * responds to fold state changes by forwarding such events to a delegate. + */ +final class DeviceStateController { + private final DeviceStateManager mDeviceStateManager; + private final Context mContext; + + private FoldStateListener mDeviceStateListener; + + public enum FoldState { + UNKNOWN, OPEN, FOLDED, HALF_FOLDED + } + + DeviceStateController(Context context, Handler handler, Consumer<FoldState> delegate) { + mContext = context; + mDeviceStateManager = mContext.getSystemService(DeviceStateManager.class); + if (mDeviceStateManager != null) { + mDeviceStateListener = new FoldStateListener(mContext, delegate); + mDeviceStateManager + .registerCallback(new HandlerExecutor(handler), + mDeviceStateListener); + } + } + + void unregisterFromDeviceStateManager() { + if (mDeviceStateListener != null) { + mDeviceStateManager.unregisterCallback(mDeviceStateListener); + } + } + + /** + * A listener for half-fold device state events that dispatches state changes to a delegate. + */ + static final class FoldStateListener implements DeviceStateManager.DeviceStateCallback { + + private final int[] mHalfFoldedDeviceStates; + private final int[] mFoldedDeviceStates; + + @Nullable + private FoldState mLastResult; + private final Consumer<FoldState> mDelegate; + + FoldStateListener(Context context, Consumer<FoldState> delegate) { + mFoldedDeviceStates = context.getResources().getIntArray( + com.android.internal.R.array.config_foldedDeviceStates); + mHalfFoldedDeviceStates = context.getResources().getIntArray( + com.android.internal.R.array.config_halfFoldedDeviceStates); + mDelegate = delegate; + } + + @Override + public void onStateChanged(int state) { + final boolean halfFolded = ArrayUtils.contains(mHalfFoldedDeviceStates, state); + FoldState result; + if (halfFolded) { + result = FoldState.HALF_FOLDED; + } else { + final boolean folded = ArrayUtils.contains(mFoldedDeviceStates, state); + result = folded ? FoldState.FOLDED : FoldState.OPEN; + } + if (mLastResult == null || !mLastResult.equals(result)) { + mLastResult = result; + mDelegate.accept(result); + } + } + } +} diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 71c80fb9a97c..38f6a53982c4 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -564,6 +564,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp final FixedRotationTransitionListener mFixedRotationTransitionListener = new FixedRotationTransitionListener(); + private final DeviceStateController mDeviceStateController; private final PhysicalDisplaySwitchTransitionLauncher mDisplaySwitchTransitionLauncher; final RemoteDisplayChangeController mRemoteDisplayChangeController; @@ -1119,6 +1120,13 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mDisplayPolicy = new DisplayPolicy(mWmService, this); mDisplayRotation = new DisplayRotation(mWmService, this); + + mDeviceStateController = new DeviceStateController(mWmService.mContext, mWmService.mH, + newFoldState -> { + mDisplaySwitchTransitionLauncher.foldStateChanged(newFoldState); + mDisplayRotation.foldStateChanged(newFoldState); + }); + mCloseToSquareMaxAspectRatio = mWmService.mContext.getResources().getFloat( R.dimen.config_closeToSquareDisplayMaxAspectRatio); if (isDefaultDisplay) { @@ -3218,7 +3226,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mTransitionController.unregisterLegacyListener(mFixedRotationTransitionListener); handleAnimatingStoppedAndTransition(); mWmService.stopFreezingDisplayLocked(); - mDisplaySwitchTransitionLauncher.destroy(); + mDeviceStateController.unregisterFromDeviceStateManager(); super.removeImmediately(); if (DEBUG_DISPLAY) Slog.v(TAG_WM, "Removing display=" + this); mPointerEventDispatcher.dispose(); diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java index 97609a7dd8ba..a8d13c57ffa2 100644 --- a/services/core/java/com/android/server/wm/DisplayRotation.java +++ b/services/core/java/com/android/server/wm/DisplayRotation.java @@ -40,6 +40,7 @@ import static com.android.server.wm.WindowManagerService.WINDOW_FREEZE_TIMEOUT_D import android.annotation.AnimRes; import android.annotation.IntDef; +import android.annotation.Nullable; import android.app.ActivityManager; import android.content.ContentResolver; import android.content.Context; @@ -108,6 +109,8 @@ public class DisplayRotation { private OrientationListener mOrientationListener; private StatusBarManagerInternal mStatusBarManagerInternal; private SettingsObserver mSettingsObserver; + @Nullable + private FoldController mFoldController; @ScreenOrientation private int mCurrentAppOrientation = SCREEN_ORIENTATION_UNSPECIFIED; @@ -238,6 +241,10 @@ public class DisplayRotation { mOrientationListener.setCurrentRotation(mRotation); mSettingsObserver = new SettingsObserver(uiHandler); mSettingsObserver.observe(); + if (mSupportAutoRotation && mContext.getResources().getBoolean( + R.bool.config_windowManagerHalfFoldAutoRotateOverride)) { + mFoldController = new FoldController(); + } } } @@ -436,7 +443,17 @@ public class DisplayRotation { final int oldRotation = mRotation; final int lastOrientation = mLastOrientation; - final int rotation = rotationForOrientation(lastOrientation, oldRotation); + int rotation = rotationForOrientation(lastOrientation, oldRotation); + // Use the saved rotation for tabletop mode, if set. + if (mFoldController != null && mFoldController.shouldRevertOverriddenRotation()) { + int prevRotation = rotation; + rotation = mFoldController.revertOverriddenRotation(); + ProtoLog.v(WM_DEBUG_ORIENTATION, + "Reverting orientation. Rotating to %s from %s rather than %s.", + Surface.rotationToString(rotation), + Surface.rotationToString(oldRotation), + Surface.rotationToString(prevRotation)); + } ProtoLog.v(WM_DEBUG_ORIENTATION, "Computed rotation=%s (%d) for display id=%d based on lastOrientation=%s (%d) and " + "oldRotation=%s (%d)", @@ -1138,7 +1155,8 @@ public class DisplayRotation { // If we don't support auto-rotation then bail out here and ignore // the sensor and any rotation lock settings. preferredRotation = -1; - } else if ((mUserRotationMode == WindowManagerPolicy.USER_ROTATION_FREE + } else if (((mUserRotationMode == WindowManagerPolicy.USER_ROTATION_FREE + || isTabletopAutoRotateOverrideEnabled()) && (orientation == ActivityInfo.SCREEN_ORIENTATION_USER || orientation == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED || orientation == ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE @@ -1292,10 +1310,17 @@ public class DisplayRotation { return false; } + private boolean isTabletopAutoRotateOverrideEnabled() { + return mFoldController != null && mFoldController.overrideFrozenRotation(); + } + private boolean isRotationChoicePossible(int orientation) { // Rotation choice is only shown when the user is in locked mode. if (mUserRotationMode != WindowManagerPolicy.USER_ROTATION_LOCKED) return false; + // Don't show rotation choice if we are in tabletop or book modes. + if (isTabletopAutoRotateOverrideEnabled()) return false; + // We should only enable rotation choice if the rotation isn't forced by the lid, dock, // demo, hdmi, vr, etc mode. @@ -1496,6 +1521,74 @@ public class DisplayRotation { proto.end(token); } + /** + * Called by the DeviceStateManager callback when the device state changes. + */ + void foldStateChanged(DeviceStateController.FoldState foldState) { + if (mFoldController != null) { + synchronized (mLock) { + mFoldController.foldStateChanged(foldState); + } + } + } + + private class FoldController { + @Surface.Rotation + private int mHalfFoldSavedRotation = -1; // No saved rotation + private DeviceStateController.FoldState mFoldState = + DeviceStateController.FoldState.UNKNOWN; + + boolean overrideFrozenRotation() { + return mFoldState == DeviceStateController.FoldState.HALF_FOLDED; + } + + boolean shouldRevertOverriddenRotation() { + return mFoldState == DeviceStateController.FoldState.OPEN // When transitioning to open. + && mHalfFoldSavedRotation != -1 // Ignore if we've already reverted. + && mUserRotationMode + == WindowManagerPolicy.USER_ROTATION_LOCKED; // Ignore if we're unlocked. + } + + int revertOverriddenRotation() { + int savedRotation = mHalfFoldSavedRotation; + mHalfFoldSavedRotation = -1; + return savedRotation; + } + + void foldStateChanged(DeviceStateController.FoldState newState) { + ProtoLog.v(WM_DEBUG_ORIENTATION, + "foldStateChanged: displayId %d, halfFoldStateChanged %s, " + + "saved rotation: %d, mUserRotation: %d, mLastSensorRotation: %d, " + + "mLastOrientation: %d, mRotation: %d", + mDisplayContent.getDisplayId(), newState.name(), mHalfFoldSavedRotation, + mUserRotation, mLastSensorRotation, mLastOrientation, mRotation); + if (mFoldState == DeviceStateController.FoldState.UNKNOWN) { + mFoldState = newState; + return; + } + if (newState == DeviceStateController.FoldState.HALF_FOLDED + && mFoldState != DeviceStateController.FoldState.HALF_FOLDED) { + // The device has transitioned to HALF_FOLDED state: save the current rotation and + // update the device rotation. + mHalfFoldSavedRotation = mRotation; + mFoldState = newState; + // Now mFoldState is set to HALF_FOLDED, the overrideFrozenRotation function will + // return true, so rotation is unlocked. + mService.updateRotation(false /* alwaysSendConfiguration */, + false /* forceRelayout */); + } else { + // Revert the rotation to our saved value if we transition from HALF_FOLDED. + mRotation = mHalfFoldSavedRotation; + // Tell the device to update its orientation (mFoldState is still HALF_FOLDED here + // so we will override USER_ROTATION_LOCKED and allow a rotation). + mService.updateRotation(false /* alwaysSendConfiguration */, + false /* forceRelayout */); + // Once we are rotated, set mFoldstate, effectively removing the lock override. + mFoldState = newState; + } + } + } + private class OrientationListener extends WindowOrientationListener implements Runnable { transient boolean mEnabled; diff --git a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java b/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java index a89894db4b4b..30bdc3477edf 100644 --- a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java +++ b/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java @@ -24,10 +24,7 @@ import static com.android.server.wm.ActivityTaskManagerService.POWER_MODE_REASON import android.animation.ValueAnimator; import android.annotation.NonNull; import android.annotation.Nullable; -import android.content.Context; import android.graphics.Rect; -import android.hardware.devicestate.DeviceStateManager; -import android.os.HandlerExecutor; import android.window.DisplayAreaInfo; import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; @@ -36,11 +33,8 @@ public class PhysicalDisplaySwitchTransitionLauncher { private final DisplayContent mDisplayContent; private final WindowManagerService mService; - private final DeviceStateManager mDeviceStateManager; private final TransitionController mTransitionController; - private DeviceStateListener mDeviceStateListener; - /** * If on a foldable device represents whether the device is folded or not */ @@ -52,21 +46,15 @@ public class PhysicalDisplaySwitchTransitionLauncher { mDisplayContent = displayContent; mService = displayContent.mWmService; mTransitionController = transitionController; - - mDeviceStateManager = mService.mContext.getSystemService(DeviceStateManager.class); - - if (mDeviceStateManager != null) { - mDeviceStateListener = new DeviceStateListener(mService.mContext); - mDeviceStateManager - .registerCallback(new HandlerExecutor(mDisplayContent.mWmService.mH), - mDeviceStateListener); - } } - public void destroy() { - if (mDeviceStateManager != null) { - mDeviceStateManager.unregisterCallback(mDeviceStateListener); - } + /** + * Called by the DeviceStateManager callback when the state changes. + */ + void foldStateChanged(DeviceStateController.FoldState newFoldState) { + // Ignore transitions to/from half-folded. + if (newFoldState == DeviceStateController.FoldState.HALF_FOLDED) return; + mIsFolded = newFoldState == DeviceStateController.FoldState.FOLDED; } /** @@ -143,10 +131,4 @@ public class PhysicalDisplaySwitchTransitionLauncher { mTransition = null; } - class DeviceStateListener extends DeviceStateManager.FoldStateListener { - - DeviceStateListener(Context context) { - super(context, newIsFolded -> mIsFolded = newIsFolded); - } - } } diff --git a/services/tests/wmtests/src/com/android/server/wm/DeviceStateControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DeviceStateControllerTests.java new file mode 100644 index 000000000000..86732c9a3f46 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/DeviceStateControllerTests.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2022 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 com.android.dx.mockito.inline.extended.ExtendedMockito.any; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.times; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; + +import static org.junit.Assert.assertEquals; + +import android.content.Context; +import android.content.res.Resources; +import android.hardware.devicestate.DeviceStateManager; +import android.os.Handler; +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import java.util.function.Consumer; + +/** + * Test class for {@link DeviceStateController}. + * + * Build/Install/Run: + * atest WmTests:DeviceStateControllerTests + */ +@SmallTest +@Presubmit +public class DeviceStateControllerTests { + + private DeviceStateController.FoldStateListener mFoldStateListener; + private DeviceStateController mTarget; + private DeviceStateControllerBuilder mBuilder; + + private Context mMockContext; + private Handler mMockHandler; + private Resources mMockRes; + private DeviceStateManager mMockDeviceStateManager; + + private Consumer<DeviceStateController.FoldState> mDelegate; + private DeviceStateController.FoldState mCurrentState = DeviceStateController.FoldState.UNKNOWN; + + @Before + public void setUp() { + mBuilder = new DeviceStateControllerBuilder(); + mCurrentState = DeviceStateController.FoldState.UNKNOWN; + } + + private void initialize(boolean supportFold, boolean supportHalfFold) throws Exception { + mBuilder.setSupportFold(supportFold, supportHalfFold); + mDelegate = (newFoldState) -> { + mCurrentState = newFoldState; + }; + mBuilder.setDelegate(mDelegate); + mBuilder.build(); + verifyFoldStateListenerRegistration(1); + } + + @Test + public void testInitialization() throws Exception { + initialize(true /* supportFold */, true /* supportHalfFolded */); + mFoldStateListener.onStateChanged(mUnfoldedStates[0]); + assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN); + } + + @Test + public void testInitializationWithNoFoldSupport() throws Exception { + initialize(false /* supportFold */, false /* supportHalfFolded */); + mFoldStateListener.onStateChanged(mFoldedStates[0]); + // Note that the folded state is ignored. + assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN); + } + + @Test + public void testWithFoldSupported() throws Exception { + initialize(true /* supportFold */, false /* supportHalfFolded */); + mFoldStateListener.onStateChanged(mUnfoldedStates[0]); + assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN); + mFoldStateListener.onStateChanged(mFoldedStates[0]); + assertEquals(mCurrentState, DeviceStateController.FoldState.FOLDED); + mFoldStateListener.onStateChanged(mHalfFoldedStates[0]); + assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN); // Ignored + } + + @Test + public void testWithHalfFoldSupported() throws Exception { + initialize(true /* supportFold */, true /* supportHalfFolded */); + mFoldStateListener.onStateChanged(mUnfoldedStates[0]); + assertEquals(mCurrentState, DeviceStateController.FoldState.OPEN); + mFoldStateListener.onStateChanged(mFoldedStates[0]); + assertEquals(mCurrentState, DeviceStateController.FoldState.FOLDED); + mFoldStateListener.onStateChanged(mHalfFoldedStates[0]); + assertEquals(mCurrentState, DeviceStateController.FoldState.HALF_FOLDED); + } + + + private final int[] mFoldedStates = {0}; + private final int[] mUnfoldedStates = {1}; + private final int[] mHalfFoldedStates = {2}; + + + private void verifyFoldStateListenerRegistration(int numOfInvocation) { + final ArgumentCaptor<DeviceStateController.FoldStateListener> listenerCaptor = + ArgumentCaptor.forClass(DeviceStateController.FoldStateListener.class); + verify(mMockDeviceStateManager, times(numOfInvocation)).registerCallback( + any(), + listenerCaptor.capture()); + if (numOfInvocation > 0) { + mFoldStateListener = listenerCaptor.getValue(); + } + } + + private class DeviceStateControllerBuilder { + private boolean mSupportFold = false; + private boolean mSupportHalfFold = false; + private Consumer<DeviceStateController.FoldState> mDelegate; + + DeviceStateControllerBuilder setSupportFold( + boolean supportFold, boolean supportHalfFold) { + mSupportFold = supportFold; + mSupportHalfFold = supportHalfFold; + return this; + } + + DeviceStateControllerBuilder setDelegate( + Consumer<DeviceStateController.FoldState> delegate) { + mDelegate = delegate; + return this; + } + + private void mockFold(boolean enableFold, boolean enableHalfFold) { + if (enableFold) { + when(mMockContext.getResources().getIntArray( + com.android.internal.R.array.config_foldedDeviceStates)) + .thenReturn(mFoldedStates); + } + if (enableHalfFold) { + when(mMockContext.getResources().getIntArray( + com.android.internal.R.array.config_halfFoldedDeviceStates)) + .thenReturn(mHalfFoldedStates); + } + } + + private void build() throws Exception { + mMockContext = mock(Context.class); + mMockRes = mock(Resources.class); + when(mMockContext.getResources()).thenReturn((mMockRes)); + mMockDeviceStateManager = mock(DeviceStateManager.class); + when(mMockContext.getSystemService(DeviceStateManager.class)) + .thenReturn(mMockDeviceStateManager); + mockFold(mSupportFold, mSupportHalfFold); + mMockHandler = mock(Handler.class); + mTarget = new DeviceStateController(mMockContext, mMockHandler, mDelegate); + } + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java index 89f71110f3c2..b45c37f9da0c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java @@ -28,6 +28,7 @@ import static android.view.IWindowManager.FIXED_TO_USER_ROTATION_ENABLED; import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyBoolean; import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.atLeast; import static com.android.dx.mockito.inline.extended.ExtendedMockito.atMost; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq; @@ -103,7 +104,7 @@ public class DisplayRotationTests { private Context mMockContext; private Resources mMockRes; private SensorManager mMockSensorManager; - private Sensor mFakeSensor; + private Sensor mFakeOrientationSensor; private DisplayWindowSettings mMockDisplayWindowSettings; private ContentResolver mMockResolver; private FakeSettingsProvider mFakeSettingsProvider; @@ -323,7 +324,7 @@ public class DisplayRotationTests { waitForUiHandler(); verify(mMockSensorManager, times(numOfInvocation)).registerListener( listenerCaptor.capture(), - same(mFakeSensor), + same(mFakeOrientationSensor), anyInt(), any()); if (numOfInvocation > 0) { @@ -460,7 +461,7 @@ public class DisplayRotationTests { SensorEvent.class.getDeclaredConstructor(int.class); constructor.setAccessible(true); final SensorEvent event = constructor.newInstance(1); - event.sensor = mFakeSensor; + event.sensor = mFakeOrientationSensor; event.values[0] = rotation; event.timestamp = SystemClock.elapsedRealtimeNanos(); return event; @@ -691,6 +692,43 @@ public class DisplayRotationTests { SCREEN_ORIENTATION_SENSOR, Surface.ROTATION_0)); } + // ==================================================== + // Tests for half-fold auto-rotate override of rotation + // ==================================================== + @Test + public void testUpdatesRotationWhenSensorUpdates_RotationLocked_HalfFolded() throws Exception { + mBuilder.setSupportHalfFoldAutoRotateOverride(true); + mBuilder.build(); + configureDisplayRotation(SCREEN_ORIENTATION_LANDSCAPE, false, false); + + enableOrientationSensor(); + + mTarget.foldStateChanged(DeviceStateController.FoldState.OPEN); + freezeRotation(Surface.ROTATION_270); + + mOrientationSensorListener.onSensorChanged(createSensorEvent(Surface.ROTATION_0)); + assertTrue(waitForUiHandler()); + // No rotation... + assertEquals(Surface.ROTATION_270, mTarget.rotationForOrientation( + SCREEN_ORIENTATION_UNSPECIFIED, Surface.ROTATION_0)); + + // ... until half-fold + mTarget.foldStateChanged(DeviceStateController.FoldState.HALF_FOLDED); + assertTrue(waitForUiHandler()); + verify(sMockWm).updateRotation(false, false); + assertTrue(waitForUiHandler()); + assertEquals(Surface.ROTATION_0, mTarget.rotationForOrientation( + SCREEN_ORIENTATION_UNSPECIFIED, Surface.ROTATION_0)); + + // ... then transition back to flat + mTarget.foldStateChanged(DeviceStateController.FoldState.OPEN); + assertTrue(waitForUiHandler()); + verify(sMockWm, atLeast(1)).updateRotation(false, false); + assertTrue(waitForUiHandler()); + assertEquals(Surface.ROTATION_270, mTarget.rotationForOrientation( + SCREEN_ORIENTATION_UNSPECIFIED, Surface.ROTATION_0)); + } + // ================================= // Tests for Policy based Rotation // ================================= @@ -884,6 +922,7 @@ public class DisplayRotationTests { private class DisplayRotationBuilder { private boolean mIsDefaultDisplay = true; private boolean mSupportAutoRotation = true; + private boolean mSupportHalfFoldAutoRotateOverride = false; private int mLidOpenRotation = WindowManagerPolicy.WindowManagerFuncs.LID_ABSENT; private int mCarDockRotation; @@ -920,6 +959,12 @@ public class DisplayRotationTests { return this; } + private DisplayRotationBuilder setSupportHalfFoldAutoRotateOverride( + boolean supportHalfFoldAutoRotateOverride) { + mSupportHalfFoldAutoRotateOverride = supportHalfFoldAutoRotateOverride; + return this; + } + private void captureObservers() { ArgumentCaptor<ContentObserver> captor = ArgumentCaptor.forClass( ContentObserver.class); @@ -1032,9 +1077,13 @@ public class DisplayRotationTests { mMockSensorManager = mock(SensorManager.class); when(mMockContext.getSystemService(Context.SENSOR_SERVICE)) .thenReturn(mMockSensorManager); - mFakeSensor = createSensor(Sensor.TYPE_DEVICE_ORIENTATION); + mFakeOrientationSensor = createSensor(Sensor.TYPE_DEVICE_ORIENTATION); when(mMockSensorManager.getSensorList(Sensor.TYPE_DEVICE_ORIENTATION)).thenReturn( - Collections.singletonList(mFakeSensor)); + Collections.singletonList(mFakeOrientationSensor)); + + when(mMockContext.getResources().getBoolean( + com.android.internal.R.bool.config_windowManagerHalfFoldAutoRotateOverride)) + .thenReturn(mSupportHalfFoldAutoRotateOverride); mMockResolver = mock(ContentResolver.class); when(mMockContext.getContentResolver()).thenReturn(mMockResolver); |