diff --git a/core/java/android/app/ActivityClient.java b/core/java/android/app/ActivityClient.java index 324b8e7a784f5..0074a0dd4c96a 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 ef6c5a6c82726..2beb64d98c99c 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 a7566fdaae64a..2c70c4e901576 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 8b655b9bf315e..969f97577c5aa 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 d94f08b6aac1c..b159f336cd850 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 0000000000000..74abab22b0a6a --- /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. + * + *

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 CREATOR = + new Creator() { + + 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 de1d38a64163f..1ff0b796fb1ec 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/core/res/res/values/config.xml b/core/res/res/values/config.xml index 50761bfadbaf7..a0d6e470041aa 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -5344,6 +5344,12 @@ TODO(b/255532890) Enable when ignoreOrientationRequest is set --> false + + false + false diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index fb77b3be5faf8..371dbfb215876 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -4460,6 +4460,7 @@ + diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 49704d971b8df..f47d9c6e0c2d6 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -295,6 +295,12 @@ "group": "WM_DEBUG_IME", "at": "com\/android\/server\/wm\/DisplayContent.java" }, + "-1812743677": { + "message": "Display id=%d is ignoring all orientation requests, camera is active and the top activity is eligible for force rotation, return %s,portrait activity: %b, is natural orientation portrait: %b.", + "level": "VERBOSE", + "group": "WM_DEBUG_ORIENTATION", + "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java" + }, "-1810446914": { "message": "Trying to update display configuration for system\/invalid process.", "level": "WARN", @@ -481,6 +487,12 @@ "group": "WM_DEBUG_ORIENTATION", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "-1631991057": { + "message": "Display id=%d is notified that Camera %s is closed but activity is still refreshing. Rescheduling an update.", + "level": "VERBOSE", + "group": "WM_DEBUG_ORIENTATION", + "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java" + }, "-1630752478": { "message": "removeLockedTask: removed %s", "level": "DEBUG", @@ -619,6 +631,12 @@ "group": "WM_DEBUG_WINDOW_INSETS", "at": "com\/android\/server\/wm\/InsetsSourceProvider.java" }, + "-1480918485": { + "message": "Refreshed activity: %s", + "level": "INFO", + "group": "WM_DEBUG_STATES", + "at": "com\/android\/server\/wm\/ActivityRecord.java" + }, "-1480772131": { "message": "No app or window is requesting an orientation, return %d for display id=%d", "level": "VERBOSE", @@ -1321,6 +1339,12 @@ "group": "WM_DEBUG_CONFIGURATION", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, + "-799396645": { + "message": "Display id=%d is notified that Camera %s is closed, updating rotation.", + "level": "VERBOSE", + "group": "WM_DEBUG_ORIENTATION", + "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java" + }, "-799003045": { "message": "Set animatingExit: reason=remove\/replaceWindow win=%s", "level": "VERBOSE", @@ -1543,6 +1567,12 @@ "group": "WM_DEBUG_SCREEN_ON", "at": "com\/android\/server\/wm\/DisplayContent.java" }, + "-627759820": { + "message": "Display id=%d is notified that Camera %s is open for package %s", + "level": "VERBOSE", + "group": "WM_DEBUG_ORIENTATION", + "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java" + }, "-622997754": { "message": "postWindowRemoveCleanupLocked: %s", "level": "VERBOSE", @@ -2101,6 +2131,12 @@ "group": "WM_SHOW_TRANSACTIONS", "at": "com\/android\/server\/wm\/Session.java" }, + "-81260230": { + "message": "Display id=%d is notified that Camera %s is closed, scheduling rotation update.", + "level": "VERBOSE", + "group": "WM_DEBUG_ORIENTATION", + "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java" + }, "-81121442": { "message": "ImeContainer just became organized but it doesn't have a parent or the parent doesn't have a surface control. mSurfaceControl=%s imeParentSurfaceControl=%s", "level": "ERROR", @@ -4201,6 +4237,12 @@ "group": "WM_DEBUG_REMOTE_ANIMATIONS", "at": "com\/android\/server\/wm\/RemoteAnimationController.java" }, + "1967643923": { + "message": "Refershing activity for camera compatibility treatment, activityRecord=%s", + "level": "VERBOSE", + "group": "WM_DEBUG_STATES", + "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java" + }, "1967975839": { "message": "Changing app %s visible=%b performLayout=%b", "level": "VERBOSE", diff --git a/services/core/java/com/android/server/camera/CameraServiceProxy.java b/services/core/java/com/android/server/camera/CameraServiceProxy.java index aec60deba9335..7bbc604de6603 100644 --- a/services/core/java/com/android/server/camera/CameraServiceProxy.java +++ b/services/core/java/com/android/server/camera/CameraServiceProxy.java @@ -74,6 +74,7 @@ import android.view.Surface; import android.view.WindowManagerGlobal; import com.android.framework.protobuf.nano.MessageNano; +import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.FrameworkStatsLog; import com.android.server.LocalServices; @@ -389,6 +390,16 @@ public class CameraServiceProxy extends SystemService return CaptureRequest.SCALER_ROTATE_AND_CROP_NONE; } + // When config_isWindowManagerCameraCompatTreatmentEnabled is true, + // DisplayRotationCompatPolicy in WindowManager force rotates fullscreen activities with + // fixed orientation to align them with the natural orientation of the device. + if (ctx.getResources().getBoolean( + R.bool.config_isWindowManagerCameraCompatTreatmentEnabled)) { + Slog.v(TAG, "Disable Rotate and Crop to avoid conflicts with" + + " WM force rotation treatment."); + return CaptureRequest.SCALER_ROTATE_AND_CROP_NONE; + } + // External cameras do not need crop-rotate-scale. if (lensFacing != CameraMetadata.LENS_FACING_FRONT && lensFacing != CameraMetadata.LENS_FACING_BACK) { diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java index da6e7e8436eb1..725254513519e 100644 --- a/services/core/java/com/android/server/wm/ActivityClientController.java +++ b/services/core/java/com/android/server/wm/ActivityClientController.java @@ -162,6 +162,15 @@ class ActivityClientController extends IActivityClientController.Stub { Binder.restoreCallingIdentity(origId); } + @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(); diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 57eeb9a0a9055..50169b4ba6670 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 4c19322a97909..36f86d10e2c5f 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -431,6 +431,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp private final DisplayMetrics mDisplayMetrics = new DisplayMetrics(); private final DisplayPolicy mDisplayPolicy; private final DisplayRotation mDisplayRotation; + @Nullable final DisplayRotationCompatPolicy mDisplayRotationCompatPolicy; DisplayFrames mDisplayFrames; private final RemoteCallbackList @@ -1158,6 +1159,13 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp onDisplayChanged(this); updateDisplayAreaOrganizers(); + mDisplayRotationCompatPolicy = + // Not checking DeviceConfig value here to allow enabling via DeviceConfig + // without the need to restart the device. + mWmService.mLetterboxConfiguration.isCameraCompatTreatmentEnabled( + /* checkDeviceConfig */ false) + ? new DisplayRotationCompatPolicy(this) : null; + mInputMonitor = new InputMonitor(mWmService, this); mInsetsPolicy = new InsetsPolicy(mInsetsStateController, this); mMinSizeOfResizeableTaskDp = getMinimalTaskSizeDp(); @@ -2704,6 +2712,14 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } } + if (mDisplayRotationCompatPolicy != null) { + int compatOrientation = mDisplayRotationCompatPolicy.getOrientation(); + if (compatOrientation != SCREEN_ORIENTATION_UNSPECIFIED) { + mLastOrientationSource = null; + return compatOrientation; + } + } + final int orientation = super.getOrientation(); if (!handlesOrientationChangeFromDescendant(orientation)) { @@ -3260,6 +3276,10 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // on the next traversal if it's removed from RootWindowContainer child list. getPendingTransaction().apply(); mWmService.mWindowPlacerLocked.requestTraversal(); + + if (mDisplayRotationCompatPolicy != null) { + mDisplayRotationCompatPolicy.dispose(); + } } /** Returns true if a removal action is still being deferred. */ diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java index 34bdb7a2f4169..cf3a6880e7127 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 new file mode 100644 index 0000000000000..18c5c3b82b19b --- /dev/null +++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java @@ -0,0 +1,433 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE; +import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; +import static android.content.pm.ActivityInfo.screenOrientationToString; +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static android.content.res.Configuration.ORIENTATION_UNDEFINED; +import static android.view.Display.TYPE_INTERNAL; + +import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION; +import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.servertransaction.ClientTransaction; +import android.app.servertransaction.RefreshCallbackItem; +import android.app.servertransaction.ResumeActivityItem; +import android.content.pm.ActivityInfo.ScreenOrientation; +import android.content.res.Configuration; +import android.hardware.camera2.CameraManager; +import android.os.Handler; +import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.ArraySet; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; + +import java.util.Map; +import java.util.Set; + +/** + * Controls camera compatibility treatment that handles orientation mismatch between camera + * buffers and an app window for a particular display that can lead to camera issues like sideways + * or stretched viewfinder. + * + *

This includes force rotation of fixed orientation activities connected to the camera. + * + *

The treatment is enabled for internal displays that have {@code ignoreOrientationRequest} + * display setting enabled and when {@code + * R.bool.config_isWindowManagerCameraCompatTreatmentEnabled} is {@code true}. + */ + // TODO(b/261444714): Consider moving Camera-specific logic outside of the WM Core path +final class DisplayRotationCompatPolicy { + + // Delay for updating display rotation after Camera connection is closed. Needed to avoid + // rotation flickering when an app is flipping between front and rear cameras or when size + // compat mode is restarted. + // TODO(b/263114289): Consider associating this delay with a specific activity so that if + // the new non-camera activity started on top of the camer one we can rotate faster. + private static final int CAMERA_CLOSED_ROTATION_UPDATE_DELAY_MS = 2000; + // Delay for updating display rotation after Camera connection is opened. This delay is + // selected to be long enough to avoid conflicts with transitions on the app's side. + // Using a delay < CAMERA_CLOSED_ROTATION_UPDATE_DELAY_MS to avoid flickering when an app + // is flipping between front and rear cameras (in case requested orientation changes at + // runtime at the same time) or when size compat mode is restarted. + private static final int CAMERA_OPENED_ROTATION_UPDATE_DELAY_MS = + CAMERA_CLOSED_ROTATION_UPDATE_DELAY_MS / 2; + // Delay for ensuring that onActivityRefreshed is always called after an activity refresh. The + // client process may not always report the event back to the server, such as process is + // crashed or got killed. + private static final int REFRESH_CALLBACK_TIMEOUT_MS = 2000; + + private final DisplayContent mDisplayContent; + private final WindowManagerService mWmService; + private final CameraManager mCameraManager; + private final Handler mHandler; + + // Bi-directional map between package names and active camera IDs since we need to 1) get a + // camera id by a package name when determining rotation; 2) get a package name by a camera id + // when camera connection is closed and we need to clean up our records. + @GuardedBy("this") + private final CameraIdPackageNameBiMap mCameraIdPackageBiMap = new CameraIdPackageNameBiMap(); + @GuardedBy("this") + private final Set mScheduledToBeRemovedCameraIdSet = new ArraySet<>(); + @GuardedBy("this") + private final Set mScheduledOrientationUpdateCameraIdSet = new ArraySet<>(); + + private final CameraManager.AvailabilityCallback mAvailabilityCallback = + new CameraManager.AvailabilityCallback() { + @Override + public void onCameraOpened(@NonNull String cameraId, @NonNull String packageId) { + notifyCameraOpened(cameraId, packageId); + } + + @Override + public void onCameraClosed(@NonNull String cameraId) { + notifyCameraClosed(cameraId); + } + }; + + @ScreenOrientation + private int mLastReportedOrientation = SCREEN_ORIENTATION_UNSET; + + DisplayRotationCompatPolicy(@NonNull DisplayContent displayContent) { + this(displayContent, displayContent.mWmService.mH); + } + + @VisibleForTesting + DisplayRotationCompatPolicy(@NonNull DisplayContent displayContent, Handler handler) { + // This constructor is called from DisplayContent constructor. Don't use any fields in + // DisplayContent here since they aren't guaranteed to be set. + mHandler = handler; + mDisplayContent = displayContent; + mWmService = displayContent.mWmService; + mCameraManager = mWmService.mContext.getSystemService(CameraManager.class); + mCameraManager.registerAvailabilityCallback( + mWmService.mContext.getMainExecutor(), mAvailabilityCallback); + } + + void dispose() { + mCameraManager.unregisterAvailabilityCallback(mAvailabilityCallback); + } + + /** + * Determines orientation for Camera compatibility. + * + *

The goal of this function is to compute a orientation which would align orientations of + * portrait app window and natural orientation of the device and set opposite to natural + * orientation for a landscape app window. This is one of the strongest assumptions that apps + * make when they implement camera previews. Since app and natural display orientations aren't + * guaranteed to match, the rotation can cause letterboxing. + * + *

If treatment isn't applicable returns {@link SCREEN_ORIENTATION_UNSPECIFIED}. See {@link + * #shouldComputeCameraCompatOrientation} for conditions enabling the treatment. + */ + @ScreenOrientation + int getOrientation() { + mLastReportedOrientation = getOrientationInternal(); + return mLastReportedOrientation; + } + + @ScreenOrientation + private synchronized int getOrientationInternal() { + if (!isTreatmentEnabledForDisplay()) { + return SCREEN_ORIENTATION_UNSPECIFIED; + } + ActivityRecord topActivity = mDisplayContent.topRunningActivity( + /* considerKeyguardState= */ true); + if (!isTreatmentEnabledForActivity(topActivity)) { + return SCREEN_ORIENTATION_UNSPECIFIED; + } + boolean isPortraitActivity = + topActivity.getRequestedConfigurationOrientation() == ORIENTATION_PORTRAIT; + boolean isNaturalDisplayOrientationPortrait = + mDisplayContent.getNaturalOrientation() == ORIENTATION_PORTRAIT; + // Rotate portrait-only activity in the natural orientation of the displays (and in the + // opposite to natural orientation for landscape-only) since many apps assume that those + // are aligned when they compute orientation of the preview. + // This means that even for a landscape-only activity and a device with landscape natural + // orientation this would return SCREEN_ORIENTATION_PORTRAIT because an assumption that + // natural orientation = portrait window = portait camera is the main wrong assumption + // that apps make when they implement camera previews so landscape windows need be + // rotated in the orientation oposite to the natural one even if it's portrait. + // TODO(b/261475895): Consider allowing more rotations for "sensor" and "user" versions + // of the portrait and landscape orientation requests. + int orientation = (isPortraitActivity && isNaturalDisplayOrientationPortrait) + || (!isPortraitActivity && !isNaturalDisplayOrientationPortrait) + ? SCREEN_ORIENTATION_PORTRAIT + : SCREEN_ORIENTATION_LANDSCAPE; + ProtoLog.v(WM_DEBUG_ORIENTATION, + "Display id=%d is ignoring all orientation requests, camera is active " + + "and the top activity is eligible for force rotation, return %s," + + "portrait activity: %b, is natural orientation portrait: %b.", + mDisplayContent.mDisplayId, screenOrientationToString(orientation), + isPortraitActivity, isNaturalDisplayOrientationPortrait); + return orientation; + } + + /** + * "Refreshes" activity by going through "stopped -> resumed" or "paused -> resumed" cycle. + * This allows to clear cached values in apps (e.g. display or camera rotation) that influence + * camera preview and can lead to sideways or stretching issues persisting even after force + * rotation. + */ + void onActivityConfigurationChanging(ActivityRecord activity, Configuration newConfig, + Configuration lastReportedConfig) { + if (!isTreatmentEnabledForDisplay() + || !mWmService.mLetterboxConfiguration.isCameraCompatRefreshEnabled() + || !shouldRefreshActivity(activity, newConfig, lastReportedConfig)) { + return; + } + boolean cycleThroughStop = mWmService.mLetterboxConfiguration + .isCameraCompatRefreshCycleThroughStopEnabled(); + try { + activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(true); + ProtoLog.v(WM_DEBUG_STATES, + "Refershing activity for camera compatibility treatment, " + + "activityRecord=%s", activity); + final ClientTransaction transaction = ClientTransaction.obtain( + activity.app.getThread(), activity.token); + transaction.addCallback( + RefreshCallbackItem.obtain(cycleThroughStop ? ON_STOP : ON_PAUSE)); + transaction.setLifecycleStateRequest(ResumeActivityItem.obtain(/* isForward */ false)); + activity.mAtmService.getLifecycleManager().scheduleTransaction(transaction); + mHandler.postDelayed( + () -> onActivityRefreshed(activity), + REFRESH_CALLBACK_TIMEOUT_MS); + } catch (RemoteException e) { + activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(false); + } + } + + void onActivityRefreshed(@NonNull ActivityRecord activity) { + activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(false); + } + + String getSummaryForDisplayRotationHistoryRecord() { + String summaryIfEnabled = ""; + if (isTreatmentEnabledForDisplay()) { + ActivityRecord topActivity = mDisplayContent.topRunningActivity( + /* considerKeyguardState= */ true); + summaryIfEnabled = + " mLastReportedOrientation=" + + screenOrientationToString(mLastReportedOrientation) + + " topActivity=" + + (topActivity == null ? "null" : topActivity.shortComponentName) + + " isTreatmentEnabledForActivity=" + + isTreatmentEnabledForActivity(topActivity) + + " CameraIdPackageNameBiMap=" + + mCameraIdPackageBiMap.getSummaryForDisplayRotationHistoryRecord(); + } + return "DisplayRotationCompatPolicy{" + + " isTreatmentEnabledForDisplay=" + isTreatmentEnabledForDisplay() + + summaryIfEnabled + + " }"; + } + + // Refreshing only when configuration changes after rotation. + private boolean shouldRefreshActivity(ActivityRecord activity, Configuration newConfig, + Configuration lastReportedConfig) { + return newConfig.windowConfiguration.getDisplayRotation() + != lastReportedConfig.windowConfiguration.getDisplayRotation() + && isTreatmentEnabledForActivity(activity); + } + + /** + * Whether camera compat treatment is enabled for the display. + * + *

Conditions that need to be met: + *

+ */ + private boolean isTreatmentEnabledForDisplay() { + return mWmService.mLetterboxConfiguration.isCameraCompatTreatmentEnabled( + /* checkDeviceConfig */ true) + && mDisplayContent.getIgnoreOrientationRequest() + // TODO(b/225928882): Support camera compat rotation for external displays + && mDisplayContent.getDisplay().getType() == TYPE_INTERNAL; + } + + /** + * Whether camera compat treatment is applicable for the given activity. + * + *

Conditions that need to be met: + *

+ */ + private boolean isTreatmentEnabledForActivity(@Nullable ActivityRecord activity) { + return activity != null && !activity.inMultiWindowMode() + && activity.getRequestedConfigurationOrientation() != ORIENTATION_UNDEFINED + // "locked" and "nosensor" values are often used by camera apps that can't + // handle dynamic changes so we shouldn't force rotate them. + && activity.getRequestedOrientation() != SCREEN_ORIENTATION_NOSENSOR + && activity.getRequestedOrientation() != SCREEN_ORIENTATION_LOCKED + && mCameraIdPackageBiMap.containsPackageName(activity.packageName); + } + + private synchronized void notifyCameraOpened( + @NonNull String cameraId, @NonNull String packageName) { + // If an activity is restarting or camera is flipping, the camera connection can be + // quickly closed and reopened. + mScheduledToBeRemovedCameraIdSet.remove(cameraId); + ProtoLog.v(WM_DEBUG_ORIENTATION, + "Display id=%d is notified that Camera %s is open for package %s", + mDisplayContent.mDisplayId, cameraId, packageName); + // Some apps can’t handle configuration changes coming at the same time with Camera setup + // so delaying orientation update to accomadate for that. + mScheduledOrientationUpdateCameraIdSet.add(cameraId); + mHandler.postDelayed( + () -> delayedUpdateOrientationWithWmLock(cameraId, packageName), + CAMERA_OPENED_ROTATION_UPDATE_DELAY_MS); + } + + private void updateOrientationWithWmLock() { + synchronized (mWmService.mGlobalLock) { + mDisplayContent.updateOrientation(); + } + } + + private void delayedUpdateOrientationWithWmLock( + @NonNull String cameraId, @NonNull String packageName) { + synchronized (this) { + if (!mScheduledOrientationUpdateCameraIdSet.remove(cameraId)) { + // Orientation update has happened already or was cancelled because + // camera was closed. + return; + } + mCameraIdPackageBiMap.put(packageName, cameraId); + } + updateOrientationWithWmLock(); + } + + private synchronized void notifyCameraClosed(@NonNull String cameraId) { + ProtoLog.v(WM_DEBUG_ORIENTATION, + "Display id=%d is notified that Camera %s is closed, scheduling rotation update.", + mDisplayContent.mDisplayId, cameraId); + mScheduledToBeRemovedCameraIdSet.add(cameraId); + // No need to update orientation for this camera if it's already closed. + mScheduledOrientationUpdateCameraIdSet.remove(cameraId); + scheduleRemoveCameraId(cameraId); + } + + // Delay is needed to avoid rotation flickering when an app is flipping between front and + // rear cameras, when size compat mode is restarted or activity is being refreshed. + private void scheduleRemoveCameraId(@NonNull String cameraId) { + mHandler.postDelayed( + () -> removeCameraId(cameraId), + CAMERA_CLOSED_ROTATION_UPDATE_DELAY_MS); + } + + private void removeCameraId(String cameraId) { + synchronized (this) { + if (!mScheduledToBeRemovedCameraIdSet.remove(cameraId)) { + // Already reconnected to this camera, no need to clean up. + return; + } + if (isActivityForCameraIdRefreshing(cameraId)) { + ProtoLog.v(WM_DEBUG_ORIENTATION, + "Display id=%d is notified that Camera %s is closed but activity is" + + " still refreshing. Rescheduling an update.", + mDisplayContent.mDisplayId, cameraId); + mScheduledToBeRemovedCameraIdSet.add(cameraId); + scheduleRemoveCameraId(cameraId); + return; + } + mCameraIdPackageBiMap.removeCameraId(cameraId); + } + ProtoLog.v(WM_DEBUG_ORIENTATION, + "Display id=%d is notified that Camera %s is closed, updating rotation.", + mDisplayContent.mDisplayId, cameraId); + updateOrientationWithWmLock(); + } + + private boolean isActivityForCameraIdRefreshing(String cameraId) { + ActivityRecord topActivity = mDisplayContent.topRunningActivity( + /* considerKeyguardState= */ true); + if (!isTreatmentEnabledForActivity(topActivity)) { + return false; + } + String activeCameraId = mCameraIdPackageBiMap.getCameraId(topActivity.packageName); + if (activeCameraId == null || activeCameraId != cameraId) { + return false; + } + return topActivity.mLetterboxUiController.isRefreshAfterRotationRequested(); + } + + private static class CameraIdPackageNameBiMap { + + private final Map mPackageToCameraIdMap = new ArrayMap<>(); + private final Map mCameraIdToPackageMap = new ArrayMap<>(); + + void put(String packageName, String cameraId) { + // Always using the last connected camera ID for the package even for the concurrent + // camera use case since we can't guess which camera is more important anyway. + removePackageName(packageName); + removeCameraId(cameraId); + mPackageToCameraIdMap.put(packageName, cameraId); + mCameraIdToPackageMap.put(cameraId, packageName); + } + + boolean containsPackageName(String packageName) { + return mPackageToCameraIdMap.containsKey(packageName); + } + + @Nullable + String getCameraId(String packageName) { + return mPackageToCameraIdMap.get(packageName); + } + + void removeCameraId(String cameraId) { + String packageName = mCameraIdToPackageMap.get(cameraId); + if (packageName == null) { + return; + } + mPackageToCameraIdMap.remove(packageName, cameraId); + mCameraIdToPackageMap.remove(cameraId, packageName); + } + + String getSummaryForDisplayRotationHistoryRecord() { + return "{ mPackageToCameraIdMap=" + mPackageToCameraIdMap + " }"; + } + + private void removePackageName(String packageName) { + String cameraId = mPackageToCameraIdMap.get(packageName); + if (cameraId == null) { + return; + } + mPackageToCameraIdMap.remove(packageName, cameraId); + mCameraIdToPackageMap.remove(cameraId, packageName); + } + } +} diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java index 3eca364ff5fc0..a7bf595fa673c 100644 --- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java +++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java @@ -191,6 +191,20 @@ final class LetterboxConfiguration { // Allows to enable letterboxing strategy for translucent activities ignoring flags. private boolean mTranslucentLetterboxingOverrideEnabled; + // Whether camera compatibility treatment is enabled. + // See DisplayRotationCompatPolicy for context. + private final boolean mIsCameraCompatTreatmentEnabled; + + // Whether activity "refresh" in camera compatibility treatment is enabled. + // See RefreshCallbackItem for context. + private boolean mIsCameraCompatTreatmentRefreshEnabled = true; + + // Whether activity "refresh" in camera compatibility treatment should happen using the + // "stopped -> resumed" cycle rather than "paused -> resumed" cycle. Using "stop -> resumed" + // cycle by default due to higher success rate confirmed with app compatibility testing. + // See RefreshCallbackItem for context. + private boolean mIsCameraCompatRefreshCycleThroughStopEnabled = true; + LetterboxConfiguration(Context systemUiContext) { this(systemUiContext, new LetterboxConfigurationPersister(systemUiContext, () -> readLetterboxHorizontalReachabilityPositionFromConfig(systemUiContext, @@ -241,6 +255,8 @@ final class LetterboxConfiguration { R.bool.config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled); mTranslucentLetterboxingEnabled = mContext.getResources().getBoolean( R.bool.config_letterboxIsEnabledForTranslucentActivities); + mIsCameraCompatTreatmentEnabled = mContext.getResources().getBoolean( + R.bool.config_isWindowManagerCameraCompatTreatmentEnabled); mLetterboxConfigurationPersister = letterboxConfigurationPersister; mLetterboxConfigurationPersister.start(); } @@ -947,9 +963,65 @@ final class LetterboxConfiguration { isDeviceInTabletopMode, nextVerticalPosition); } - // TODO(b/262378106): Cache runtime flag and implement DeviceConfig.OnPropertiesChangedListener + // TODO(b/262378106): Cache a runtime flag and implement + // DeviceConfig.OnPropertiesChangedListener static boolean isTranslucentLetterboxingAllowed() { return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER, "enable_translucent_activity_letterbox", false); } + + /** Whether camera compatibility treatment is enabled. */ + boolean isCameraCompatTreatmentEnabled(boolean checkDeviceConfig) { + return mIsCameraCompatTreatmentEnabled + && (!checkDeviceConfig || isCameraCompatTreatmentAllowed()); + } + + // TODO(b/262977416): Cache a runtime flag and implement + // DeviceConfig.OnPropertiesChangedListener + private static boolean isCameraCompatTreatmentAllowed() { + return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER, + "enable_camera_compat_treatment", false); + } + + /** Whether camera compatibility refresh is enabled. */ + boolean isCameraCompatRefreshEnabled() { + return mIsCameraCompatTreatmentRefreshEnabled; + } + + /** Overrides whether camera compatibility treatment is enabled. */ + void setCameraCompatRefreshEnabled(boolean enabled) { + mIsCameraCompatTreatmentRefreshEnabled = enabled; + } + + /** + * Resets whether camera compatibility treatment is enabled to {@code true}. + */ + void resetCameraCompatRefreshEnabled() { + mIsCameraCompatTreatmentRefreshEnabled = true; + } + + /** + * Whether activity "refresh" in camera compatibility treatment should happen using the + * "stopped -> resumed" cycle rather than "paused -> resumed" cycle. + */ + boolean isCameraCompatRefreshCycleThroughStopEnabled() { + return mIsCameraCompatRefreshCycleThroughStopEnabled; + } + + /** + * Overrides whether activity "refresh" in camera compatibility treatment should happen using + * "stopped -> resumed" cycle rather than "paused -> resumed" cycle. + */ + void setCameraCompatRefreshCycleThroughStopEnabled(boolean enabled) { + mIsCameraCompatRefreshCycleThroughStopEnabled = enabled; + } + + /** + * Resets whether activity "refresh" in camera compatibility treatment should happen using + * "stopped -> resumed" cycle rather than "paused -> resumed" cycle to {@code true}. + */ + void resetCameraCompatRefreshCycleThroughStopEnabled() { + mIsCameraCompatRefreshCycleThroughStopEnabled = true; + } + } diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index 9cb94c68583ff..fd7e082beed4a 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 85aa9425c4fd6..aef6d1d155102 100644 --- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java +++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java @@ -53,6 +53,7 @@ import com.android.server.wm.LetterboxConfiguration.LetterboxVerticalReachabilit import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipEntry; @@ -806,54 +807,6 @@ public class WindowManagerShellCommand extends ShellCommand { return 0; } - private int runSetLetterboxIsHorizontalReachabilityEnabled(PrintWriter pw) - throws RemoteException { - String arg = getNextArg(); - final boolean enabled; - switch (arg) { - case "true": - case "1": - enabled = true; - break; - case "false": - case "0": - enabled = false; - break; - default: - getErrPrintWriter().println("Error: expected true, 1, false, 0, but got " + arg); - return -1; - } - - synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(enabled); - } - return 0; - } - - private int runSetLetterboxIsVerticalReachabilityEnabled(PrintWriter pw) - throws RemoteException { - String arg = getNextArg(); - final boolean enabled; - switch (arg) { - case "true": - case "1": - enabled = true; - break; - case "false": - case "0": - enabled = false; - break; - default: - getErrPrintWriter().println("Error: expected true, 1, false, 0, but got " + arg); - return -1; - } - - synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setIsVerticalReachabilityEnabled(enabled); - } - return 0; - } - private int runSetLetterboxDefaultPositionForHorizontalReachability(PrintWriter pw) throws RemoteException { @LetterboxHorizontalReachabilityPosition final int position; @@ -916,32 +869,13 @@ public class WindowManagerShellCommand extends ShellCommand { return 0; } - private int runSetLetterboxIsEducationEnabled(PrintWriter pw) throws RemoteException { - String arg = getNextArg(); - final boolean enabled; - switch (arg) { - case "true": - case "1": - enabled = true; - break; - case "false": - case "0": - enabled = false; - break; - default: - getErrPrintWriter().println("Error: expected true, 1, false, 0, but got " + arg); - return -1; - } - - synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setIsEducationEnabled(enabled); - } - return 0; - } - - private int runSetLetterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled(PrintWriter pw) + private int runSetBooleanFlag(PrintWriter pw, Consumer setter) throws RemoteException { String arg = getNextArg(); + if (arg == null) { + getErrPrintWriter().println("Error: expected true, 1, false, 0, but got empty input."); + return -1; + } final boolean enabled; switch (arg) { case "true": @@ -958,30 +892,7 @@ public class WindowManagerShellCommand extends ShellCommand { } synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setIsSplitScreenAspectRatioForUnresizableAppsEnabled(enabled); - } - return 0; - } - - private int runSetTranslucentLetterboxingEnabled(PrintWriter pw) { - String arg = getNextArg(); - final boolean enabled; - switch (arg) { - case "true": - case "1": - enabled = true; - break; - case "false": - case "0": - enabled = false; - break; - default: - getErrPrintWriter().println("Error: expected true, 1, false, 0, but got " + arg); - return -1; - } - - synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setTranslucentLetterboxingOverrideEnabled(enabled); + setter.accept(enabled); } return 0; } @@ -1024,10 +935,12 @@ public class WindowManagerShellCommand extends ShellCommand { runSetLetterboxVerticalPositionMultiplier(pw); break; case "--isHorizontalReachabilityEnabled": - runSetLetterboxIsHorizontalReachabilityEnabled(pw); + runSetBooleanFlag(pw, mLetterboxConfiguration + ::setIsHorizontalReachabilityEnabled); break; case "--isVerticalReachabilityEnabled": - runSetLetterboxIsVerticalReachabilityEnabled(pw); + runSetBooleanFlag(pw, mLetterboxConfiguration + ::setIsVerticalReachabilityEnabled); break; case "--defaultPositionForHorizontalReachability": runSetLetterboxDefaultPositionForHorizontalReachability(pw); @@ -1036,13 +949,23 @@ public class WindowManagerShellCommand extends ShellCommand { runSetLetterboxDefaultPositionForVerticalReachability(pw); break; case "--isEducationEnabled": - runSetLetterboxIsEducationEnabled(pw); + runSetBooleanFlag(pw, mLetterboxConfiguration::setIsEducationEnabled); break; case "--isSplitScreenAspectRatioForUnresizableAppsEnabled": - runSetLetterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled(pw); + runSetBooleanFlag(pw, mLetterboxConfiguration + ::setIsSplitScreenAspectRatioForUnresizableAppsEnabled); break; case "--isTranslucentLetterboxingEnabled": - runSetTranslucentLetterboxingEnabled(pw); + runSetBooleanFlag(pw, mLetterboxConfiguration + ::setTranslucentLetterboxingOverrideEnabled); + break; + case "--isCameraCompatRefreshEnabled": + runSetBooleanFlag(pw, enabled -> mLetterboxConfiguration + .setCameraCompatRefreshEnabled(enabled)); + break; + case "--isCameraCompatRefreshCycleThroughStopEnabled": + runSetBooleanFlag(pw, enabled -> mLetterboxConfiguration + .setCameraCompatRefreshCycleThroughStopEnabled(enabled)); break; default: getErrPrintWriter().println( @@ -1089,27 +1012,34 @@ public class WindowManagerShellCommand extends ShellCommand { mLetterboxConfiguration.resetLetterboxVerticalPositionMultiplier(); break; case "isHorizontalReachabilityEnabled": - mLetterboxConfiguration.getIsHorizontalReachabilityEnabled(); + mLetterboxConfiguration.resetIsHorizontalReachabilityEnabled(); break; case "isVerticalReachabilityEnabled": - mLetterboxConfiguration.getIsVerticalReachabilityEnabled(); + mLetterboxConfiguration.resetIsVerticalReachabilityEnabled(); break; case "defaultPositionForHorizontalReachability": - mLetterboxConfiguration.getDefaultPositionForHorizontalReachability(); + mLetterboxConfiguration.resetDefaultPositionForHorizontalReachability(); break; case "defaultPositionForVerticalReachability": - mLetterboxConfiguration.getDefaultPositionForVerticalReachability(); + mLetterboxConfiguration.resetDefaultPositionForVerticalReachability(); break; case "isEducationEnabled": - mLetterboxConfiguration.getIsEducationEnabled(); + mLetterboxConfiguration.resetIsEducationEnabled(); break; case "isSplitScreenAspectRatioForUnresizableAppsEnabled": mLetterboxConfiguration - .getIsSplitScreenAspectRatioForUnresizableAppsEnabled(); + .resetIsSplitScreenAspectRatioForUnresizableAppsEnabled(); break; case "isTranslucentLetterboxingEnabled": mLetterboxConfiguration.resetTranslucentLetterboxingEnabled(); break; + case "isCameraCompatRefreshEnabled": + mLetterboxConfiguration.resetCameraCompatRefreshEnabled(); + break; + case "isCameraCompatRefreshCycleThroughStopEnabled": + mLetterboxConfiguration + .resetCameraCompatRefreshCycleThroughStopEnabled(); + break; default: getErrPrintWriter().println( "Error: Unrecognized letterbox style option: " + arg); @@ -1211,6 +1141,8 @@ public class WindowManagerShellCommand extends ShellCommand { mLetterboxConfiguration.resetIsEducationEnabled(); mLetterboxConfiguration.resetIsSplitScreenAspectRatioForUnresizableAppsEnabled(); mLetterboxConfiguration.resetTranslucentLetterboxingEnabled(); + mLetterboxConfiguration.resetCameraCompatRefreshEnabled(); + mLetterboxConfiguration.resetCameraCompatRefreshCycleThroughStopEnabled(); } } @@ -1255,6 +1187,12 @@ public class WindowManagerShellCommand extends ShellCommand { pw.println("Is using split screen aspect ratio as aspect ratio for unresizable apps: " + mLetterboxConfiguration .getIsSplitScreenAspectRatioForUnresizableAppsEnabled()); + + pw.println(" Is activity \"refresh\" in camera compatibility treatment enabled: " + + mLetterboxConfiguration.isCameraCompatRefreshEnabled()); + pw.println(" Refresh using \"stopped -> resumed\" cycle: " + + mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()); + pw.println("Background type: " + LetterboxConfiguration.letterboxBackgroundTypeToString( mLetterboxConfiguration.getLetterboxBackgroundType())); @@ -1464,7 +1402,12 @@ 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 new file mode 100644 index 0000000000000..d1234e3de81af --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java @@ -0,0 +1,428 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE; +import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static android.view.Surface.ROTATION_0; +import static android.view.Surface.ROTATION_90; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.servertransaction.ClientTransaction; +import android.app.servertransaction.RefreshCallbackItem; +import android.app.servertransaction.ResumeActivityItem; +import android.content.ComponentName; +import android.content.pm.ActivityInfo.ScreenOrientation; +import android.content.res.Configuration; +import android.content.res.Configuration.Orientation; +import android.hardware.camera2.CameraManager; +import android.os.Handler; +import android.platform.test.annotations.Presubmit; +import android.view.Display; +import android.view.Surface.Rotation; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.Executor; + +/** + * Tests for {@link DisplayRotationCompatPolicy}. + * + * Build/Install/Run: + * atest WmTests:DisplayRotationCompatPolicyTests + */ +@SmallTest +@Presubmit +@RunWith(WindowTestRunner.class) +public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { + + private static final String TEST_PACKAGE_1 = "com.test.package.one"; + private static final String TEST_PACKAGE_2 = "com.test.package.two"; + private static final String CAMERA_ID_1 = "camera-1"; + private static final String CAMERA_ID_2 = "camera-2"; + + private CameraManager mMockCameraManager; + private Handler mMockHandler; + private LetterboxConfiguration mLetterboxConfiguration; + + private DisplayRotationCompatPolicy mDisplayRotationCompatPolicy; + private CameraManager.AvailabilityCallback mCameraAvailabilityCallback; + + private ActivityRecord mActivity; + private Task mTask; + + @Before + public void setUp() throws Exception { + mLetterboxConfiguration = mDisplayContent.mWmService.mLetterboxConfiguration; + spyOn(mLetterboxConfiguration); + when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled( + /* checkDeviceConfig */ anyBoolean())) + .thenReturn(true); + when(mLetterboxConfiguration.isCameraCompatRefreshEnabled()) + .thenReturn(true); + when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) + .thenReturn(true); + + mMockCameraManager = mock(CameraManager.class); + doAnswer(invocation -> { + mCameraAvailabilityCallback = invocation.getArgument(1); + return null; + }).when(mMockCameraManager).registerAvailabilityCallback( + any(Executor.class), any(CameraManager.AvailabilityCallback.class)); + + spyOn(mContext); + when(mContext.getSystemService(CameraManager.class)).thenReturn(mMockCameraManager); + + spyOn(mDisplayContent); + + mDisplayContent.setIgnoreOrientationRequest(true); + + mMockHandler = mock(Handler.class); + + when(mMockHandler.postDelayed(any(Runnable.class), anyLong())).thenAnswer( + invocation -> { + ((Runnable) invocation.getArgument(0)).run(); + return null; + }); + mDisplayRotationCompatPolicy = new DisplayRotationCompatPolicy( + mDisplayContent, mMockHandler); + } + + @Test + public void testTreatmentNotEnabled_noForceRotationOrRefresh() throws Exception { + when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled( + /* checkDeviceConfig */ anyBoolean())) + .thenReturn(false); + + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + assertEquals(mDisplayRotationCompatPolicy.getOrientation(), + SCREEN_ORIENTATION_UNSPECIFIED); + + assertNoForceRotationOrRefresh(); + } + + @Test + public void testTreatmentDisabledViaDeviceConfig_noForceRotationOrRefresh() throws Exception { + when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled( + /* checkDeviceConfig */ true)) + .thenReturn(false); + + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + assertNoForceRotationOrRefresh(); + } + + @Test + public void testMultiWindowMode_returnUnspecified_noForceRotationOrRefresh() throws Exception { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + final TestSplitOrganizer organizer = new TestSplitOrganizer(mAtm, mDisplayContent); + mActivity.getTask().reparent(organizer.mPrimary, WindowContainer.POSITION_TOP, + false /* moveParents */, "test" /* reason */); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + assertTrue(mActivity.inMultiWindowMode()); + assertNoForceRotationOrRefresh(); + } + + @Test + public void testOrientationUnspecified_noForceRotationOrRefresh() throws Exception { + configureActivity(SCREEN_ORIENTATION_UNSPECIFIED); + + assertNoForceRotationOrRefresh(); + } + + @Test + public void testOrientationLocked_noForceRotationOrRefresh() throws Exception { + configureActivity(SCREEN_ORIENTATION_LOCKED); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + assertNoForceRotationOrRefresh(); + } + + @Test + public void testOrientationNoSensor_noForceRotationOrRefresh() throws Exception { + configureActivity(SCREEN_ORIENTATION_NOSENSOR); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + assertNoForceRotationOrRefresh(); + } + + @Test + public void testIgnoreOrientationRequestIsFalse_noForceRotationOrRefresh() throws Exception { + mDisplayContent.setIgnoreOrientationRequest(false); + + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + assertNoForceRotationOrRefresh(); + } + + @Test + public void testDisplayNotInternal_noForceRotationOrRefresh() throws Exception { + Display display = mDisplayContent.getDisplay(); + spyOn(display); + + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + when(display.getType()).thenReturn(Display.TYPE_EXTERNAL); + assertNoForceRotationOrRefresh(); + + when(display.getType()).thenReturn(Display.TYPE_WIFI); + assertNoForceRotationOrRefresh(); + + when(display.getType()).thenReturn(Display.TYPE_OVERLAY); + assertNoForceRotationOrRefresh(); + + when(display.getType()).thenReturn(Display.TYPE_VIRTUAL); + assertNoForceRotationOrRefresh(); + } + + @Test + public void testNoCameraConnection_noForceRotationOrRefresh() throws Exception { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + + assertNoForceRotationOrRefresh(); + } + + @Test + public void testCameraReconnected_forceRotationAndRefresh() throws Exception { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1); + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true); + + assertEquals(mDisplayRotationCompatPolicy.getOrientation(), + SCREEN_ORIENTATION_PORTRAIT); + assertActivityRefreshRequested(/* refreshRequested */ true); + } + + @Test + public void testReconnectedToDifferentCamera_forceRotationAndRefresh() throws Exception { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1); + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_2, TEST_PACKAGE_1); + callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true); + + assertEquals(mDisplayRotationCompatPolicy.getOrientation(), + SCREEN_ORIENTATION_PORTRAIT); + assertActivityRefreshRequested(/* refreshRequested */ true); + } + + @Test + public void testGetOrientation_cameraConnectionClosed_returnUnspecified() { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + assertEquals(mDisplayRotationCompatPolicy.getOrientation(), + SCREEN_ORIENTATION_PORTRAIT); + + mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1); + + assertEquals(mDisplayRotationCompatPolicy.getOrientation(), + SCREEN_ORIENTATION_UNSPECIFIED); + } + + @Test + public void testCameraOpenedForDifferentPackage_noForceRotationOrRefresh() throws Exception { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_2); + + assertNoForceRotationOrRefresh(); + } + + @Test + public void testGetOrientation_portraitActivity_portraitNaturalOrientation_returnPortrait() { + testGetOrientationForActivityAndNaturalOrientations( + /* activityOrientation */ SCREEN_ORIENTATION_PORTRAIT, + /* naturalOrientation */ ORIENTATION_PORTRAIT, + /* expectedOrientation */ SCREEN_ORIENTATION_PORTRAIT); + } + + @Test + public void testGetOrientation_portraitActivity_landscapeNaturalOrientation_returnLandscape() { + testGetOrientationForActivityAndNaturalOrientations( + /* activityOrientation */ SCREEN_ORIENTATION_PORTRAIT, + /* naturalOrientation */ ORIENTATION_LANDSCAPE, + /* expectedOrientation */ SCREEN_ORIENTATION_LANDSCAPE); + } + + @Test + public void testGetOrientation_landscapeActivity_portraitNaturalOrientation_returnLandscape() { + testGetOrientationForActivityAndNaturalOrientations( + /* activityOrientation */ SCREEN_ORIENTATION_LANDSCAPE, + /* naturalOrientation */ ORIENTATION_PORTRAIT, + /* expectedOrientation */ SCREEN_ORIENTATION_LANDSCAPE); + } + + @Test + public void testGetOrientation_landscapeActivity_landscapeNaturalOrientation_returnPortrait() { + testGetOrientationForActivityAndNaturalOrientations( + /* activityOrientation */ SCREEN_ORIENTATION_LANDSCAPE, + /* naturalOrientation */ ORIENTATION_LANDSCAPE, + /* expectedOrientation */ SCREEN_ORIENTATION_PORTRAIT); + } + + private void testGetOrientationForActivityAndNaturalOrientations( + @ScreenOrientation int activityOrientation, + @Orientation int naturalOrientation, + @ScreenOrientation int expectedOrientation) { + configureActivityAndDisplay(activityOrientation, naturalOrientation); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + assertEquals(mDisplayRotationCompatPolicy.getOrientation(), + expectedOrientation); + } + + @Test + public void testOnActivityConfigurationChanging_refreshDisabled_noRefresh() throws Exception { + when(mLetterboxConfiguration.isCameraCompatRefreshEnabled()).thenReturn(false); + + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true); + + assertActivityRefreshRequested(/* refreshRequested */ false); + } + + @Test + public void testOnActivityConfigurationChanging_displayRotationNotChanging_noRefresh() + throws Exception { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ false); + + assertActivityRefreshRequested(/* refreshRequested */ false); + } + + @Test + public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() throws Exception { + when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) + .thenReturn(false); + + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true); + + assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false); + } + + private void configureActivity(@ScreenOrientation int activityOrientation) { + configureActivityAndDisplay(activityOrientation, ORIENTATION_PORTRAIT); + } + + private void configureActivityAndDisplay(@ScreenOrientation int activityOrientation, + @Orientation int naturalOrientation) { + + mTask = new TaskBuilder(mSupervisor) + .setDisplay(mDisplayContent) + .build(); + + mActivity = new ActivityBuilder(mAtm) + .setComponent(new ComponentName(TEST_PACKAGE_1, ".TestActivity")) + .setScreenOrientation(activityOrientation) + .setTask(mTask) + .build(); + + spyOn(mActivity.mAtmService.getLifecycleManager()); + spyOn(mActivity.mLetterboxUiController); + + doReturn(mActivity).when(mDisplayContent).topRunningActivity(anyBoolean()); + doReturn(naturalOrientation).when(mDisplayContent).getNaturalOrientation(); + } + + private void assertActivityRefreshRequested(boolean refreshRequested) throws Exception { + assertActivityRefreshRequested(refreshRequested, /* cycleThroughStop*/ true); + } + + private void assertActivityRefreshRequested(boolean refreshRequested, + boolean cycleThroughStop) throws Exception { + verify(mActivity.mLetterboxUiController, times(refreshRequested ? 1 : 0)) + .setIsRefreshAfterRotationRequested(true); + + final ClientTransaction transaction = ClientTransaction.obtain( + mActivity.app.getThread(), mActivity.token); + transaction.addCallback(RefreshCallbackItem.obtain(cycleThroughStop ? ON_STOP : ON_PAUSE)); + transaction.setLifecycleStateRequest(ResumeActivityItem.obtain(/* isForward */ false)); + + verify(mActivity.mAtmService.getLifecycleManager(), times(refreshRequested ? 1 : 0)) + .scheduleTransaction(eq(transaction)); + } + + private void assertNoForceRotationOrRefresh() throws Exception { + callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true); + + assertEquals(mDisplayRotationCompatPolicy.getOrientation(), + SCREEN_ORIENTATION_UNSPECIFIED); + assertActivityRefreshRequested(/* refreshRequested */ false); + } + + private void callOnActivityConfigurationChanging( + ActivityRecord activity, boolean isDisplayRotationChanging) { + mDisplayRotationCompatPolicy.onActivityConfigurationChanging(activity, + /* newConfig */ createConfigurationWithDisplayRotation(ROTATION_0), + /* newConfig */ createConfigurationWithDisplayRotation( + isDisplayRotationChanging ? ROTATION_90 : ROTATION_0)); + } + + private static Configuration createConfigurationWithDisplayRotation(@Rotation int rotation) { + final Configuration config = new Configuration(); + config.windowConfiguration.setDisplayRotation(rotation); + return config; + } +}