Merge changes from topic "camera-compat-force-rotation" into tm-qpr-dev am: 13459ada3a

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/19694267

Change-Id: I5640e4afc438a51b81919d5ed0b649fb35ce73b8
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
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 f2b773e..9385e87 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/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index f55a91e..f47d9c6 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -487,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",
@@ -625,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",
@@ -4225,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/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 c97d7a9..36f86d1 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -431,7 +431,7 @@
     private final DisplayMetrics mDisplayMetrics = new DisplayMetrics();
     private final DisplayPolicy mDisplayPolicy;
     private final DisplayRotation mDisplayRotation;
-    @Nullable private final DisplayRotationCompatPolicy mDisplayRotationCompatPolicy;
+    @Nullable final DisplayRotationCompatPolicy mDisplayRotationCompatPolicy;
     DisplayFrames mDisplayFrames;
 
     private final RemoteCallbackList<ISystemGestureExclusionListener>
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
index 0d3f784..18c5c3b 100644
--- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
@@ -16,10 +16,13 @@
 
 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;
@@ -27,12 +30,18 @@
 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;
 
@@ -65,11 +74,15 @@
     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 half CAMERA_CLOSED_ROTATION_UPDATE_DELAY_MS to avoid flickering when an app
+    // 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;
@@ -99,6 +112,9 @@
                 }
             };
 
+    @ScreenOrientation
+    private int mLastReportedOrientation = SCREEN_ORIENTATION_UNSET;
+
     DisplayRotationCompatPolicy(@NonNull DisplayContent displayContent) {
         this(displayContent, displayContent.mWmService.mH);
     }
@@ -132,7 +148,13 @@
      * #shouldComputeCameraCompatOrientation} for conditions enabling the treatment.
      */
     @ScreenOrientation
-    synchronized int getOrientation() {
+    int getOrientation() {
+        mLastReportedOrientation = getOrientationInternal();
+        return mLastReportedOrientation;
+    }
+
+    @ScreenOrientation
+    private synchronized int getOrientationInternal() {
         if (!isTreatmentEnabledForDisplay()) {
             return SCREEN_ORIENTATION_UNSPECIFIED;
         }
@@ -169,6 +191,73 @@
     }
 
     /**
+     * "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:
@@ -221,8 +310,6 @@
         mHandler.postDelayed(
                 () ->  delayedUpdateOrientationWithWmLock(cameraId, packageName),
                 CAMERA_OPENED_ROTATION_UPDATE_DELAY_MS);
-        // TODO(b/218352945): Restart activity after forced rotation to avoid issues cased by
-        // in-app caching of pre-rotation display / camera properties.
     }
 
     private void updateOrientationWithWmLock() {
@@ -251,8 +338,12 @@
         mScheduledToBeRemovedCameraIdSet.add(cameraId);
         // No need to update orientation for this camera if it's already closed.
         mScheduledOrientationUpdateCameraIdSet.remove(cameraId);
-        // Delay is needed to avoid rotation flickering when an app is flipping between front and
-        // rear cameras or when size compat mode is restarted.
+        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);
@@ -264,6 +355,15 @@
                 // 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,
@@ -272,6 +372,19 @@
         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<>();
@@ -290,6 +403,11 @@
             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) {
@@ -299,6 +417,10 @@
             mCameraIdToPackageMap.remove(cameraId, packageName);
         }
 
+        String getSummaryForDisplayRotationHistoryRecord() {
+            return "{ mPackageToCameraIdMap=" + mPackageToCameraIdMap + " }";
+        }
+
         private void removePackageName(String packageName) {
             String cameraId = mPackageToCameraIdMap.get(packageName);
             if (cameraId == null) {
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index 793a352..a7bf595f 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -195,6 +195,16 @@
     // 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,
@@ -973,4 +983,45 @@
                 "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 554f271..aef6d1d 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -959,6 +959,14 @@
                     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(
                             "Error: Unrecognized letterbox style option: " + arg);
@@ -1025,6 +1033,13 @@
                     case "isTranslucentLetterboxingEnabled":
                         mLetterboxConfiguration.resetTranslucentLetterboxingEnabled();
                         break;
+                    case "isCameraCompatRefreshEnabled":
+                        mLetterboxConfiguration.resetCameraCompatRefreshEnabled();
+                        break;
+                    case "isCameraCompatRefreshCycleThroughStopEnabled":
+                        mLetterboxConfiguration
+                                .resetCameraCompatRefreshCycleThroughStopEnabled();
+                        break;
                     default:
                         getErrPrintWriter().println(
                                 "Error: Unrecognized letterbox style option: " + arg);
@@ -1126,6 +1141,8 @@
             mLetterboxConfiguration.resetIsEducationEnabled();
             mLetterboxConfiguration.resetIsSplitScreenAspectRatioForUnresizableAppsEnabled();
             mLetterboxConfiguration.resetTranslucentLetterboxingEnabled();
+            mLetterboxConfiguration.resetCameraCompatRefreshEnabled();
+            mLetterboxConfiguration.resetCameraCompatRefreshCycleThroughStopEnabled();
         }
     }
 
@@ -1171,6 +1188,11 @@
                     + 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()));
@@ -1380,6 +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
index d6621505..d1234e3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
@@ -16,6 +16,8 @@
 
 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;
@@ -23,6 +25,8 @@
 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;
@@ -34,15 +38,23 @@
 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;
 
@@ -85,6 +97,10 @@
         when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
                     /* checkDeviceConfig */ anyBoolean()))
                 .thenReturn(true);
