[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
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 78a6816..e4ad1b8 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_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_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 @@
@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 @@
// 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 0000000..0c751cf
--- /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 a3a6b51..fa9b747 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.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 @@
@Nullable
final DisplayRotationCompatPolicy mDisplayRotationCompatPolicy;
@Nullable
+ final CameraCompatFreeformPolicy mCameraCompatFreeformPolicy;
+ @Nullable
final CameraStateMonitor mCameraStateMonitor;
@Nullable
final ActivityRefresher mActivityRefresher;
@@ -683,7 +686,6 @@
*/
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 @@
// 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 @@
mCameraStateMonitor = null;
mActivityRefresher = null;
mDisplayRotationCompatPolicy = null;
+ mCameraCompatFreeformPolicy = null;
}
-
mRotationReversionController = new DisplayRotationReversionController(this);
mInputMonitor = new InputMonitor(mWmService, this);
@@ -3350,6 +3367,11 @@
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 e0cc064..6ecafdb 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 @@
mDisplayContent = displayContent;
mWmService = displayContent.mWmService;
mCameraStateMonitor = cameraStateMonitor;
- mCameraStateMonitor.addCameraStateListener(this);
mActivityRefresher = activityRefresher;
+ }
+
+ void start() {
+ mCameraStateMonitor.addCameraStateListener(this);
mActivityRefresher.addEvaluator(this);
}
@@ -365,7 +368,7 @@
}
// 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 5e93e89..53b2002 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 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 @@
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 @@
.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 22f718d..787c5d6 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 @@
&& !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 0000000..b3f1502
--- /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 417ee6b..6957502 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.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.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 @@
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 c76acd7..c65371f 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 @@
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 c45c86c..c962a3f 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 @@
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 6ecaea9..e01cea3 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 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 @@
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();
}