Merge changes from topic "camera-compat-force-rotation" into tm-qpr-dev

* changes:
  [3/n] Camera Compat: Refresh activity
  [2/n] Camera Compat: Add DeviceConfig flag
  [1/n] Camera Compat: Force rotate activities
diff --git a/core/java/android/app/ActivityClient.java b/core/java/android/app/ActivityClient.java
index 324b8e7..0074a0d 100644
--- a/core/java/android/app/ActivityClient.java
+++ b/core/java/android/app/ActivityClient.java
@@ -58,6 +58,15 @@
         }
     }
 
+    /** Reports {@link android.app.servertransaction.RefreshCallbackItem} is executed. */
+    public void activityRefreshed(IBinder token) {
+        try {
+            getActivityClientController().activityRefreshed(token);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
     /**
      * Reports after {@link Activity#onTopResumedActivityChanged(boolean)} is called for losing the
      * top most position.
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index ef6c5a6..2beb64d 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -5249,6 +5249,11 @@
         }
     }
 
+    @Override
+    public void reportRefresh(ActivityClientRecord r) {
+        ActivityClient.getInstance().activityRefreshed(r.token);
+    }
+
     private void handleSetCoreSettings(Bundle coreSettings) {
         synchronized (mCoreSettingsLock) {
             mCoreSettings = coreSettings;
diff --git a/core/java/android/app/ClientTransactionHandler.java b/core/java/android/app/ClientTransactionHandler.java
index a7566fd..2c70c4e 100644
--- a/core/java/android/app/ClientTransactionHandler.java
+++ b/core/java/android/app/ClientTransactionHandler.java
@@ -140,6 +140,9 @@
     /** Restart the activity after it was stopped. */
     public abstract void performRestartActivity(@NonNull ActivityClientRecord r, boolean start);
 
+     /** Report that activity was refreshed to server. */
+    public abstract void reportRefresh(@NonNull ActivityClientRecord r);
+
     /** Set pending activity configuration in case it will be updated by other transaction item. */
     public abstract void updatePendingActivityConfiguration(@NonNull IBinder token,
             Configuration overrideConfig);
diff --git a/core/java/android/app/IActivityClientController.aidl b/core/java/android/app/IActivityClientController.aidl
index 8b655b9..969f975 100644
--- a/core/java/android/app/IActivityClientController.aidl
+++ b/core/java/android/app/IActivityClientController.aidl
@@ -38,6 +38,7 @@
 interface IActivityClientController {
     oneway void activityIdle(in IBinder token, in Configuration config, in boolean stopProfiling);
     oneway void activityResumed(in IBinder token, in boolean handleSplashScreenExit);
+    oneway void activityRefreshed(in IBinder token);
     /**
      * This call is not one-way because {@link #activityPaused()) is not one-way, or
      * the top-resumed-lost could be reported after activity paused.
diff --git a/core/java/android/app/servertransaction/ClientTransactionItem.java b/core/java/android/app/servertransaction/ClientTransactionItem.java
index d94f08b..b159f33 100644
--- a/core/java/android/app/servertransaction/ClientTransactionItem.java
+++ b/core/java/android/app/servertransaction/ClientTransactionItem.java
@@ -38,6 +38,9 @@
         return UNDEFINED;
     }
 
+    boolean shouldHaveDefinedPreExecutionState() {
+        return true;
+    }
 
     // Parcelable
 
diff --git a/core/java/android/app/servertransaction/RefreshCallbackItem.java b/core/java/android/app/servertransaction/RefreshCallbackItem.java
new file mode 100644
index 0000000..74abab2
--- /dev/null
+++ b/core/java/android/app/servertransaction/RefreshCallbackItem.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2022 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 android.app.servertransaction;
+
+import static android.app.servertransaction.ActivityLifecycleItem.LifecycleState;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityThread.ActivityClientRecord;
+import android.app.ClientTransactionHandler;
+import android.os.IBinder;
+import android.os.Parcel;
+
+/**
+ * Callback that allows to {@link TransactionExecutor#cycleToPath} to {@link ON_PAUSE} or
+ * {@link ON_STOP} in {@link TransactionExecutor#executeCallbacks} for activity "refresh" flow
+ * that goes through "paused -> resumed" or "stopped -> resumed" cycle.
+ *
+ * <p>This is used in combination with {@link com.android.server.wm.DisplayRotationCompatPolicy}
+ * for camera compatibility treatment that handles orientation mismatch between camera buffers and
+ * an app window. 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.
+ *
+ * @hide
+ */
+public class RefreshCallbackItem extends ActivityTransactionItem {
+
+    // Whether refresh should happen using the "stopped -> resumed" cycle or
+    // "paused -> resumed" cycle.
+    @LifecycleState
+    private int mPostExecutionState;
+
+    @Override
+    public void execute(@NonNull ClientTransactionHandler client,
+            @NonNull ActivityClientRecord r, PendingTransactionActions pendingActions) {}
+
+    @Override
+    public void postExecute(ClientTransactionHandler client, IBinder token,
+            PendingTransactionActions pendingActions) {
+        final ActivityClientRecord r = getActivityClientRecord(client, token);
+        client.reportRefresh(r);
+    }
+
+    @Override
+    public int getPostExecutionState() {
+        return mPostExecutionState;
+    }
+
+    @Override
+    boolean shouldHaveDefinedPreExecutionState() {
+        return false;
+    }
+
+    // ObjectPoolItem implementation
+
+    @Override
+    public void recycle() {
+        ObjectPool.recycle(this);
+    }
+
+    /**
+    * Obtain an instance initialized with provided params.
+    * @param postExecutionState indicating whether refresh should happen using the
+    *        "stopped -> resumed" cycle or "paused -> resumed" cycle.
+    */
+    public static RefreshCallbackItem obtain(@LifecycleState int postExecutionState) {
+        if (postExecutionState != ON_STOP && postExecutionState != ON_PAUSE) {
+            throw new IllegalArgumentException(
+                    "Only ON_STOP or ON_PAUSE are allowed as a post execution state for "
+                            + "RefreshCallbackItem but got " + postExecutionState);
+        }
+        RefreshCallbackItem instance =
+                ObjectPool.obtain(RefreshCallbackItem.class);
+        if (instance == null) {
+            instance = new RefreshCallbackItem();
+        }
+        instance.mPostExecutionState = postExecutionState;
+        return instance;
+    }
+
+    private RefreshCallbackItem() {}
+
+    // Parcelable implementation
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mPostExecutionState);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        final RefreshCallbackItem other = (RefreshCallbackItem) o;
+        return mPostExecutionState == other.mPostExecutionState;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = 17;
+        result = 31 * result + mPostExecutionState;
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "RefreshCallbackItem{mPostExecutionState=" + mPostExecutionState + "}";
+    }
+
+    private RefreshCallbackItem(Parcel in) {
+        mPostExecutionState = in.readInt();
+    }
+
+    public static final @NonNull Creator<RefreshCallbackItem> CREATOR =
+            new Creator<RefreshCallbackItem>() {
+
+        public RefreshCallbackItem createFromParcel(Parcel in) {
+            return new RefreshCallbackItem(in);
+        }
+
+        public RefreshCallbackItem[] newArray(int size) {
+            return new RefreshCallbackItem[size];
+        }
+    };
+}
diff --git a/core/java/android/app/servertransaction/TransactionExecutor.java b/core/java/android/app/servertransaction/TransactionExecutor.java
index de1d38a..1ff0b79 100644
--- a/core/java/android/app/servertransaction/TransactionExecutor.java
+++ b/core/java/android/app/servertransaction/TransactionExecutor.java
@@ -126,10 +126,13 @@
             final ClientTransactionItem item = callbacks.get(i);
             if (DEBUG_RESOLVER) Slog.d(TAG, tId(transaction) + "Resolving callback: " + item);
             final int postExecutionState = item.getPostExecutionState();
-            final int closestPreExecutionState = mHelper.getClosestPreExecutionState(r,
-                    item.getPostExecutionState());
-            if (closestPreExecutionState != UNDEFINED) {
-                cycleToPath(r, closestPreExecutionState, transaction);
+
+            if (item.shouldHaveDefinedPreExecutionState()) {
+                final int closestPreExecutionState = mHelper.getClosestPreExecutionState(r,
+                        item.getPostExecutionState());
+                if (closestPreExecutionState != UNDEFINED) {
+                    cycleToPath(r, closestPreExecutionState, transaction);
+                }
             }
 
             item.execute(mTransactionHandler, token, mPendingActions);
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 50761bf..a0d6e47 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -5344,6 +5344,12 @@
          TODO(b/255532890) Enable when ignoreOrientationRequest is set -->
     <bool name="config_letterboxIsEnabledForTranslucentActivities">false</bool>
 
+    <!-- Whether camera compat treatment is enabled for issues caused by orientation mismatch
+        between camera buffers and an app window. This includes force rotation of fixed
+        orientation activities connected to the camera in fullscreen and showing a tooltip in
+        split screen. -->
+    <bool name="config_isWindowManagerCameraCompatTreatmentEnabled">false</bool>
+
     <!-- Whether a camera compat controller is enabled to allow the user to apply or revert
          treatment for stretched issues in camera viewfinder. -->
     <bool name="config_isCameraCompatControlForStretchedIssuesEnabled">false</bool>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index fb77b3b..371dbfb 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4460,6 +4460,7 @@
   <java-symbol type="bool" name="config_letterboxIsEducationEnabled" />
   <java-symbol type="dimen" name="config_letterboxDefaultMinAspectRatioForUnresizableApps" />
   <java-symbol type="bool" name="config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled" />
+  <java-symbol type="bool" name="config_isWindowManagerCameraCompatTreatmentEnabled" />
   <java-symbol type="bool" name="config_isCameraCompatControlForStretchedIssuesEnabled" />
 
   <java-symbol type="bool" name="config_hideDisplayCutoutWithDisplayArea" />
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 49704d9..f47d9c6 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -295,6 +295,12 @@
       "group": "WM_DEBUG_IME",
       "at": "com\/android\/server\/wm\/DisplayContent.java"
     },
+    "-1812743677": {
+      "message": "Display id=%d is ignoring all orientation requests, camera is active and the top activity is eligible for force rotation, return %s,portrait activity: %b, is natural orientation portrait: %b.",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_ORIENTATION",
+      "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java"
+    },
     "-1810446914": {
       "message": "Trying to update display configuration for system\/invalid process.",
       "level": "WARN",
@@ -481,6 +487,12 @@
       "group": "WM_DEBUG_ORIENTATION",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "-1631991057": {
+      "message": "Display id=%d is notified that Camera %s is closed but activity is still refreshing. Rescheduling an update.",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_ORIENTATION",
+      "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java"
+    },
     "-1630752478": {
       "message": "removeLockedTask: removed %s",
       "level": "DEBUG",
@@ -619,6 +631,12 @@
       "group": "WM_DEBUG_WINDOW_INSETS",
       "at": "com\/android\/server\/wm\/InsetsSourceProvider.java"
     },
+    "-1480918485": {
+      "message": "Refreshed activity: %s",
+      "level": "INFO",
+      "group": "WM_DEBUG_STATES",
+      "at": "com\/android\/server\/wm\/ActivityRecord.java"
+    },
     "-1480772131": {
       "message": "No app or window is requesting an orientation, return %d for display id=%d",
       "level": "VERBOSE",
@@ -1321,6 +1339,12 @@
       "group": "WM_DEBUG_CONFIGURATION",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
+    "-799396645": {
+      "message": "Display id=%d is notified that Camera %s is closed, updating rotation.",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_ORIENTATION",
+      "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java"
+    },
     "-799003045": {
       "message": "Set animatingExit: reason=remove\/replaceWindow win=%s",
       "level": "VERBOSE",
@@ -1543,6 +1567,12 @@
       "group": "WM_DEBUG_SCREEN_ON",
       "at": "com\/android\/server\/wm\/DisplayContent.java"
     },
+    "-627759820": {
+      "message": "Display id=%d is notified that Camera %s is open for package %s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_ORIENTATION",
+      "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java"
+    },
     "-622997754": {
       "message": "postWindowRemoveCleanupLocked: %s",
       "level": "VERBOSE",
@@ -2101,6 +2131,12 @@
       "group": "WM_SHOW_TRANSACTIONS",
       "at": "com\/android\/server\/wm\/Session.java"
     },
+    "-81260230": {
+      "message": "Display id=%d is notified that Camera %s is closed, scheduling rotation update.",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_ORIENTATION",
+      "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java"
+    },
     "-81121442": {
       "message": "ImeContainer just became organized but it doesn't have a parent or the parent doesn't have a surface control. mSurfaceControl=%s imeParentSurfaceControl=%s",
       "level": "ERROR",
@@ -4201,6 +4237,12 @@
       "group": "WM_DEBUG_REMOTE_ANIMATIONS",
       "at": "com\/android\/server\/wm\/RemoteAnimationController.java"
     },
+    "1967643923": {
+      "message": "Refershing activity for camera compatibility treatment, activityRecord=%s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_STATES",
+      "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java"
+    },
     "1967975839": {
       "message": "Changing app %s visible=%b performLayout=%b",
       "level": "VERBOSE",
diff --git a/services/core/java/com/android/server/camera/CameraServiceProxy.java b/services/core/java/com/android/server/camera/CameraServiceProxy.java
index aec60de..7bbc604 100644
--- a/services/core/java/com/android/server/camera/CameraServiceProxy.java
+++ b/services/core/java/com/android/server/camera/CameraServiceProxy.java
@@ -74,6 +74,7 @@
 import android.view.WindowManagerGlobal;
 
 import com.android.framework.protobuf.nano.MessageNano;
+import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.server.LocalServices;
@@ -389,6 +390,16 @@
             return CaptureRequest.SCALER_ROTATE_AND_CROP_NONE;
         }
 
+        // When config_isWindowManagerCameraCompatTreatmentEnabled is true,
+        // DisplayRotationCompatPolicy in WindowManager force rotates fullscreen activities with
+        // fixed orientation to align them with the natural orientation of the device.
+        if (ctx.getResources().getBoolean(
+                R.bool.config_isWindowManagerCameraCompatTreatmentEnabled)) {
+            Slog.v(TAG, "Disable Rotate and Crop to avoid conflicts with"
+                    + " WM force rotation treatment.");
+            return CaptureRequest.SCALER_ROTATE_AND_CROP_NONE;
+        }
+
         // External cameras do not need crop-rotate-scale.
         if (lensFacing != CameraMetadata.LENS_FACING_FRONT
                 && lensFacing != CameraMetadata.LENS_FACING_BACK) {
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index da6e7e8..7252545 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -163,6 +163,15 @@
     }
 
     @Override
+    public void activityRefreshed(IBinder token) {
+        final long origId = Binder.clearCallingIdentity();
+        synchronized (mGlobalLock) {
+            ActivityRecord.activityRefreshedLocked(token);
+        }
+        Binder.restoreCallingIdentity(origId);
+    }
+
+    @Override
     public void activityTopResumedStateLost() {
         final long origId = Binder.clearCallingIdentity();
         synchronized (mGlobalLock) {
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 57eeb9a..50169b4 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -736,7 +736,6 @@
      */
     private boolean mWillCloseOrEnterPip;
 
-    @VisibleForTesting
     final LetterboxUiController mLetterboxUiController;
 
     /**
@@ -6111,6 +6110,19 @@
         r.mDisplayContent.mUnknownAppVisibilityController.notifyAppResumedFinished(r);
     }
 
+    static void activityRefreshedLocked(IBinder token) {
+        final ActivityRecord r = ActivityRecord.forTokenLocked(token);
+        ProtoLog.i(WM_DEBUG_STATES, "Refreshed activity: %s", r);
+        if (r == null) {
+            // In case the record on server side has been removed (e.g. destroy timeout)
+            // and the token could be null.
+            return;
+        }
+        if (r.mDisplayContent.mDisplayRotationCompatPolicy != null) {
+            r.mDisplayContent.mDisplayRotationCompatPolicy.onActivityRefreshed(r);
+        }
+    }
+
     static void splashScreenAttachedLocked(IBinder token) {
         final ActivityRecord r = ActivityRecord.forTokenLocked(token);
         if (r == null) {
@@ -9143,6 +9155,8 @@
             } else {
                 scheduleConfigurationChanged(newMergedOverrideConfig);
             }
+            notifyDisplayCompatPolicyAboutConfigurationChange(
+                    mLastReportedConfiguration.getMergedConfiguration(), mTmpConfig);
             return true;
         }
 
@@ -9211,11 +9225,24 @@
         } else {
             scheduleConfigurationChanged(newMergedOverrideConfig);
         }
+        notifyDisplayCompatPolicyAboutConfigurationChange(
+                mLastReportedConfiguration.getMergedConfiguration(), mTmpConfig);
+
         stopFreezingScreenLocked(false);
 
         return true;
     }
 
+    private void notifyDisplayCompatPolicyAboutConfigurationChange(
+            Configuration newConfig, Configuration lastReportedConfig) {
+        if (mDisplayContent.mDisplayRotationCompatPolicy == null
+                || !shouldBeResumed(/* activeActivity */ null)) {
+            return;
+        }
+        mDisplayContent.mDisplayRotationCompatPolicy.onActivityConfigurationChanging(
+                this, newConfig, lastReportedConfig);
+    }
+
     /** Get process configuration, or global config if the process is not set. */
     private Configuration getProcessGlobalConfiguration() {
         return app != null ? app.getConfiguration() : mAtmService.getGlobalConfiguration();
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 4c19322a..36f86d1 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -431,6 +431,7 @@
     private final DisplayMetrics mDisplayMetrics = new DisplayMetrics();
     private final DisplayPolicy mDisplayPolicy;
     private final DisplayRotation mDisplayRotation;
+    @Nullable final DisplayRotationCompatPolicy mDisplayRotationCompatPolicy;
     DisplayFrames mDisplayFrames;
 
     private final RemoteCallbackList<ISystemGestureExclusionListener>
@@ -1158,6 +1159,13 @@
         onDisplayChanged(this);
         updateDisplayAreaOrganizers();
 
+        mDisplayRotationCompatPolicy =
+                // Not checking DeviceConfig value here to allow enabling via DeviceConfig
+                // without the need to restart the device.
+                mWmService.mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+                            /* checkDeviceConfig */ false)
+                        ? new DisplayRotationCompatPolicy(this) : null;
+
         mInputMonitor = new InputMonitor(mWmService, this);
         mInsetsPolicy = new InsetsPolicy(mInsetsStateController, this);
         mMinSizeOfResizeableTaskDp = getMinimalTaskSizeDp();
@@ -2704,6 +2712,14 @@
             }
         }
 
