[4/n] Camera Compat: per-app controls for force rotation and refresh

Add the following per-app controls using overrides and component properties:
- Way to disable force rotation with OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION and PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION
- Way to disable activity refresh after force rotation with OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH and PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH
- Way to switch to activity refresh via "onPause -> onResume" with OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE and PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE

Test: atest WmTests:LetterboxUiControllerTest WmTests:DisplayRotationCompatPolicyTests
Fix: 263959004
Fix: 264304459
Fix: 264301586
Change-Id: I41698c577057a74ef5c3b6afda1b78f1d98bde77
Signed-off-by: Mariia Sandrikova <mariiasand@google.com>
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index 9e5e8de..2e3b5d2 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -1058,6 +1058,41 @@
     public static final long ALWAYS_SANDBOX_DISPLAY_APIS = 185004937L; // buganizer id
 
     /**
+     * This change id excludes the packages it is applied to from the camera compat force rotation
+     * treatment. See com.android.server.wm.DisplayRotationCompatPolicy for context.
+     * @hide
+     */
+    @ChangeId
+    @Overridable
+    @Disabled
+    public static final long OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION =
+            263959004L; // buganizer id
+
+    /**
+     * This change id excludes the packages it is applied to from activity refresh after camera
+     * compat force rotation treatment. See com.android.server.wm.DisplayRotationCompatPolicy for
+     * context.
+     * @hide
+     */
+    @ChangeId
+    @Overridable
+    @Disabled
+    public static final long OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH = 264304459L; // buganizer id
+
+    /**
+     * This change id makes the packages it is applied to do activity refresh after camera compat
+     * force rotation treatment using "resumed -> paused -> resumed" cycle rather than "resumed ->
+     * ... -> stopped -> ... -> resumed" cycle. See
+     * com.android.server.wm.DisplayRotationCompatPolicy for context.
+     * @hide
+     */
+    @ChangeId
+    @Overridable
+    @Disabled
+    public static final long OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE =
+            264301586L; // buganizer id
+
+    /**
      * This change id is the gatekeeper for all treatments that force a given min aspect ratio.
      * Enabling this change will allow the following min aspect ratio treatments to be applied:
      * OVERRIDE_MIN_ASPECT_RATIO_MEDIUM
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index ed9cb00..abc4926 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -853,6 +853,143 @@
             "android.window.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION";
 
     /**
+     * Activity level {@link android.content.pm.PackageManager.Property PackageManager
+     * .Property} for an app to inform the system that the activity should be excluded from the
+     * camera compatibility force rotation treatment.
+     *
+     * <p>The camera compatibility treatment aligns orientations of portrait app window and natural
+     * orientation of the device and set opposite to natural orientation for a landscape app
+     * window. Mismatch between them can lead to camera issues like sideways or stretched
+     * viewfinder since this is one of the strongest assumptions that apps make when they implement
+     * camera previews. Since app and natural display orientations aren't guaranteed to match, the
+     * rotation can cause letterboxing. The forced rotation is triggered as soon as app opens to
+     * camera and is removed once camera is closed.
+     *
+     * <p>The camera compatibility can be enabled by device manufacturers on the displays that have
+     * ignoreOrientationRequest display setting enabled (enables compatibility mode for fixed
+     * orientation, see <a href="https://developer.android.com/guide/practices/enhanced-letterboxing">Enhanced letterboxing</a>
+     * for more details).
+     *
+     * <p>With this property set to {@code true} or unset, the system may apply the force rotation
+     * treatment to fixed orientation activities. Device manufacturers can exclude packages from the
+     * treatment using their discretion to improve display compatibility.
+     *
+     * <p>With this property set to {@code false}, the system will not apply the force rotation
+     * treatment.
+     *
+     * <p><b>Syntax:</b>
+     * <pre>
+     * &lt;activity&gt;
+     *   &lt;property
+     *     android:name="android.window.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION"
+     *     android:value="true|false"/&gt;
+     * &lt;/activity&gt;
+     * </pre>
+     *
+     * @hide
+     */
+    // TODO(b/263984287): Make this public API.
+    String PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION =
+            "android.window.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION";
+
+    /**
+     * Activity level {@link android.content.pm.PackageManager.Property PackageManager
+     * .Property} for an app to inform the system that the activity should be excluded
+     * from the activity "refresh" after the camera compatibility force rotation treatment.
+     *
+     * <p>The camera compatibility treatment aligns orientations of portrait app window and natural
+     * orientation of the device and set opposite to natural orientation for a landscape app
+     * window. Mismatch between them can lead to camera issues like sideways or stretched
+     * viewfinder since this is one of the strongest assumptions that apps make when they implement
+     * camera previews. Since app and natural display orientations aren't guaranteed to match, the
+     * rotation can cause letterboxing. The forced rotation is triggered as soon as app opens to
+     * camera and is removed once camera is closed.
+     *
+     * <p>Force rotation is followed by the "Refresh" of the activity by going through "resumed ->
+     * ... -> stopped -> ... -> resumed" cycle (by default) or "resumed -> paused -> resumed" cycle
+     * (if overridden, see {@link #PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE} for context).
+     * This allows to clear cached values in apps (e.g. display or camera rotation) that influence
+     * camera preview and can lead to sideways or stretching issues persisting even after force
+     * rotation.
+     *
+     * <p>The camera compatibility can be enabled by device manufacturers on the displays that have
+     * ignoreOrientationRequest display setting enabled (enables compatibility mode for fixed
+     * orientation, see <a href="https://developer.android.com/guide/practices/enhanced-letterboxing">Enhanced letterboxing</a>
+     * for more details).
+     *
+     * <p>With this property set to {@code true} or unset, the system may "refresh" activity after
+     * the force rotation treatment. Device manufacturers can exclude packages from the "refresh"
+     * using their discretion to improve display compatibility.
+     *
+     * <p>With this property set to {@code false}, the system will not "refresh" activity after the
+     * force rotation treatment.
+     *
+     * <p><b>Syntax:</b>
+     * <pre>
+     * &lt;activity&gt;
+     *   &lt;property
+     *     android:name="android.window.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH"
+     *     android:value="true|false"/&gt;
+     * &lt;/activity&gt;
+     * </pre>
+     *
+     * @hide
+     */
+    // TODO(b/263984287): Make this public API.
+    String PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH =
+            "android.window.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH";
+
+    /**
+     * Activity level {@link android.content.pm.PackageManager.Property PackageManager
+     * .Property} for an app to inform the system that the activity should be or shouldn't be
+     * "refreshed" after the camera compatibility force rotation treatment using "paused ->
+     * resumed" cycle rather than "stopped -> resumed".
+     *
+     * <p>The camera compatibility treatment aligns orientations of portrait app window and natural
+     * orientation of the device and set opposite to natural orientation for a landscape app
+     * window. Mismatch between them can lead to camera issues like sideways or stretched
+     * viewfinder since this is one of the strongest assumptions that apps make when they implement
+     * camera previews. Since app and natural display orientations aren't guaranteed to match, the
+     * rotation can cause letterboxing. The forced rotation is triggered as soon as app opens to
+     * camera and is removed once camera is closed.
+     *
+     * <p>Force rotation is followed by the "Refresh" of the activity by going through "resumed ->
+     * ... -> stopped -> ... -> resumed" cycle (by default) or "resumed -> paused -> resumed" cycle
+     * (if overridden by device manufacturers or using this property). This allows to clear cached
+     * values in apps (e.g., display or camera rotation) that influence camera preview and can lead
+     * to sideways or stretching issues persisting even after force rotation.
+     *
+     * <p>The camera compatibility can be enabled by device manufacturers on the displays that have
+     * ignoreOrientationRequest display setting enabled (enables compatibility mode for fixed
+     * orientation, see <a href="https://developer.android.com/guide/practices/enhanced-letterboxing">Enhanced letterboxing</a>
+     * for more details).
+     *
+     * <p>Device manufacturers can override packages to "refresh" via "resumed -> paused -> resumed"
+     * cycle using their discretion to improve display compatibility.
+     *
+     * <p>With this property set to {@code true}, the system will "refresh" activity after the
+     * force rotation treatment using "resumed -> paused -> resumed" cycle.
+     *
+     * <p>With this property set to {@code false}, the system will not "refresh" activity after the
+     * force rotation treatment using "resumed -> paused -> resumed" cycle even if the device
+     * manufacturer adds the corresponding override.
+     *
+     * <p><b>Syntax:</b>
+     * <pre>
+     * &lt;activity&gt;
+     *   &lt;property
+     *     android:name="android.window.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE"
+     *     android:value="true|false"/&gt;
+     * &lt;/activity&gt;
+     * </pre>
+     *
+     * @hide
+     */
+    // TODO(b/263984287): Make this public API.
+    String PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE =
+            "android.window.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE";
+
+    /**
      * @hide
      */
     public static final String PARCEL_KEY_SHORTCUTS_ARRAY = "shortcuts_array";
diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
index ba0413d..c6037da 100644
--- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
@@ -203,8 +203,11 @@
                 || !shouldRefreshActivity(activity, newConfig, lastReportedConfig)) {
             return;
         }
-        boolean cycleThroughStop = mWmService.mLetterboxConfiguration
-                .isCameraCompatRefreshCycleThroughStopEnabled();
+        boolean cycleThroughStop =
+                mWmService.mLetterboxConfiguration
+                        .isCameraCompatRefreshCycleThroughStopEnabled()
+                && !activity.mLetterboxUiController
+                        .shouldRefreshActivityViaPauseForCameraCompat();
         try {
             activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(true);
             ProtoLog.v(WM_DEBUG_STATES,
@@ -255,7 +258,8 @@
             Configuration lastReportedConfig) {
         return newConfig.windowConfiguration.getDisplayRotation()
                         != lastReportedConfig.windowConfiguration.getDisplayRotation()
-                && isTreatmentEnabledForActivity(activity);
+                && isTreatmentEnabledForActivity(activity)
+                && activity.mLetterboxUiController.shouldRefreshActivityForCameraCompat();
     }
 
     /**
@@ -294,7 +298,8 @@
                 // handle dynamic changes so we shouldn't force rotate them.
                 && activity.getRequestedOrientation() != SCREEN_ORIENTATION_NOSENSOR
                 && activity.getRequestedOrientation() != SCREEN_ORIENTATION_LOCKED
-                && mCameraIdPackageBiMap.containsPackageName(activity.packageName);
+                && mCameraIdPackageBiMap.containsPackageName(activity.packageName)
+                && activity.mLetterboxUiController.shouldForceRotateForCameraCompat();
     }
 
     private synchronized void notifyCameraOpened(
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index 0c8a645..75ba214 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -17,12 +17,18 @@
 package com.android.server.wm;
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION;
+import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH;
+import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
 import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
 import static android.content.pm.ActivityInfo.screenOrientationToString;
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
+import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION;
+import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH;
+import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
 import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION;
 
 import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__BOTTOM;
@@ -132,6 +138,15 @@
     @Nullable
     private Letterbox mLetterbox;
 
+    @Nullable
+    private final Boolean mBooleanPropertyCameraCompatAllowForceRotation;
+
+    @Nullable
+    private final Boolean mBooleanPropertyCameraCompatAllowRefresh;
+
+    @Nullable
+    private final Boolean mBooleanPropertyCameraCompatEnableRefreshViaPause;
+
     // Whether activity "refresh" was requested but not finished in
     // ActivityRecord#activityResumedLocked following the camera compat force rotation in
     // DisplayRotationCompatPolicy.
@@ -154,8 +169,33 @@
                 readComponentProperty(packageManager, mActivityRecord.packageName,
                         mLetterboxConfiguration::isPolicyForIgnoringRequestedOrientationEnabled,
                         PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION);
+        mBooleanPropertyCameraCompatAllowForceRotation =
+                readComponentProperty(packageManager, mActivityRecord.packageName,
+                        () -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+                                /* checkDeviceConfig */ true),
+                        PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION);
+        mBooleanPropertyCameraCompatAllowRefresh =
+                readComponentProperty(packageManager, mActivityRecord.packageName,
+                        () -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+                                /* checkDeviceConfig */ true),
+                        PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH);
+        mBooleanPropertyCameraCompatEnableRefreshViaPause =
+                readComponentProperty(packageManager, mActivityRecord.packageName,
+                        () -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+                                /* checkDeviceConfig */ true),
+                        PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE);
     }
 
