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();
     }