+        if (mDisplayRotationCompatPolicy != null) {
+            int compatOrientation = mDisplayRotationCompatPolicy.getOrientation();
+            if (compatOrientation != SCREEN_ORIENTATION_UNSPECIFIED) {
+                mLastOrientationSource = null;
+                return compatOrientation;
+            }
+        }
+
         final int orientation = super.getOrientation();
 
         if (!handlesOrientationChangeFromDescendant(orientation)) {
@@ -3260,6 +3276,10 @@
         // on the next traversal if it's removed from RootWindowContainer child list.
         getPendingTransaction().apply();
         mWmService.mWindowPlacerLocked.requestTraversal();
+
+        if (mDisplayRotationCompatPolicy != null) {
+            mDisplayRotationCompatPolicy.dispose();
+        }
     }
 
     /** Returns true if a removal action is still being deferred. */
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index 34bdb7a..cf3a688 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -1805,6 +1805,7 @@
             final int mHalfFoldSavedRotation;
             final boolean mInHalfFoldTransition;
             final DeviceStateController.FoldState mFoldState;
+            @Nullable final String mDisplayRotationCompatPolicySummary;
 
             Record(DisplayRotation dr, int fromRotation, int toRotation) {
                 mFromRotation = fromRotation;
@@ -1839,6 +1840,10 @@
                     mInHalfFoldTransition = false;
                     mFoldState = DeviceStateController.FoldState.UNKNOWN;
                 }
