diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index 226278cbe44d4..319f1298a87fb 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -1034,6 +1034,20 @@ public class ActivityInfo extends ComponentInfo implements Parcelable {
public static final long OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION =
254631730L; // buganizer id
+ /**
+ * This change id enables compat policy that ignores app requested orientation in
+ * response to an app calling {@link android.app.Activity#setRequestedOrientation} more
+ * than twice in one second if an activity is not letterboxed for fixed orientation.
+ * See com.android.server.wm.LetterboxUiController#shouldIgnoreRequestedOrientation
+ * for details.
+ * @hide
+ */
+ @ChangeId
+ @Overridable
+ @Disabled
+ public static final long OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED =
+ 273509367L; // 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
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index 6bffc4c95e647..4f0f22732136f 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -22,6 +22,7 @@ import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FOR
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH;
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS;
+import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED;
import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION;
import static android.content.pm.ActivityInfo.OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE;
import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA;
@@ -125,6 +126,14 @@ final class LetterboxUiController {
private static final float UNDEFINED_ASPECT_RATIO = 0f;
+ // Minimum value of mSetOrientationRequestCounter before qualifying as orientation request loop
+ @VisibleForTesting
+ static final int MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP = 2;
+ // Used to determine reset of mSetOrientationRequestCounter if next app requested
+ // orientation is after timeout value
+ @VisibleForTesting
+ static final int SET_ORIENTATION_REQUEST_COUNTER_TIMEOUT_MS = 1000;
+
private final Point mTmpPoint = new Point();
private final LetterboxConfiguration mLetterboxConfiguration;
@@ -163,6 +172,8 @@ final class LetterboxUiController {
// Corresponds to OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION
private final boolean mIsOverrideEnableCompatIgnoreRequestedOrientationEnabled;
+ // Corresponds to OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED
+ private final boolean mIsOverrideEnableCompatIgnoreOrientationRequestWhenLoopDetectedEnabled;
// Corresponds to OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS
private final boolean mIsOverrideEnableCompatFakeFocusEnabled;
@@ -187,12 +198,18 @@ final class LetterboxUiController {
private float mInheritedMinAspectRatio = UNDEFINED_ASPECT_RATIO;
private float mInheritedMaxAspectRatio = UNDEFINED_ASPECT_RATIO;
+ // Updated when ActivityRecord#setRequestedOrientation is called
+ private long mTimeMsLastSetOrientationRequest = 0;
+
@Configuration.Orientation
private int mInheritedOrientation = Configuration.ORIENTATION_UNDEFINED;
// The app compat state for the opaque activity if any
private int mInheritedAppCompatState = APP_COMPAT_STATE_CHANGED__STATE__UNKNOWN;
+ // Counter for ActivityRecord#setRequestedOrientation
+ private int mSetOrientationRequestCounter = 0;
+
// The CompatDisplayInsets of the opaque activity beneath the translucent one.
private ActivityRecord.CompatDisplayInsets mInheritedCompatDisplayInsets;
@@ -288,6 +305,9 @@ final class LetterboxUiController {
mIsOverrideEnableCompatIgnoreRequestedOrientationEnabled =
isCompatChangeEnabled(OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION);
+ mIsOverrideEnableCompatIgnoreOrientationRequestWhenLoopDetectedEnabled =
+ isCompatChangeEnabled(
+ OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED);
mIsOverrideEnableCompatFakeFocusEnabled =
isCompatChangeEnabled(OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS);
@@ -354,37 +374,87 @@ final class LetterboxUiController {
*
Opt-in component property or per-app override are enabled
* Activity is relaunched after {@link android.app.Activity#setRequestedOrientation}
* call from an app or camera compat force rotation treatment is active for the activity.
+ * Orientation request loop detected and is not letterboxed for fixed orientation
*
*/
boolean shouldIgnoreRequestedOrientation(@ScreenOrientation int requestedOrientation) {
- if (!shouldEnableWithOverrideAndProperty(
+ if (shouldEnableWithOverrideAndProperty(
/* gatingCondition */ mLetterboxConfiguration
::isPolicyForIgnoringRequestedOrientationEnabled,
mIsOverrideEnableCompatIgnoreRequestedOrientationEnabled,
mBooleanPropertyIgnoreRequestedOrientation)) {
- 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;
+ }
}
- if (mIsRelauchingAfterRequestedOrientationChanged) {
+
+ if (shouldIgnoreOrientationRequestLoop()) {
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);
+ + " as orientation request loop was detected for "
+ + mActivityRecord);
return true;
}
return false;
}
+ /**
+ * Whether an app is calling {@link android.app.Activity#setRequestedOrientation}
+ * in a loop and orientation request should be ignored.
+ *
+ * This should only be called once in response to
+ * {@link android.app.Activity#setRequestedOrientation}. See
+ * {@link #shouldIgnoreRequestedOrientation} for more details.
+ *
+ *
This treatment is enabled when the following conditions are met:
+ *
+ * - Per-app override is enabled
+ *
- App has requested orientation more than 2 times within 1-second
+ * timer and activity is not letterboxed for fixed orientation
+ *
+ */
+ @VisibleForTesting
+ boolean shouldIgnoreOrientationRequestLoop() {
+ if (!mIsOverrideEnableCompatIgnoreOrientationRequestWhenLoopDetectedEnabled) {
+ return false;
+ }
+
+ final long currTimeMs = System.currentTimeMillis();
+ if (currTimeMs - mTimeMsLastSetOrientationRequest
+ < SET_ORIENTATION_REQUEST_COUNTER_TIMEOUT_MS) {
+ mSetOrientationRequestCounter += 1;
+ } else {
+ // Resets app setOrientationRequest counter if timed out
+ mSetOrientationRequestCounter = 0;
+ }
+ // Update time last called
+ mTimeMsLastSetOrientationRequest = currTimeMs;
+
+ return mSetOrientationRequestCounter >= MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP
+ && !mActivityRecord.isLetterboxedForFixedOrientationAndAspectRatio();
+ }
+
+ @VisibleForTesting
+ int getSetOrientationRequestCounter() {
+ return mSetOrientationRequestCounter;
+ }
+
/**
* Whether sending compat fake focus for split screen resumed activities is enabled. Needed
* because some game engines wait to get focus before drawing the content of the app which isn't
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
index 693f1ced78ddc..23a3d520ea3e6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
@@ -21,6 +21,7 @@ import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FOR
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH;
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS;
+import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED;
import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION;
import static android.content.pm.ActivityInfo.OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE;
import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA;
@@ -46,6 +47,8 @@ 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.mock;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.server.wm.LetterboxUiController.MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP;
+import static com.android.server.wm.LetterboxUiController.SET_ORIENTATION_REQUEST_COUNTER_TIMEOUT_MS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -184,6 +187,69 @@ public class LetterboxUiControllerTest extends WindowTestsBase {
assertFalse(mController.shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED));
}
+ @Test
+ public void testShouldIgnoreOrientationRequestLoop_overrideDisabled_returnsFalse() {
+ doReturn(false).when(mActivity).isLetterboxedForFixedOrientationAndAspectRatio();
+ // Request 3 times to simulate orientation request loop
+ for (int i = 0; i <= MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP; i++) {
+ assertShouldIgnoreOrientationRequestLoop(/* shouldIgnore */ false,
+ /* expectedCount */ 0);
+ }
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED})
+ public void testShouldIgnoreOrientationRequestLoop_isLetterboxed_returnsFalse() {
+ doReturn(true).when(mActivity).isLetterboxedForFixedOrientationAndAspectRatio();
+ // Request 3 times to simulate orientation request loop
+ for (int i = 0; i <= MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP; i++) {
+ assertShouldIgnoreOrientationRequestLoop(/* shouldIgnore */ false,
+ /* expectedCount */ i);
+ }
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED})
+ public void testShouldIgnoreOrientationRequestLoop_noLoop_returnsFalse() {
+ doReturn(false).when(mActivity).isLetterboxedForFixedOrientationAndAspectRatio();
+ // No orientation request loop
+ assertShouldIgnoreOrientationRequestLoop(/* shouldIgnore */ false,
+ /* expectedCount */ 0);
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED})
+ public void testShouldIgnoreOrientationRequestLoop_timeout_returnsFalse()
+ throws InterruptedException {
+ doReturn(false).when(mActivity).isLetterboxedForFixedOrientationAndAspectRatio();
+ for (int i = MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP; i > 0; i--) {
+ assertShouldIgnoreOrientationRequestLoop(/* shouldIgnore */ false,
+ /* expectedCount */ 0);
+ Thread.sleep(SET_ORIENTATION_REQUEST_COUNTER_TIMEOUT_MS);
+ }
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_ORIENTATION_REQUEST_WHEN_LOOP_DETECTED})
+ public void testShouldIgnoreOrientationRequestLoop_returnsTrue() {
+ doReturn(false).when(mActivity).isLetterboxedForFixedOrientationAndAspectRatio();
+ for (int i = 0; i < MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP; i++) {
+ assertShouldIgnoreOrientationRequestLoop(/* shouldIgnore */ false,
+ /* expectedCount */ i);
+ }
+ assertShouldIgnoreOrientationRequestLoop(/* shouldIgnore */ true,
+ /* expectedCount */ MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP);
+ }
+
+ private void assertShouldIgnoreOrientationRequestLoop(boolean shouldIgnore, int expectedCount) {
+ if (shouldIgnore) {
+ assertTrue(mController.shouldIgnoreOrientationRequestLoop());
+ } else {
+ assertFalse(mController.shouldIgnoreOrientationRequestLoop());
+ }
+ assertEquals(expectedCount, mController.getSetOrientationRequestCounter());
+ }
+
@Test
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH})
public void testShouldIgnoreRequestedOrientation_flagIsDisabled_returnsFalse() {