[3/n] Camera Compat: Refresh activity
Introduce RefreshCallbackItem that allows to "refresh" 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 in DisplayRotationCompatPolicy.
Using "stop -> resumed" cycle by default due to higher success rate confirmed with app compatibility testing. But introduce ADB commands that allow trying both.
Bug: 218352945
Test: atest WmTests:DisplayRotationCompatPolicyTests
Change-Id: Iaca54b197937147f93d52297543b106fd5b97322
diff --git a/core/java/android/app/ActivityClient.java b/core/java/android/app/ActivityClient.java
index 324b8e7..0074a0d 100644
--- a/core/java/android/app/ActivityClient.java
+++ b/core/java/android/app/ActivityClient.java
@@ -58,6 +58,15 @@
}
}
+ /** Reports {@link android.app.servertransaction.RefreshCallbackItem} is executed. */
+ public void activityRefreshed(IBinder token) {
+ try {
+ getActivityClientController().activityRefreshed(token);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
/**
* Reports after {@link Activity#onTopResumedActivityChanged(boolean)} is called for losing the
* top most position.
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index ef6c5a6..2beb64d 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -5249,6 +5249,11 @@
}
}
+ @Override
+ public void reportRefresh(ActivityClientRecord r) {
+ ActivityClient.getInstance().activityRefreshed(r.token);
+ }
+
private void handleSetCoreSettings(Bundle coreSettings) {
synchronized (mCoreSettingsLock) {
mCoreSettings = coreSettings;
diff --git a/core/java/android/app/ClientTransactionHandler.java b/core/java/android/app/ClientTransactionHandler.java
index a7566fd..2c70c4e 100644
--- a/core/java/android/app/ClientTransactionHandler.java
+++ b/core/java/android/app/ClientTransactionHandler.java
@@ -140,6 +140,9 @@
/** Restart the activity after it was stopped. */
public abstract void performRestartActivity(@NonNull ActivityClientRecord r, boolean start);
+ /** Report that activity was refreshed to server. */
+ public abstract void reportRefresh(@NonNull ActivityClientRecord r);
+
/** Set pending activity configuration in case it will be updated by other transaction item. */
public abstract void updatePendingActivityConfiguration(@NonNull IBinder token,
Configuration overrideConfig);
diff --git a/core/java/android/app/IActivityClientController.aidl b/core/java/android/app/IActivityClientController.aidl
index 8b655b9..969f975 100644
--- a/core/java/android/app/IActivityClientController.aidl
+++ b/core/java/android/app/IActivityClientController.aidl
@@ -38,6 +38,7 @@
interface IActivityClientController {
oneway void activityIdle(in IBinder token, in Configuration config, in boolean stopProfiling);
oneway void activityResumed(in IBinder token, in boolean handleSplashScreenExit);
+ oneway void activityRefreshed(in IBinder token);
/**
* This call is not one-way because {@link #activityPaused()) is not one-way, or
* the top-resumed-lost could be reported after activity paused.
diff --git a/core/java/android/app/servertransaction/ClientTransactionItem.java b/core/java/android/app/servertransaction/ClientTransactionItem.java
index d94f08b..b159f33 100644
--- a/core/java/android/app/servertransaction/ClientTransactionItem.java
+++ b/core/java/android/app/servertransaction/ClientTransactionItem.java
@@ -38,6 +38,9 @@
return UNDEFINED;
}
+ boolean shouldHaveDefinedPreExecutionState() {
+ return true;
+ }
// Parcelable
diff --git a/core/java/android/app/servertransaction/RefreshCallbackItem.java b/core/java/android/app/servertransaction/RefreshCallbackItem.java
new file mode 100644
index 0000000..74abab2
--- /dev/null
+++ b/core/java/android/app/servertransaction/RefreshCallbackItem.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.servertransaction;
+
+import static android.app.servertransaction.ActivityLifecycleItem.LifecycleState;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityThread.ActivityClientRecord;
+import android.app.ClientTransactionHandler;
+import android.os.IBinder;
+import android.os.Parcel;
+
+/**
+ * Callback that allows to {@link TransactionExecutor#cycleToPath} to {@link ON_PAUSE} or
+ * {@link ON_STOP} in {@link TransactionExecutor#executeCallbacks} for activity "refresh" flow
+ * that goes through "paused -> resumed" or "stopped -> resumed" cycle.
+ *
+ * <p>This is used in combination with {@link com.android.server.wm.DisplayRotationCompatPolicy}
+ * for camera compatibility treatment that handles orientation mismatch between camera buffers and
+ * an app window. This allows to clear cached values in apps (e.g. display or camera rotation) that
+ * influence camera preview and can lead to sideways or stretching issues.
+ *
+ * @hide
+ */
+public class RefreshCallbackItem extends ActivityTransactionItem {
+
+ // Whether refresh should happen using the "stopped -> resumed" cycle or
+ // "paused -> resumed" cycle.
+ @LifecycleState
+ private int mPostExecutionState;
+
+ @Override
+ public void execute(@NonNull ClientTransactionHandler client,
+ @NonNull ActivityClientRecord r, PendingTransactionActions pendingActions) {}
+
+ @Override
+ public void postExecute(ClientTransactionHandler client, IBinder token,
+ PendingTransactionActions pendingActions) {
+ final ActivityClientRecord r = getActivityClientRecord(client, token);
+ client.reportRefresh(r);
+ }
+
+ @Override
+ public int getPostExecutionState() {
+ return mPostExecutionState;
+ }
+
+ @Override
+ boolean shouldHaveDefinedPreExecutionState() {
+ return false;
+ }
+
+ // ObjectPoolItem implementation
+
+ @Override
+ public void recycle() {
+ ObjectPool.recycle(this);
+ }
+
+ /**
+ * Obtain an instance initialized with provided params.
+ * @param postExecutionState indicating whether refresh should happen using the
+ * "stopped -> resumed" cycle or "paused -> resumed" cycle.
+ */
+ public static RefreshCallbackItem obtain(@LifecycleState int postExecutionState) {
+ if (postExecutionState != ON_STOP && postExecutionState != ON_PAUSE) {
+ throw new IllegalArgumentException(
+ "Only ON_STOP or ON_PAUSE are allowed as a post execution state for "
+ + "RefreshCallbackItem but got " + postExecutionState);
+ }
+ RefreshCallbackItem instance =
+ ObjectPool.obtain(RefreshCallbackItem.class);
+ if (instance == null) {
+ instance = new RefreshCallbackItem();
+ }
+ instance.mPostExecutionState = postExecutionState;
+ return instance;
+ }
+
+ private RefreshCallbackItem() {}
+
+ // Parcelable implementation
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mPostExecutionState);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ final RefreshCallbackItem other = (RefreshCallbackItem) o;
+ return mPostExecutionState == other.mPostExecutionState;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + mPostExecutionState;
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "RefreshCallbackItem{mPostExecutionState=" + mPostExecutionState + "}";
+ }
+
+ private RefreshCallbackItem(Parcel in) {
+ mPostExecutionState = in.readInt();
+ }
+
+ public static final @NonNull Creator<RefreshCallbackItem> CREATOR =
+ new Creator<RefreshCallbackItem>() {
+
+ public RefreshCallbackItem createFromParcel(Parcel in) {
+ return new RefreshCallbackItem(in);
+ }
+
+ public RefreshCallbackItem[] newArray(int size) {
+ return new RefreshCallbackItem[size];
+ }
+ };
+}
diff --git a/core/java/android/app/servertransaction/TransactionExecutor.java b/core/java/android/app/servertransaction/TransactionExecutor.java
index de1d38a..1ff0b79 100644
--- a/core/java/android/app/servertransaction/TransactionExecutor.java
+++ b/core/java/android/app/servertransaction/TransactionExecutor.java
@@ -126,10 +126,13 @@
final ClientTransactionItem item = callbacks.get(i);
if (DEBUG_RESOLVER) Slog.d(TAG, tId(transaction) + "Resolving callback: " + item);
final int postExecutionState = item.getPostExecutionState();
- final int closestPreExecutionState = mHelper.getClosestPreExecutionState(r,
- item.getPostExecutionState());
- if (closestPreExecutionState != UNDEFINED) {
- cycleToPath(r, closestPreExecutionState, transaction);
+
+ if (item.shouldHaveDefinedPreExecutionState()) {
+ final int closestPreExecutionState = mHelper.getClosestPreExecutionState(r,
+ item.getPostExecutionState());
+ if (closestPreExecutionState != UNDEFINED) {
+ cycleToPath(r, closestPreExecutionState, transaction);
+ }
}
item.execute(mTransactionHandler, token, mPendingActions);
diff --git a/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;
+ }
}