+                mDisplayRotationCompatPolicySummary = dc.mDisplayRotationCompatPolicy == null
+                        ? null
+                        : dc.mDisplayRotationCompatPolicy
+                                .getSummaryForDisplayRotationHistoryRecord();
             }
 
             void dump(String prefix, PrintWriter pw) {
@@ -1861,6 +1866,9 @@
                             + " mInHalfFoldTransition=" + mInHalfFoldTransition
                             + " mFoldState=" + mFoldState);
                 }
+                if (mDisplayRotationCompatPolicySummary != null) {
+                    pw.println(prefix + mDisplayRotationCompatPolicySummary);
+                }
             }
         }
 
diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
new file mode 100644
index 0000000..18c5c3b
--- /dev/null
+++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
@@ -0,0 +1,433 @@
+/*
+ * Copyright (C) 2022 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.servertransaction.ActivityLifecycleItem.ON_PAUSE;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.content.pm.ActivityInfo.screenOrientationToString;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
+import static android.view.Display.TYPE_INTERNAL;
+
+import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION;
+import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.servertransaction.ClientTransaction;
+import android.app.servertransaction.RefreshCallbackItem;
+import android.app.servertransaction.ResumeActivityItem;
+import android.content.pm.ActivityInfo.ScreenOrientation;
+import android.content.res.Configuration;
+import android.hardware.camera2.CameraManager;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.common.ProtoLog;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Controls camera compatibility treatment that handles orientation mismatch between camera
+ * buffers and an app window for a particular display that can lead to camera issues like sideways
+ * or stretched viewfinder.
+ *
+ * <p>This includes force rotation of fixed orientation activities connected to the camera.
+ *
+ * <p>The treatment is enabled for internal displays that have {@code ignoreOrientationRequest}
+ * display setting enabled and when {@code
+ * R.bool.config_isWindowManagerCameraCompatTreatmentEnabled} is {@code true}.
+ */
+ // TODO(b/261444714): Consider moving Camera-specific logic outside of the WM Core path
+final class DisplayRotationCompatPolicy {
+
+    // Delay for updating display rotation after Camera connection is closed. Needed to avoid
+    // rotation flickering when an app is flipping between front and rear cameras or when size
+    // compat mode is restarted.
+    // TODO(b/263114289): Consider associating this delay with a specific activity so that if
+    // the new non-camera activity started on top of the camer one we can rotate faster.
+    private static final int CAMERA_CLOSED_ROTATION_UPDATE_DELAY_MS = 2000;
+    // Delay for updating display rotation after Camera connection is opened. This delay is
+    // selected to be long enough to avoid conflicts with transitions on the app's side.
+    // Using a delay < CAMERA_CLOSED_ROTATION_UPDATE_DELAY_MS to avoid flickering when an app
+    // is flipping between front and rear cameras (in case requested orientation changes at
+    // runtime at the same time) or when size compat mode is restarted.
+    private static final int CAMERA_OPENED_ROTATION_UPDATE_DELAY_MS =
+            CAMERA_CLOSED_ROTATION_UPDATE_DELAY_MS / 2;
+    // Delay for ensuring that onActivityRefreshed is always called after an activity refresh. The
+    // client process may not always report the event back to the server, such as process is
+    // crashed or got killed.
+    private static final int REFRESH_CALLBACK_TIMEOUT_MS = 2000;
+
+    private final DisplayContent mDisplayContent;
+    private final WindowManagerService mWmService;
+    private final CameraManager mCameraManager;
+    private final Handler mHandler;
+
+    // Bi-directional map between package names and active camera IDs since we need to 1) get a
+    // camera id by a package name when determining rotation; 2) get a package name by a camera id
+    // when camera connection is closed and we need to clean up our records.
+    @GuardedBy("this")
+    private final CameraIdPackageNameBiMap mCameraIdPackageBiMap = new CameraIdPackageNameBiMap();
+    @GuardedBy("this")
+    private final Set<String> mScheduledToBeRemovedCameraIdSet = new ArraySet<>();
+    @GuardedBy("this")
+    private final Set<String> mScheduledOrientationUpdateCameraIdSet = new ArraySet<>();
+
+    private final CameraManager.AvailabilityCallback mAvailabilityCallback =
+            new  CameraManager.AvailabilityCallback() {
+                @Override
+                public void onCameraOpened(@NonNull String cameraId, @NonNull String packageId) {
+                    notifyCameraOpened(cameraId, packageId);
+                }
+
+                @Override
+                public void onCameraClosed(@NonNull String cameraId) {
+                    notifyCameraClosed(cameraId);
+                }
+            };
+
+    @ScreenOrientation
+    private int mLastReportedOrientation = SCREEN_ORIENTATION_UNSET;
+
+    DisplayRotationCompatPolicy(@NonNull DisplayContent displayContent) {
+        this(displayContent, displayContent.mWmService.mH);
+    }
+
+    @VisibleForTesting
+    DisplayRotationCompatPolicy(@NonNull DisplayContent displayContent, Handler handler) {
+        // This constructor is called from DisplayContent constructor. Don't use any fields in
+        // DisplayContent here since they aren't guaranteed to be set.
+        mHandler = handler;
+        mDisplayContent = displayContent;
+        mWmService = displayContent.mWmService;
+        mCameraManager = mWmService.mContext.getSystemService(CameraManager.class);
+        mCameraManager.registerAvailabilityCallback(
+                mWmService.mContext.getMainExecutor(), mAvailabilityCallback);
+    }
+
+    void dispose() {
+        mCameraManager.unregisterAvailabilityCallback(mAvailabilityCallback);
+    }
+
+    /**
+     * Determines orientation for Camera compatibility.
+     *
+     * <p>The goal of this function is to compute a orientation which would align orientations of
+     * portrait app window and natural orientation of the device and set opposite to natural
+     * orientation for a landscape app window. 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.
+     *
+     * <p>If treatment isn't applicable returns {@link SCREEN_ORIENTATION_UNSPECIFIED}. See {@link
+     * #shouldComputeCameraCompatOrientation} for conditions enabling the treatment.
+     */
+    @ScreenOrientation
+    int getOrientation() {
+        mLastReportedOrientation = getOrientationInternal();
+        return mLastReportedOrientation;
+    }
+
+    @ScreenOrientation
+    private synchronized int getOrientationInternal() {
+        if (!isTreatmentEnabledForDisplay()) {
+            return SCREEN_ORIENTATION_UNSPECIFIED;
+        }
+        ActivityRecord topActivity = mDisplayContent.topRunningActivity(
+                /* considerKeyguardState= */ true);
+        if (!isTreatmentEnabledForActivity(topActivity)) {
+            return SCREEN_ORIENTATION_UNSPECIFIED;
+        }
+        boolean isPortraitActivity =
+                topActivity.getRequestedConfigurationOrientation() == ORIENTATION_PORTRAIT;
+        boolean isNaturalDisplayOrientationPortrait =
+                mDisplayContent.getNaturalOrientation() == ORIENTATION_PORTRAIT;
+        // Rotate portrait-only activity in the natural orientation of the displays (and in the
+        // opposite to natural orientation for landscape-only) since many apps assume that those
+        // are aligned when they compute orientation of the preview.
+        // This means that even for a landscape-only activity and a device with landscape natural
+        // orientation this would return SCREEN_ORIENTATION_PORTRAIT because an assumption that
+        // natural orientation = portrait window = portait camera is the main wrong assumption
+        // that apps make when they implement camera previews so landscape windows need be
+        // rotated in the orientation oposite to the natural one even if it's portrait.
+        // TODO(b/261475895): Consider allowing more rotations for "sensor" and "user" versions
+        // of the portrait and landscape orientation requests.
+        int orientation = (isPortraitActivity && isNaturalDisplayOrientationPortrait)
+                || (!isPortraitActivity && !isNaturalDisplayOrientationPortrait)
+                        ? SCREEN_ORIENTATION_PORTRAIT
+                        : SCREEN_ORIENTATION_LANDSCAPE;
+        ProtoLog.v(WM_DEBUG_ORIENTATION,
+                "Display id=%d is ignoring all orientation requests, camera is active "
+                        + "and the top activity is eligible for force rotation, return %s,"
+                        + "portrait activity: %b, is natural orientation portrait: %b.",
+                mDisplayContent.mDisplayId, screenOrientationToString(orientation),
+                isPortraitActivity, isNaturalDisplayOrientationPortrait);
+        return orientation;
+    }
+
+    /**
+     * "Refreshes" activity by going through "stopped -> resumed" or "paused -> resumed" cycle.
+     * 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.
+     */
+    void onActivityConfigurationChanging(ActivityRecord activity, Configuration newConfig,
+            Configuration lastReportedConfig) {
+        if (!isTreatmentEnabledForDisplay()
+                || !mWmService.mLetterboxConfiguration.isCameraCompatRefreshEnabled()
+                || !shouldRefreshActivity(activity, newConfig, lastReportedConfig)) {
+            return;
+        }
+        boolean cycleThroughStop = mWmService.mLetterboxConfiguration
+                .isCameraCompatRefreshCycleThroughStopEnabled();
+        try {
+            activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(true);
+            ProtoLog.v(WM_DEBUG_STATES,
+                    "Refershing activity for camera compatibility treatment, "
+                            + "activityRecord=%s", activity);
+            final ClientTransaction transaction = ClientTransaction.obtain(
+                    activity.app.getThread(), activity.token);
+            transaction.addCallback(
+                    RefreshCallbackItem.obtain(cycleThroughStop ? ON_STOP : ON_PAUSE));
+            transaction.setLifecycleStateRequest(ResumeActivityItem.obtain(/* isForward */ false));
+            activity.mAtmService.getLifecycleManager().scheduleTransaction(transaction);
+            mHandler.postDelayed(
+                    () -> onActivityRefreshed(activity),
+                    REFRESH_CALLBACK_TIMEOUT_MS);
+        } catch (RemoteException e) {
+            activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(false);
+        }
+    }
+
+    void onActivityRefreshed(@NonNull ActivityRecord activity) {
+        activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(false);
+    }
+
+    String getSummaryForDisplayRotationHistoryRecord() {
+        String summaryIfEnabled = "";
+        if (isTreatmentEnabledForDisplay()) {
+            ActivityRecord topActivity = mDisplayContent.topRunningActivity(
+                    /* considerKeyguardState= */ true);
+            summaryIfEnabled =
+                    " mLastReportedOrientation="
+                            + screenOrientationToString(mLastReportedOrientation)
+                    + " topActivity="
+                            + (topActivity == null ? "null" : topActivity.shortComponentName)
+                    + " isTreatmentEnabledForActivity="
+                            + isTreatmentEnabledForActivity(topActivity)
+                    + " CameraIdPackageNameBiMap="
+                            + mCameraIdPackageBiMap.getSummaryForDisplayRotationHistoryRecord();
+        }
+        return "DisplayRotationCompatPolicy{"
+                + " isTreatmentEnabledForDisplay=" + isTreatmentEnabledForDisplay()
+                + summaryIfEnabled
+                + " }";
+    }
+
+    // Refreshing only when configuration changes after rotation.
+    private boolean shouldRefreshActivity(ActivityRecord activity, Configuration newConfig,
+            Configuration lastReportedConfig) {
+        return newConfig.windowConfiguration.getDisplayRotation()
+                        != lastReportedConfig.windowConfiguration.getDisplayRotation()
+                && isTreatmentEnabledForActivity(activity);
+    }
+
+    /**
+     * Whether camera compat treatment is enabled for the display.
+     *
+     * <p>Conditions that need to be met:
+     * <ul>
+     *     <li>{@code R.bool.config_isWindowManagerCameraCompatTreatmentEnabled} is {@code true}.
+     *     <li>Setting {@code ignoreOrientationRequest} is enabled for the display.
+     *     <li>Associated {@link DisplayContent} is for internal display. See b/225928882
+     *     that tracks supporting external displays in the future.
+     * </ul>
+     */
+    private boolean isTreatmentEnabledForDisplay() {
+        return mWmService.mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+                    /* checkDeviceConfig */ true)
+                && mDisplayContent.getIgnoreOrientationRequest()
+                // TODO(b/225928882): Support camera compat rotation for external displays
+                && mDisplayContent.getDisplay().getType() == TYPE_INTERNAL;
+    }
+
+    /**
+     * Whether camera compat treatment is applicable for the given activity.
+     *
+     * <p>Conditions that need to be met:
+     * <ul>
+     *     <li>{@link #isCameraActiveForPackage} is {@code true} for the activity.
+     *     <li>The activity is in fullscreen
+     *     <li>The activity has fixed orientation but not "locked" or "nosensor" one.
+     * </ul>
+     */
+    private boolean isTreatmentEnabledForActivity(@Nullable ActivityRecord activity) {
+        return activity != null && !activity.inMultiWindowMode()
+                && activity.getRequestedConfigurationOrientation() != ORIENTATION_UNDEFINED
+                // "locked" and "nosensor" values are often used by camera apps that can't
+                // handle dynamic changes so we shouldn't force rotate them.
+                && activity.getRequestedOrientation() != SCREEN_ORIENTATION_NOSENSOR
+                && activity.getRequestedOrientation() != SCREEN_ORIENTATION_LOCKED
+                && mCameraIdPackageBiMap.containsPackageName(activity.packageName);
+    }
+
+    private synchronized void notifyCameraOpened(
+            @NonNull String cameraId, @NonNull String packageName) {
+        // If an activity is restarting or camera is flipping, the camera connection can be
+        // quickly closed and reopened.
+        mScheduledToBeRemovedCameraIdSet.remove(cameraId);
+        ProtoLog.v(WM_DEBUG_ORIENTATION,
+                "Display id=%d is notified that Camera %s is open for package %s",
+                mDisplayContent.mDisplayId, cameraId, packageName);
+        // Some apps can’t handle configuration changes coming at the same time with Camera setup
+        // so delaying orientation update to accomadate for that.
+        mScheduledOrientationUpdateCameraIdSet.add(cameraId);
+        mHandler.postDelayed(
+                () ->  delayedUpdateOrientationWithWmLock(cameraId, packageName),
+                CAMERA_OPENED_ROTATION_UPDATE_DELAY_MS);
+    }
+
+    private void updateOrientationWithWmLock() {
+        synchronized (mWmService.mGlobalLock) {
+            mDisplayContent.updateOrientation();
+        }
+    }
+
+    private void delayedUpdateOrientationWithWmLock(
+            @NonNull String cameraId, @NonNull String packageName) {
+        synchronized (this) {
+            if (!mScheduledOrientationUpdateCameraIdSet.remove(cameraId)) {
+                // Orientation update has happened already or was cancelled because
+                // camera was closed.
+                return;
+            }
+            mCameraIdPackageBiMap.put(packageName, cameraId);
+        }
+        updateOrientationWithWmLock();
+    }
+
+    private synchronized void notifyCameraClosed(@NonNull String cameraId) {
+        ProtoLog.v(WM_DEBUG_ORIENTATION,
+                "Display id=%d is notified that Camera %s is closed, scheduling rotation update.",
+                mDisplayContent.mDisplayId, cameraId);
+        mScheduledToBeRemovedCameraIdSet.add(cameraId);
+        // No need to update orientation for this camera if it's already closed.
+        mScheduledOrientationUpdateCameraIdSet.remove(cameraId);
+        scheduleRemoveCameraId(cameraId);
+    }
+
+    // Delay is needed to avoid rotation flickering when an app is flipping between front and
+    // rear cameras, when size compat mode is restarted or activity is being refreshed.
+    private void scheduleRemoveCameraId(@NonNull String cameraId) {
+        mHandler.postDelayed(
+                () -> removeCameraId(cameraId),
+                CAMERA_CLOSED_ROTATION_UPDATE_DELAY_MS);
+    }
+
+    private void removeCameraId(String cameraId) {
+        synchronized (this) {
+            if (!mScheduledToBeRemovedCameraIdSet.remove(cameraId)) {
+                // Already reconnected to this camera, no need to clean up.
+                return;
+            }
+            if (isActivityForCameraIdRefreshing(cameraId)) {
+                ProtoLog.v(WM_DEBUG_ORIENTATION,
+                        "Display id=%d is notified that Camera %s is closed but activity is"
+                                + " still refreshing. Rescheduling an update.",
+                        mDisplayContent.mDisplayId, cameraId);
+                mScheduledToBeRemovedCameraIdSet.add(cameraId);
+                scheduleRemoveCameraId(cameraId);
+                return;
+            }
+            mCameraIdPackageBiMap.removeCameraId(cameraId);
+        }
+        ProtoLog.v(WM_DEBUG_ORIENTATION,
+                "Display id=%d is notified that Camera %s is closed, updating rotation.",
+                mDisplayContent.mDisplayId, cameraId);
+        updateOrientationWithWmLock();
+    }
+
+    private boolean isActivityForCameraIdRefreshing(String cameraId) {
+        ActivityRecord topActivity = mDisplayContent.topRunningActivity(
+                /* considerKeyguardState= */ true);
+        if (!isTreatmentEnabledForActivity(topActivity)) {
+            return false;
+        }
+        String activeCameraId = mCameraIdPackageBiMap.getCameraId(topActivity.packageName);
+        if (activeCameraId == null || activeCameraId != cameraId) {
+            return false;
+        }
+        return topActivity.mLetterboxUiController.isRefreshAfterRotationRequested();
+    }
+
+    private static class CameraIdPackageNameBiMap {
+
+        private final Map<String, String> mPackageToCameraIdMap = new ArrayMap<>();
+        private final Map<String, String> mCameraIdToPackageMap = new ArrayMap<>();
+
+        void put(String packageName, String cameraId) {
+            // Always using the last connected camera ID for the package even for the concurrent
+            // camera use case since we can't guess which camera is more important anyway.
+            removePackageName(packageName);
+            removeCameraId(cameraId);
+            mPackageToCameraIdMap.put(packageName, cameraId);
+            mCameraIdToPackageMap.put(cameraId, packageName);
+        }
+
+        boolean containsPackageName(String packageName) {
+            return mPackageToCameraIdMap.containsKey(packageName);
+        }
+
+        @Nullable
+        String getCameraId(String packageName) {
+            return mPackageToCameraIdMap.get(packageName);
+        }
+
+        void removeCameraId(String cameraId) {
+            String packageName = mCameraIdToPackageMap.get(cameraId);
+            if (packageName == null) {
+                return;
+            }
+            mPackageToCameraIdMap.remove(packageName, cameraId);
+            mCameraIdToPackageMap.remove(cameraId, packageName);
+        }
+
+        String getSummaryForDisplayRotationHistoryRecord() {
+            return "{ mPackageToCameraIdMap=" + mPackageToCameraIdMap + " }";
+        }
+
+        private void removePackageName(String packageName) {
+            String cameraId = mPackageToCameraIdMap.get(packageName);
+            if (cameraId == null) {
+                return;
+            }
+            mPackageToCameraIdMap.remove(packageName, cameraId);
+            mCameraIdToPackageMap.remove(cameraId, packageName);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index 3eca364..a7bf595f 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -191,6 +191,20 @@
     // Allows to enable letterboxing strategy for translucent activities ignoring flags.
     private boolean mTranslucentLetterboxingOverrideEnabled;
 
+    // Whether camera compatibility treatment is enabled.
+    // See DisplayRotationCompatPolicy for context.
+    private final boolean mIsCameraCompatTreatmentEnabled;
+
+    // Whether activity "refresh" in camera compatibility treatment is enabled.
+    // See RefreshCallbackItem for context.
+    private boolean mIsCameraCompatTreatmentRefreshEnabled = true;
+
+    // Whether activity "refresh" in camera compatibility treatment should happen using the
+    // "stopped -> resumed" cycle rather than "paused -> resumed" cycle. Using "stop -> resumed"
+    // cycle by default due to higher success rate confirmed with app compatibility testing.
+    // See RefreshCallbackItem for context.
+    private boolean mIsCameraCompatRefreshCycleThroughStopEnabled = true;
+
     LetterboxConfiguration(Context systemUiContext) {
         this(systemUiContext, new LetterboxConfigurationPersister(systemUiContext,
                 () -> readLetterboxHorizontalReachabilityPositionFromConfig(systemUiContext,
@@ -241,6 +255,8 @@
                 R.bool.config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled);
         mTranslucentLetterboxingEnabled = mContext.getResources().getBoolean(
                 R.bool.config_letterboxIsEnabledForTranslucentActivities);
+        mIsCameraCompatTreatmentEnabled = mContext.getResources().getBoolean(
+                R.bool.config_isWindowManagerCameraCompatTreatmentEnabled);
         mLetterboxConfigurationPersister = letterboxConfigurationPersister;
         mLetterboxConfigurationPersister.start();
     }
@@ -947,9 +963,65 @@
                 isDeviceInTabletopMode, nextVerticalPosition);
     }
 
-    // TODO(b/262378106): Cache runtime flag and implement DeviceConfig.OnPropertiesChangedListener
+    // TODO(b/262378106): Cache a runtime flag and implement
+    // DeviceConfig.OnPropertiesChangedListener
     static boolean isTranslucentLetterboxingAllowed() {
         return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
                 "enable_translucent_activity_letterbox", false);
     }
+
+    /** Whether camera compatibility treatment is enabled. */
+    boolean isCameraCompatTreatmentEnabled(boolean checkDeviceConfig) {
+        return mIsCameraCompatTreatmentEnabled
+                && (!checkDeviceConfig || isCameraCompatTreatmentAllowed());
+    }
+
+    // TODO(b/262977416): Cache a runtime flag and implement
+    // DeviceConfig.OnPropertiesChangedListener
+    private static boolean isCameraCompatTreatmentAllowed() {
+        return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
+                "enable_camera_compat_treatment", false);
+    }
+
+    /** Whether camera compatibility refresh is enabled. */
+    boolean isCameraCompatRefreshEnabled() {
+        return mIsCameraCompatTreatmentRefreshEnabled;
+    }
+
+    /** Overrides whether camera compatibility treatment is enabled. */
+    void setCameraCompatRefreshEnabled(boolean enabled) {
+        mIsCameraCompatTreatmentRefreshEnabled = enabled;
+    }
+
+    /**
+     * Resets whether camera compatibility treatment is enabled to {@code true}.
+     */
+    void resetCameraCompatRefreshEnabled() {
+        mIsCameraCompatTreatmentRefreshEnabled = true;
+    }
+
+    /**
+     * Whether activity "refresh" in camera compatibility treatment should happen using the
+     * "stopped -> resumed" cycle rather than "paused -> resumed" cycle.
+     */
+    boolean isCameraCompatRefreshCycleThroughStopEnabled() {
+        return mIsCameraCompatRefreshCycleThroughStopEnabled;
+    }
+
+    /**
+     * Overrides whether activity "refresh" in camera compatibility treatment should happen using
+     * "stopped -> resumed" cycle rather than "paused -> resumed" cycle.
+     */
+    void setCameraCompatRefreshCycleThroughStopEnabled(boolean enabled) {
+        mIsCameraCompatRefreshCycleThroughStopEnabled = enabled;
+    }
+
+    /**
+     * Resets  whether activity "refresh" in camera compatibility treatment should happen using
+     * "stopped -> resumed" cycle rather than "paused -> resumed" cycle to {@code true}.
+     */
+    void resetCameraCompatRefreshCycleThroughStopEnabled() {
+        mIsCameraCompatRefreshCycleThroughStopEnabled = true;
+    }
+
 }
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index 9cb94c6..fd7e082 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -80,6 +80,7 @@
 // SizeCompatTests and LetterboxTests but not all.
 // TODO(b/185264020): Consider making LetterboxUiController applicable to any level of the
 // hierarchy in addition to ActivityRecord (Task, DisplayArea, ...).
