diff options
17 files changed, 587 insertions, 52 deletions
diff --git a/core/java/android/app/ActivityClient.java b/core/java/android/app/ActivityClient.java index 324b8e7a784f..0074a0dd4c96 100644 --- a/core/java/android/app/ActivityClient.java +++ b/core/java/android/app/ActivityClient.java @@ -58,6 +58,15 @@ public class ActivityClient { } } + /** 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 f2b773ed9d7e..9385e8760a02 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -5249,6 +5249,11 @@ public final class ActivityThread extends ClientTransactionHandler } } + @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 a7566fdaae64..2c70c4e90157 100644 --- a/core/java/android/app/ClientTransactionHandler.java +++ b/core/java/android/app/ClientTransactionHandler.java @@ -140,6 +140,9 @@ public abstract class ClientTransactionHandler { /** 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 8b655b9bf315..969f97577c5a 100644 --- a/core/java/android/app/IActivityClientController.aidl +++ b/core/java/android/app/IActivityClientController.aidl @@ -38,6 +38,7 @@ import com.android.internal.policy.IKeyguardDismissCallback; 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 d94f08b6aac1..b159f336cd85 100644 --- a/core/java/android/app/servertransaction/ClientTransactionItem.java +++ b/core/java/android/app/servertransaction/ClientTransactionItem.java @@ -38,6 +38,9 @@ public abstract class ClientTransactionItem implements BaseClientRequest, Parcel 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 000000000000..74abab22b0a6 --- /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 de1d38a64163..1ff0b796fb1e 100644 --- a/core/java/android/app/servertransaction/TransactionExecutor.java +++ b/core/java/android/app/servertransaction/TransactionExecutor.java @@ -126,10 +126,13 @@ public class TransactionExecutor { 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 f55a91e9f1bf..f47d9c6e0c2d 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 da6e7e8436eb..725254513519 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 @@ class ActivityClientController extends IActivityClientController.Stub { } @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 57eeb9a0a905..50169b4ba667 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 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A */ private boolean mWillCloseOrEnterPip; - @VisibleForTesting final LetterboxUiController mLetterboxUiController; /** @@ -6111,6 +6110,19 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A 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 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } else { scheduleConfigurationChanged(newMergedOverrideConfig); } + notifyDisplayCompatPolicyAboutConfigurationChange( + mLastReportedConfiguration.getMergedConfiguration(), mTmpConfig); return true; } @@ -9211,11 +9225,24 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } 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 c97d7a97d886..36f86d10e2c5 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 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp 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 34bdb7a2f416..cf3a6880e712 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 @@ public class DisplayRotation { 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 @@ public class DisplayRotation { mInHalfFoldTransition = false; mFoldState = DeviceStateController.FoldState.UNKNOWN; } + mDisplayRotationCompatPolicySummary = dc.mDisplayRotationCompatPolicy == null + ? null + : dc.mDisplayRotationCompatPolicy + .getSummaryForDisplayRotationHistoryRecord(); } void dump(String prefix, PrintWriter pw) { @@ -1861,6 +1866,9 @@ public class DisplayRotation { + " 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 0d3f784849ba..18c5c3b82b19 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.content.res.Configuration.ORIENTATION_UNDEFINED; import static android.view.Display.TYPE_INTERNAL; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION; +import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.servertransaction.ClientTransaction; +import android.app.servertransaction.RefreshCallbackItem; +import android.app.servertransaction.ResumeActivityItem; import android.content.pm.ActivityInfo.ScreenOrientation; +import android.content.res.Configuration; import android.hardware.camera2.CameraManager; import android.os.Handler; +import android.os.RemoteException; import android.util.ArrayMap; import android.util.ArraySet; @@ -65,11 +74,15 @@ final class DisplayRotationCompatPolicy { 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 @@ final class DisplayRotationCompatPolicy { } }; + @ScreenOrientation + private int mLastReportedOrientation = SCREEN_ORIENTATION_UNSET; + DisplayRotationCompatPolicy(@NonNull DisplayContent displayContent) { this(displayContent, displayContent.mWmService.mH); } @@ -132,7 +148,13 @@ final class DisplayRotationCompatPolicy { * #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 @@ final class DisplayRotationCompatPolicy { } /** + * "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 @@ final class DisplayRotationCompatPolicy { 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 @@ final class DisplayRotationCompatPolicy { 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 @@ final class DisplayRotationCompatPolicy { // 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 @@ final class DisplayRotationCompatPolicy { 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 @@ final class DisplayRotationCompatPolicy { 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 @@ final class DisplayRotationCompatPolicy { 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 793a352aeec6..a7bf595fa673 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 @@ final class LetterboxConfiguration { // 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 @@ final class LetterboxConfiguration { "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 9cb94c68583f..fd7e082beed4 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 @@ import java.io.PrintWriter; // 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 @@ final class LetterboxUiController { @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 @@ final class LetterboxUiController { } } + /** + * 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 554f271a31a7..aef6d1d15510 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 @@ public class WindowManagerShellCommand extends ShellCommand { 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 @@ public class WindowManagerShellCommand extends ShellCommand { 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 @@ public class WindowManagerShellCommand extends ShellCommand { mLetterboxConfiguration.resetIsEducationEnabled(); mLetterboxConfiguration.resetIsSplitScreenAspectRatioForUnresizableAppsEnabled(); mLetterboxConfiguration.resetTranslucentLetterboxingEnabled(); + mLetterboxConfiguration.resetCameraCompatRefreshEnabled(); + mLetterboxConfiguration.resetCameraCompatRefreshCycleThroughStopEnabled(); } } @@ -1171,6 +1188,11 @@ public class WindowManagerShellCommand extends ShellCommand { + 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 @@ public class WindowManagerShellCommand extends ShellCommand { 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 d66215051d62..d1234e3de81a 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_PORTRAIT; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static android.view.Surface.ROTATION_0; +import static android.view.Surface.ROTATION_90; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -34,15 +38,23 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import android.app.servertransaction.ClientTransaction; +import android.app.servertransaction.RefreshCallbackItem; +import android.app.servertransaction.ResumeActivityItem; import android.content.ComponentName; import android.content.pm.ActivityInfo.ScreenOrientation; +import android.content.res.Configuration; import android.content.res.Configuration.Orientation; import android.hardware.camera2.CameraManager; import android.os.Handler; import android.platform.test.annotations.Presubmit; import android.view.Display; +import android.view.Surface.Rotation; import androidx.test.filters.SmallTest; @@ -85,6 +97,10 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { 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 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { } @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 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { 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 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { 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 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { } @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 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { 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 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { .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; + } } |