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: + *

    + */ + @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() {