+// TODO(b/263021211): Consider renaming to more generic CompatUIController.
 final class LetterboxUiController {
 
     private static final String TAG = TAG_WITH_CLASS_NAME ? "LetterboxUiController" : TAG_ATM;
@@ -125,6 +126,11 @@
     @Nullable
     private Letterbox mLetterbox;
 
+    // Whether activity "refresh" was requested but not finished in
+    // ActivityRecord#activityResumedLocked following the camera compat force rotation in
+    // DisplayRotationCompatPolicy.
+    private boolean mIsRefreshAfterRotationRequested;
+
     LetterboxUiController(WindowManagerService wmService, ActivityRecord activityRecord) {
         mLetterboxConfiguration = wmService.mLetterboxConfiguration;
         // Given activityRecord may not be fully constructed since LetterboxUiController
@@ -147,6 +153,18 @@
         }
     }
 
+    /**
+     * Whether activity "refresh" was requested but not finished in {@link #activityResumedLocked}
+     * following the camera compat force rotation in {@link DisplayRotationCompatPolicy}.
+     */
+    boolean isRefreshAfterRotationRequested() {
+        return mIsRefreshAfterRotationRequested;
+    }
+
+    void setIsRefreshAfterRotationRequested(boolean isRequested) {
+        mIsRefreshAfterRotationRequested = isRequested;
+    }
+
     boolean hasWallpaperBackgroundForLetterbox() {
         return mShowWallpaperForLetterboxBackground;
     }
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index 85aa942..aef6d1d 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -53,6 +53,7 @@
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.function.Consumer;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.zip.ZipEntry;
@@ -806,54 +807,6 @@
         return 0;
     }
 
