diff options
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 9eb9222078fb..d9dc7ba9ad12 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; @@ -855,7 +857,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 @@ -8556,11 +8557,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 87ee5d8f7f13..ad711cb2af31 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 864ac6582ece..9a513752dee2 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(); } |