Merge changes from topic "camera-compat-force-rotation" into tm-qpr-dev

* changes:
  [3/n] Camera Compat: Refresh activity
  [2/n] Camera Compat: Add DeviceConfig flag
  [1/n] Camera Compat: Force rotate activities
This commit is contained in:
Mariia Sandrikova
2022-12-22 04:39:22 +00:00
committed by Android (Google) Code Review
20 changed files with 1301 additions and 114 deletions

View File

@@ -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.

View File

@@ -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;

View File

@@ -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);

View File

@@ -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.

View File

@@ -38,6 +38,9 @@ public abstract class ClientTransactionItem implements BaseClientRequest, Parcel
return UNDEFINED;
}
boolean shouldHaveDefinedPreExecutionState() {
return true;
}
// Parcelable

View File

@@ -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];
}
};
}

View File

@@ -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);

View File

@@ -5344,6 +5344,12 @@
TODO(b/255532890) Enable when ignoreOrientationRequest is set -->
<bool name="config_letterboxIsEnabledForTranslucentActivities">false</bool>
<!-- Whether camera compat treatment is enabled for issues caused by orientation mismatch
between camera buffers and an app window. This includes force rotation of fixed
orientation activities connected to the camera in fullscreen and showing a tooltip in
split screen. -->
<bool name="config_isWindowManagerCameraCompatTreatmentEnabled">false</bool>
<!-- Whether a camera compat controller is enabled to allow the user to apply or revert
treatment for stretched issues in camera viewfinder. -->
<bool name="config_isCameraCompatControlForStretchedIssuesEnabled">false</bool>

View File

@@ -4460,6 +4460,7 @@
<java-symbol type="bool" name="config_letterboxIsEducationEnabled" />
<java-symbol type="dimen" name="config_letterboxDefaultMinAspectRatioForUnresizableApps" />
<java-symbol type="bool" name="config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled" />
<java-symbol type="bool" name="config_isWindowManagerCameraCompatTreatmentEnabled" />
<java-symbol type="bool" name="config_isCameraCompatControlForStretchedIssuesEnabled" />
<java-symbol type="bool" name="config_hideDisplayCutoutWithDisplayArea" />

View File

@@ -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",

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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();

View File

@@ -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<ISystemGestureExclusionListener>
@@ -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. */

View File

@@ -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);
}
}
}

View File

@@ -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.
*
* <p>This includes force rotation of fixed orientation activities connected to the camera.
*
* <p>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<String> mScheduledToBeRemovedCameraIdSet = new ArraySet<>();
@GuardedBy("this")
private final Set<String> 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.
*
* <p>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.
*
* <p>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.
*
* <p>Conditions that need to be met:
* <ul>
* <li>{@code R.bool.config_isWindowManagerCameraCompatTreatmentEnabled} is {@code true}.
* <li>Setting {@code ignoreOrientationRequest} is enabled for the display.
* <li>Associated {@link DisplayContent} is for internal display. See b/225928882
* that tracks supporting external displays in the future.
* </ul>
*/
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.
*
* <p>Conditions that need to be met:
* <ul>
* <li>{@link #isCameraActiveForPackage} is {@code true} for the activity.
* <li>The activity is in fullscreen
* <li>The activity has fixed orientation but not "locked" or "nosensor" one.
* </ul>
*/
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<String, String> mPackageToCameraIdMap = new ArrayMap<>();
private final Map<String, String> 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);
}
}
}

View File

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

View File

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

View File

@@ -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<Boolean> 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");

View File

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