-    private int runSetLetterboxIsHorizontalReachabilityEnabled(PrintWriter pw)
-            throws RemoteException {
-        String arg = getNextArg();
-        final boolean enabled;
-        switch (arg) {
-            case "true":
-            case "1":
-                enabled = true;
-                break;
-            case "false":
-            case "0":
-                enabled = false;
-                break;
-            default:
-                getErrPrintWriter().println("Error: expected true, 1, false, 0, but got " + arg);
-                return -1;
-        }
-
-        synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(enabled);
-        }
-        return 0;
-    }
-
-    private int runSetLetterboxIsVerticalReachabilityEnabled(PrintWriter pw)
-            throws RemoteException {
-        String arg = getNextArg();
-        final boolean enabled;
-        switch (arg) {
-            case "true":
-            case "1":
-                enabled = true;
-                break;
-            case "false":
-            case "0":
-                enabled = false;
-                break;
-            default:
-                getErrPrintWriter().println("Error: expected true, 1, false, 0, but got " + arg);
-                return -1;
-        }
-
-        synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setIsVerticalReachabilityEnabled(enabled);
-        }
-        return 0;
-    }
-
     private int runSetLetterboxDefaultPositionForHorizontalReachability(PrintWriter pw)
             throws RemoteException {
         @LetterboxHorizontalReachabilityPosition final int position;
@@ -916,32 +869,13 @@
         return 0;
     }
 
