Merge "Per-app compat treatment for setRequestedOrientation loops." into tm-qpr-dev am: 300cd48c7a am: ead0f1a138

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/20838889

Change-Id: I00c50dfe4b723e55eea6374dfbbadf56533ef18c
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Mariia Sandrikova
2023-01-04 17:08:21 +00:00
committed by Automerger Merge Worker
9 changed files with 368 additions and 8 deletions

View File

@@ -1065,6 +1065,19 @@ public class ActivityInfo extends ComponentInfo implements Parcelable {
@Retention(RetentionPolicy.SOURCE)
public @interface SizeChangesSupportMode {}
/**
* This change id enables compat policy that ignores app requested orientation in
* response to an app calling {@link android.app.Activity#setRequestedOrientation}. See
* com.android.server.wm.LetterboxUiController#shouldIgnoreRequestedOrientation for
* details.
* @hide
*/
@ChangeId
@Overridable
@Disabled
public static final long OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION =
254631730L; // buganizer id
/**
* This change id forces the packages it is applied to never have Display API sandboxing
* applied for a letterbox or SCM activity. The Display APIs will continue to provide

View File

@@ -813,6 +813,45 @@ public interface WindowManager extends ViewManager {
int SCREENSHOT_VENDOR_GESTURE = 6;
}
/**
* Activity level {@link android.content.pm.PackageManager.Property PackageManager
* .Property} for an app to inform the system that the activity can be opted-in or opted-out
* from the compatibility treatment that avoids {@link
* android.app.Activity#setRequestedOrientation} loops. The loop can be trigerred by
* ignoreRequestedOrientation display setting enabled on the device or by the landscape natural
* orientation of the device.
*
* <p>The treatment is disabled by default but device manufacturers can enable the treatment
* using their discretion to improve display compatibility.
*
* <p>With this property set to {@code true}, the system could ignore {@link
* android.app.Activity#setRequestedOrientation} call from an app if one of the following
* conditions are true:
* <ul>
* <li>Activity is relaunching due to the previous {@link
* android.app.Activity#setRequestedOrientation} call.
* <li>Camera compatibility force rotation treatment is active for the package.
* </ul>
*
* <p>Setting this property to {@code false} informs the system that the activity must be
* opted-out from the compatibility treatment even if the device manufacturer has opted the app
* into the treatment.
*
* <p><b>Syntax:</b>
* <pre>
* &lt;activity&gt;
* &lt;property
* android:name="android.window.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION"
* android:value="true|false"/&gt;
* &lt;/activity&gt;
* </pre>
*
* @hide
*/
// TODO(b/263984287): Make this public API.
String PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION =
"android.window.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION";
/**
* @hide
*/

View File

@@ -5360,6 +5360,11 @@
If given value is outside of this range, the option 0 (top) is assummed. -->
<integer name="config_letterboxDefaultPositionForTabletopModeReachability">0</integer>
<!-- Whether should ignore app requested orientation in response to an app
calling Activity#setRequestedOrientation. See
LetterboxUiController#shouldIgnoreRequestedOrientation for details. -->
<bool name="config_letterboxIsPolicyForIgnoringRequestedOrientationEnabled">false</bool>
<!-- Whether displaying letterbox education is enabled for letterboxed fullscreen apps. -->
<bool name="config_letterboxIsEducationEnabled">false</bool>

View File

@@ -4430,6 +4430,7 @@
<java-symbol type="integer" name="config_letterboxDefaultPositionForVerticalReachability" />
<java-symbol type="integer" name="config_letterboxDefaultPositionForBookModeReachability" />
<java-symbol type="integer" name="config_letterboxDefaultPositionForTabletopModeReachability" />
<java-symbol type="bool" name="config_letterboxIsPolicyForIgnoringRequestedOrientationEnabled" />
<java-symbol type="bool" name="config_letterboxIsEducationEnabled" />
<java-symbol type="dimen" name="config_letterboxDefaultMinAspectRatioForUnresizableApps" />
<java-symbol type="bool" name="config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled" />

View File

@@ -1451,8 +1451,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
updatePictureInPictureMode(null, false);
} else {
mLastReportedMultiWindowMode = inMultiWindowMode;
ensureActivityConfiguration(0 /* globalChanges */, PRESERVE_WINDOWS,
false /* ignoreVisibility */);
ensureActivityConfiguration(0 /* globalChanges */, PRESERVE_WINDOWS);
}
}
}
@@ -3981,6 +3980,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
}
void finishRelaunching() {
mLetterboxUiController.setRelauchingAfterRequestedOrientationChanged(false);
mTaskSupervisor.getActivityMetricsLogger().notifyActivityRelaunched(this);
if (mPendingRelaunchCount > 0) {
@@ -7724,13 +7724,17 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
}
void setRequestedOrientation(int requestedOrientation) {
if (mLetterboxUiController.shouldIgnoreRequestedOrientation(requestedOrientation)) {
return;
}
setOrientation(requestedOrientation, this);
// Push the new configuration to the requested app in case where it's not pushed, e.g. when
// the request is handled at task level with letterbox.
if (!getMergedOverrideConfiguration().equals(
mLastReportedConfiguration.getMergedConfiguration())) {
ensureActivityConfiguration(0 /* globalChanges */, false /* preserveWindow */);
ensureActivityConfiguration(0 /* globalChanges */, false /* preserveWindow */,
false /* ignoreVisibility */, true /* isRequestedOrientationChanged */);
}
mAtmService.getTaskChangeNotificationController().notifyActivityRequestedOrientationChanged(
@@ -9060,7 +9064,13 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
boolean ensureActivityConfiguration(int globalChanges, boolean preserveWindow) {
return ensureActivityConfiguration(globalChanges, preserveWindow,
false /* ignoreVisibility */);
false /* ignoreVisibility */, false /* isRequestedOrientationChanged */);
}
boolean ensureActivityConfiguration(int globalChanges, boolean preserveWindow,
boolean ignoreVisibility) {
return ensureActivityConfiguration(globalChanges, preserveWindow, ignoreVisibility,
false /* isRequestedOrientationChanged */);
}
/**
@@ -9074,11 +9084,13 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
* (stopped state). This is useful for the case where we know the
* activity will be visible soon and we want to ensure its configuration
* before we make it visible.
* @param isRequestedOrientationChanged whether this is triggered in response to an app calling
* {@link android.app.Activity#setRequestedOrientation}.
* @return False if the activity was relaunched and true if it wasn't relaunched because we
* can't or the app handles the specific configuration that is changing.
*/
boolean ensureActivityConfiguration(int globalChanges, boolean preserveWindow,
boolean ignoreVisibility) {
boolean ignoreVisibility, boolean isRequestedOrientationChanged) {
final Task rootTask = getRootTask();
if (rootTask.mConfigWillChange) {
ProtoLog.v(WM_DEBUG_CONFIGURATION, "Skipping config check "
@@ -9202,6 +9214,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
} else {
mRelaunchReason = RELAUNCH_REASON_NONE;
}
if (isRequestedOrientationChanged) {
mLetterboxUiController.setRelauchingAfterRequestedOrientationChanged(true);
}
if (mState == PAUSING) {
// A little annoying: we are waiting for this activity to finish pausing. Let's not
// do anything now, but just flag that it needs to be restarted when done pausing.

View File

@@ -287,7 +287,7 @@ final class DisplayRotationCompatPolicy {
* <li>The activity has fixed orientation but not "locked" or "nosensor" one.
* </ul>
*/
private boolean isTreatmentEnabledForActivity(@Nullable ActivityRecord activity) {
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

View File

@@ -222,6 +222,11 @@ final class LetterboxConfiguration {
// See RefreshCallbackItem for context.
private boolean mIsCameraCompatRefreshCycleThroughStopEnabled = true;
// Whether should ignore app requested orientation in response to an app
// calling Activity#setRequestedOrientation. See
// LetterboxUiController#shouldIgnoreRequestedOrientation for details.
private final boolean mIsPolicyForIgnoringRequestedOrientationEnabled;
LetterboxConfiguration(Context systemUiContext) {
this(systemUiContext, new LetterboxConfigurationPersister(systemUiContext,
() -> readLetterboxHorizontalReachabilityPositionFromConfig(systemUiContext,
@@ -274,10 +279,13 @@ final class LetterboxConfiguration {
R.bool.config_letterboxIsEnabledForTranslucentActivities);
mIsCameraCompatTreatmentEnabled = mContext.getResources().getBoolean(
R.bool.config_isWindowManagerCameraCompatTreatmentEnabled);
mIsCompatFakeFocusEnabled = mContext.getResources().getBoolean(
R.bool.config_isCompatFakeFocusEnabled);
mIsPolicyForIgnoringRequestedOrientationEnabled = mContext.getResources().getBoolean(
R.bool.config_letterboxIsPolicyForIgnoringRequestedOrientationEnabled);
mLetterboxConfigurationPersister = letterboxConfigurationPersister;
mLetterboxConfigurationPersister.start();
mIsCompatFakeFocusEnabled = mContext.getResources()
.getBoolean(R.bool.config_isCompatFakeFocusEnabled);
}
/**
@@ -1034,6 +1042,15 @@ final class LetterboxConfiguration {
mIsCompatFakeFocusEnabled = enabled;
}
/**
* Whether should ignore app requested orientation in response to an app calling
* {@link android.app.Activity#setRequestedOrientation}. See {@link
* LetterboxUiController#shouldIgnoreRequestedOrientation} for details.
*/
boolean isPolicyForIgnoringRequestedOrientationEnabled() {
return mIsPolicyForIgnoringRequestedOrientationEnabled;
}
/** Whether camera compatibility treatment is enabled. */
boolean isCameraCompatTreatmentEnabled(boolean checkDeviceConfig) {
return mIsCameraCompatTreatmentEnabled

View File

@@ -17,10 +17,13 @@
package com.android.server.wm;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
import static android.content.pm.ActivityInfo.screenOrientationToString;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION;
import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__BOTTOM;
import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER;
@@ -55,6 +58,8 @@ import static com.android.server.wm.LetterboxConfiguration.letterboxBackgroundTy
import android.annotation.Nullable;
import android.app.ActivityManager.TaskDescription;
import android.content.pm.ActivityInfo.ScreenOrientation;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Color;
@@ -74,6 +79,7 @@ import com.android.internal.statusbar.LetterboxDetails;
import com.android.server.wm.LetterboxConfiguration.LetterboxBackgroundType;
import java.io.PrintWriter;
import java.util.function.BooleanSupplier;
/** Controls behaviour of the letterbox UI for {@link mActivityRecord}. */
// TODO(b/185262487): Improve test coverage of this class. Parts of it are tested in
@@ -131,12 +137,37 @@ final class LetterboxUiController {
// DisplayRotationCompatPolicy.
private boolean mIsRefreshAfterRotationRequested;
@Nullable
private final Boolean mBooleanPropertyIgnoreRequestedOrientation;
private boolean mIsRelauchingAfterRequestedOrientationChanged;
LetterboxUiController(WindowManagerService wmService, ActivityRecord activityRecord) {
mLetterboxConfiguration = wmService.mLetterboxConfiguration;
// Given activityRecord may not be fully constructed since LetterboxUiController
// is created in its constructor. It shouldn't be used in this constructor but it's safe
// to use it after since controller is only used in ActivityRecord.
mActivityRecord = activityRecord;
PackageManager packageManager = wmService.mContext.getPackageManager();
mBooleanPropertyIgnoreRequestedOrientation =
readComponentProperty(packageManager, mActivityRecord.packageName,
mLetterboxConfiguration::isPolicyForIgnoringRequestedOrientationEnabled,
PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION);
}
@Nullable
private static Boolean readComponentProperty(PackageManager packageManager, String packageName,
BooleanSupplier gatingCondition, String propertyName) {
if (!gatingCondition.getAsBoolean()) {
return null;
}
try {
return packageManager.getProperty(propertyName, packageName).getBoolean();
} catch (PackageManager.NameNotFoundException e) {
// No such property name.
}
return null;
}
/** Cleans up {@link Letterbox} if it exists.*/
@@ -153,6 +184,72 @@ final class LetterboxUiController {
}
}
/**
* Whether should ignore app requested orientation in response to an app
* calling {@link android.app.Activity#setRequestedOrientation}.
*
* <p>This is needed to avoid getting into {@link android.app.Activity#setRequestedOrientation}
* loop when {@link DisplayContent#getIgnoreOrientationRequest} is enabled or device has
* landscape natural orientation which app developers don't expect. For example, the loop can
* look like this:
* <ol>
* <li>App sets default orientation to "unspecified" at runtime
* <li>App requests to "portrait" after checking some condition (e.g. display rotation).
* <li>(2) leads to fullscreen -> letterboxed bounds change and activity relaunch because
* app can't handle the corresponding config changes.
* <li>Loop goes back to (1)
* </ol>
*
* <p>This treatment is enabled when the following conditions are met:
* <ul>
* <li>Flag gating the treatment is enabled
* <li>Opt-out component property isn't enabled
* <li>Opt-in component property or per-app override are enabled
* <li>Activity is relaunched after {@link android.app.Activity#setRequestedOrientation}
* call from an app or camera compat force rotation treatment is active for the activity.
* </ul>
*/
boolean shouldIgnoreRequestedOrientation(@ScreenOrientation int requestedOrientation) {
if (!mLetterboxConfiguration.isPolicyForIgnoringRequestedOrientationEnabled()) {
return false;
}
if (Boolean.FALSE.equals(mBooleanPropertyIgnoreRequestedOrientation)) {
return false;
}
if (!Boolean.TRUE.equals(mBooleanPropertyIgnoreRequestedOrientation)
&& !mActivityRecord.info.isChangeEnabled(
OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION)) {
return false;
}
if (mIsRelauchingAfterRequestedOrientationChanged) {
Slog.w(TAG, "Ignoring orientation update to "
+ screenOrientationToString(requestedOrientation)
+ " due to relaunching after setRequestedOrientation for " + mActivityRecord);
return true;
}
DisplayContent displayContent = mActivityRecord.mDisplayContent;
if (displayContent == null) {
return false;
}
if (displayContent.mDisplayRotationCompatPolicy != null
&& displayContent.mDisplayRotationCompatPolicy
.isTreatmentEnabledForActivity(mActivityRecord)) {
Slog.w(TAG, "Ignoring orientation update to "
+ screenOrientationToString(requestedOrientation)
+ " due to camera compat treatment for " + mActivityRecord);
return true;
}
return false;
}
/**
* Sets whether an activity is relaunching after the app has called {@link
* android.app.Activity#setRequestedOrientation}.
*/
void setRelauchingAfterRequestedOrientationChanged(boolean isRelaunching) {
mIsRelauchingAfterRequestedOrientationChanged = isRelaunching;
}
/**
* Whether activity "refresh" was requested but not finished in {@link #activityResumedLocked}
* following the camera compat force rotation in {@link DisplayRotationCompatPolicy}.

View File

@@ -0,0 +1,173 @@
/*
* 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.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import android.compat.testing.PlatformCompatChangeRule;
import android.content.ComponentName;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.Property;
import android.platform.test.annotations.Presubmit;
import androidx.test.filters.SmallTest;
import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
/**
* Test class for {@link LetterboxUiControllerTest}.
*
* Build/Install/Run:
* atest WmTests:LetterboxUiControllerTest
*/
@SmallTest
@Presubmit
@RunWith(WindowTestRunner.class)
public class LetterboxUiControllerTest extends WindowTestsBase {
@Rule
public TestRule compatChangeRule = new PlatformCompatChangeRule();
private ActivityRecord mActivity;
private DisplayContent mDisplayContent;
private LetterboxUiController mController;
private LetterboxConfiguration mLetterboxConfiguration;
@Before
public void setUp() throws Exception {
mActivity = setUpActivityWithComponent();
mLetterboxConfiguration = mWm.mLetterboxConfiguration;
spyOn(mLetterboxConfiguration);
mController = new LetterboxUiController(mWm, mActivity);
}
@Test
@EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
public void testShouldIgnoreRequestedOrientation_activityRelaunching_returnsTrue() {
prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch();
assertTrue(mController.shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED));
}
@Test
@EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
public void testShouldIgnoreRequestedOrientation_cameraCompatTreatment_returnsTrue() {
doReturn(true).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled(anyBoolean());
// Recreate DisplayContent with DisplayRotationCompatPolicy
mActivity = setUpActivityWithComponent();
mController = new LetterboxUiController(mWm, mActivity);
prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch();
mController.setRelauchingAfterRequestedOrientationChanged(false);
spyOn(mDisplayContent.mDisplayRotationCompatPolicy);
doReturn(true).when(mDisplayContent.mDisplayRotationCompatPolicy)
.isTreatmentEnabledForActivity(eq(mActivity));
assertTrue(mController.shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED));
}
@Test
public void testShouldIgnoreRequestedOrientation_overrideDisabled_returnsFalse() {
prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch();
assertFalse(mController.shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED));
}
@Test
public void testShouldIgnoreRequestedOrientation_propertyIsTrue_returnsTrue()
throws Exception {
doReturn(true).when(mLetterboxConfiguration)
.isPolicyForIgnoringRequestedOrientationEnabled();
mockThatProperty(PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION, /* value */ true);
mController = new LetterboxUiController(mWm, mActivity);
prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch();
assertTrue(mController.shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED));
}
@Test
@EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
public void testShouldIgnoreRequestedOrientation_propertyIsFalseAndOverride_returnsFalse()
throws Exception {
doReturn(true).when(mLetterboxConfiguration)
.isPolicyForIgnoringRequestedOrientationEnabled();
mockThatProperty(PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION, /* value */ false);
mController = new LetterboxUiController(mWm, mActivity);
prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch();
assertFalse(mController.shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED));
}
@Test
@EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION})
public void testShouldIgnoreRequestedOrientation_flagIsDisabled_returnsFalse() {
prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch();
doReturn(false).when(mLetterboxConfiguration)
.isPolicyForIgnoringRequestedOrientationEnabled();
assertFalse(mController.shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED));
}
private void mockThatProperty(String propertyName, boolean value) throws Exception {
Property property = new Property(propertyName, /* value */ value, /* packageName */ "",
/* className */ "");
PackageManager pm = mWm.mContext.getPackageManager();
spyOn(pm);
doReturn(property).when(pm).getProperty(eq(propertyName), anyString());
}
private void prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch() {
doReturn(true).when(mLetterboxConfiguration)
.isPolicyForIgnoringRequestedOrientationEnabled();
mController.setRelauchingAfterRequestedOrientationChanged(true);
}
private ActivityRecord setUpActivityWithComponent() {
mDisplayContent = new TestDisplayContent
.Builder(mAtm, /* dw */ 1000, /* dh */ 2000).build();
Task task = new TaskBuilder(mSupervisor).setDisplay(mDisplayContent).build();
final ActivityRecord activity = new ActivityBuilder(mAtm)
.setOnTop(true)
.setTask(task)
// Set the component to be that of the test class in order to enable compat changes
.setComponent(ComponentName.createRelative(mContext,
com.android.server.wm.LetterboxUiControllerTest.class.getName()))
.build();
return activity;
}
}