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;
+ }
}