-    private int runSetLetterboxIsEducationEnabled(PrintWriter pw) throws RemoteException {
-        String arg = getNextArg();
-        final boolean enabled;
-        switch (arg) {
-            case "true":
-            case "1":
-                enabled = true;
-                break;
-            case "false":
-            case "0":
-                enabled = false;
-                break;
-            default:
-                getErrPrintWriter().println("Error: expected true, 1, false, 0, but got " + arg);
-                return -1;
-        }
-
-        synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setIsEducationEnabled(enabled);
-        }
-        return 0;
-    }
-
-    private int runSetLetterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled(PrintWriter pw)
+    private int runSetBooleanFlag(PrintWriter pw, Consumer<Boolean> setter)
             throws RemoteException {
         String arg = getNextArg();
+        if (arg == null) {
+            getErrPrintWriter().println("Error: expected true, 1, false, 0, but got empty input.");
+            return -1;
+        }
         final boolean enabled;
         switch (arg) {
             case "true":
@@ -958,30 +892,7 @@
         }
 
         synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setIsSplitScreenAspectRatioForUnresizableAppsEnabled(enabled);
-        }
-        return 0;
-    }
-
-    private int runSetTranslucentLetterboxingEnabled(PrintWriter pw) {
-        String arg = getNextArg();
-        final boolean enabled;
-        switch (arg) {
-            case "true":
-            case "1":
-                enabled = true;
-                break;
-            case "false":
-            case "0":
-                enabled = false;
-                break;
-            default:
-                getErrPrintWriter().println("Error: expected true, 1, false, 0, but got " + arg);
-                return -1;
-        }
-
-        synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setTranslucentLetterboxingOverrideEnabled(enabled);
+            setter.accept(enabled);
         }
         return 0;
     }
@@ -1024,10 +935,12 @@
                     runSetLetterboxVerticalPositionMultiplier(pw);
                     break;
                 case "--isHorizontalReachabilityEnabled":
-                    runSetLetterboxIsHorizontalReachabilityEnabled(pw);
+                    runSetBooleanFlag(pw, mLetterboxConfiguration
+                            ::setIsHorizontalReachabilityEnabled);
                     break;
                 case "--isVerticalReachabilityEnabled":
-                    runSetLetterboxIsVerticalReachabilityEnabled(pw);
+                    runSetBooleanFlag(pw, mLetterboxConfiguration
+                            ::setIsVerticalReachabilityEnabled);
                     break;
                 case "--defaultPositionForHorizontalReachability":
                     runSetLetterboxDefaultPositionForHorizontalReachability(pw);
@@ -1036,13 +949,23 @@
                     runSetLetterboxDefaultPositionForVerticalReachability(pw);
                     break;
                 case "--isEducationEnabled":
-                    runSetLetterboxIsEducationEnabled(pw);
+                    runSetBooleanFlag(pw, mLetterboxConfiguration::setIsEducationEnabled);
                     break;
                 case "--isSplitScreenAspectRatioForUnresizableAppsEnabled":