+    /**
+     * Reads a {@link Boolean} component property fot a given {@code packageName} and a {@code
+     * propertyName}. Returns {@code null} if {@code gatingCondition} is {@code false} or if the
+     * property isn't specified for the package.
+     *
+     * <p>Return value is {@link Boolean} rather than {@code boolean} so we can know when the
+     * property is unset. Particularly, when this returns {@code null}, {@link
+     * #shouldEnableWithOverrideAndProperty} will check the value of override for the final
+     * decision.
+     */
     @Nullable
     private static Boolean readComponentProperty(PackageManager packageManager, String packageName,
             BooleanSupplier gatingCondition, String propertyName) {
@@ -210,15 +250,11 @@
      * </ul>
      */
     boolean shouldIgnoreRequestedOrientation(@ScreenOrientation int requestedOrientation) {
-        if (!mLetterboxConfiguration.isPolicyForIgnoringRequestedOrientationEnabled()) {
-            return false;
-        }
-        if (Boolean.FALSE.equals(mBooleanPropertyIgnoreRequestedOrientation)) {
-            return false;
-        }
-        if (!Boolean.TRUE.equals(mBooleanPropertyIgnoreRequestedOrientation)
-                && !mActivityRecord.info.isChangeEnabled(
-                        OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION)) {
+        if (!shouldEnableWithOverrideAndProperty(
+                /* gatingCondition */ mLetterboxConfiguration
+                        ::isPolicyForIgnoringRequestedOrientationEnabled,
+                OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION,
+                mBooleanPropertyIgnoreRequestedOrientation)) {
             return false;
         }
         if (mIsRelauchingAfterRequestedOrientationChanged) {
@@ -262,6 +298,109 @@
         mIsRefreshAfterRotationRequested = isRequested;
     }
 
+    /**
+     * Whether activity is eligible for activity "refresh" after camera compat force rotation
+     * treatment. See {@link DisplayRotationCompatPolicy} for context.
+     *
+     * <p>This treatment is enabled when the following conditions are met:
+     * <ul>
+     *     <li>Flag gating the camera compat treatment is enabled.
+     *     <li>Activity isn't opted out by the device manufacturer with override or by the app
+     *     developers with the component property.
+     * </ul>
+     */
+    boolean shouldRefreshActivityForCameraCompat() {
+        return shouldEnableWithOptOutOverrideAndProperty(
+                /* gatingCondition */ () -> mLetterboxConfiguration
+                        .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true),
+                OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH,
+                mBooleanPropertyCameraCompatAllowRefresh);
+    }
+
+    /**
+     * Whether activity should be "refreshed" after the camera compat force rotation treatment
+     * using the "resumed -> paused -> resumed" cycle rather than the "resumed -> ... -> stopped
+     * -> ... -> resumed" cycle. See {@link DisplayRotationCompatPolicy} for context.
+     *
+     * <p>This treatment is enabled when the following conditions are met:
+     * <ul>
+     *     <li>Flag gating the camera compat treatment is enabled.
+     *     <li>Activity "refresh" via "resumed -> paused -> resumed" cycle isn't disabled with the
+     *     component property by the app developers.
+     *     <li>Activity "refresh" via "resumed -> paused -> resumed" cycle is enabled by the device
+     *     manufacturer with override / by the app developers with the component property.
+     * </ul>
+     */
+    boolean shouldRefreshActivityViaPauseForCameraCompat() {
+        return shouldEnableWithOverrideAndProperty(
+                /* gatingCondition */ () -> mLetterboxConfiguration
+                        .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true),
+                OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE,
+                mBooleanPropertyCameraCompatEnableRefreshViaPause);
+    }
+
+    /**
+     * Whether activity is eligible for camera compat force rotation treatment. See {@link
+     * DisplayRotationCompatPolicy} for context.
+     *
+     * <p>This treatment is enabled when the following conditions are met:
+     * <ul>
+     *     <li>Flag gating the camera compat treatment is enabled.
+     *     <li>Activity isn't opted out by the device manufacturer with override or by the app
+     *     developers with the component property.
+     * </ul>
+     */
+    boolean shouldForceRotateForCameraCompat() {
+        return shouldEnableWithOptOutOverrideAndProperty(
+                /* gatingCondition */ () -> mLetterboxConfiguration
+                        .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true),
+                OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION,
+                mBooleanPropertyCameraCompatAllowForceRotation);
+    }
+
+    /**
+     * Returns {@code true} when the following conditions are met:
+     * <ul>
+     *     <li>{@code gatingCondition} isn't {@code false}
+     *     <li>OEM didn't opt out with a {@code overrideChangeId} override
+     *     <li>App developers didn't opt out with a component {@code property}
+     * </ul>
+     *
+     * <p>This is used for the treatments that are enabled based with the heuristic but can be
+     * disabled on per-app basis by OEMs or app developers.
+     */
+    private boolean shouldEnableWithOptOutOverrideAndProperty(BooleanSupplier gatingCondition,
+            long overrideChangeId, Boolean property) {
+        if (!gatingCondition.getAsBoolean()) {
+            return false;
+        }
+        return !Boolean.FALSE.equals(property)
+                && !mActivityRecord.info.isChangeEnabled(overrideChangeId);
+    }
+
+    /**
+     * Returns {@code true} when the following conditions are met:
+     * <ul>
+     *     <li>{@code gatingCondition} isn't {@code false}
+     *     <li>App developers didn't opt out with a component {@code property}
+     *     <li>App developers opted in with a component {@code property} or an OEM opted in with a
+     *     component {@code property}
+     * </ul>
+     *
+     * <p>This is used for the treatments that are enabled only on per-app basis.
+     */
+    private boolean shouldEnableWithOverrideAndProperty(BooleanSupplier gatingCondition,
+            long overrideChangeId, Boolean property) {
+        if (!gatingCondition.getAsBoolean()) {
+            return false;
+        }
+        if (Boolean.FALSE.equals(property)) {
+            return false;
+        }
+        return Boolean.TRUE.equals(property)
+                || mActivityRecord.info.isChangeEnabled(overrideChangeId);
+    }
+
     boolean hasWallpaperBackgroundForLetterbox() {
         return mShowWallpaperForLetterboxBackground;
     }
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 8bb79e3..45b30b2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
@@ -155,6 +155,18 @@
     }
 
     @Test
