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 b2942dff463da..14eaf34883941 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -983,6 +983,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 592c677cb0ff4..6fc9161c71ff0 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -595,7 +595,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;
@@ -1091,7 +1092,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()
@@ -1151,11 +1153,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) -> {
@@ -3326,7 +3328,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 3404279d2c593..7071aa728230b 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;
@@ -252,6 +260,19 @@ public class DisplayRotation {
int defaultRotation = readDefaultDisplayRotation(displayAddress);
mRotation = defaultRotation;
+ 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 =
@@ -494,8 +515,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()) {
@@ -507,6 +531,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)",
@@ -525,6 +557,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
@@ -1142,17 +1178,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) {
@@ -1167,6 +1198,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
@@ -1536,6 +1568,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 e8aa2c8d4e609..f38de6e6436c7 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 abc0c14372e82..1ce8c61fa07ac 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);