-                    runSetLetterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled(pw);
+                    runSetBooleanFlag(pw, mLetterboxConfiguration
+                            ::setIsSplitScreenAspectRatioForUnresizableAppsEnabled);
                     break;
                 case "--isTranslucentLetterboxingEnabled":
-                    runSetTranslucentLetterboxingEnabled(pw);
+                    runSetBooleanFlag(pw, mLetterboxConfiguration
+                            ::setTranslucentLetterboxingOverrideEnabled);
+                    break;
+                case "--isCameraCompatRefreshEnabled":
+                    runSetBooleanFlag(pw, enabled -> mLetterboxConfiguration
+                            .setCameraCompatRefreshEnabled(enabled));
+                    break;
+                case "--isCameraCompatRefreshCycleThroughStopEnabled":
+                    runSetBooleanFlag(pw, enabled -> mLetterboxConfiguration
+                            .setCameraCompatRefreshCycleThroughStopEnabled(enabled));
                     break;
                 default:
                     getErrPrintWriter().println(
@@ -1089,27 +1012,34 @@
                         mLetterboxConfiguration.resetLetterboxVerticalPositionMultiplier();
                         break;
                     case "isHorizontalReachabilityEnabled":
-                        mLetterboxConfiguration.getIsHorizontalReachabilityEnabled();
+                        mLetterboxConfiguration.resetIsHorizontalReachabilityEnabled();
                         break;
                     case "isVerticalReachabilityEnabled":
-                        mLetterboxConfiguration.getIsVerticalReachabilityEnabled();
+                        mLetterboxConfiguration.resetIsVerticalReachabilityEnabled();
                         break;
                     case "defaultPositionForHorizontalReachability":
-                        mLetterboxConfiguration.getDefaultPositionForHorizontalReachability();
+                        mLetterboxConfiguration.resetDefaultPositionForHorizontalReachability();
                         break;
                     case "defaultPositionForVerticalReachability":
-                        mLetterboxConfiguration.getDefaultPositionForVerticalReachability();
+                        mLetterboxConfiguration.resetDefaultPositionForVerticalReachability();
                         break;
                     case "isEducationEnabled":
-                        mLetterboxConfiguration.getIsEducationEnabled();
+                        mLetterboxConfiguration.resetIsEducationEnabled();
                         break;
                     case "isSplitScreenAspectRatioForUnresizableAppsEnabled":
                         mLetterboxConfiguration
-                                .getIsSplitScreenAspectRatioForUnresizableAppsEnabled();
+                                .resetIsSplitScreenAspectRatioForUnresizableAppsEnabled();
                         break;
                     case "isTranslucentLetterboxingEnabled":
                         mLetterboxConfiguration.resetTranslucentLetterboxingEnabled();
                         break;
+                    case "isCameraCompatRefreshEnabled":
+                        mLetterboxConfiguration.resetCameraCompatRefreshEnabled();
+                        break;
+                    case "isCameraCompatRefreshCycleThroughStopEnabled":
+                        mLetterboxConfiguration
+                                .resetCameraCompatRefreshCycleThroughStopEnabled();
+                        break;
                     default:
                         getErrPrintWriter().println(
                                 "Error: Unrecognized letterbox style option: " + arg);
@@ -1211,6 +1141,8 @@
             mLetterboxConfiguration.resetIsEducationEnabled();
             mLetterboxConfiguration.resetIsSplitScreenAspectRatioForUnresizableAppsEnabled();
             mLetterboxConfiguration.resetTranslucentLetterboxingEnabled();
+            mLetterboxConfiguration.resetCameraCompatRefreshEnabled();
+            mLetterboxConfiguration.resetCameraCompatRefreshCycleThroughStopEnabled();
         }
     }
 
@@ -1255,6 +1187,12 @@
             pw.println("Is using split screen aspect ratio as aspect ratio for unresizable apps: "
                     + mLetterboxConfiguration
                             .getIsSplitScreenAspectRatioForUnresizableAppsEnabled());
+
+            pw.println("    Is activity \"refresh\" in camera compatibility treatment enabled: "
+                    + mLetterboxConfiguration.isCameraCompatRefreshEnabled());
+            pw.println("    Refresh using \"stopped -> resumed\" cycle: "
+                    + mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled());
+
             pw.println("Background type: "
                     + LetterboxConfiguration.letterboxBackgroundTypeToString(
                             mLetterboxConfiguration.getLetterboxBackgroundType()));
@@ -1464,7 +1402,12 @@
         pw.println("        unresizable apps.");
         pw.println("      --isTranslucentLetterboxingEnabled [true|1|false|0]");
         pw.println("        Whether letterboxing for translucent activities is enabled.");