+    public void testTreatmentDisabledPerApp_noForceRotationOrRefresh()
+            throws Exception {
+        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+        when(mActivity.mLetterboxUiController.shouldForceRotateForCameraCompat())
+                .thenReturn(false);
+
+        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+        assertNoForceRotationOrRefresh();
+    }
+
+    @Test
     public void testMultiWindowMode_returnUnspecified_noForceRotationOrRefresh() throws Exception {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
         final TestSplitOrganizer organizer = new TestSplitOrganizer(mAtm, mDisplayContent);
@@ -327,7 +339,21 @@
     }
 
     @Test
-    public void testOnActivityConfigurationChanging_refreshDisabled_noRefresh() throws Exception {
+    public void testOnActivityConfigurationChanging_refreshDisabledViaFlag_noRefresh()
+            throws Exception {
+        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+        when(mActivity.mLetterboxUiController.shouldRefreshActivityForCameraCompat())
+                .thenReturn(false);
+
+        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+        callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true);
+
+        assertActivityRefreshRequested(/* refreshRequested */ false);
+    }
+
+    @Test
+    public void testOnActivityConfigurationChanging_refreshDisabledPerApp_noRefresh()
+            throws Exception {
         when(mLetterboxConfiguration.isCameraCompatRefreshEnabled()).thenReturn(false);
 
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
@@ -362,6 +388,19 @@
         assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false);
     }
 
