From 5f6167b72e90b0b0d4ea7b1c42489e430358429b Mon Sep 17 00:00:00 2001 From: Kevin Chyn Date: Wed, 25 Jan 2023 01:20:47 +0000 Subject: [PATCH] Coordinate rotation for concurrent mode 1) Adds DisplayRotationCoordinator which is used to coordinate default display rotation changes with non-default displays. 2) We now create DeviceStateController in RootWindowContainer and pass a single instance to DisplayContent/DisplayRotation. This allows for (3) below 3) Adds TestDisplayContent.Builder#setDeviceStateController, which allows tests to mock deviceStateController behavior Bug: 251747141 Bug: 267343444 Test: atest DisplayRotationCoordinatorTests Test: atest DisplayRotationTests Test: atest DisplayContentTests Test: atest DualDisplayAreaGroupPolicyTest Test: presubmit for other unit tests Test: manual Change-Id: Ifae3806079ece87d6d14d09b3025ee19f9c9d226 --- core/java/android/util/RotationUtils.java | 20 ++++ core/res/res/values/config.xml | 7 ++ core/res/res/values/symbols.xml | 7 ++ .../server/wm/DeviceStateController.java | 32 ++++-- .../com/android/server/wm/DisplayContent.java | 12 +- .../android/server/wm/DisplayRotation.java | 58 ++++++++-- .../server/wm/DisplayRotationCoordinator.java | 99 ++++++++++++++++ .../server/wm/RootWindowContainer.java | 15 ++- .../server/wm/DisplayContentTests.java | 107 ++++++++++++++++++ .../wm/DisplayRotationCoordinatorTests.java | 95 ++++++++++++++++ .../server/wm/DisplayRotationTests.java | 2 +- .../wm/DualDisplayAreaGroupPolicyTest.java | 2 +- .../android/server/wm/TestDisplayContent.java | 16 ++- 13 files changed, 441 insertions(+), 31 deletions(-) create mode 100644 services/core/java/com/android/server/wm/DisplayRotationCoordinator.java create mode 100644 services/tests/wmtests/src/com/android/server/wm/DisplayRotationCoordinatorTests.java diff --git a/core/java/android/util/RotationUtils.java b/core/java/android/util/RotationUtils.java index 3e7c67e720315..f20767b7485ac 100644 --- a/core/java/android/util/RotationUtils.java +++ b/core/java/android/util/RotationUtils.java @@ -27,6 +27,7 @@ import android.graphics.Matrix; import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; +import android.view.Surface; import android.view.Surface.Rotation; import android.view.SurfaceControl; @@ -245,4 +246,23 @@ public class RotationUtils { throw new IllegalArgumentException("Unknown rotation: " + rotation); } } + + /** + * Reverses the rotation direction around the Z axis. Note that this method assumes all + * rotations are relative to {@link Surface.ROTATION_0}. + * + * @param rotation the original rotation. + * @return the new rotation that should be applied. + */ + @Surface.Rotation + public static int reverseRotationDirectionAroundZAxis(@Surface.Rotation int rotation) { + // Flipping 270 and 90 has the same effect as changing the direction which rotation is + // applied. + if (rotation == Surface.ROTATION_90) { + rotation = Surface.ROTATION_270; + } else if (rotation == Surface.ROTATION_270) { + rotation = Surface.ROTATION_90; + } + return rotation; + } } diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index d3aee4329dd2a..1256cfb382492 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -980,6 +980,13 @@ + + true + + + + diff --git a/services/core/java/com/android/server/wm/DeviceStateController.java b/services/core/java/com/android/server/wm/DeviceStateController.java index 7d9a4ec4b5c33..3f2852212fc66 100644 --- a/services/core/java/com/android/server/wm/DeviceStateController.java +++ b/services/core/java/com/android/server/wm/DeviceStateController.java @@ -24,6 +24,7 @@ import android.os.Handler; import android.os.HandlerExecutor; import com.android.internal.R; +import com.android.internal.annotations.GuardedBy; import com.android.internal.util.ArrayUtils; import java.util.ArrayList; @@ -48,9 +49,12 @@ final class DeviceStateController implements DeviceStateManager.DeviceStateCallb private final int[] mRearDisplayDeviceStates; @NonNull private final int[] mReverseRotationAroundZAxisStates; + @GuardedBy("this") @NonNull private final List> mDeviceStateCallbacks = new ArrayList<>(); + private final boolean mMatchBuiltInDisplayOrientationToDefaultDisplay; + @Nullable private DeviceState mLastDeviceState; private int mCurrentState; @@ -72,20 +76,19 @@ final class DeviceStateController implements DeviceStateManager.DeviceStateCallb .getIntArray(R.array.config_rearDisplayDeviceStates); mReverseRotationAroundZAxisStates = context.getResources() .getIntArray(R.array.config_deviceStatesToReverseDefaultDisplayRotationAroundZAxis); + mMatchBuiltInDisplayOrientationToDefaultDisplay = context.getResources() + .getBoolean(R.bool + .config_matchSecondaryInternalDisplaysOrientationToReverseDefaultDisplay); if (mDeviceStateManager != null) { mDeviceStateManager.registerCallback(new HandlerExecutor(handler), this); } } - void unregisterFromDeviceStateManager() { - if (mDeviceStateManager != null) { - mDeviceStateManager.unregisterCallback(this); - } - } - void registerDeviceStateCallback(@NonNull Consumer callback) { - mDeviceStateCallbacks.add(callback); + synchronized (this) { + mDeviceStateCallbacks.add(callback); + } } /** @@ -95,6 +98,15 @@ final class DeviceStateController implements DeviceStateManager.DeviceStateCallb return ArrayUtils.contains(mReverseRotationAroundZAxisStates, mCurrentState); } + /** + * @return true if non-default built-in displays should match the default display's rotation. + */ + boolean shouldMatchBuiltInDisplayOrientationToReverseDefaultDisplay() { + // TODO(b/265991392): This should come from display_settings.xml once it's easier to + // extend with complex configurations. + return mMatchBuiltInDisplayOrientationToDefaultDisplay; + } + @Override public void onStateChanged(int state) { mCurrentState = state; @@ -115,8 +127,10 @@ final class DeviceStateController implements DeviceStateManager.DeviceStateCallb if (mLastDeviceState == null || !mLastDeviceState.equals(deviceState)) { mLastDeviceState = deviceState; - for (Consumer callback : mDeviceStateCallbacks) { - callback.accept(mLastDeviceState); + synchronized (this) { + for (Consumer callback : mDeviceStateCallbacks) { + callback.accept(mLastDeviceState); + } } } } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index f183760ec8e3b..267d0cc919403 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -590,7 +590,8 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp final FixedRotationTransitionListener mFixedRotationTransitionListener = new FixedRotationTransitionListener(); - private final DeviceStateController mDeviceStateController; + @VisibleForTesting + final DeviceStateController mDeviceStateController; private final PhysicalDisplaySwitchTransitionLauncher mDisplaySwitchTransitionLauncher; final RemoteDisplayChangeController mRemoteDisplayChangeController; @@ -1086,7 +1087,8 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp * @param display May not be null. * @param root {@link RootWindowContainer} */ - DisplayContent(Display display, RootWindowContainer root) { + DisplayContent(Display display, RootWindowContainer root, + @NonNull DeviceStateController deviceStateController) { super(root.mWindowManager, "DisplayContent", FEATURE_ROOT); if (mWmService.mRoot.getDisplayContent(display.getDisplayId()) != null) { throw new IllegalArgumentException("Display with ID=" + display.getDisplayId() @@ -1146,11 +1148,11 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mWmService.mAtmService.getRecentTasks().getInputListener()); } - mDeviceStateController = new DeviceStateController(mWmService.mContext, mWmService.mH); + mDeviceStateController = deviceStateController; mDisplayPolicy = new DisplayPolicy(mWmService, this); mDisplayRotation = new DisplayRotation(mWmService, this, mDisplayInfo.address, - mDeviceStateController); + mDeviceStateController, root.getDisplayRotationCoordinator()); final Consumer deviceStateConsumer = (@NonNull DeviceStateController.DeviceState newFoldState) -> { @@ -3325,7 +3327,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mTransitionController.unregisterLegacyListener(mFixedRotationTransitionListener); handleAnimatingStoppedAndTransition(); mWmService.stopFreezingDisplayLocked(); - mDeviceStateController.unregisterFromDeviceStateManager(); + mDisplayRotation.removeDefaultDisplayRotationChangedCallback(); 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 6f7ff5c21d94c..dcdac09933b19 100644 --- a/services/core/java/com/android/server/wm/DisplayRotation.java +++ b/services/core/java/com/android/server/wm/DisplayRotation.java @@ -58,6 +58,7 @@ import android.os.SystemProperties; import android.os.UserHandle; import android.provider.Settings; import android.util.ArraySet; +import android.util.RotationUtils; import android.util.Slog; import android.util.TimeUtils; import android.util.proto.ProtoOutputStream; @@ -120,6 +121,11 @@ public class DisplayRotation { private FoldController mFoldController; @NonNull private final DeviceStateController mDeviceStateController; + @NonNull + private final DisplayRotationCoordinator mDisplayRotationCoordinator; + @NonNull + @VisibleForTesting + final Runnable mDefaultDisplayRotationChangedCallback; @ScreenOrientation private int mCurrentAppOrientation = SCREEN_ORIENTATION_UNSPECIFIED; @@ -221,17 +227,19 @@ public class DisplayRotation { private boolean mDemoRotationLock; DisplayRotation(WindowManagerService service, DisplayContent displayContent, - DisplayAddress displayAddress, @NonNull DeviceStateController deviceStateController) { + DisplayAddress displayAddress, @NonNull DeviceStateController deviceStateController, + @NonNull DisplayRotationCoordinator displayRotationCoordinator) { this(service, displayContent, displayAddress, displayContent.getDisplayPolicy(), service.mDisplayWindowSettings, service.mContext, service.getWindowManagerLock(), - deviceStateController); + deviceStateController, displayRotationCoordinator); } @VisibleForTesting DisplayRotation(WindowManagerService service, DisplayContent displayContent, DisplayAddress displayAddress, DisplayPolicy displayPolicy, DisplayWindowSettings displayWindowSettings, Context context, Object lock, - @NonNull DeviceStateController deviceStateController) { + @NonNull DeviceStateController deviceStateController, + @NonNull DisplayRotationCoordinator displayRotationCoordinator) { mService = service; mDisplayContent = displayContent; mDisplayPolicy = displayPolicy; @@ -251,6 +259,19 @@ public class DisplayRotation { mRotation = readDefaultDisplayRotation(displayAddress); + mDisplayRotationCoordinator = displayRotationCoordinator; + if (isDefaultDisplay) { + mDisplayRotationCoordinator.setDefaultDisplayDefaultRotation(mRotation); + } + mDefaultDisplayRotationChangedCallback = this::updateRotationAndSendNewConfigIfChanged; + + if (DisplayRotationCoordinator.isSecondaryInternalDisplay(displayContent) + && mDeviceStateController + .shouldMatchBuiltInDisplayOrientationToReverseDefaultDisplay()) { + mDisplayRotationCoordinator.setDefaultDisplayRotationChangedCallback( + mDefaultDisplayRotationChangedCallback); + } + if (isDefaultDisplay) { final Handler uiHandler = UiThread.getHandler(); mOrientationListener = new OrientationListener(mContext, uiHandler); @@ -492,8 +513,11 @@ public class DisplayRotation { return false; } + @Surface.Rotation final int oldRotation = mRotation; + @ScreenOrientation final int lastOrientation = mLastOrientation; + @Surface.Rotation int rotation = rotationForOrientation(lastOrientation, oldRotation); // Use the saved rotation for tabletop mode, if set. if (mFoldController != null && mFoldController.shouldRevertOverriddenRotation()) { @@ -505,6 +529,14 @@ public class DisplayRotation { Surface.rotationToString(oldRotation), Surface.rotationToString(prevRotation)); } + + if (DisplayRotationCoordinator.isSecondaryInternalDisplay(mDisplayContent) + && mDeviceStateController + .shouldMatchBuiltInDisplayOrientationToReverseDefaultDisplay()) { + rotation = RotationUtils.reverseRotationDirectionAroundZAxis( + mDisplayRotationCoordinator.getDefaultDisplayCurrentRotation()); + } + ProtoLog.v(WM_DEBUG_ORIENTATION, "Computed rotation=%s (%d) for display id=%d based on lastOrientation=%s (%d) and " + "oldRotation=%s (%d)", @@ -523,6 +555,10 @@ public class DisplayRotation { return false; } + if (isDefaultDisplay) { + mDisplayRotationCoordinator.onDefaultDisplayRotationChanged(rotation); + } + // Preemptively cancel the running recents animation -- SysUI can't currently handle this // case properly since the signals it receives all happen post-change. We do this earlier // in the rotation flow, since DisplayContent.updateDisplayOverrideConfigurationLocked seems @@ -1140,17 +1176,12 @@ public class DisplayRotation { return mUserRotation; } + @Surface.Rotation int sensorRotation = mOrientationListener != null ? mOrientationListener.getProposedRotation() // may be -1 : -1; if (mDeviceStateController.shouldReverseRotationDirectionAroundZAxis()) { - // Flipping 270 and 90 has the same effect as changing the direction which rotation is - // applied. - if (sensorRotation == Surface.ROTATION_90) { - sensorRotation = Surface.ROTATION_270; - } else if (sensorRotation == Surface.ROTATION_270) { - sensorRotation = Surface.ROTATION_90; - } + sensorRotation = RotationUtils.reverseRotationDirectionAroundZAxis(sensorRotation); } mLastSensorRotation = sensorRotation; if (sensorRotation < 0) { @@ -1165,6 +1196,7 @@ public class DisplayRotation { final boolean deskDockEnablesAccelerometer = mDisplayPolicy.isDeskDockEnablesAccelerometer(); + @Surface.Rotation final int preferredRotation; if (!isDefaultDisplay) { // For secondary displays we ignore things like displays sensors, docking mode and @@ -1534,6 +1566,12 @@ public class DisplayRotation { return shouldUpdateRotation; } + void removeDefaultDisplayRotationChangedCallback() { + if (DisplayRotationCoordinator.isSecondaryInternalDisplay(mDisplayContent)) { + mDisplayRotationCoordinator.removeDefaultDisplayRotationChangedCallback(); + } + } + void dump(String prefix, PrintWriter pw) { pw.println(prefix + "DisplayRotation"); pw.println(prefix + " mCurrentAppOrientation=" diff --git a/services/core/java/com/android/server/wm/DisplayRotationCoordinator.java b/services/core/java/com/android/server/wm/DisplayRotationCoordinator.java new file mode 100644 index 0000000000000..ae3787cffa23b --- /dev/null +++ b/services/core/java/com/android/server/wm/DisplayRotationCoordinator.java @@ -0,0 +1,99 @@ +/* + * 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 android.annotation.NonNull; +import android.annotation.Nullable; +import android.view.Display; +import android.view.Surface; + +import com.android.internal.annotations.VisibleForTesting; + +/** + * Singleton for coordinating rotation across multiple displays. Used to notify non-default + * displays when the default display rotates. + * + * Note that this class does not need locking because it is always protected by WindowManagerService + * mGlobalLock. + */ +class DisplayRotationCoordinator { + + private static final String TAG = "DisplayRotationCoordinator"; + + @Surface.Rotation + private int mDefaultDisplayDefaultRotation; + + @Nullable + @VisibleForTesting + Runnable mDefaultDisplayRotationChangedCallback; + + @Surface.Rotation + private int mDefaultDisplayCurrentRotation; + + /** + * Notifies clients when the default display rotation changes. + */ + void onDefaultDisplayRotationChanged(@Surface.Rotation int rotation) { + mDefaultDisplayCurrentRotation = rotation; + + if (mDefaultDisplayRotationChangedCallback != null) { + mDefaultDisplayRotationChangedCallback.run(); + } + } + + void setDefaultDisplayDefaultRotation(@Surface.Rotation int rotation) { + mDefaultDisplayDefaultRotation = rotation; + } + + @Surface.Rotation + int getDefaultDisplayCurrentRotation() { + return mDefaultDisplayCurrentRotation; + } + + /** + * Register a callback to be notified when the default display's rotation changes. Clients can + * query the default display's current rotation via {@link #getDefaultDisplayCurrentRotation()}. + */ + void setDefaultDisplayRotationChangedCallback(@NonNull Runnable callback) { + if (mDefaultDisplayRotationChangedCallback != null) { + throw new UnsupportedOperationException("Multiple clients unsupported"); + } + + mDefaultDisplayRotationChangedCallback = callback; + + if (mDefaultDisplayCurrentRotation != mDefaultDisplayDefaultRotation) { + callback.run(); + } + } + + /** + * Removes the callback that was added via + * {@link #setDefaultDisplayRotationChangedCallback(Runnable)}. + */ + void removeDefaultDisplayRotationChangedCallback() { + mDefaultDisplayRotationChangedCallback = null; + } + + static boolean isSecondaryInternalDisplay(@NonNull DisplayContent displayContent) { + if (displayContent.isDefaultDisplay) { + return false; + } else if (displayContent.mDisplay == null) { + return false; + } + return displayContent.mDisplay.getType() == Display.TYPE_INTERNAL; + } +} diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 8ba54ff4d6697..42bfebc559b05 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -234,6 +234,10 @@ class RootWindowContainer extends WindowContainer WindowManagerService mWindowManager; DisplayManager mDisplayManager; private DisplayManagerInternal mDisplayManagerInternal; + @NonNull + private final DeviceStateController mDeviceStateController; + @NonNull + private final DisplayRotationCoordinator mDisplayRotationCoordinator; /** Reference to default display so we can quickly look it up. */ private DisplayContent mDefaultDisplay; @@ -440,6 +444,8 @@ class RootWindowContainer extends WindowContainer mTaskSupervisor = mService.mTaskSupervisor; mTaskSupervisor.mRootWindowContainer = this; mDisplayOffTokenAcquirer = mService.new SleepTokenAcquirerImpl(DISPLAY_OFF_SLEEP_TOKEN_TAG); + mDeviceStateController = new DeviceStateController(service.mContext, service.mH); + mDisplayRotationCoordinator = new DisplayRotationCoordinator(); } /** @@ -1279,7 +1285,8 @@ class RootWindowContainer extends WindowContainer final Display[] displays = mDisplayManager.getDisplays(); for (int displayNdx = 0; displayNdx < displays.length; ++displayNdx) { final Display display = displays[displayNdx]; - final DisplayContent displayContent = new DisplayContent(display, this); + final DisplayContent displayContent = + new DisplayContent(display, this, mDeviceStateController); addChild(displayContent, POSITION_BOTTOM); if (displayContent.mDisplayId == DEFAULT_DISPLAY) { mDefaultDisplay = displayContent; @@ -1297,6 +1304,10 @@ class RootWindowContainer extends WindowContainer return mDefaultDisplay; } + DisplayRotationCoordinator getDisplayRotationCoordinator() { + return mDisplayRotationCoordinator; + } + /** * Get the default display area on the device dedicated to app windows. This one should be used * only as a fallback location for activity launches when no target display area is specified, @@ -1358,7 +1369,7 @@ class RootWindowContainer extends WindowContainer return null; } // The display hasn't been added to ActivityManager yet, create a new record now. - displayContent = new DisplayContent(display, this); + displayContent = new DisplayContent(display, this, mDeviceStateController); addChild(displayContent, POSITION_BOTTOM); return displayContent; } diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 3b34ba45fa952..16fe466f3a9f1 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -105,6 +105,7 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.when; +import android.annotation.NonNull; import android.app.ActivityTaskManager; import android.app.WindowConfiguration; import android.content.res.Configuration; @@ -123,6 +124,7 @@ import android.platform.test.annotations.Presubmit; import android.util.ArraySet; import android.util.DisplayMetrics; import android.view.ContentRecordingSession; +import android.view.Display; import android.view.DisplayCutout; import android.view.DisplayInfo; import android.view.Gravity; @@ -1830,6 +1832,111 @@ public class DisplayContentTests extends WindowTestsBase { assertFalse(recentsActivity.hasFixedRotationTransform()); } + @Test + public void testSecondaryInternalDisplayRotationFollowsDefaultDisplay() { + // Skip freezing so the unrelated conditions in updateRotationUnchecked won't disturb. + doNothing().when(mWm).startFreezingDisplay(anyInt(), anyInt(), any(), anyInt()); + + final DisplayRotationCoordinator coordinator = + mRootWindowContainer.getDisplayRotationCoordinator(); + final DisplayContent defaultDisplayContent = mDisplayContent; + final DisplayRotation defaultDisplayRotation = defaultDisplayContent.getDisplayRotation(); + coordinator.removeDefaultDisplayRotationChangedCallback(); + + DeviceStateController deviceStateController = mock(DeviceStateController.class); + when(deviceStateController.shouldMatchBuiltInDisplayOrientationToReverseDefaultDisplay()) + .thenReturn(true); + + // Create secondary display + final DisplayContent secondaryDisplayContent = + createSecondaryDisplayContent(Display.TYPE_INTERNAL, deviceStateController); + final DisplayRotation secondaryDisplayRotation = + secondaryDisplayContent.getDisplayRotation(); + try { + // TestDisplayContent bypasses this method but we need it for this test + doCallRealMethod().when(secondaryDisplayRotation).updateRotationUnchecked(anyBoolean()); + + // TestDisplayContent creates this as a mock. Lets set it up to test our use case. + when(secondaryDisplayContent.mDeviceStateController + .shouldMatchBuiltInDisplayOrientationToReverseDefaultDisplay()).thenReturn( + true); + + // Check that secondary display registered callback + assertEquals(secondaryDisplayRotation.mDefaultDisplayRotationChangedCallback, + coordinator.mDefaultDisplayRotationChangedCallback); + + // Set the default display to a known orientation. This may be a zero or non-zero + // rotation since mDisplayInfo.logicalWidth/Height depends on the DUT's default display + defaultDisplayRotation.updateOrientation(SCREEN_ORIENTATION_PORTRAIT, false); + assertEquals(defaultDisplayRotation.mPortraitRotation, + defaultDisplayRotation.getRotation()); + assertEquals(defaultDisplayRotation.mPortraitRotation, + coordinator.getDefaultDisplayCurrentRotation()); + + // Check that in the initial state, the secondary display is in the right rotation + assertRotationsAreCorrectlyReversed(defaultDisplayRotation.getRotation(), + secondaryDisplayRotation.getRotation()); + + // Update primary display rotation, check display coordinator rotation is the default + // display's landscape rotation, and that the secondary display rotation is correct. + defaultDisplayRotation.updateOrientation(SCREEN_ORIENTATION_LANDSCAPE, false); + assertEquals(defaultDisplayRotation.mLandscapeRotation, + defaultDisplayRotation.getRotation()); + assertEquals(defaultDisplayRotation.mLandscapeRotation, + coordinator.getDefaultDisplayCurrentRotation()); + assertRotationsAreCorrectlyReversed(defaultDisplayRotation.getRotation(), + secondaryDisplayRotation.getRotation()); + } finally { + secondaryDisplayRotation.removeDefaultDisplayRotationChangedCallback(); + } + } + + @Test + public void testSecondaryNonInternalDisplayDoesNotFollowDefaultDisplay() { + // Skip freezing so the unrelated conditions in updateRotationUnchecked won't disturb. + doNothing().when(mWm).startFreezingDisplay(anyInt(), anyInt(), any(), anyInt()); + + final DisplayRotationCoordinator coordinator = + mRootWindowContainer.getDisplayRotationCoordinator(); + coordinator.removeDefaultDisplayRotationChangedCallback(); + + DeviceStateController deviceStateController = mock(DeviceStateController.class); + when(deviceStateController.shouldMatchBuiltInDisplayOrientationToReverseDefaultDisplay()) + .thenReturn(true); + + // Create secondary non-internal displays + createSecondaryDisplayContent(Display.TYPE_EXTERNAL, deviceStateController); + assertNull(coordinator.mDefaultDisplayRotationChangedCallback); + createSecondaryDisplayContent(Display.TYPE_VIRTUAL, deviceStateController); + assertNull(coordinator.mDefaultDisplayRotationChangedCallback); + } + + private DisplayContent createSecondaryDisplayContent(int displayType, + @NonNull DeviceStateController deviceStateController) { + final DisplayInfo secondaryDisplayInfo = new DisplayInfo(); + secondaryDisplayInfo.copyFrom(mDisplayInfo); + secondaryDisplayInfo.type = displayType; + + return new TestDisplayContent.Builder(mAtm, secondaryDisplayInfo) + .setDeviceStateController(deviceStateController) + .build(); + } + + private static void assertRotationsAreCorrectlyReversed(@Surface.Rotation int rotation1, + @Surface.Rotation int rotation2) { + if (rotation1 == ROTATION_0) { + assertEquals(rotation1, rotation2); + } else if (rotation1 == ROTATION_180) { + assertEquals(rotation1, rotation2); + } else if (rotation1 == ROTATION_90) { + assertEquals(ROTATION_270, rotation2); + } else if (rotation1 == ROTATION_270) { + assertEquals(ROTATION_90, rotation2); + } else { + throw new IllegalArgumentException("Unknown rotation: " + rotation1 + ", " + rotation2); + } + } + @Test public void testRemoteRotation() { final DisplayContent dc = mDisplayContent; diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCoordinatorTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCoordinatorTests.java new file mode 100644 index 0000000000000..4557df0e9c986 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCoordinatorTests.java @@ -0,0 +1,95 @@ +/* + * 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 com.android.dx.mockito.inline.extended.ExtendedMockito.mock; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.never; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; + +import static org.junit.Assert.assertEquals; + +import android.annotation.NonNull; +import android.platform.test.annotations.Presubmit; +import android.view.Surface; + +import androidx.test.filters.SmallTest; + +import org.junit.Test; + +/** + * Test class for {@link DisplayRotationCoordinator} + * + * Build/Install/Run: + * atest DisplayRotationCoordinatorTests + */ +@SmallTest +@Presubmit +public class DisplayRotationCoordinatorTests { + + @NonNull + private final DisplayRotationCoordinator mCoordinator = new DisplayRotationCoordinator(); + + @Test + public void testDefaultDisplayRotationChangedWhenNoCallbackRegistered() { + // Does not cause NPE + mCoordinator.onDefaultDisplayRotationChanged(Surface.ROTATION_90); + } + + @Test (expected = UnsupportedOperationException.class) + public void testSecondRegistrationWithoutRemovingFirst() { + Runnable callback1 = mock(Runnable.class); + Runnable callback2 = mock(Runnable.class); + mCoordinator.setDefaultDisplayRotationChangedCallback(callback1); + mCoordinator.setDefaultDisplayRotationChangedCallback(callback2); + assertEquals(callback1, mCoordinator.mDefaultDisplayRotationChangedCallback); + } + + @Test + public void testSecondRegistrationAfterRemovingFirst() { + Runnable callback1 = mock(Runnable.class); + mCoordinator.setDefaultDisplayRotationChangedCallback(callback1); + mCoordinator.removeDefaultDisplayRotationChangedCallback(); + + Runnable callback2 = mock(Runnable.class); + mCoordinator.setDefaultDisplayRotationChangedCallback(callback2); + + mCoordinator.onDefaultDisplayRotationChanged(Surface.ROTATION_90); + verify(callback2).run(); + verify(callback1, never()).run(); + } + + @Test + public void testRegisterThenDefaultDisplayRotationChanged() { + Runnable callback = mock(Runnable.class); + mCoordinator.setDefaultDisplayRotationChangedCallback(callback); + assertEquals(Surface.ROTATION_0, mCoordinator.getDefaultDisplayCurrentRotation()); + verify(callback, never()).run(); + + mCoordinator.onDefaultDisplayRotationChanged(Surface.ROTATION_90); + verify(callback).run(); + assertEquals(Surface.ROTATION_90, mCoordinator.getDefaultDisplayCurrentRotation()); + } + + @Test + public void testDefaultDisplayRotationChangedThenRegister() { + mCoordinator.onDefaultDisplayRotationChanged(Surface.ROTATION_90); + Runnable callback = mock(Runnable.class); + mCoordinator.setDefaultDisplayRotationChangedCallback(callback); + verify(callback).run(); + assertEquals(Surface.ROTATION_90, mCoordinator.getDefaultDisplayCurrentRotation()); + } +} 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 ed2b0a36cd5c8..21e8ec4a1ce83 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java @@ -1135,7 +1135,7 @@ public class DisplayRotationTests { mDeviceStateController = mock(DeviceStateController.class); mTarget = new DisplayRotation(sMockWm, mMockDisplayContent, mMockDisplayAddress, mMockDisplayPolicy, mMockDisplayWindowSettings, mMockContext, new Object(), - mDeviceStateController) { + mDeviceStateController, mock(DisplayRotationCoordinator.class)) { @Override DisplayRotationImmersiveAppCompatPolicy initImmersiveAppCompatPolicy( WindowManagerService service, DisplayContent displayContent) { diff --git a/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java index 4eaae9fb82406..d1a41ae2bcde6 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java @@ -552,7 +552,7 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase { /** Please use the {@link Builder} to create. */ DualDisplayContent(RootWindowContainer rootWindowContainer, Display display) { - super(rootWindowContainer, display); + super(rootWindowContainer, display, mock(DeviceStateController.class)); mFirstRoot = getGroupRoot(FEATURE_FIRST_ROOT); mSecondRoot = getGroupRoot(FEATURE_SECOND_ROOT); diff --git a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java index 83be4f0ac20b4..fec079b7b15b0 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java +++ b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java @@ -27,8 +27,10 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.content.res.Configuration; @@ -51,8 +53,9 @@ class TestDisplayContent extends DisplayContent { public static final int DEFAULT_LOGICAL_DISPLAY_DENSITY = 300; /** Please use the {@link Builder} to create, visible for use in test builder overrides only. */ - TestDisplayContent(RootWindowContainer rootWindowContainer, Display display) { - super(display, rootWindowContainer); + TestDisplayContent(RootWindowContainer rootWindowContainer, Display display, + @NonNull DeviceStateController deviceStateController) { + super(display, rootWindowContainer, deviceStateController); // Normally this comes from display-properties as exposed by WM. Without that, just // hard-code to FULLSCREEN for tests. setWindowingMode(WINDOWING_MODE_FULLSCREEN); @@ -97,6 +100,8 @@ class TestDisplayContent extends DisplayContent { private int mStatusBarHeight = 0; private SettingsEntry mOverrideSettings; private DisplayMetrics mDisplayMetrics; + @NonNull + private DeviceStateController mDeviceStateController = mock(DeviceStateController.class); @Mock Context mMockContext; @Mock @@ -198,8 +203,13 @@ class TestDisplayContent extends DisplayContent { com.android.internal.R.dimen.default_minimal_size_resizable_task); return this; } + Builder setDeviceStateController(@NonNull DeviceStateController deviceStateController) { + mDeviceStateController = deviceStateController; + return this; + } TestDisplayContent createInternal(Display display) { - return new TestDisplayContent(mService.mRootWindowContainer, display); + return new TestDisplayContent(mService.mRootWindowContainer, display, + mDeviceStateController); } TestDisplayContent build() { SystemServicesTestRule.checkHoldsLock(mService.mGlobalLock);