-
+        pw.println("      --isCameraCompatRefreshEnabled [true|1|false|0]");
+        pw.println("        Whether camera compatibility refresh is enabled.");
+        pw.println("      --isCameraCompatRefreshCycleThroughStopEnabled [true|1|false|0]");
+        pw.println("        Whether activity \"refresh\" in camera compatibility treatment should");
+        pw.println("        happen using the \"stopped -> resumed\" cycle rather than");
+        pw.println("        \"paused -> resumed\" cycle.");
         pw.println("  reset-letterbox-style [aspectRatio|cornerRadius|backgroundType");
         pw.println("      |backgroundColor|wallpaperBlurRadius|wallpaperDarkScrimAlpha");
         pw.println("      |horizontalPositionMultiplier|verticalPositionMultiplier");
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
new file mode 100644
index 0000000..d1234e3
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
@@ -0,0 +1,428 @@
+/*
+ * Copyright (C) 2022 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.servertransaction.ActivityLifecycleItem.ON_PAUSE;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
+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 android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_90;
+
+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 org.junit.Assert.assertEquals;
+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.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.servertransaction.ClientTransaction;
+import android.app.servertransaction.RefreshCallbackItem;
+import android.app.servertransaction.ResumeActivityItem;
+import android.content.ComponentName;
+import android.content.pm.ActivityInfo.ScreenOrientation;
+import android.content.res.Configuration;
+import android.content.res.Configuration.Orientation;
+import android.hardware.camera2.CameraManager;
+import android.os.Handler;
+import android.platform.test.annotations.Presubmit;
+import android.view.Display;
+import android.view.Surface.Rotation;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Tests for {@link DisplayRotationCompatPolicy}.
+ *
+ * Build/Install/Run:
+ *  atest WmTests:DisplayRotationCompatPolicyTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public final class DisplayRotationCompatPolicyTests extends WindowTestsBase {
+
+    private static final String TEST_PACKAGE_1 = "com.test.package.one";
+    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 DisplayRotationCompatPolicy mDisplayRotationCompatPolicy;
+    private CameraManager.AvailabilityCallback mCameraAvailabilityCallback;
+
+    private ActivityRecord mActivity;
+    private Task mTask;
+
+    @Before
+    public void setUp() throws Exception {
+        mLetterboxConfiguration = mDisplayContent.mWmService.mLetterboxConfiguration;
+        spyOn(mLetterboxConfiguration);
+        when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+                    /* checkDeviceConfig */ anyBoolean()))
+                .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));
+
+        spyOn(mContext);
+        when(mContext.getSystemService(CameraManager.class)).thenReturn(mMockCameraManager);
+
+        spyOn(mDisplayContent);
+
+        mDisplayContent.setIgnoreOrientationRequest(true);
+
+        mMockHandler = mock(Handler.class);
+
+        when(mMockHandler.postDelayed(any(Runnable.class), anyLong())).thenAnswer(
+                invocation -> {
+                    ((Runnable) invocation.getArgument(0)).run();
+                    return null;
+                });
+        mDisplayRotationCompatPolicy = new DisplayRotationCompatPolicy(
+                mDisplayContent, mMockHandler);
+    }
+
+    @Test
+    public void testTreatmentNotEnabled_noForceRotationOrRefresh() throws Exception {
+        when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+                    /* checkDeviceConfig */ anyBoolean()))
+                .thenReturn(false);
+
+        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+        assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
+                SCREEN_ORIENTATION_UNSPECIFIED);
+
+        assertNoForceRotationOrRefresh();
+    }
+
+    @Test
+    public void testTreatmentDisabledViaDeviceConfig_noForceRotationOrRefresh() throws Exception {
+        when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+                    /* checkDeviceConfig */ true))
+                .thenReturn(false);
+
+        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+        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);
+        mActivity.getTask().reparent(organizer.mPrimary, WindowContainer.POSITION_TOP,
+                false /* moveParents */, "test" /* reason */);
+
+        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+        assertTrue(mActivity.inMultiWindowMode());
+        assertNoForceRotationOrRefresh();
+    }
+
+    @Test
+    public void testOrientationUnspecified_noForceRotationOrRefresh() throws Exception {
+        configureActivity(SCREEN_ORIENTATION_UNSPECIFIED);
+
+        assertNoForceRotationOrRefresh();
+    }
+
+    @Test
+    public void testOrientationLocked_noForceRotationOrRefresh() throws Exception {
+        configureActivity(SCREEN_ORIENTATION_LOCKED);
+
+        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+        assertNoForceRotationOrRefresh();
+    }
+
+    @Test
+    public void testOrientationNoSensor_noForceRotationOrRefresh() throws Exception {
+        configureActivity(SCREEN_ORIENTATION_NOSENSOR);
+
+        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+        assertNoForceRotationOrRefresh();
+    }
+
+    @Test
+    public void testIgnoreOrientationRequestIsFalse_noForceRotationOrRefresh() throws Exception {
+        mDisplayContent.setIgnoreOrientationRequest(false);
+
+        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+        assertNoForceRotationOrRefresh();
+    }
+
+    @Test
+    public void testDisplayNotInternal_noForceRotationOrRefresh() throws Exception {
+        Display display = mDisplayContent.getDisplay();
+        spyOn(display);
+
+        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+        when(display.getType()).thenReturn(Display.TYPE_EXTERNAL);
+        assertNoForceRotationOrRefresh();
+
+        when(display.getType()).thenReturn(Display.TYPE_WIFI);
+        assertNoForceRotationOrRefresh();
+
+        when(display.getType()).thenReturn(Display.TYPE_OVERLAY);
+        assertNoForceRotationOrRefresh();
+
+        when(display.getType()).thenReturn(Display.TYPE_VIRTUAL);
+        assertNoForceRotationOrRefresh();
+    }
+
+    @Test
+    public void testNoCameraConnection_noForceRotationOrRefresh() throws Exception {
+        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+        assertNoForceRotationOrRefresh();
+    }
+
+    @Test
+    public void testCameraReconnected_forceRotationAndRefresh() 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, /* isDisplayRotationChanging */ true);
+
+        assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
+                SCREEN_ORIENTATION_PORTRAIT);
+        assertActivityRefreshRequested(/* refreshRequested */ true);
+    }
+
+    @Test
+    public void testReconnectedToDifferentCamera_forceRotationAndRefresh() 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, /* isDisplayRotationChanging */ true);
+
+        assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
+                SCREEN_ORIENTATION_PORTRAIT);
+        assertActivityRefreshRequested(/* refreshRequested */ true);
+    }
+
+    @Test
+    public void testGetOrientation_cameraConnectionClosed_returnUnspecified() {
+        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+        assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
+                SCREEN_ORIENTATION_PORTRAIT);
+
+        mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1);
+
+        assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
+                SCREEN_ORIENTATION_UNSPECIFIED);
+    }
+
+    @Test
+    public void testCameraOpenedForDifferentPackage_noForceRotationOrRefresh() throws Exception {
+        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_2);
+
+        assertNoForceRotationOrRefresh();
+    }
+
+    @Test
+    public void testGetOrientation_portraitActivity_portraitNaturalOrientation_returnPortrait() {
+        testGetOrientationForActivityAndNaturalOrientations(
+                /* activityOrientation */ SCREEN_ORIENTATION_PORTRAIT,
+                /* naturalOrientation */ ORIENTATION_PORTRAIT,
+                /* expectedOrientation */ SCREEN_ORIENTATION_PORTRAIT);
+    }
+
+    @Test
+    public void testGetOrientation_portraitActivity_landscapeNaturalOrientation_returnLandscape() {
+        testGetOrientationForActivityAndNaturalOrientations(
+                /* activityOrientation */ SCREEN_ORIENTATION_PORTRAIT,
+                /* naturalOrientation */ ORIENTATION_LANDSCAPE,
+                /* expectedOrientation */ SCREEN_ORIENTATION_LANDSCAPE);
+    }
+
+    @Test
+    public void testGetOrientation_landscapeActivity_portraitNaturalOrientation_returnLandscape() {
+        testGetOrientationForActivityAndNaturalOrientations(
+                /* activityOrientation */ SCREEN_ORIENTATION_LANDSCAPE,
+                /* naturalOrientation */ ORIENTATION_PORTRAIT,
+                /* expectedOrientation */ SCREEN_ORIENTATION_LANDSCAPE);
+    }
+
+    @Test
+    public void testGetOrientation_landscapeActivity_landscapeNaturalOrientation_returnPortrait() {
+        testGetOrientationForActivityAndNaturalOrientations(
+                /* activityOrientation */ SCREEN_ORIENTATION_LANDSCAPE,
+                /* naturalOrientation */ ORIENTATION_LANDSCAPE,
+                /* expectedOrientation */ SCREEN_ORIENTATION_PORTRAIT);
+    }
+
+    private void testGetOrientationForActivityAndNaturalOrientations(
+            @ScreenOrientation int activityOrientation,
+            @Orientation int naturalOrientation,
+            @ScreenOrientation int expectedOrientation) {
+        configureActivityAndDisplay(activityOrientation, naturalOrientation);
+
+        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+        assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
+                expectedOrientation);
+    }
+
+    @Test
+    public void testOnActivityConfigurationChanging_refreshDisabled_noRefresh() throws Exception {
+        when(mLetterboxConfiguration.isCameraCompatRefreshEnabled()).thenReturn(false);
+
+        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+        callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true);
+
+        assertActivityRefreshRequested(/* refreshRequested */ false);
+    }
+
+    @Test
+    public void testOnActivityConfigurationChanging_displayRotationNotChanging_noRefresh()
+            throws Exception {
+        configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+        callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ false);
+
+        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, /* isDisplayRotationChanging */ true);
+
+        assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false);
+    }
+
+    private void configureActivity(@ScreenOrientation int activityOrientation) {
+        configureActivityAndDisplay(activityOrientation, ORIENTATION_PORTRAIT);
+    }
+
+    private void configureActivityAndDisplay(@ScreenOrientation int activityOrientation,
+            @Orientation int naturalOrientation) {
+
+        mTask = new TaskBuilder(mSupervisor)
+                .setDisplay(mDisplayContent)
+                .build();
+
+        mActivity = new ActivityBuilder(mAtm)
+                .setComponent(new ComponentName(TEST_PACKAGE_1, ".TestActivity"))
+                .setScreenOrientation(activityOrientation)
+                .setTask(mTask)
+                .build();
+
+        spyOn(mActivity.mAtmService.getLifecycleManager());
+        spyOn(mActivity.mLetterboxUiController);
+
+        doReturn(mActivity).when(mDisplayContent).topRunningActivity(anyBoolean());
+        doReturn(naturalOrientation).when(mDisplayContent).getNaturalOrientation();
+    }
+
+    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))
+                .setIsRefreshAfterRotationRequested(true);
+
+        final ClientTransaction transaction = ClientTransaction.obtain(
+                mActivity.app.getThread(), mActivity.token);
+        transaction.addCallback(RefreshCallbackItem.obtain(cycleThroughStop ? ON_STOP : ON_PAUSE));
+        transaction.setLifecycleStateRequest(ResumeActivityItem.obtain(/* isForward */ false));
+
+        verify(mActivity.mAtmService.getLifecycleManager(), times(refreshRequested ? 1 : 0))
+                .scheduleTransaction(eq(transaction));
+    }
+
+    private void assertNoForceRotationOrRefresh() throws Exception {
+        callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true);
+
+        assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
+                SCREEN_ORIENTATION_UNSPECIFIED);
+        assertActivityRefreshRequested(/* refreshRequested */ false);
+    }
+
+    private void callOnActivityConfigurationChanging(
+            ActivityRecord activity, boolean isDisplayRotationChanging) {
+        mDisplayRotationCompatPolicy.onActivityConfigurationChanging(activity,
+                /* newConfig */ createConfigurationWithDisplayRotation(ROTATION_0),
+                /* newConfig */ createConfigurationWithDisplayRotation(
+                        isDisplayRotationChanging ? ROTATION_90 : ROTATION_0));
+    }
+
+    private static Configuration createConfigurationWithDisplayRotation(@Rotation int rotation) {
+        final Configuration config = new Configuration();
+        config.windowConfiguration.setDisplayRotation(rotation);
+        return config;
+    }
+}