Loading services/core/java/com/android/server/wm/AppCompatCameraRotationState.java 0 → 100644 +167 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.wm; import static android.app.WindowConfiguration.ROTATION_UNDEFINED; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_180; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; import android.annotation.NonNull; import android.annotation.Nullable; import android.view.Display; import android.view.OrientationEventListener; import android.view.Surface; import android.window.DesktopExperienceFlags; import com.android.internal.annotations.VisibleForTesting; /** * Provider for the camera rotation and sensor orientation info used to setup camera compat mode. * * <p>{@link AppCompatCameraRotationState} monitors whether the app currently using the camera is * on a built-in display (rotates with the built-in camera) or an external display, and returns the * display rotation apps should use to keep the preview upright: * <ul> * <li>If running on a built-in display, relevant rotation is {@link Display#getRotation()}. * <li>If running on an external display, only the sensor rotation matters, received from * {@link OrientationEventListener}. * </ul> */ class AppCompatCameraRotationState { @Nullable @VisibleForTesting OrientationEventListener mOrientationEventListener; @NonNull private final DisplayContent mDisplayContent; private int mDisplayRotationIfExternal = ROTATION_UNDEFINED; AppCompatCameraRotationState(@NonNull DisplayContent displayContent) { mDisplayContent = displayContent; } /** Sets up listening to the orientation of the primary device if on an external display. */ void start() { if (isExternalDisplay()) { // Listen to orientation changes of the host device. setupSensorOrientationListener(); } } /** Disables {@link OrientationEventListener} if set up. */ void dispose() { if (mOrientationEventListener != null) { mOrientationEventListener.disable(); mOrientationEventListener = null; } } /** Creates and enables {@link OrientationEventListener}. */ void setupSensorOrientationListener() { mOrientationEventListener = new OrientationEventListener( mDisplayContent.mWmService.mContext) { @Override public void onOrientationChanged(int orientation) { synchronized (mDisplayContent.mWmService.mGlobalLock) { mDisplayRotationIfExternal = transformSensorOrientationToDisplayRotation( orientation); } } }; mOrientationEventListener.enable(); } /** * Whether the natural orientation (not the current rotation) of the camera is portrait. * * <p>This orientation is equal to the natural orientation of the display it is tied to * (built-in display). */ boolean isCameraDeviceNaturalOrientationPortrait() { // Per CDD (7.5.5 C-1-1), camera sensor orientation and display natural orientation have to // be the same (portrait or landscape). return getDisplayContentTiedToCamera().getNaturalOrientation() == ORIENTATION_PORTRAIT; } /** * Returns relevant rotation of the relevant "device", whether it is a camera or display. * *<p>This is the offset that apps should use to rotate the camera preview. Difference in this * value and what the app expects given their requested orientation informs camera compat setup. */ @Surface.Rotation int getCameraDeviceRotation() { return isExternalDisplay() ? mDisplayRotationIfExternal : mDisplayContent.getRotation(); } // TODO(b/425599049): support external cameras. /** * Whether the device relevant for camera is in portrait orientation. * * <p>This is either the display rotation when running on an internal display, or the camera * rotation when running on an external display. */ boolean isCameraDeviceOrientationPortrait() { final int cameraDisplayRotation = getCameraDeviceRotation(); final boolean isDisplayInItsNaturalOrientation = (cameraDisplayRotation == ROTATION_0 || cameraDisplayRotation == ROTATION_180); // Display is in portrait if and only if: portrait device is in its natural orientation, // or landscape device is not in its natural orientation. // `isPortraitCamera <=> isDisplayInItsNaturalOrientation` is equivalent to // `isPortraitCamera XOR !isDisplayInItsNaturalOrientation`. return isCameraDeviceNaturalOrientationPortrait() ^ !isDisplayInItsNaturalOrientation; } @Surface.Rotation private int transformSensorOrientationToDisplayRotation(int orientation) { // Sensor rotation is continuous, and counted in the opposite direction from display // rotation. final int displayRotationInt = ((360 - orientation) + 360) % 360; // Choose the closest display rotation. When using the `OrientationEventListener`, this is // the recommended way in developer documentation for apps to orient the preview or a // captured image. if (displayRotationInt > 45 && displayRotationInt <= 135) { return ROTATION_90; } else if (displayRotationInt > 135 && displayRotationInt <= 225) { return ROTATION_180; } else if (displayRotationInt > 225 && displayRotationInt <= 315) { return ROTATION_270; } else { return ROTATION_0; } } @NonNull private DisplayContent getDisplayContentTiedToCamera() { return isExternalDisplay() // If camera app is on the external display, the display rotation should be // overridden to use the primary device rotation which the camera sensor is tied to. ? mDisplayContent.mWmService.getDefaultDisplayContentLocked() : mDisplayContent; } private boolean isExternalDisplay() { return DesktopExperienceFlags.ENABLE_CAMERA_COMPAT_EXTERNAL_DISPLAY_ROTATION_BUGFIX.isTrue() && mDisplayContent.getDisplay().getType() == Display.TYPE_EXTERNAL; } } services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java +30 −31 Original line number Diff line number Diff line Loading @@ -30,8 +30,6 @@ import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.content.res.Configuration.ORIENTATION_UNDEFINED; import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_180; import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; Loading @@ -44,7 +42,6 @@ import android.content.res.CompatibilityInfo; import android.content.res.Configuration; import android.os.RemoteException; import android.util.Slog; import android.view.DisplayInfo; import android.view.Surface; import com.android.internal.annotations.VisibleForTesting; Loading Loading @@ -72,6 +69,8 @@ final class CameraCompatFreeformPolicy implements AppCompatCameraStatePolicy, private final AppCompatCameraStateSource mCameraStateNotifier; @NonNull private final CameraStateMonitor mCameraStateMonitor; @NonNull private final AppCompatCameraRotationState mCameraDisplayRotationProvider; // TODO(b/380840084): Clean up after flag is launched. @Nullable Loading @@ -90,23 +89,21 @@ final class CameraCompatFreeformPolicy implements AppCompatCameraStatePolicy, mCameraStateMonitor = cameraStateMonitor; mCameraStateNotifier = cameraStateNotifier; mActivityRefresher = activityRefresher; mCameraDisplayRotationProvider = new AppCompatCameraRotationState(displayContent); } void start() { mCameraStateNotifier.addCameraStatePolicy(this); mActivityRefresher.addEvaluator(this); mCameraDisplayRotationProvider.start(); mIsRunning = true; } int getCameraDeviceRotation() { // TODO(b/276432441): Check device orientation when running on an external display. return mDisplayContent.getRotation(); } /** Releases camera callback listener. */ void dispose() { mCameraStateNotifier.removeCameraStatePolicy(this); mActivityRefresher.removeEvaluator(this); mCameraDisplayRotationProvider.dispose(); mIsRunning = false; } Loading @@ -115,6 +112,11 @@ final class CameraCompatFreeformPolicy implements AppCompatCameraStatePolicy, return mIsRunning; } @Surface.Rotation int getCameraDeviceRotation() { return mCameraDisplayRotationProvider.getCameraDeviceRotation(); } // Refreshing only when configuration changes after applying camera compat treatment. @Override public boolean shouldRefreshActivity(@NonNull ActivityRecord activity, Loading Loading @@ -297,37 +299,34 @@ final class CameraCompatFreeformPolicy implements AppCompatCameraStatePolicy, if (!isTreatmentEnabledForActivity(topActivity, /* shouldCheckOrientation= */ true)) { return CAMERA_COMPAT_FREEFORM_NONE; } final int appOrientation = topActivity.getRequestedConfigurationOrientation(); // It is very important to check the original (actual) display rotation, and not the // sandboxed rotation that camera compat treatment sets. final DisplayInfo displayInfo = topActivity.mWmService.mDisplayManagerInternal .getDisplayInfo(topActivity.getDisplayId()); // This treatment targets only devices with portrait natural orientation, which most tablets // have. if (!mCameraDisplayRotationProvider.isCameraDeviceNaturalOrientationPortrait()) { // TODO(b/365725400): handle landscape natural orientation. if (displayInfo.getNaturalHeight() > displayInfo.getNaturalWidth()) { return CAMERA_COMPAT_FREEFORM_NONE; } final int appOrientation = topActivity.getRequestedConfigurationOrientation(); final boolean isDisplayRotationPortrait = mCameraDisplayRotationProvider .isCameraDeviceOrientationPortrait(); if (appOrientation == ORIENTATION_PORTRAIT) { if (isDisplayRotationPortrait(displayInfo.rotation)) { if (isDisplayRotationPortrait) { return CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_PORTRAIT; } else { return CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE; } } else if (appOrientation == ORIENTATION_LANDSCAPE) { if (isDisplayRotationPortrait(displayInfo.rotation)) { if (isDisplayRotationPortrait) { return CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_PORTRAIT; } else { return CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_LANDSCAPE; } } } return CAMERA_COMPAT_FREEFORM_NONE; } private static boolean isDisplayRotationPortrait(@Surface.Rotation int displayRotation) { return displayRotation == ROTATION_0 || displayRotation == ROTATION_180; } /** * Whether camera compat treatment is applicable for the given activity, ignoring its windowing * mode. Loading services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java +16 −2 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; import static android.content.pm.ApplicationInfo.CATEGORY_GAME; import static android.content.pm.ApplicationInfo.CATEGORY_UNDEFINED; import static android.view.Display.TYPE_INTERNAL; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; Loading Loading @@ -136,7 +137,11 @@ class AppCompatActivityRobot { } void createActivityWithComponentInNewTaskAndDisplay() { createActivityWithComponentInNewTask(/* inNewTask */ true, /* inNewDisplay */ true); createActivityWithComponentInNewTaskAndDisplay(TYPE_INTERNAL); } void createActivityWithComponentInNewTaskAndDisplay(int displayType) { createActivityWithComponentInNewTask(/* inNewTask */ true, /* inNewDisplay */ true, displayType); } void configureTopActivity(float minAspect, float maxAspect, int screenOrientation, Loading Loading @@ -358,7 +363,11 @@ class AppCompatActivityRobot { } void createNewDisplay() { createNewDisplay(TYPE_INTERNAL); } void createNewDisplay(int type) { mDisplayContent = new TestDisplayContent.Builder(mAtm, mDisplayWidth, mDisplayHeight) .setType(type) .build(); onPostDisplayContentCreation(mDisplayContent); } Loading Loading @@ -569,8 +578,13 @@ class AppCompatActivityRobot { } private void createActivityWithComponentInNewTask(boolean inNewTask, boolean inNewDisplay) { createActivityWithComponentInNewTask(inNewTask, inNewDisplay, TYPE_INTERNAL); } private void createActivityWithComponentInNewTask(boolean inNewTask, boolean inNewDisplay, int displayType) { if (inNewDisplay) { createNewDisplay(); createNewDisplay(displayType); } if (inNewTask) { createNewTask(); Loading services/tests/wmtests/src/com/android/server/wm/AppCompatCameraRotationStateTests.java 0 → 100644 +246 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.wm; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.view.Display.TYPE_EXTERNAL; import static android.view.Display.TYPE_INTERNAL; import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_90; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_EXTERNAL_DISPLAY_ROTATION_BUGFIX; import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING; import static org.junit.Assert.assertEquals; import android.annotation.NonNull; import android.compat.testing.PlatformCompatChangeRule; import android.content.res.Configuration; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.view.Surface; import androidx.test.filters.SmallTest; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; import org.junit.runner.RunWith; import java.util.function.Consumer; /** * Tests for {@link AppCompatCameraRotationState}. * * Build/Install/Run: * atest WmTests:AppCompatCameraInfoProviderTests */ @SmallTest @Presubmit @RunWith(WindowTestRunner.class) public class AppCompatCameraRotationStateTests extends WindowTestsBase { @Rule public TestRule compatChangeRule = new PlatformCompatChangeRule(); @Test @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_EXTERNAL_DISPLAY_ROTATION_BUGFIX) @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testFeatureDisabled_returnsCurrentDisplayRotation() { runTestScenario((robot) -> { robot.configureActivityAndDisplay(ROTATION_90, ORIENTATION_PORTRAIT, TYPE_INTERNAL); robot.makeCurrentDisplayDefault(); // The last created display is 'current'. robot.configureActivityAndDisplay(ROTATION_0, ORIENTATION_LANDSCAPE, TYPE_EXTERNAL); robot.checkOrientationEventListenerSetUp(/* expected= */ false); robot.checkDisplayRotation(/* expected= */ Surface.ROTATION_0); }); } @Test @EnableFlags({FLAG_ENABLE_CAMERA_COMPAT_EXTERNAL_DISPLAY_ROTATION_BUGFIX, FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING}) public void testFeatureEnabled_internalDisplay_returnsCurrentDisplayRotation() { runTestScenario((robot) -> { robot.configureActivityAndDisplay(ROTATION_0, ORIENTATION_PORTRAIT, TYPE_INTERNAL); robot.makeCurrentDisplayDefault(); // The last created display is 'current'. robot.configureActivityAndDisplay(ROTATION_90, ORIENTATION_PORTRAIT, TYPE_INTERNAL); robot.checkOrientationEventListenerSetUp(/* expected= */ false); robot.checkDisplayRotation(/* expected= */ Surface.ROTATION_90); }); } @Test @EnableFlags({FLAG_ENABLE_CAMERA_COMPAT_EXTERNAL_DISPLAY_ROTATION_BUGFIX, FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING}) public void testFeatureEnabled_externalDisplay_returnsSensorRotation() { runTestScenario((robot) -> { robot.configureActivityAndDisplay(ROTATION_90, ORIENTATION_PORTRAIT, TYPE_INTERNAL); robot.makeCurrentDisplayDefault(); robot.configureActivityAndDisplay(ROTATION_0, ORIENTATION_LANDSCAPE, TYPE_EXTERNAL); robot.checkOrientationEventListenerSetUp(/* expected= */ true); // Sensor rotation is continuous, and counted in the opposite direction from display // rotation: 360 - 100 = 260, and 260 is closest to ROTATION_270. robot.setSensorOrientation(100); // Sensor rotation should be returned on external displays, even if built-in display has // different rotation (for example from disabled auto-rotation). robot.checkDisplayRotation(/* expected= */ Surface.ROTATION_270); }); } @Test @EnableFlags({FLAG_ENABLE_CAMERA_COMPAT_EXTERNAL_DISPLAY_ROTATION_BUGFIX, FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING}) public void testIsCameraDeviceOrientationPortrait_rotatesToLandscape_returnsFalse() { runTestScenario((robot) -> { robot.configureActivityAndDisplay(ROTATION_0, ORIENTATION_PORTRAIT, TYPE_INTERNAL); robot.makeCurrentDisplayDefault(); // The last created display is 'current'. robot.configureActivityAndDisplay(ROTATION_0, ORIENTATION_LANDSCAPE, TYPE_EXTERNAL); robot.checkOrientationEventListenerSetUp(/* expected= */ true); // Sensor rotation is continuous, and counted in the opposite direction from display // rotation: 360 - 100 = 260, and 260 is closest to ROTATION_270. robot.setSensorOrientation(100); // Sensor rotation should be returned on external displays, even if built-in display has // different rotation (for example from disabled auto-rotation). robot.checkIsCameraDisplayRotationPortrait(/* expected= */ false); }); } @Test @EnableFlags({FLAG_ENABLE_CAMERA_COMPAT_EXTERNAL_DISPLAY_ROTATION_BUGFIX, FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING}) public void testIsPortraitCamera_portraitInnerDisplay_rotatesToLandscape_true() { runTestScenario((robot) -> { robot.configureActivityAndDisplay(ROTATION_0, ORIENTATION_PORTRAIT, TYPE_INTERNAL); robot.makeCurrentDisplayDefault(); robot.configureActivityAndDisplay(ROTATION_0, ORIENTATION_LANDSCAPE, TYPE_EXTERNAL); robot.checkOrientationEventListenerSetUp(/* expected= */ true); // Sensor rotation is continuous, and counted in the opposite direction from display // rotation: 360 - 100 = 260, and 260 is closest to ROTATION_270. robot.setSensorOrientation(100); // Current rotation does not affect whether camera sensor is portrait or landscape. robot.checkIsPortraitCamera(/* expected= */ true); }); } @Test @EnableFlags({FLAG_ENABLE_CAMERA_COMPAT_EXTERNAL_DISPLAY_ROTATION_BUGFIX, FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING}) public void testIsCamera_DeviceNaturalOrientationPortrait_landscapeDisplay_returnsFalse() { runTestScenario((robot) -> { robot.configureActivityAndDisplay(ROTATION_0, ORIENTATION_LANDSCAPE, TYPE_INTERNAL); // Current rotation does not affect whether camera sensor is portrait or landscape. robot.checkIsPortraitCamera(/* expected= */ false); }); } /** * Runs a test scenario providing a Robot. */ void runTestScenario(@NonNull Consumer<AppCompatCameraInfoProviderRobotTests> consumer) { final AppCompatCameraInfoProviderRobotTests robot = new AppCompatCameraInfoProviderRobotTests(mWm, mAtm, mSupervisor); consumer.accept(robot); } private static class AppCompatCameraInfoProviderRobotTests extends AppCompatRobotBase { private AppCompatCameraRotationState mCameraInfoProvider; private final WindowManagerService mWm; AppCompatCameraInfoProviderRobotTests(@NonNull WindowManagerService wm, @NonNull ActivityTaskManagerService atm, @NonNull ActivityTaskSupervisor supervisor) { super(wm, atm, supervisor); mWm = wm; setupAppCompatConfiguration(); } @Override void onPostDisplayContentCreation(@NonNull DisplayContent displayContent) { super.onPostDisplayContentCreation(displayContent); } @Override void onPostActivityCreation(@NonNull ActivityRecord activity) { super.onPostActivityCreation(activity); mCameraInfoProvider = new AppCompatCameraRotationState(activity.mDisplayContent); mCameraInfoProvider.start(); } private void setupAppCompatConfiguration() { applyOnConf((c) -> { c.enableCameraCompatForceRotateTreatment(true); c.enableCameraCompatForceRotateTreatmentAtBuildTime(true); }); } private void configureActivityAndDisplay( @Surface.Rotation int displayRotation, @Configuration.Orientation int naturalOrientation, int displayType) { applyOnActivity(a -> { dw().allowEnterDesktopMode(true); a.createActivityWithComponentInNewTaskAndDisplay(displayType); a.setIgnoreOrientationRequest(true); a.rotateDisplayForTopActivity(displayRotation); a.setDisplayNaturalOrientation(naturalOrientation); spyOn(a.top().mAppCompatController.getCameraOverrides()); spyOn(a.top().info); doReturn(a.displayContent().getDisplayInfo()).when( a.displayContent().mWmService.mDisplayManagerInternal).getDisplayInfo( a.displayContent().mDisplayId); }); } void makeCurrentDisplayDefault() { doReturn(activity().displayContent()).when(mWm).getDefaultDisplayContentLocked(); } void checkDisplayRotation(@Surface.Rotation int expected) { assertEquals(expected, mCameraInfoProvider.getCameraDeviceRotation()); } void checkIsPortraitCamera(boolean expected) { assertEquals(expected, mCameraInfoProvider.isCameraDeviceNaturalOrientationPortrait()); } void checkIsCameraDisplayRotationPortrait(boolean expected) { assertEquals(expected, mCameraInfoProvider.isCameraDeviceOrientationPortrait()); } void checkOrientationEventListenerSetUp(boolean expected) { assertEquals(expected, mCameraInfoProvider.mOrientationEventListener != null); } void setSensorOrientation(int orientation) { mCameraInfoProvider.mOrientationEventListener.onOrientationChanged(orientation); } } } Loading
services/core/java/com/android/server/wm/AppCompatCameraRotationState.java 0 → 100644 +167 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.wm; import static android.app.WindowConfiguration.ROTATION_UNDEFINED; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_180; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; import android.annotation.NonNull; import android.annotation.Nullable; import android.view.Display; import android.view.OrientationEventListener; import android.view.Surface; import android.window.DesktopExperienceFlags; import com.android.internal.annotations.VisibleForTesting; /** * Provider for the camera rotation and sensor orientation info used to setup camera compat mode. * * <p>{@link AppCompatCameraRotationState} monitors whether the app currently using the camera is * on a built-in display (rotates with the built-in camera) or an external display, and returns the * display rotation apps should use to keep the preview upright: * <ul> * <li>If running on a built-in display, relevant rotation is {@link Display#getRotation()}. * <li>If running on an external display, only the sensor rotation matters, received from * {@link OrientationEventListener}. * </ul> */ class AppCompatCameraRotationState { @Nullable @VisibleForTesting OrientationEventListener mOrientationEventListener; @NonNull private final DisplayContent mDisplayContent; private int mDisplayRotationIfExternal = ROTATION_UNDEFINED; AppCompatCameraRotationState(@NonNull DisplayContent displayContent) { mDisplayContent = displayContent; } /** Sets up listening to the orientation of the primary device if on an external display. */ void start() { if (isExternalDisplay()) { // Listen to orientation changes of the host device. setupSensorOrientationListener(); } } /** Disables {@link OrientationEventListener} if set up. */ void dispose() { if (mOrientationEventListener != null) { mOrientationEventListener.disable(); mOrientationEventListener = null; } } /** Creates and enables {@link OrientationEventListener}. */ void setupSensorOrientationListener() { mOrientationEventListener = new OrientationEventListener( mDisplayContent.mWmService.mContext) { @Override public void onOrientationChanged(int orientation) { synchronized (mDisplayContent.mWmService.mGlobalLock) { mDisplayRotationIfExternal = transformSensorOrientationToDisplayRotation( orientation); } } }; mOrientationEventListener.enable(); } /** * Whether the natural orientation (not the current rotation) of the camera is portrait. * * <p>This orientation is equal to the natural orientation of the display it is tied to * (built-in display). */ boolean isCameraDeviceNaturalOrientationPortrait() { // Per CDD (7.5.5 C-1-1), camera sensor orientation and display natural orientation have to // be the same (portrait or landscape). return getDisplayContentTiedToCamera().getNaturalOrientation() == ORIENTATION_PORTRAIT; } /** * Returns relevant rotation of the relevant "device", whether it is a camera or display. * *<p>This is the offset that apps should use to rotate the camera preview. Difference in this * value and what the app expects given their requested orientation informs camera compat setup. */ @Surface.Rotation int getCameraDeviceRotation() { return isExternalDisplay() ? mDisplayRotationIfExternal : mDisplayContent.getRotation(); } // TODO(b/425599049): support external cameras. /** * Whether the device relevant for camera is in portrait orientation. * * <p>This is either the display rotation when running on an internal display, or the camera * rotation when running on an external display. */ boolean isCameraDeviceOrientationPortrait() { final int cameraDisplayRotation = getCameraDeviceRotation(); final boolean isDisplayInItsNaturalOrientation = (cameraDisplayRotation == ROTATION_0 || cameraDisplayRotation == ROTATION_180); // Display is in portrait if and only if: portrait device is in its natural orientation, // or landscape device is not in its natural orientation. // `isPortraitCamera <=> isDisplayInItsNaturalOrientation` is equivalent to // `isPortraitCamera XOR !isDisplayInItsNaturalOrientation`. return isCameraDeviceNaturalOrientationPortrait() ^ !isDisplayInItsNaturalOrientation; } @Surface.Rotation private int transformSensorOrientationToDisplayRotation(int orientation) { // Sensor rotation is continuous, and counted in the opposite direction from display // rotation. final int displayRotationInt = ((360 - orientation) + 360) % 360; // Choose the closest display rotation. When using the `OrientationEventListener`, this is // the recommended way in developer documentation for apps to orient the preview or a // captured image. if (displayRotationInt > 45 && displayRotationInt <= 135) { return ROTATION_90; } else if (displayRotationInt > 135 && displayRotationInt <= 225) { return ROTATION_180; } else if (displayRotationInt > 225 && displayRotationInt <= 315) { return ROTATION_270; } else { return ROTATION_0; } } @NonNull private DisplayContent getDisplayContentTiedToCamera() { return isExternalDisplay() // If camera app is on the external display, the display rotation should be // overridden to use the primary device rotation which the camera sensor is tied to. ? mDisplayContent.mWmService.getDefaultDisplayContentLocked() : mDisplayContent; } private boolean isExternalDisplay() { return DesktopExperienceFlags.ENABLE_CAMERA_COMPAT_EXTERNAL_DISPLAY_ROTATION_BUGFIX.isTrue() && mDisplayContent.getDisplay().getType() == Display.TYPE_EXTERNAL; } }
services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java +30 −31 Original line number Diff line number Diff line Loading @@ -30,8 +30,6 @@ import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.content.res.Configuration.ORIENTATION_UNDEFINED; import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_180; import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; Loading @@ -44,7 +42,6 @@ import android.content.res.CompatibilityInfo; import android.content.res.Configuration; import android.os.RemoteException; import android.util.Slog; import android.view.DisplayInfo; import android.view.Surface; import com.android.internal.annotations.VisibleForTesting; Loading Loading @@ -72,6 +69,8 @@ final class CameraCompatFreeformPolicy implements AppCompatCameraStatePolicy, private final AppCompatCameraStateSource mCameraStateNotifier; @NonNull private final CameraStateMonitor mCameraStateMonitor; @NonNull private final AppCompatCameraRotationState mCameraDisplayRotationProvider; // TODO(b/380840084): Clean up after flag is launched. @Nullable Loading @@ -90,23 +89,21 @@ final class CameraCompatFreeformPolicy implements AppCompatCameraStatePolicy, mCameraStateMonitor = cameraStateMonitor; mCameraStateNotifier = cameraStateNotifier; mActivityRefresher = activityRefresher; mCameraDisplayRotationProvider = new AppCompatCameraRotationState(displayContent); } void start() { mCameraStateNotifier.addCameraStatePolicy(this); mActivityRefresher.addEvaluator(this); mCameraDisplayRotationProvider.start(); mIsRunning = true; } int getCameraDeviceRotation() { // TODO(b/276432441): Check device orientation when running on an external display. return mDisplayContent.getRotation(); } /** Releases camera callback listener. */ void dispose() { mCameraStateNotifier.removeCameraStatePolicy(this); mActivityRefresher.removeEvaluator(this); mCameraDisplayRotationProvider.dispose(); mIsRunning = false; } Loading @@ -115,6 +112,11 @@ final class CameraCompatFreeformPolicy implements AppCompatCameraStatePolicy, return mIsRunning; } @Surface.Rotation int getCameraDeviceRotation() { return mCameraDisplayRotationProvider.getCameraDeviceRotation(); } // Refreshing only when configuration changes after applying camera compat treatment. @Override public boolean shouldRefreshActivity(@NonNull ActivityRecord activity, Loading Loading @@ -297,37 +299,34 @@ final class CameraCompatFreeformPolicy implements AppCompatCameraStatePolicy, if (!isTreatmentEnabledForActivity(topActivity, /* shouldCheckOrientation= */ true)) { return CAMERA_COMPAT_FREEFORM_NONE; } final int appOrientation = topActivity.getRequestedConfigurationOrientation(); // It is very important to check the original (actual) display rotation, and not the // sandboxed rotation that camera compat treatment sets. final DisplayInfo displayInfo = topActivity.mWmService.mDisplayManagerInternal .getDisplayInfo(topActivity.getDisplayId()); // This treatment targets only devices with portrait natural orientation, which most tablets // have. if (!mCameraDisplayRotationProvider.isCameraDeviceNaturalOrientationPortrait()) { // TODO(b/365725400): handle landscape natural orientation. if (displayInfo.getNaturalHeight() > displayInfo.getNaturalWidth()) { return CAMERA_COMPAT_FREEFORM_NONE; } final int appOrientation = topActivity.getRequestedConfigurationOrientation(); final boolean isDisplayRotationPortrait = mCameraDisplayRotationProvider .isCameraDeviceOrientationPortrait(); if (appOrientation == ORIENTATION_PORTRAIT) { if (isDisplayRotationPortrait(displayInfo.rotation)) { if (isDisplayRotationPortrait) { return CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_PORTRAIT; } else { return CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE; } } else if (appOrientation == ORIENTATION_LANDSCAPE) { if (isDisplayRotationPortrait(displayInfo.rotation)) { if (isDisplayRotationPortrait) { return CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_PORTRAIT; } else { return CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_LANDSCAPE; } } } return CAMERA_COMPAT_FREEFORM_NONE; } private static boolean isDisplayRotationPortrait(@Surface.Rotation int displayRotation) { return displayRotation == ROTATION_0 || displayRotation == ROTATION_180; } /** * Whether camera compat treatment is applicable for the given activity, ignoring its windowing * mode. Loading
services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java +16 −2 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; import static android.content.pm.ApplicationInfo.CATEGORY_GAME; import static android.content.pm.ApplicationInfo.CATEGORY_UNDEFINED; import static android.view.Display.TYPE_INTERNAL; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; Loading Loading @@ -136,7 +137,11 @@ class AppCompatActivityRobot { } void createActivityWithComponentInNewTaskAndDisplay() { createActivityWithComponentInNewTask(/* inNewTask */ true, /* inNewDisplay */ true); createActivityWithComponentInNewTaskAndDisplay(TYPE_INTERNAL); } void createActivityWithComponentInNewTaskAndDisplay(int displayType) { createActivityWithComponentInNewTask(/* inNewTask */ true, /* inNewDisplay */ true, displayType); } void configureTopActivity(float minAspect, float maxAspect, int screenOrientation, Loading Loading @@ -358,7 +363,11 @@ class AppCompatActivityRobot { } void createNewDisplay() { createNewDisplay(TYPE_INTERNAL); } void createNewDisplay(int type) { mDisplayContent = new TestDisplayContent.Builder(mAtm, mDisplayWidth, mDisplayHeight) .setType(type) .build(); onPostDisplayContentCreation(mDisplayContent); } Loading Loading @@ -569,8 +578,13 @@ class AppCompatActivityRobot { } private void createActivityWithComponentInNewTask(boolean inNewTask, boolean inNewDisplay) { createActivityWithComponentInNewTask(inNewTask, inNewDisplay, TYPE_INTERNAL); } private void createActivityWithComponentInNewTask(boolean inNewTask, boolean inNewDisplay, int displayType) { if (inNewDisplay) { createNewDisplay(); createNewDisplay(displayType); } if (inNewTask) { createNewTask(); Loading
services/tests/wmtests/src/com/android/server/wm/AppCompatCameraRotationStateTests.java 0 → 100644 +246 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.wm; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.view.Display.TYPE_EXTERNAL; import static android.view.Display.TYPE_INTERNAL; import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_90; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_EXTERNAL_DISPLAY_ROTATION_BUGFIX; import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING; import static org.junit.Assert.assertEquals; import android.annotation.NonNull; import android.compat.testing.PlatformCompatChangeRule; import android.content.res.Configuration; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.view.Surface; import androidx.test.filters.SmallTest; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; import org.junit.runner.RunWith; import java.util.function.Consumer; /** * Tests for {@link AppCompatCameraRotationState}. * * Build/Install/Run: * atest WmTests:AppCompatCameraInfoProviderTests */ @SmallTest @Presubmit @RunWith(WindowTestRunner.class) public class AppCompatCameraRotationStateTests extends WindowTestsBase { @Rule public TestRule compatChangeRule = new PlatformCompatChangeRule(); @Test @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_EXTERNAL_DISPLAY_ROTATION_BUGFIX) @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testFeatureDisabled_returnsCurrentDisplayRotation() { runTestScenario((robot) -> { robot.configureActivityAndDisplay(ROTATION_90, ORIENTATION_PORTRAIT, TYPE_INTERNAL); robot.makeCurrentDisplayDefault(); // The last created display is 'current'. robot.configureActivityAndDisplay(ROTATION_0, ORIENTATION_LANDSCAPE, TYPE_EXTERNAL); robot.checkOrientationEventListenerSetUp(/* expected= */ false); robot.checkDisplayRotation(/* expected= */ Surface.ROTATION_0); }); } @Test @EnableFlags({FLAG_ENABLE_CAMERA_COMPAT_EXTERNAL_DISPLAY_ROTATION_BUGFIX, FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING}) public void testFeatureEnabled_internalDisplay_returnsCurrentDisplayRotation() { runTestScenario((robot) -> { robot.configureActivityAndDisplay(ROTATION_0, ORIENTATION_PORTRAIT, TYPE_INTERNAL); robot.makeCurrentDisplayDefault(); // The last created display is 'current'. robot.configureActivityAndDisplay(ROTATION_90, ORIENTATION_PORTRAIT, TYPE_INTERNAL); robot.checkOrientationEventListenerSetUp(/* expected= */ false); robot.checkDisplayRotation(/* expected= */ Surface.ROTATION_90); }); } @Test @EnableFlags({FLAG_ENABLE_CAMERA_COMPAT_EXTERNAL_DISPLAY_ROTATION_BUGFIX, FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING}) public void testFeatureEnabled_externalDisplay_returnsSensorRotation() { runTestScenario((robot) -> { robot.configureActivityAndDisplay(ROTATION_90, ORIENTATION_PORTRAIT, TYPE_INTERNAL); robot.makeCurrentDisplayDefault(); robot.configureActivityAndDisplay(ROTATION_0, ORIENTATION_LANDSCAPE, TYPE_EXTERNAL); robot.checkOrientationEventListenerSetUp(/* expected= */ true); // Sensor rotation is continuous, and counted in the opposite direction from display // rotation: 360 - 100 = 260, and 260 is closest to ROTATION_270. robot.setSensorOrientation(100); // Sensor rotation should be returned on external displays, even if built-in display has // different rotation (for example from disabled auto-rotation). robot.checkDisplayRotation(/* expected= */ Surface.ROTATION_270); }); } @Test @EnableFlags({FLAG_ENABLE_CAMERA_COMPAT_EXTERNAL_DISPLAY_ROTATION_BUGFIX, FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING}) public void testIsCameraDeviceOrientationPortrait_rotatesToLandscape_returnsFalse() { runTestScenario((robot) -> { robot.configureActivityAndDisplay(ROTATION_0, ORIENTATION_PORTRAIT, TYPE_INTERNAL); robot.makeCurrentDisplayDefault(); // The last created display is 'current'. robot.configureActivityAndDisplay(ROTATION_0, ORIENTATION_LANDSCAPE, TYPE_EXTERNAL); robot.checkOrientationEventListenerSetUp(/* expected= */ true); // Sensor rotation is continuous, and counted in the opposite direction from display // rotation: 360 - 100 = 260, and 260 is closest to ROTATION_270. robot.setSensorOrientation(100); // Sensor rotation should be returned on external displays, even if built-in display has // different rotation (for example from disabled auto-rotation). robot.checkIsCameraDisplayRotationPortrait(/* expected= */ false); }); } @Test @EnableFlags({FLAG_ENABLE_CAMERA_COMPAT_EXTERNAL_DISPLAY_ROTATION_BUGFIX, FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING}) public void testIsPortraitCamera_portraitInnerDisplay_rotatesToLandscape_true() { runTestScenario((robot) -> { robot.configureActivityAndDisplay(ROTATION_0, ORIENTATION_PORTRAIT, TYPE_INTERNAL); robot.makeCurrentDisplayDefault(); robot.configureActivityAndDisplay(ROTATION_0, ORIENTATION_LANDSCAPE, TYPE_EXTERNAL); robot.checkOrientationEventListenerSetUp(/* expected= */ true); // Sensor rotation is continuous, and counted in the opposite direction from display // rotation: 360 - 100 = 260, and 260 is closest to ROTATION_270. robot.setSensorOrientation(100); // Current rotation does not affect whether camera sensor is portrait or landscape. robot.checkIsPortraitCamera(/* expected= */ true); }); } @Test @EnableFlags({FLAG_ENABLE_CAMERA_COMPAT_EXTERNAL_DISPLAY_ROTATION_BUGFIX, FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING}) public void testIsCamera_DeviceNaturalOrientationPortrait_landscapeDisplay_returnsFalse() { runTestScenario((robot) -> { robot.configureActivityAndDisplay(ROTATION_0, ORIENTATION_LANDSCAPE, TYPE_INTERNAL); // Current rotation does not affect whether camera sensor is portrait or landscape. robot.checkIsPortraitCamera(/* expected= */ false); }); } /** * Runs a test scenario providing a Robot. */ void runTestScenario(@NonNull Consumer<AppCompatCameraInfoProviderRobotTests> consumer) { final AppCompatCameraInfoProviderRobotTests robot = new AppCompatCameraInfoProviderRobotTests(mWm, mAtm, mSupervisor); consumer.accept(robot); } private static class AppCompatCameraInfoProviderRobotTests extends AppCompatRobotBase { private AppCompatCameraRotationState mCameraInfoProvider; private final WindowManagerService mWm; AppCompatCameraInfoProviderRobotTests(@NonNull WindowManagerService wm, @NonNull ActivityTaskManagerService atm, @NonNull ActivityTaskSupervisor supervisor) { super(wm, atm, supervisor); mWm = wm; setupAppCompatConfiguration(); } @Override void onPostDisplayContentCreation(@NonNull DisplayContent displayContent) { super.onPostDisplayContentCreation(displayContent); } @Override void onPostActivityCreation(@NonNull ActivityRecord activity) { super.onPostActivityCreation(activity); mCameraInfoProvider = new AppCompatCameraRotationState(activity.mDisplayContent); mCameraInfoProvider.start(); } private void setupAppCompatConfiguration() { applyOnConf((c) -> { c.enableCameraCompatForceRotateTreatment(true); c.enableCameraCompatForceRotateTreatmentAtBuildTime(true); }); } private void configureActivityAndDisplay( @Surface.Rotation int displayRotation, @Configuration.Orientation int naturalOrientation, int displayType) { applyOnActivity(a -> { dw().allowEnterDesktopMode(true); a.createActivityWithComponentInNewTaskAndDisplay(displayType); a.setIgnoreOrientationRequest(true); a.rotateDisplayForTopActivity(displayRotation); a.setDisplayNaturalOrientation(naturalOrientation); spyOn(a.top().mAppCompatController.getCameraOverrides()); spyOn(a.top().info); doReturn(a.displayContent().getDisplayInfo()).when( a.displayContent().mWmService.mDisplayManagerInternal).getDisplayInfo( a.displayContent().mDisplayId); }); } void makeCurrentDisplayDefault() { doReturn(activity().displayContent()).when(mWm).getDefaultDisplayContentLocked(); } void checkDisplayRotation(@Surface.Rotation int expected) { assertEquals(expected, mCameraInfoProvider.getCameraDeviceRotation()); } void checkIsPortraitCamera(boolean expected) { assertEquals(expected, mCameraInfoProvider.isCameraDeviceNaturalOrientationPortrait()); } void checkIsCameraDisplayRotationPortrait(boolean expected) { assertEquals(expected, mCameraInfoProvider.isCameraDeviceOrientationPortrait()); } void checkOrientationEventListenerSetUp(boolean expected) { assertEquals(expected, mCameraInfoProvider.mOrientationEventListener != null); } void setSensorOrientation(int orientation) { mCameraInfoProvider.mOrientationEventListener.onOrientationChanged(orientation); } } }