+    @Test
+    public void testOnActivityConfigurationChanging_cycleThroughStopDisabledForApp()
+            throws Exception {
+        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+        when(mActivity.mLetterboxUiController.shouldRefreshActivityViaPauseForCameraCompat())
+                .thenReturn(true);
+
+        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+        callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true);
+
+        assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false);
+    }
+
     private void configureActivity(@ScreenOrientation int activityOrientation) {
         configureActivityAndDisplay(activityOrientation, ORIENTATION_PORTRAIT);
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
index 6d778afe..5e087f0 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
@@ -16,8 +16,14 @@
 
 package com.android.server.wm;
 
+import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION;
+import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH;
+import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
 import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION;
+import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH;
+import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
 import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -74,6 +80,8 @@
         mController = new LetterboxUiController(mWm, mActivity);
     }
 
+    // shouldIgnoreRequestedOrientation
+
     @Test
     @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
     public void testShouldIgnoreRequestedOrientation_activityRelaunching_returnsTrue() {
@@ -134,7 +142,7 @@
     }
 
     @Test
-    @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH})
     public void testShouldIgnoreRequestedOrientation_flagIsDisabled_returnsFalse() {
         prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch();
         doReturn(false).when(mLetterboxConfiguration)
@@ -143,6 +151,163 @@
         assertFalse(mController.shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED));
     }
 
