diff options
| author | 2024-04-26 14:34:46 +0000 | |
|---|---|---|
| committer | 2024-06-14 09:16:01 +0000 | |
| commit | 880dfc1c8ab67006e5c635901c5ed1850839c2dd (patch) | |
| tree | ef32b9afe3b06f5fc60fb949975bf7f316dcd846 | |
| parent | 2e63bd90c3935d04f60bdde47a72343e9522578f (diff) | |
[1/n] Turn on camera compat mode for fixed orientation freeform activities when using camera.
Camera compat mode letterboxes camera activities that are likely to be untested on large screens and likely to be broken (fixed-orientation activities). If activated, camera compat mode will cause the camera to rotate and crop the preview to portrait if the camera feed is landscape, and changes camera and display rotation signals to match the natural orientation portrait (future changes).
Bug: 314960895
Test: atest CameraFreeformCompatPolicyTest
Change-Id: I8435fce70ecdef55bebaffd56c4643170138913c
11 files changed, 654 insertions, 9 deletions
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 78a6816593ae..e4ad1b84c66f 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -37,6 +37,7 @@ import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED; import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; +import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE; import static android.app.CameraCompatTaskInfo.cameraCompatControlStateToString; import static android.app.WaitResult.INVALID_DELAY; import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT; @@ -46,6 +47,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.app.WindowConfiguration.ROTATION_UNDEFINED; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; @@ -854,7 +856,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A @CameraCompatControlState private int mCameraCompatControlState = CAMERA_COMPAT_CONTROL_HIDDEN; - // The callback that allows to ask the calling View to apply the treatment for stretched // issues affecting camera viewfinders when the user clicks on the camera compat control. @Nullable @@ -8549,11 +8550,15 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // and back which can cause visible issues (see b/184078928). final int parentWindowingMode = newParentConfiguration.windowConfiguration.getWindowingMode(); + final boolean isInCameraCompatFreeform = parentWindowingMode == WINDOWING_MODE_FREEFORM + && mLetterboxUiController.getFreeformCameraCompatMode() + != CAMERA_COMPAT_FREEFORM_NONE; // Bubble activities should always fill their parent and should not be letterboxed. final boolean isFixedOrientationLetterboxAllowed = !getLaunchedFromBubble() && (parentWindowingMode == WINDOWING_MODE_MULTI_WINDOW || parentWindowingMode == WINDOWING_MODE_FULLSCREEN + || isInCameraCompatFreeform // When starting to switch between PiP and fullscreen, the task is pinned // and the activity is fullscreen. But only allow to apply letterbox if the // activity is exiting PiP because an entered PiP should fill the task. diff --git a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java new file mode 100644 index 000000000000..0c751cfe4f46 --- /dev/null +++ b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2024 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.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED; +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 com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; +import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; + +import android.annotation.NonNull; +import android.app.CameraCompatTaskInfo; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.ProtoLogGroup; +import com.android.internal.protolog.common.ProtoLog; +import com.android.window.flags.Flags; + +/** + * Policy for camera compatibility freeform treatment. + * + * <p>The treatment is applied to a fixed-orientation camera activity in freeform windowing mode. + * The treatment letterboxes or pillarboxes the activity to the expected orientation and provides + * changes to the camera and display orientation signals to match those expected on a portrait + * device in that orientation (for example, on a standard phone). + */ +final class CameraCompatFreeformPolicy implements CameraStateMonitor.CameraCompatStateListener, + ActivityRefresher.Evaluator { + private static final String TAG = TAG_WITH_CLASS_NAME ? "CameraCompatFreeformPolicy" : TAG_WM; + + @NonNull + private final DisplayContent mDisplayContent; + @NonNull + private final ActivityRefresher mActivityRefresher; + @NonNull + private final CameraStateMonitor mCameraStateMonitor; + + private boolean mIsCameraCompatTreatmentPending = false; + + CameraCompatFreeformPolicy(@NonNull DisplayContent displayContent, + @NonNull CameraStateMonitor cameraStateMonitor, + @NonNull ActivityRefresher activityRefresher) { + mDisplayContent = displayContent; + mCameraStateMonitor = cameraStateMonitor; + mActivityRefresher = activityRefresher; + } + + void start() { + mCameraStateMonitor.addCameraStateListener(this); + mActivityRefresher.addEvaluator(this); + } + + /** Releases camera callback listener. */ + void dispose() { + mCameraStateMonitor.removeCameraStateListener(this); + mActivityRefresher.removeEvaluator(this); + } + + // Refreshing only when configuration changes after rotation or camera split screen aspect ratio + // treatment is enabled. + @Override + public boolean shouldRefreshActivity(@NonNull ActivityRecord activity, + @NonNull Configuration newConfig, + @NonNull Configuration lastReportedConfig) { + return isTreatmentEnabledForActivity(activity) && mIsCameraCompatTreatmentPending; + } + + /** + * Whether activity is eligible for camera compatibility free-form treatment. + * + * <p>The treatment is applied to a fixed-orientation camera activity in free-form windowing + * mode. The treatment letterboxes or pillarboxes the activity to the expected orientation and + * provides changes to the camera and display orientation signals to match those expected on a + * portrait device in that orientation (for example, on a standard phone). + * + * <p>The treatment is enabled when the following conditions are met: + * <ul> + * <li>Property gating the camera compatibility free-form treatment is enabled. + * <li>Activity isn't opted out by the device manufacturer with override. + * </ul> + */ + @VisibleForTesting + boolean shouldApplyFreeformTreatmentForCameraCompat(@NonNull ActivityRecord activity) { + return Flags.cameraCompatForFreeform() && !activity.info.isChangeEnabled( + ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT); + } + + @Override + public boolean onCameraOpened(@NonNull ActivityRecord cameraActivity, + @NonNull String cameraId) { + if (!isTreatmentEnabledForActivity(cameraActivity)) { + return false; + } + final int existingCameraCompatMode = + cameraActivity.mLetterboxUiController.getFreeformCameraCompatMode(); + final int newCameraCompatMode = getCameraCompatMode(cameraActivity); + if (newCameraCompatMode != existingCameraCompatMode) { + mIsCameraCompatTreatmentPending = true; + cameraActivity.mLetterboxUiController.setFreeformCameraCompatMode(newCameraCompatMode); + forceUpdateActivityAndTask(cameraActivity); + return true; + } else { + mIsCameraCompatTreatmentPending = false; + } + return false; + } + + @Override + public boolean onCameraClosed(@NonNull ActivityRecord cameraActivity, + @NonNull String cameraId) { + if (isActivityForCameraIdRefreshing(cameraId)) { + ProtoLog.v(ProtoLogGroup.WM_DEBUG_STATES, + "Display id=%d is notified that Camera %s is closed but activity is" + + " still refreshing. Rescheduling an update.", + mDisplayContent.mDisplayId, cameraId); + return false; + } + cameraActivity.mLetterboxUiController.setFreeformCameraCompatMode( + CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE); + forceUpdateActivityAndTask(cameraActivity); + mIsCameraCompatTreatmentPending = false; + return true; + } + + private void forceUpdateActivityAndTask(ActivityRecord cameraActivity) { + cameraActivity.recomputeConfiguration(); + cameraActivity.updateReportedConfigurationAndSend(); + Task cameraTask = cameraActivity.getTask(); + if (cameraTask != null) { + cameraTask.dispatchTaskInfoChangedIfNeeded(/* force= */ true); + } + } + + private static int getCameraCompatMode(@NonNull ActivityRecord topActivity) { + return switch (topActivity.getRequestedConfigurationOrientation()) { + case ORIENTATION_PORTRAIT -> CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_PORTRAIT; + case ORIENTATION_LANDSCAPE -> CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_LANDSCAPE; + default -> CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE; + }; + } + + /** + * Whether camera compat treatment is applicable for the given activity, ignoring its windowing + * mode. + * + * <p>Conditions that need to be met: + * <ul> + * <li>Treatment is enabled. + * <li>Camera is active for the package. + * <li>The app has a fixed orientation. + * <li>The app is in freeform windowing mode. + * </ul> + */ + private boolean isTreatmentEnabledForActivity(@NonNull ActivityRecord activity) { + int orientation = activity.getRequestedConfigurationOrientation(); + return shouldApplyFreeformTreatmentForCameraCompat(activity) + && mCameraStateMonitor.isCameraRunningForActivity(activity) + && orientation != ORIENTATION_UNDEFINED + && activity.inFreeformWindowingMode() + // "locked" and "nosensor" values are often used by camera apps that can't + // handle dynamic changes so we shouldn't force-letterbox them. + && activity.getRequestedOrientation() != SCREEN_ORIENTATION_NOSENSOR + && activity.getRequestedOrientation() != SCREEN_ORIENTATION_LOCKED + // TODO(b/332665280): investigate whether we can support activity embedding. + && !activity.isEmbedded(); + } + + private boolean isActivityForCameraIdRefreshing(@NonNull String cameraId) { + final ActivityRecord topActivity = mDisplayContent.topRunningActivity( + /* considerKeyguardState= */ true); + if (topActivity == null || !isTreatmentEnabledForActivity(topActivity) + || mCameraStateMonitor.isCameraWithIdRunningForActivity(topActivity, cameraId)) { + return false; + } + return topActivity.mLetterboxUiController.isRefreshRequested(); + } +} diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index a3a6b51521f2..fa9b7471f213 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -263,6 +263,7 @@ import com.android.server.policy.WindowManagerPolicy; import com.android.server.wm.utils.RegionUtils; import com.android.server.wm.utils.RotationCache; import com.android.server.wm.utils.WmDisplayCutout; +import com.android.window.flags.Flags; import java.io.PrintWriter; import java.lang.annotation.Retention; @@ -477,6 +478,8 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp @Nullable final DisplayRotationCompatPolicy mDisplayRotationCompatPolicy; @Nullable + final CameraCompatFreeformPolicy mCameraCompatFreeformPolicy; + @Nullable final CameraStateMonitor mCameraStateMonitor; @Nullable final ActivityRefresher mActivityRefresher; @@ -683,7 +686,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp */ private InputTarget mLastImeInputTarget; - /** * Tracks the windowToken of the input method input target and the corresponding * {@link WindowContainerListener} for monitoring changes (e.g. the requested visibility @@ -1233,11 +1235,26 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // without the need to restart the device. final boolean shouldCreateDisplayRotationCompatPolicy = mWmService.mLetterboxConfiguration.isCameraCompatTreatmentEnabledAtBuildTime(); - if (shouldCreateDisplayRotationCompatPolicy) { + final boolean shouldCreateCameraCompatFreeformPolicy = Flags.cameraCompatForFreeform() + && DesktopModeLaunchParamsModifier.canEnterDesktopMode(mWmService.mContext); + if (shouldCreateDisplayRotationCompatPolicy || shouldCreateCameraCompatFreeformPolicy) { mCameraStateMonitor = new CameraStateMonitor(this, mWmService.mH); mActivityRefresher = new ActivityRefresher(mWmService, mWmService.mH); - mDisplayRotationCompatPolicy = new DisplayRotationCompatPolicy( - this, mCameraStateMonitor, mActivityRefresher); + if (shouldCreateDisplayRotationCompatPolicy) { + mDisplayRotationCompatPolicy = new DisplayRotationCompatPolicy(this, + mCameraStateMonitor, mActivityRefresher); + mDisplayRotationCompatPolicy.start(); + } else { + mDisplayRotationCompatPolicy = null; + } + + if (shouldCreateCameraCompatFreeformPolicy) { + mCameraCompatFreeformPolicy = new CameraCompatFreeformPolicy(this, + mCameraStateMonitor, mActivityRefresher); + mCameraCompatFreeformPolicy.start(); + } else { + mCameraCompatFreeformPolicy = null; + } mCameraStateMonitor.startListeningToCameraState(); } else { @@ -1245,9 +1262,9 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mCameraStateMonitor = null; mActivityRefresher = null; mDisplayRotationCompatPolicy = null; + mCameraCompatFreeformPolicy = null; } - mRotationReversionController = new DisplayRotationReversionController(this); mInputMonitor = new InputMonitor(mWmService, this); @@ -3350,6 +3367,11 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp if (mDisplayRotationCompatPolicy != null) { mDisplayRotationCompatPolicy.dispose(); } + + if (mCameraCompatFreeformPolicy != null) { + mCameraCompatFreeformPolicy.dispose(); + } + if (mCameraStateMonitor != null) { mCameraStateMonitor.dispose(); } diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java index e0cc064fcacc..6ecafdb03d20 100644 --- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java @@ -80,8 +80,11 @@ final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraComp mDisplayContent = displayContent; mWmService = displayContent.mWmService; mCameraStateMonitor = cameraStateMonitor; - mCameraStateMonitor.addCameraStateListener(this); mActivityRefresher = activityRefresher; + } + + void start() { + mCameraStateMonitor.addCameraStateListener(this); mActivityRefresher.addEvaluator(this); } @@ -365,7 +368,7 @@ final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraComp } // TODO(b/336474959): Do we need cameraId here? - private boolean isActivityForCameraIdRefreshing(String cameraId) { + private boolean isActivityForCameraIdRefreshing(@NonNull String cameraId) { final ActivityRecord topActivity = mDisplayContent.topRunningActivity( /* considerKeyguardState= */ true); if (!isTreatmentEnabledForActivity(topActivity) diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index 5e93e8930bab..53b20024f4f7 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -16,6 +16,7 @@ package com.android.server.wm; +import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.content.pm.ActivityInfo.FORCE_NON_RESIZE_APP; import static android.content.pm.ActivityInfo.FORCE_RESIZE_APP; @@ -103,6 +104,7 @@ import static com.android.server.wm.LetterboxConfiguration.letterboxBackgroundTy import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager.TaskDescription; +import android.app.CameraCompatTaskInfo.FreeformCameraCompatMode; import android.content.pm.ActivityInfo.ScreenOrientation; import android.content.pm.PackageManager; import android.content.res.Configuration; @@ -231,6 +233,9 @@ final class LetterboxUiController { private boolean mDoubleTapEvent; + @FreeformCameraCompatMode + private int mFreeformCameraCompatMode = CAMERA_COMPAT_FREEFORM_NONE; + LetterboxUiController(WindowManagerService wmService, ActivityRecord activityRecord) { mLetterboxConfiguration = wmService.mLetterboxConfiguration; // Given activityRecord may not be fully constructed since LetterboxUiController @@ -711,6 +716,15 @@ final class LetterboxUiController { .isTreatmentEnabledForActivity(mActivityRecord); } + @FreeformCameraCompatMode + int getFreeformCameraCompatMode() { + return mFreeformCameraCompatMode; + } + + void setFreeformCameraCompatMode(@FreeformCameraCompatMode int freeformCameraCompatMode) { + mFreeformCameraCompatMode = freeformCameraCompatMode; + } + private boolean isCompatChangeEnabled(long overrideChangeId) { return mActivityRecord.info.isChangeEnabled(overrideChangeId); } diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 22f718ddbd22..787c5d6f7699 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -3515,7 +3515,10 @@ class Task extends TaskFragment { && !appCompatTaskInfo.topActivityInSizeCompat && top.mLetterboxUiController.shouldEnableUserAspectRatioSettings() && !info.isTopActivityTransparent; - appCompatTaskInfo.topActivityBoundsLetterboxed = top != null && top.areBoundsLetterboxed(); + appCompatTaskInfo.topActivityBoundsLetterboxed = top != null && top.areBoundsLetterboxed(); + appCompatTaskInfo.cameraCompatTaskInfo.freeformCameraCompatMode = top == null + ? CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE + : top.mLetterboxUiController.getFreeformCameraCompatMode(); } /** diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java new file mode 100644 index 000000000000..b3f150241115 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java @@ -0,0 +1,357 @@ +/* + * Copyright (C) 2024 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.WINDOWING_MODE_FREEFORM; +import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE; +import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; +import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.CameraCompatTaskInfo; +import android.app.WindowConfiguration.WindowingMode; +import android.app.servertransaction.RefreshCallbackItem; +import android.app.servertransaction.ResumeActivityItem; +import android.compat.testing.PlatformCompatChangeRule; +import android.content.ComponentName; +import android.content.pm.ActivityInfo.ScreenOrientation; +import android.content.res.Configuration; +import android.content.res.Configuration.Orientation; +import android.graphics.Rect; +import android.hardware.camera2.CameraManager; +import android.os.Handler; +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; + +import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runner.RunWith; + +import java.util.concurrent.Executor; + +/** + * Tests for {@link CameraCompatFreeformPolicy}. + * + * Build/Install/Run: + * atest WmTests:CameraCompatFreeformPolicyTests + */ +@SmallTest +@Presubmit +@RunWith(WindowTestRunner.class) +public class CameraCompatFreeformPolicyTests extends WindowTestsBase { + @Rule + public TestRule compatChangeRule = new PlatformCompatChangeRule(); + + // Main activity package name needs to be the same as the process to test overrides. + private static final String TEST_PACKAGE_1 = "com.android.frameworks.wmtests"; + private static final String TEST_PACKAGE_2 = "com.test.package.two"; + private static final String CAMERA_ID_1 = "camera-1"; + private static final String CAMERA_ID_2 = "camera-2"; + private CameraManager mMockCameraManager; + private Handler mMockHandler; + private LetterboxConfiguration mLetterboxConfiguration; + + private CameraManager.AvailabilityCallback mCameraAvailabilityCallback; + private CameraCompatFreeformPolicy mCameraCompatFreeformPolicy; + private ActivityRecord mActivity; + private Task mTask; + private ActivityRefresher mActivityRefresher; + + @Before + public void setUp() throws Exception { + mLetterboxConfiguration = mDisplayContent.mWmService.mLetterboxConfiguration; + spyOn(mLetterboxConfiguration); + when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled()) + .thenReturn(true); + when(mLetterboxConfiguration.isCameraCompatRefreshEnabled()) + .thenReturn(true); + when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) + .thenReturn(true); + + mMockCameraManager = mock(CameraManager.class); + doAnswer(invocation -> { + mCameraAvailabilityCallback = invocation.getArgument(1); + return null; + }).when(mMockCameraManager).registerAvailabilityCallback( + any(Executor.class), any(CameraManager.AvailabilityCallback.class)); + + when(mContext.getSystemService(CameraManager.class)).thenReturn(mMockCameraManager); + + mDisplayContent.setIgnoreOrientationRequest(true); + + mMockHandler = mock(Handler.class); + + when(mMockHandler.postDelayed(any(Runnable.class), anyLong())).thenAnswer( + invocation -> { + ((Runnable) invocation.getArgument(0)).run(); + return null; + }); + + mActivityRefresher = new ActivityRefresher(mDisplayContent.mWmService, mMockHandler); + mSetFlagsRule.enableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM); + CameraStateMonitor cameraStateMonitor = + new CameraStateMonitor(mDisplayContent, mMockHandler); + mCameraCompatFreeformPolicy = + new CameraCompatFreeformPolicy(mDisplayContent, cameraStateMonitor, + mActivityRefresher); + + mCameraCompatFreeformPolicy.start(); + cameraStateMonitor.startListeningToCameraState(); + } + + @Test + public void testFullscreen_doesNotActivateCameraCompatMode() { + configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN); + doReturn(false).when(mActivity).inFreeformWindowingMode(); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + assertNotInCameraCompatMode(); + } + + @Test + public void testOrientationUnspecified_doesNotActivateCameraCompatMode() { + configureActivity(SCREEN_ORIENTATION_UNSPECIFIED); + + assertNotInCameraCompatMode(); + } + + @Test + public void testNoCameraConnection_doesNotActivateCameraCompatMode() { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + assertNotInCameraCompatMode(); + } + + @Test + public void testCameraConnected_activatesCameraCompatMode() throws Exception { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + assertInCameraCompatMode(); + assertActivityRefreshRequested(/* refreshRequested */ false); + } + + @Test + public void testCameraReconnected_cameraCompatModeAndRefresh() throws Exception { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1); + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + callOnActivityConfigurationChanging(mActivity); + + assertInCameraCompatMode(); + assertActivityRefreshRequested(/* refreshRequested */ true); + } + + @Test + public void testReconnectedToDifferentCamera_activatesCameraCompatModeAndRefresh() + throws Exception { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1); + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_2, TEST_PACKAGE_1); + callOnActivityConfigurationChanging(mActivity); + + assertInCameraCompatMode(); + assertActivityRefreshRequested(/* refreshRequested */ true); + } + + @Test + public void testCameraDisconnected_deactivatesCameraCompatMode() { + configureActivityAndDisplay(SCREEN_ORIENTATION_PORTRAIT, ORIENTATION_LANDSCAPE, + WINDOWING_MODE_FREEFORM); + // Open camera and test for compat treatment + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + assertInCameraCompatMode(); + + // Close camera and test for revert + mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1); + + assertNotInCameraCompatMode(); + } + + @Test + public void testCameraOpenedForDifferentPackage_notInCameraCompatMode() { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_2); + + assertNotInCameraCompatMode(); + } + + @Test + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT}) + public void testShouldApplyCameraCompatFreeformTreatment_overrideEnabled_returnsFalse() { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + + assertTrue(mActivity.info + .isChangeEnabled(OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT)); + assertFalse(mCameraCompatFreeformPolicy + .shouldApplyFreeformTreatmentForCameraCompat(mActivity)); + } + + @Test + public void testShouldApplyCameraCompatFreeformTreatment_notDisabledByOverride_returnsTrue() { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + + assertTrue(mCameraCompatFreeformPolicy + .shouldApplyFreeformTreatmentForCameraCompat(mActivity)); + } + + @Test + public void testOnActivityConfigurationChanging_refreshDisabledViaFlag_noRefresh() + throws Exception { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + + doReturn(false).when( + mActivity.mLetterboxUiController).shouldRefreshActivityForCameraCompat(); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + callOnActivityConfigurationChanging(mActivity); + + assertActivityRefreshRequested(/* refreshRequested */ false); + } + + @Test + public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() throws Exception { + when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) + .thenReturn(false); + + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + callOnActivityConfigurationChanging(mActivity); + + assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false); + } + + @Test + public void testOnActivityConfigurationChanging_cycleThroughStopDisabledForApp() + throws Exception { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + doReturn(true).when(mActivity.mLetterboxUiController) + .shouldRefreshActivityViaPauseForCameraCompat(); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + callOnActivityConfigurationChanging(mActivity); + + assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false); + } + + private void configureActivity(@ScreenOrientation int activityOrientation) { + configureActivity(activityOrientation, WINDOWING_MODE_FREEFORM); + } + + private void configureActivity(@ScreenOrientation int activityOrientation, + @WindowingMode int windowingMode) { + configureActivityAndDisplay(activityOrientation, ORIENTATION_PORTRAIT, windowingMode); + } + + private void configureActivityAndDisplay(@ScreenOrientation int activityOrientation, + @Orientation int naturalOrientation, @WindowingMode int windowingMode) { + mTask = new TaskBuilder(mSupervisor) + .setDisplay(mDisplayContent) + .setWindowingMode(windowingMode) + .build(); + + mActivity = new ActivityBuilder(mAtm) + // Set the component to be that of the test class in order to enable compat changes + .setComponent(ComponentName.createRelative(mContext, + com.android.server.wm.CameraCompatFreeformPolicyTests.class.getName())) + .setScreenOrientation(activityOrientation) + .setTask(mTask) + .build(); + + spyOn(mActivity.mLetterboxUiController); + spyOn(mActivity.info); + + doReturn(mActivity).when(mDisplayContent).topRunningActivity(anyBoolean()); + doReturn(naturalOrientation).when(mDisplayContent).getNaturalOrientation(); + + doReturn(true).when(mActivity).inFreeformWindowingMode(); + } + + private void assertInCameraCompatMode() { + assertNotEquals(CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE, + mActivity.mLetterboxUiController.getFreeformCameraCompatMode()); + } + + private void assertNotInCameraCompatMode() { + assertEquals(CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE, + mActivity.mLetterboxUiController.getFreeformCameraCompatMode()); + } + + private void assertActivityRefreshRequested(boolean refreshRequested) throws Exception { + assertActivityRefreshRequested(refreshRequested, /* cycleThroughStop*/ true); + } + + private void assertActivityRefreshRequested(boolean refreshRequested, + boolean cycleThroughStop) throws Exception { + verify(mActivity.mLetterboxUiController, times(refreshRequested ? 1 : 0)) + .setIsRefreshRequested(true); + + final RefreshCallbackItem refreshCallbackItem = RefreshCallbackItem.obtain(mActivity.token, + cycleThroughStop ? ON_STOP : ON_PAUSE); + final ResumeActivityItem resumeActivityItem = ResumeActivityItem.obtain(mActivity.token, + /* isForward */ false, /* shouldSendCompatFakeFocus */ false); + + verify(mActivity.mAtmService.getLifecycleManager(), times(refreshRequested ? 1 : 0)) + .scheduleTransactionAndLifecycleItems(mActivity.app.getThread(), + refreshCallbackItem, resumeActivityItem); + } + + private void callOnActivityConfigurationChanging(ActivityRecord activity) { + mActivityRefresher.onActivityConfigurationChanging(activity, + /* newConfig */ createConfiguration(/*letterbox=*/ true), + /* lastReportedConfig */ createConfiguration(/*letterbox=*/ false)); + } + + private Configuration createConfiguration(boolean letterbox) { + final Configuration configuration = new Configuration(); + Rect bounds = letterbox ? new Rect(300, 0, 700, 600) : new Rect(0, 0, 1000, 600); + configuration.windowConfiguration.setAppBounds(bounds); + return configuration; + } +} 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 417ee6be17bc..695750217cac 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -83,6 +83,8 @@ import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_TOKEN_TRANSFO import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS; import static com.android.server.wm.WindowContainer.POSITION_TOP; import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL; +import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM; +import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE; import static com.google.common.truth.Truth.assertThat; @@ -115,6 +117,8 @@ import android.os.Binder; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.Presubmit; import android.util.ArraySet; import android.view.Display; @@ -2822,6 +2826,31 @@ public class DisplayContentTests extends WindowTestsBase { verify(mWm.mUmInternal, never()).isUserVisible(userId2, displayId); } + @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @Test + public void cameraCompatFreeformFlagEnabled_cameraCompatFreeformPolicyNotNull() { + doReturn(true).when(() -> + DesktopModeLaunchParamsModifier.canEnterDesktopMode(any())); + + assertNotNull(createNewDisplay().mCameraCompatFreeformPolicy); + } + + @DisableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @Test + public void cameraCompatFreeformFlagNotEnabled_cameraCompatFreeformPolicyIsNull() { + doReturn(true).when(() -> + DesktopModeLaunchParamsModifier.canEnterDesktopMode(any())); + + assertNull(createNewDisplay().mCameraCompatFreeformPolicy); + } + + @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + @Test + public void desktopWindowingFlagNotEnabled_cameraCompatFreeformPolicyIsNull() { + assertNull(createNewDisplay().mCameraCompatFreeformPolicy); + } + private void removeRootTaskTests(Runnable runnable) { final TaskDisplayArea taskDisplayArea = mRootWindowContainer.getDefaultTaskDisplayArea(); final Task rootTask1 = taskDisplayArea.createRootTask(WINDOWING_MODE_FULLSCREEN, diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java index c76acd7e1d6b..c65371fc3320 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java @@ -142,6 +142,7 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { doNothing().when(mDisplayRotationCompatPolicy).showToast(anyInt()); doNothing().when(mDisplayRotationCompatPolicy).showToast(anyInt(), anyString()); + mDisplayRotationCompatPolicy.start(); cameraStateMonitor.startListeningToCameraState(); } diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java index c45c86cec5a7..c962a3f9ea4d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java +++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java @@ -445,6 +445,9 @@ public class SystemServicesTestRule implements TestRule { if (dc.mDisplayRotationCompatPolicy != null) { dc.mDisplayRotationCompatPolicy.dispose(); } + if (dc.mCameraCompatFreeformPolicy != null) { + dc.mCameraCompatFreeformPolicy.dispose(); + } if (dc.mCameraStateMonitor != null) { dc.mCameraStateMonitor.dispose(); } diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java index 6ecaea90b85b..e01cea3d62f8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java @@ -69,6 +69,7 @@ import static org.mockito.Mockito.never; import android.app.ActivityManager; import android.app.ActivityOptions; +import android.app.CameraCompatTaskInfo; import android.app.TaskInfo; import android.app.WindowConfiguration; import android.content.ComponentName; @@ -1986,6 +1987,17 @@ public class TaskTests extends WindowTestsBase { assertNotEquals(activityDifferentPackage, task.getBottomMostActivityInSamePackage()); } + @Test + public void getTaskInfoPropagatesCameraCompatMode() { + final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).build(); + final ActivityRecord activity = task.getTopMostActivity(); + activity.mLetterboxUiController + .setFreeformCameraCompatMode(CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_PORTRAIT); + + assertEquals(CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_PORTRAIT, + task.getTaskInfo().appCompatTaskInfo.cameraCompatTaskInfo.freeformCameraCompatMode); + } + private Task getTestTask() { return new TaskBuilder(mSupervisor).setCreateActivity(true).build(); } |