+        when(mLetterboxConfiguration.isCameraCompatRefreshEnabled())
+                .thenReturn(true);
+        when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
+                .thenReturn(true);
 
         mMockCameraManager = mock(CameraManager.class);
         doAnswer(invocation -> {
@@ -112,35 +128,34 @@
     }
 
     @Test
-    public void testGetOrientation_treatmentNotEnabled_returnUnspecified() {
+    public void testTreatmentNotEnabled_noForceRotationOrRefresh() throws Exception {
         when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
                     /* checkDeviceConfig */ anyBoolean()))
                 .thenReturn(false);
 
-        mDisplayRotationCompatPolicy = new DisplayRotationCompatPolicy(mDisplayContent);
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
         mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
 
         assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
                 SCREEN_ORIENTATION_UNSPECIFIED);
+
+        assertNoForceRotationOrRefresh();
     }
 
     @Test
-    public void testGetOrientation_treatmentDisabledViaDeviceConfig_returnUnspecified() {
+    public void testTreatmentDisabledViaDeviceConfig_noForceRotationOrRefresh() throws Exception {
         when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
                     /* checkDeviceConfig */ true))
                 .thenReturn(false);
 
-        mDisplayRotationCompatPolicy = new DisplayRotationCompatPolicy(mDisplayContent);
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
         mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
 
-        assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
-                SCREEN_ORIENTATION_UNSPECIFIED);
+        assertNoForceRotationOrRefresh();
     }
 
     @Test
-    public void testGetOrientation_multiWindowMode_returnUnspecified() {
+    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,
@@ -149,53 +164,46 @@
         mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
 
         assertTrue(mActivity.inMultiWindowMode());
-        assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
-                SCREEN_ORIENTATION_UNSPECIFIED);
+        assertNoForceRotationOrRefresh();
     }
 
     @Test
-    public void testGetOrientation_orientationUnspecified_returnUnspecified() {
+    public void testOrientationUnspecified_noForceRotationOrRefresh() throws Exception {
         configureActivity(SCREEN_ORIENTATION_UNSPECIFIED);
 
-        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
-
-        assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
-                SCREEN_ORIENTATION_UNSPECIFIED);
+        assertNoForceRotationOrRefresh();
     }
 
     @Test
-    public void testGetOrientation_orientationLocked_returnUnspecified() {
+    public void testOrientationLocked_noForceRotationOrRefresh() throws Exception {
         configureActivity(SCREEN_ORIENTATION_LOCKED);
 
         mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
 
-        assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
-                SCREEN_ORIENTATION_UNSPECIFIED);
+        assertNoForceRotationOrRefresh();
     }
 
     @Test
-    public void testGetOrientation_orientationNoSensor_returnUnspecified() {
+    public void testOrientationNoSensor_noForceRotationOrRefresh() throws Exception {
         configureActivity(SCREEN_ORIENTATION_NOSENSOR);
 
         mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
 
-        assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
-                SCREEN_ORIENTATION_UNSPECIFIED);
+        assertNoForceRotationOrRefresh();
     }
 
     @Test
-    public void testGetOrientation_ignoreOrientationRequestIsFalse_returnUnspecified() {
+    public void testIgnoreOrientationRequestIsFalse_noForceRotationOrRefresh() throws Exception {
         mDisplayContent.setIgnoreOrientationRequest(false);
 
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
         mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
 
-        assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
-                SCREEN_ORIENTATION_UNSPECIFIED);
+        assertNoForceRotationOrRefresh();
     }
 
     @Test
-    public void testGetOrientation_displayNotInternal_returnUnspecified() {
+    public void testDisplayNotInternal_noForceRotationOrRefresh() throws Exception {
         Display display = mDisplayContent.getDisplay();
         spyOn(display);
 
@@ -203,52 +211,51 @@
         mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
 
         when(display.getType()).thenReturn(Display.TYPE_EXTERNAL);
-        assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
-                SCREEN_ORIENTATION_UNSPECIFIED);
+        assertNoForceRotationOrRefresh();
 
         when(display.getType()).thenReturn(Display.TYPE_WIFI);
-        assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
-                SCREEN_ORIENTATION_UNSPECIFIED);
+        assertNoForceRotationOrRefresh();
 
         when(display.getType()).thenReturn(Display.TYPE_OVERLAY);
-        assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
-                SCREEN_ORIENTATION_UNSPECIFIED);
+        assertNoForceRotationOrRefresh();
 
         when(display.getType()).thenReturn(Display.TYPE_VIRTUAL);
-        assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
-                SCREEN_ORIENTATION_UNSPECIFIED);
+        assertNoForceRotationOrRefresh();
     }
 
     @Test
-    public void testGetOrientation_noCameraConnection_returnUnspecified() {
+    public void testNoCameraConnection_noForceRotationOrRefresh() throws Exception {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
 
-        assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
-                SCREEN_ORIENTATION_UNSPECIFIED);
+        assertNoForceRotationOrRefresh();
     }
 
     @Test
-    public void testGetOrientation_cameraReconnected_returnNotUnspecified() {
+    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 testGetOrientation_reconnectedToDifferentCamera_returnNotUnspecified() {
+    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
@@ -267,13 +274,12 @@
     }
 
     @Test
-    public void testGetOrientation_cameraOpenedForDifferentPackage_returnUnspecified() {
+    public void testCameraOpenedForDifferentPackage_noForceRotationOrRefresh() throws Exception {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
 
         mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_2);
 
-        assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
-                SCREEN_ORIENTATION_UNSPECIFIED);
+        assertNoForceRotationOrRefresh();
     }
 
     @Test
@@ -320,6 +326,42 @@
                 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);
     }
@@ -337,7 +379,50 @@
                 .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;
+    }
 }