+    // shouldRefreshActivityForCameraCompat
+
+    @Test
+    public void testShouldRefreshActivityForCameraCompat_flagIsDisabled_returnsFalse() {
+        doReturn(false).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+
+        assertFalse(mController.shouldRefreshActivityForCameraCompat());
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH})
+    public void testShouldRefreshActivityForCameraCompat_overrideEnabled_returnsFalse() {
+        doReturn(true).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+
+        assertFalse(mController.shouldRefreshActivityForCameraCompat());
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH})
+    public void testShouldRefreshActivityForCameraCompat_propertyIsTrueAndOverride_returnsFalse()
+            throws Exception {
+        doReturn(true).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+        mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH, /* value */ true);
+
+        mController = new LetterboxUiController(mWm, mActivity);
+
+        assertFalse(mController.shouldRefreshActivityForCameraCompat());
+    }
+
+    @Test
+    public void testShouldRefreshActivityForCameraCompat_propertyIsFalse_returnsFalse()
+            throws Exception {
+        doReturn(true).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+        mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH, /* value */ false);
+
+        mController = new LetterboxUiController(mWm, mActivity);
+
+        assertFalse(mController.shouldRefreshActivityForCameraCompat());
+    }
+
+    @Test
+    public void testShouldRefreshActivityForCameraCompat_propertyIsTrue_returnsTrue()
+            throws Exception {
+        doReturn(true).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+        mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH, /* value */ true);
+
+        mController = new LetterboxUiController(mWm, mActivity);
+
+        assertTrue(mController.shouldRefreshActivityForCameraCompat());
+    }
+
+    // shouldRefreshActivityViaPauseForCameraCompat
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE})
+    public void testShouldRefreshActivityViaPauseForCameraCompat_flagIsDisabled_returnsFalse() {
+        doReturn(false).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+
+        assertFalse(mController.shouldRefreshActivityViaPauseForCameraCompat());
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE})
+    public void testShouldRefreshActivityViaPauseForCameraCompat_overrideEnabled_returnsTrue() {
+        doReturn(true).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+
+        assertTrue(mController.shouldRefreshActivityViaPauseForCameraCompat());
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE})
+    public void testShouldRefreshActivityViaPauseForCameraCompat_propertyIsFalseAndOverride_returnFalse()
+            throws Exception {
+        doReturn(true).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+        mockThatProperty(PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE, /* value */ false);
+
+        mController = new LetterboxUiController(mWm, mActivity);
+
+        assertFalse(mController.shouldRefreshActivityViaPauseForCameraCompat());
+    }
+
+    @Test
+    public void testShouldRefreshActivityViaPauseForCameraCompat_propertyIsTrue_returnsTrue()
+            throws Exception {
+        doReturn(true).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+        mockThatProperty(PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE, /* value */ true);
+
+        mController = new LetterboxUiController(mWm, mActivity);
+
+        assertTrue(mController.shouldRefreshActivityViaPauseForCameraCompat());
+    }
+
+    // shouldForceRotateForCameraCompat
+
+    @Test
+    public void testShouldForceRotateForCameraCompat_flagIsDisabled_returnsFalse() {
+        doReturn(false).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+
+        assertFalse(mController.shouldForceRotateForCameraCompat());
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION})
+    public void testShouldForceRotateForCameraCompat_overrideEnabled_returnsFalse() {
+        doReturn(true).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+
+        assertFalse(mController.shouldForceRotateForCameraCompat());
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION})
+    public void testShouldForceRotateForCameraCompat_propertyIsTrueAndOverride_returnsFalse()
+            throws Exception {
+        doReturn(true).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+        mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION, /* value */ true);
+
+        mController = new LetterboxUiController(mWm, mActivity);
+
+        assertFalse(mController.shouldForceRotateForCameraCompat());
+    }
+
+    @Test
+    public void testShouldForceRotateForCameraCompat_propertyIsFalse_returnsFalse()
+            throws Exception {
+        doReturn(true).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+        mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION, /* value */ false);
+
+        mController = new LetterboxUiController(mWm, mActivity);
+
+        assertFalse(mController.shouldForceRotateForCameraCompat());
+    }
+
+    @Test
+    public void testShouldForceRotateForCameraCompat_propertyIsTrue_returnsTrue()
+            throws Exception {
+        doReturn(true).when(mLetterboxConfiguration)
+                .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true);
+        mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION, /* value */ true);
+
+        mController = new LetterboxUiController(mWm, mActivity);
+
+        assertTrue(mController.shouldForceRotateForCameraCompat());
+    }
+
     private void mockThatProperty(String propertyName, boolean value) throws Exception {
         Property property = new Property(propertyName, /* value */ value, /* packageName */ "",
                  /* className */ "");