Merge "[1/n] Turn on camera compat mode for fixed orientation freeform activities when using camera." into main
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 9eb9222..d9dc7ba 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;
@@ -855,7 +857,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
@@ -8556,11 +8557,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 87ee5d8..ad711cb 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 864ac65..9a51375 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();
}