From 83c300c0f91395411a7a52288a1fbf255f9cce15 Mon Sep 17 00:00:00 2001 From: Mariia Sandrikova Date: Tue, 19 Jul 2022 11:44:02 +0000 Subject: [PATCH 1/3] [1/n] Camera Compat: Force rotate activities Camera compatibility treatment that handles orientation mismatch between camera buffers and an app window that can lead to camera issues like sideways or stretched viewfinder. The treatment is enabled for internal displays that have ignoreOrientationRequest display setting enabled and when a flag config_windowManagerCameraCompatTreatmentEnabled is true. It's only applied to activities that have fixed orientation and are in fullscreen. Main parts of the solution: - Listen for the camera changes in DisplayRotationCompatPolicy to trigger orientation update when necessary - Incorporate DisplayRotationCompatPolicy#getOrientation in orientation resolution logic in DisplayContent - Don't trigger orientation updates too quickly to avoid orientation flickering during the camera flip or fold-unfold for foldable devices Bug: 218352945 Test: atest WmTests:DisplayRotationCompatPolicyTests Change-Id: Iaa3a3efa4e0fa89fbb58c7dbc59c125600972224 --- core/res/res/values/config.xml | 6 + core/res/res/values/symbols.xml | 1 + data/etc/services.core.protolog.json | 24 ++ .../server/camera/CameraServiceProxy.java | 11 + .../com/android/server/wm/DisplayContent.java | 17 + .../wm/DisplayRotationCompatPolicy.java | 319 +++++++++++++++++ .../wm/DisplayRotationCompatPolicyTests.java | 330 ++++++++++++++++++ 7 files changed, 708 insertions(+) create mode 100644 services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java create mode 100644 services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 50761bfadbaf7..a0d6e470041aa 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -5344,6 +5344,12 @@ TODO(b/255532890) Enable when ignoreOrientationRequest is set --> false + + false + false diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index fb77b3be5faf8..371dbfb215876 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -4460,6 +4460,7 @@ + diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 49704d971b8df..f55a91e9f1bff 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -295,6 +295,12 @@ "group": "WM_DEBUG_IME", "at": "com\/android\/server\/wm\/DisplayContent.java" }, + "-1812743677": { + "message": "Display id=%d is ignoring all orientation requests, camera is active and the top activity is eligible for force rotation, return %s,portrait activity: %b, is natural orientation portrait: %b.", + "level": "VERBOSE", + "group": "WM_DEBUG_ORIENTATION", + "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java" + }, "-1810446914": { "message": "Trying to update display configuration for system\/invalid process.", "level": "WARN", @@ -1321,6 +1327,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 +1555,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 +2119,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", diff --git a/services/core/java/com/android/server/camera/CameraServiceProxy.java b/services/core/java/com/android/server/camera/CameraServiceProxy.java index aec60deba9335..7bbc604de6603 100644 --- a/services/core/java/com/android/server/camera/CameraServiceProxy.java +++ b/services/core/java/com/android/server/camera/CameraServiceProxy.java @@ -74,6 +74,7 @@ import android.view.Surface; import android.view.WindowManagerGlobal; import com.android.framework.protobuf.nano.MessageNano; +import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.FrameworkStatsLog; import com.android.server.LocalServices; @@ -389,6 +390,16 @@ public class CameraServiceProxy extends SystemService return CaptureRequest.SCALER_ROTATE_AND_CROP_NONE; } + // When config_isWindowManagerCameraCompatTreatmentEnabled is true, + // DisplayRotationCompatPolicy in WindowManager force rotates fullscreen activities with + // fixed orientation to align them with the natural orientation of the device. + if (ctx.getResources().getBoolean( + R.bool.config_isWindowManagerCameraCompatTreatmentEnabled)) { + Slog.v(TAG, "Disable Rotate and Crop to avoid conflicts with" + + " WM force rotation treatment."); + return CaptureRequest.SCALER_ROTATE_AND_CROP_NONE; + } + // External cameras do not need crop-rotate-scale. if (lensFacing != CameraMetadata.LENS_FACING_FRONT && lensFacing != CameraMetadata.LENS_FACING_BACK) { diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 4c19322a97909..ccae0a87685a8 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -431,6 +431,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp private final DisplayMetrics mDisplayMetrics = new DisplayMetrics(); private final DisplayPolicy mDisplayPolicy; private final DisplayRotation mDisplayRotation; + @Nullable private final DisplayRotationCompatPolicy mDisplayRotationCompatPolicy; DisplayFrames mDisplayFrames; private final RemoteCallbackList @@ -1158,6 +1159,10 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp onDisplayChanged(this); updateDisplayAreaOrganizers(); + mDisplayRotationCompatPolicy = + DisplayRotationCompatPolicy.isTreatmentEnabled(mWmService.mContext) + ? new DisplayRotationCompatPolicy(this) : null; + mInputMonitor = new InputMonitor(mWmService, this); mInsetsPolicy = new InsetsPolicy(mInsetsStateController, this); mMinSizeOfResizeableTaskDp = getMinimalTaskSizeDp(); @@ -2704,6 +2709,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 +3273,10 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // on the next traversal if it's removed from RootWindowContainer child list. getPendingTransaction().apply(); mWmService.mWindowPlacerLocked.requestTraversal(); + + if (mDisplayRotationCompatPolicy != null) { + mDisplayRotationCompatPolicy.dispose(); + } } /** Returns true if a removal action is still being deferred. */ diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java new file mode 100644 index 0000000000000..a19539d10e5e4 --- /dev/null +++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java @@ -0,0 +1,319 @@ +/* + * 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.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.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 android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.ActivityInfo.ScreenOrientation; +import android.hardware.camera2.CameraManager; +import android.os.Handler; +import android.util.ArrayMap; +import android.util.ArraySet; + +import com.android.internal.R; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; + +import java.util.Map; +import java.util.Set; + +/** + * Controls camera compatibility treatment that handles orientation mismatch between camera + * buffers and an app window for a particular display that can lead to camera issues like sideways + * or stretched viewfinder. + * + *

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

The treatment is enabled for internal displays that have {@code ignoreOrientationRequest} + * display setting enabled and when {@code + * R.bool.config_isWindowManagerCameraCompatTreatmentEnabled} is {@code true}. + */ + // TODO(b/261444714): Consider moving Camera-specific logic outside of the WM Core path +final class DisplayRotationCompatPolicy { + + // Delay for updating display rotation after Camera connection is closed. Needed to avoid + // rotation flickering when an app is flipping between front and rear cameras or when size + // compat mode is restarted. + // TODO(b/263114289): Consider associating this delay with a specific activity so that if + // the new non-camera activity started on top of the camer one we can rotate faster. + private static final int CAMERA_CLOSED_ROTATION_UPDATE_DELAY_MS = 2000; + // Delay for updating display rotation after Camera connection is opened. This delay is + // selected to be long enough to avoid conflicts with transitions on the app's side. + // Using half 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; + + private final DisplayContent mDisplayContent; + private final WindowManagerService mWmService; + private final CameraManager mCameraManager; + private final Handler mHandler; + // TODO(b/218352945): Add an ADB command. + private final boolean mIsTreatmentEnabled; + + // Bi-directional map between package names and active camera IDs since we need to 1) get a + // camera id by a package name when determining rotation; 2) get a package name by a camera id + // when camera connection is closed and we need to clean up our records. + @GuardedBy("this") + private final CameraIdPackageNameBiMap mCameraIdPackageBiMap = new CameraIdPackageNameBiMap(); + @GuardedBy("this") + private final Set mScheduledToBeRemovedCameraIdSet = new ArraySet<>(); + @GuardedBy("this") + private final Set mScheduledOrientationUpdateCameraIdSet = new ArraySet<>(); + + private final CameraManager.AvailabilityCallback mAvailabilityCallback = + new CameraManager.AvailabilityCallback() { + @Override + public void onCameraOpened(@NonNull String cameraId, @NonNull String packageId) { + notifyCameraOpened(cameraId, packageId); + } + + @Override + public void onCameraClosed(@NonNull String cameraId) { + notifyCameraClosed(cameraId); + } + }; + + 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; + mIsTreatmentEnabled = isTreatmentEnabled(mWmService.mContext); + mCameraManager = mWmService.mContext.getSystemService(CameraManager.class); + mCameraManager.registerAvailabilityCallback( + mWmService.mContext.getMainExecutor(), mAvailabilityCallback); + } + + static boolean isTreatmentEnabled(@NonNull Context context) { + return context.getResources().getBoolean( + R.bool.config_isWindowManagerCameraCompatTreatmentEnabled); + } + + void dispose() { + mCameraManager.unregisterAvailabilityCallback(mAvailabilityCallback); + } + + /** + * Determines orientation for Camera compatibility. + * + *

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

If treatment isn't applicable returns {@link SCREEN_ORIENTATION_UNSPECIFIED}. See {@link + * #shouldComputeCameraCompatOrientation} for conditions enabling the treatment. + */ + @ScreenOrientation + synchronized int getOrientation() { + 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; + } + + /** + * Whether camera compat treatment is enabled for the display. + * + *

Conditions that need to be met: + *

    + *
  • {@code R.bool.config_isWindowManagerCameraCompatTreatmentEnabled} is {@code true}. + *
  • Setting {@code ignoreOrientationRequest} is enabled for the display. + *
  • Associated {@link DisplayContent} is for internal display. See b/225928882 + * that tracks supporting external displays in the future. + *
+ */ + private boolean isTreatmentEnabledForDisplay() { + return mIsTreatmentEnabled && mDisplayContent.getIgnoreOrientationRequest() + // TODO(b/225928882): Support camera compat rotation for external displays + && mDisplayContent.getDisplay().getType() == TYPE_INTERNAL; + } + + /** + * Whether camera compat treatment is applicable for the given activity. + * + *

Conditions that need to be met: + *

    + *
  • {@link #isCameraActiveForPackage} is {@code true} for the activity. + *
  • The activity is in fullscreen + *
  • The activity has fixed orientation but not "locked" or "nosensor" one. + *
+ */ + 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); + // TODO(b/218352945): Restart activity after forced rotation to avoid issues cased by + // in-app caching of pre-rotation display / camera properties. + } + + 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); + // Delay is needed to avoid rotation flickering when an app is flipping between front and + // rear cameras or when size compat mode is restarted. + 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; + } + 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 static class CameraIdPackageNameBiMap { + + private final Map mPackageToCameraIdMap = new ArrayMap<>(); + private final Map mCameraIdToPackageMap = new ArrayMap<>(); + + void put(String packageName, String cameraId) { + // Always using the last connected camera ID for the package even for the concurrent + // camera use case since we can't guess which camera is more important anyway. + removePackageName(packageName); + removeCameraId(cameraId); + mPackageToCameraIdMap.put(packageName, cameraId); + mCameraIdToPackageMap.put(cameraId, packageName); + } + + boolean containsPackageName(String packageName) { + return mPackageToCameraIdMap.containsKey(packageName); + } + + void removeCameraId(String cameraId) { + String packageName = mCameraIdToPackageMap.get(cameraId); + if (packageName == null) { + return; + } + mPackageToCameraIdMap.remove(packageName, cameraId); + mCameraIdToPackageMap.remove(cameraId, packageName); + } + + private void removePackageName(String packageName) { + String cameraId = mPackageToCameraIdMap.get(packageName); + if (cameraId == null) { + return; + } + mPackageToCameraIdMap.remove(packageName, cameraId); + mCameraIdToPackageMap.remove(cameraId, packageName); + } + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java new file mode 100644 index 0000000000000..fda578da32354 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java @@ -0,0 +1,330 @@ +/* + * 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.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 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.Mockito.mock; + +import android.content.ComponentName; +import android.content.pm.ActivityInfo.ScreenOrientation; +import android.content.res.Configuration.Orientation; +import android.content.res.Resources; +import android.hardware.camera2.CameraManager; +import android.os.Handler; +import android.platform.test.annotations.Presubmit; +import android.view.Display; + +import androidx.test.filters.SmallTest; + +import com.android.internal.R; + +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 Resources mResources; + + private DisplayRotationCompatPolicy mDisplayRotationCompatPolicy; + private CameraManager.AvailabilityCallback mCameraAvailabilityCallback; + + private ActivityRecord mActivity; + private Task mTask; + + @Before + public void setUp() throws Exception { + mResources = mContext.getResources(); + spyOn(mResources); + when(mResources.getBoolean(R.bool.config_isWindowManagerCameraCompatTreatmentEnabled)) + .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 testGetOrientation_treatmentNotEnabled_returnUnspecified() { + when(mResources.getBoolean(R.bool.config_isWindowManagerCameraCompatTreatmentEnabled)) + .thenReturn(false); + + mDisplayRotationCompatPolicy = new DisplayRotationCompatPolicy(mDisplayContent); + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + assertEquals(mDisplayRotationCompatPolicy.getOrientation(), + SCREEN_ORIENTATION_UNSPECIFIED); + } + + @Test + public void testGetOrientation_multiWindowMode_returnUnspecified() { + 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()); + assertEquals(mDisplayRotationCompatPolicy.getOrientation(), + SCREEN_ORIENTATION_UNSPECIFIED); + } + + @Test + public void testGetOrientation_orientationUnspecified_returnUnspecified() { + configureActivity(SCREEN_ORIENTATION_UNSPECIFIED); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + assertEquals(mDisplayRotationCompatPolicy.getOrientation(), + SCREEN_ORIENTATION_UNSPECIFIED); + } + + @Test + public void testGetOrientation_orientationLocked_returnUnspecified() { + configureActivity(SCREEN_ORIENTATION_LOCKED); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + assertEquals(mDisplayRotationCompatPolicy.getOrientation(), + SCREEN_ORIENTATION_UNSPECIFIED); + } + + @Test + public void testGetOrientation_orientationNoSensor_returnUnspecified() { + configureActivity(SCREEN_ORIENTATION_NOSENSOR); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + assertEquals(mDisplayRotationCompatPolicy.getOrientation(), + SCREEN_ORIENTATION_UNSPECIFIED); + } + + @Test + public void testGetOrientation_ignoreOrientationRequestIsFalse_returnUnspecified() { + mDisplayContent.setIgnoreOrientationRequest(false); + + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + assertEquals(mDisplayRotationCompatPolicy.getOrientation(), + SCREEN_ORIENTATION_UNSPECIFIED); + } + + @Test + public void testGetOrientation_displayNotInternal_returnUnspecified() { + 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); + assertEquals(mDisplayRotationCompatPolicy.getOrientation(), + SCREEN_ORIENTATION_UNSPECIFIED); + + when(display.getType()).thenReturn(Display.TYPE_WIFI); + assertEquals(mDisplayRotationCompatPolicy.getOrientation(), + SCREEN_ORIENTATION_UNSPECIFIED); + + when(display.getType()).thenReturn(Display.TYPE_OVERLAY); + assertEquals(mDisplayRotationCompatPolicy.getOrientation(), + SCREEN_ORIENTATION_UNSPECIFIED); + + when(display.getType()).thenReturn(Display.TYPE_VIRTUAL); + assertEquals(mDisplayRotationCompatPolicy.getOrientation(), + SCREEN_ORIENTATION_UNSPECIFIED); + } + + @Test + public void testGetOrientation_noCameraConnection_returnUnspecified() { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + + assertEquals(mDisplayRotationCompatPolicy.getOrientation(), + SCREEN_ORIENTATION_UNSPECIFIED); + } + + @Test + public void testGetOrientation_cameraReconnected_returnNotUnspecified() { + 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); + + assertEquals(mDisplayRotationCompatPolicy.getOrientation(), + SCREEN_ORIENTATION_PORTRAIT); + } + + @Test + public void testGetOrientation_reconnectedToDifferentCamera_returnNotUnspecified() { + 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); + + assertEquals(mDisplayRotationCompatPolicy.getOrientation(), + SCREEN_ORIENTATION_PORTRAIT); + } + + @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 testGetOrientation_cameraOpenedForDifferentPackage_returnUnspecified() { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_2); + + assertEquals(mDisplayRotationCompatPolicy.getOrientation(), + SCREEN_ORIENTATION_UNSPECIFIED); + } + + @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); + } + + 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(); + + doReturn(mActivity).when(mDisplayContent).topRunningActivity(anyBoolean()); + doReturn(naturalOrientation).when(mDisplayContent).getNaturalOrientation(); + } +} From da4c552378996238e899a84ddcc26be3ebb6ca29 Mon Sep 17 00:00:00 2001 From: Mariia Sandrikova Date: Sun, 21 Aug 2022 22:06:48 +0000 Subject: [PATCH 2/3] [2/n] Camera Compat: Add DeviceConfig flag Also, refactor boolean setters to reduce code duplication in WindowManagerShellCommand. Test: manual with adb shell device_config put window_manager enable_camera_compat_treatment true Bug: 218352945 Change-Id: Ieb01afa8a1941d9a5a5979139d886592122fcd7a --- .../com/android/server/wm/DisplayContent.java | 5 +- .../wm/DisplayRotationCompatPolicy.java | 14 +- .../server/wm/LetterboxConfiguration.java | 23 ++- .../server/wm/WindowManagerShellCommand.java | 131 +++--------------- .../wm/DisplayRotationCompatPolicyTests.java | 29 ++-- 5 files changed, 73 insertions(+), 129 deletions(-) diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index ccae0a87685a8..c97d7a97d8863 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -1160,7 +1160,10 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp updateDisplayAreaOrganizers(); mDisplayRotationCompatPolicy = - DisplayRotationCompatPolicy.isTreatmentEnabled(mWmService.mContext) + // 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); diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java index a19539d10e5e4..0d3f784849bae 100644 --- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java @@ -30,14 +30,12 @@ import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION; import android.annotation.NonNull; import android.annotation.Nullable; -import android.content.Context; import android.content.pm.ActivityInfo.ScreenOrientation; import android.hardware.camera2.CameraManager; import android.os.Handler; import android.util.ArrayMap; import android.util.ArraySet; -import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; @@ -77,8 +75,6 @@ final class DisplayRotationCompatPolicy { private final WindowManagerService mWmService; private final CameraManager mCameraManager; private final Handler mHandler; - // TODO(b/218352945): Add an ADB command. - private final boolean mIsTreatmentEnabled; // 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 @@ -114,17 +110,11 @@ final class DisplayRotationCompatPolicy { mHandler = handler; mDisplayContent = displayContent; mWmService = displayContent.mWmService; - mIsTreatmentEnabled = isTreatmentEnabled(mWmService.mContext); mCameraManager = mWmService.mContext.getSystemService(CameraManager.class); mCameraManager.registerAvailabilityCallback( mWmService.mContext.getMainExecutor(), mAvailabilityCallback); } - static boolean isTreatmentEnabled(@NonNull Context context) { - return context.getResources().getBoolean( - R.bool.config_isWindowManagerCameraCompatTreatmentEnabled); - } - void dispose() { mCameraManager.unregisterAvailabilityCallback(mAvailabilityCallback); } @@ -190,7 +180,9 @@ final class DisplayRotationCompatPolicy { * */ private boolean isTreatmentEnabledForDisplay() { - return mIsTreatmentEnabled && mDisplayContent.getIgnoreOrientationRequest() + return mWmService.mLetterboxConfiguration.isCameraCompatTreatmentEnabled( + /* checkDeviceConfig */ true) + && mDisplayContent.getIgnoreOrientationRequest() // TODO(b/225928882): Support camera compat rotation for external displays && mDisplayContent.getDisplay().getType() == TYPE_INTERNAL; } diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java index 3eca364ff5fc0..793a352aeec67 100644 --- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java +++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java @@ -191,6 +191,10 @@ 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; + LetterboxConfiguration(Context systemUiContext) { this(systemUiContext, new LetterboxConfigurationPersister(systemUiContext, () -> readLetterboxHorizontalReachabilityPositionFromConfig(systemUiContext, @@ -241,6 +245,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 +953,24 @@ 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); + } + } diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java index 85aa9425c4fd6..554f271a31a74 100644 --- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java +++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java @@ -53,6 +53,7 @@ import com.android.server.wm.LetterboxConfiguration.LetterboxVerticalReachabilit import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipEntry; @@ -806,54 +807,6 @@ public class WindowManagerShellCommand extends ShellCommand { return 0; } - private int runSetLetterboxIsHorizontalReachabilityEnabled(PrintWriter pw) - throws RemoteException { - String arg = getNextArg(); - final boolean enabled; - switch (arg) { - case "true": - case "1": - enabled = true; - break; - case "false": - case "0": - enabled = false; - break; - default: - getErrPrintWriter().println("Error: expected true, 1, false, 0, but got " + arg); - return -1; - } - - synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(enabled); - } - return 0; - } - - private int runSetLetterboxIsVerticalReachabilityEnabled(PrintWriter pw) - throws RemoteException { - String arg = getNextArg(); - final boolean enabled; - switch (arg) { - case "true": - case "1": - enabled = true; - break; - case "false": - case "0": - enabled = false; - break; - default: - getErrPrintWriter().println("Error: expected true, 1, false, 0, but got " + arg); - return -1; - } - - synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setIsVerticalReachabilityEnabled(enabled); - } - return 0; - } - private int runSetLetterboxDefaultPositionForHorizontalReachability(PrintWriter pw) throws RemoteException { @LetterboxHorizontalReachabilityPosition final int position; @@ -916,32 +869,13 @@ public class WindowManagerShellCommand extends ShellCommand { return 0; } - private int runSetLetterboxIsEducationEnabled(PrintWriter pw) throws RemoteException { - String arg = getNextArg(); - final boolean enabled; - switch (arg) { - case "true": - case "1": - enabled = true; - break; - case "false": - case "0": - enabled = false; - break; - default: - getErrPrintWriter().println("Error: expected true, 1, false, 0, but got " + arg); - return -1; - } - - synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setIsEducationEnabled(enabled); - } - return 0; - } - - private int runSetLetterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled(PrintWriter pw) + private int runSetBooleanFlag(PrintWriter pw, Consumer setter) throws RemoteException { String arg = getNextArg(); + if (arg == null) { + getErrPrintWriter().println("Error: expected true, 1, false, 0, but got empty input."); + return -1; + } final boolean enabled; switch (arg) { case "true": @@ -958,30 +892,7 @@ public class WindowManagerShellCommand extends ShellCommand { } synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setIsSplitScreenAspectRatioForUnresizableAppsEnabled(enabled); - } - return 0; - } - - private int runSetTranslucentLetterboxingEnabled(PrintWriter pw) { - String arg = getNextArg(); - final boolean enabled; - switch (arg) { - case "true": - case "1": - enabled = true; - break; - case "false": - case "0": - enabled = false; - break; - default: - getErrPrintWriter().println("Error: expected true, 1, false, 0, but got " + arg); - return -1; - } - - synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setTranslucentLetterboxingOverrideEnabled(enabled); + setter.accept(enabled); } return 0; } @@ -1024,10 +935,12 @@ public class WindowManagerShellCommand extends ShellCommand { runSetLetterboxVerticalPositionMultiplier(pw); break; case "--isHorizontalReachabilityEnabled": - runSetLetterboxIsHorizontalReachabilityEnabled(pw); + runSetBooleanFlag(pw, mLetterboxConfiguration + ::setIsHorizontalReachabilityEnabled); break; case "--isVerticalReachabilityEnabled": - runSetLetterboxIsVerticalReachabilityEnabled(pw); + runSetBooleanFlag(pw, mLetterboxConfiguration + ::setIsVerticalReachabilityEnabled); break; case "--defaultPositionForHorizontalReachability": runSetLetterboxDefaultPositionForHorizontalReachability(pw); @@ -1036,13 +949,15 @@ 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; default: getErrPrintWriter().println( @@ -1089,23 +1004,23 @@ 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(); @@ -1255,6 +1170,7 @@ public class WindowManagerShellCommand extends ShellCommand { pw.println("Is using split screen aspect ratio as aspect ratio for unresizable apps: " + mLetterboxConfiguration .getIsSplitScreenAspectRatioForUnresizableAppsEnabled()); + pw.println("Background type: " + LetterboxConfiguration.letterboxBackgroundTypeToString( mLetterboxConfiguration.getLetterboxBackgroundType())); @@ -1464,7 +1380,6 @@ 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(" reset-letterbox-style [aspectRatio|cornerRadius|backgroundType"); pw.println(" |backgroundColor|wallpaperBlurRadius|wallpaperDarkScrimAlpha"); pw.println(" |horizontalPositionMultiplier|verticalPositionMultiplier"); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java index fda578da32354..d66215051d625 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java @@ -39,7 +39,6 @@ import static org.mockito.Mockito.mock; import android.content.ComponentName; import android.content.pm.ActivityInfo.ScreenOrientation; import android.content.res.Configuration.Orientation; -import android.content.res.Resources; import android.hardware.camera2.CameraManager; import android.os.Handler; import android.platform.test.annotations.Presubmit; @@ -47,8 +46,6 @@ import android.view.Display; import androidx.test.filters.SmallTest; -import com.android.internal.R; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -73,7 +70,7 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { private CameraManager mMockCameraManager; private Handler mMockHandler; - private Resources mResources; + private LetterboxConfiguration mLetterboxConfiguration; private DisplayRotationCompatPolicy mDisplayRotationCompatPolicy; private CameraManager.AvailabilityCallback mCameraAvailabilityCallback; @@ -83,9 +80,10 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { @Before public void setUp() throws Exception { - mResources = mContext.getResources(); - spyOn(mResources); - when(mResources.getBoolean(R.bool.config_isWindowManagerCameraCompatTreatmentEnabled)) + mLetterboxConfiguration = mDisplayContent.mWmService.mLetterboxConfiguration; + spyOn(mLetterboxConfiguration); + when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled( + /* checkDeviceConfig */ anyBoolean())) .thenReturn(true); mMockCameraManager = mock(CameraManager.class); @@ -115,7 +113,22 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { @Test public void testGetOrientation_treatmentNotEnabled_returnUnspecified() { - when(mResources.getBoolean(R.bool.config_isWindowManagerCameraCompatTreatmentEnabled)) + when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled( + /* checkDeviceConfig */ anyBoolean())) + .thenReturn(false); + + mDisplayRotationCompatPolicy = new DisplayRotationCompatPolicy(mDisplayContent); + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + assertEquals(mDisplayRotationCompatPolicy.getOrientation(), + SCREEN_ORIENTATION_UNSPECIFIED); + } + + @Test + public void testGetOrientation_treatmentDisabledViaDeviceConfig_returnUnspecified() { + when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled( + /* checkDeviceConfig */ true)) .thenReturn(false); mDisplayRotationCompatPolicy = new DisplayRotationCompatPolicy(mDisplayContent); From ad26f036f810835b07fa3ebaec881499ce55e7b0 Mon Sep 17 00:00:00 2001 From: Mariia Sandrikova Date: Mon, 22 Aug 2022 09:47:45 +0000 Subject: [PATCH 3/3] [3/n] Camera Compat: Refresh activity Introduce RefreshCallbackItem that allows to "refresh" 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 in DisplayRotationCompatPolicy. Using "stop -> resumed" cycle by default due to higher success rate confirmed with app compatibility testing. But introduce ADB commands that allow trying both. Bug: 218352945 Test: atest WmTests:DisplayRotationCompatPolicyTests Change-Id: Iaca54b197937147f93d52297543b106fd5b97322 --- core/java/android/app/ActivityClient.java | 9 + core/java/android/app/ActivityThread.java | 5 + .../android/app/ClientTransactionHandler.java | 3 + .../app/IActivityClientController.aidl | 1 + .../ClientTransactionItem.java | 3 + .../RefreshCallbackItem.java | 145 +++++++++++++++ .../TransactionExecutor.java | 11 +- data/etc/services.core.protolog.json | 18 ++ .../server/wm/ActivityClientController.java | 9 + .../com/android/server/wm/ActivityRecord.java | 29 ++- .../com/android/server/wm/DisplayContent.java | 2 +- .../android/server/wm/DisplayRotation.java | 8 + .../wm/DisplayRotationCompatPolicy.java | 134 +++++++++++++- .../server/wm/LetterboxConfiguration.java | 51 ++++++ .../server/wm/LetterboxUiController.java | 18 ++ .../server/wm/WindowManagerShellCommand.java | 28 +++ .../wm/DisplayRotationCompatPolicyTests.java | 165 +++++++++++++----- 17 files changed, 587 insertions(+), 52 deletions(-) create mode 100644 core/java/android/app/servertransaction/RefreshCallbackItem.java diff --git a/core/java/android/app/ActivityClient.java b/core/java/android/app/ActivityClient.java index 324b8e7a784f5..0074a0dd4c96a 100644 --- a/core/java/android/app/ActivityClient.java +++ b/core/java/android/app/ActivityClient.java @@ -58,6 +58,15 @@ public class ActivityClient { } } + /** Reports {@link android.app.servertransaction.RefreshCallbackItem} is executed. */ + public void activityRefreshed(IBinder token) { + try { + getActivityClientController().activityRefreshed(token); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + /** * Reports after {@link Activity#onTopResumedActivityChanged(boolean)} is called for losing the * top most position. diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index ef6c5a6c82726..2beb64d98c99c 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -5249,6 +5249,11 @@ public final class ActivityThread extends ClientTransactionHandler } } + @Override + public void reportRefresh(ActivityClientRecord r) { + ActivityClient.getInstance().activityRefreshed(r.token); + } + private void handleSetCoreSettings(Bundle coreSettings) { synchronized (mCoreSettingsLock) { mCoreSettings = coreSettings; diff --git a/core/java/android/app/ClientTransactionHandler.java b/core/java/android/app/ClientTransactionHandler.java index a7566fdaae64a..2c70c4e901576 100644 --- a/core/java/android/app/ClientTransactionHandler.java +++ b/core/java/android/app/ClientTransactionHandler.java @@ -140,6 +140,9 @@ public abstract class ClientTransactionHandler { /** Restart the activity after it was stopped. */ public abstract void performRestartActivity(@NonNull ActivityClientRecord r, boolean start); + /** Report that activity was refreshed to server. */ + public abstract void reportRefresh(@NonNull ActivityClientRecord r); + /** Set pending activity configuration in case it will be updated by other transaction item. */ public abstract void updatePendingActivityConfiguration(@NonNull IBinder token, Configuration overrideConfig); diff --git a/core/java/android/app/IActivityClientController.aidl b/core/java/android/app/IActivityClientController.aidl index 8b655b9bf315e..969f97577c5aa 100644 --- a/core/java/android/app/IActivityClientController.aidl +++ b/core/java/android/app/IActivityClientController.aidl @@ -38,6 +38,7 @@ import com.android.internal.policy.IKeyguardDismissCallback; interface IActivityClientController { oneway void activityIdle(in IBinder token, in Configuration config, in boolean stopProfiling); oneway void activityResumed(in IBinder token, in boolean handleSplashScreenExit); + oneway void activityRefreshed(in IBinder token); /** * This call is not one-way because {@link #activityPaused()) is not one-way, or * the top-resumed-lost could be reported after activity paused. diff --git a/core/java/android/app/servertransaction/ClientTransactionItem.java b/core/java/android/app/servertransaction/ClientTransactionItem.java index d94f08b6aac1c..b159f336cd850 100644 --- a/core/java/android/app/servertransaction/ClientTransactionItem.java +++ b/core/java/android/app/servertransaction/ClientTransactionItem.java @@ -38,6 +38,9 @@ public abstract class ClientTransactionItem implements BaseClientRequest, Parcel return UNDEFINED; } + boolean shouldHaveDefinedPreExecutionState() { + return true; + } // Parcelable diff --git a/core/java/android/app/servertransaction/RefreshCallbackItem.java b/core/java/android/app/servertransaction/RefreshCallbackItem.java new file mode 100644 index 0000000000000..74abab22b0a6a --- /dev/null +++ b/core/java/android/app/servertransaction/RefreshCallbackItem.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.servertransaction; + +import static android.app.servertransaction.ActivityLifecycleItem.LifecycleState; +import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE; +import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityThread.ActivityClientRecord; +import android.app.ClientTransactionHandler; +import android.os.IBinder; +import android.os.Parcel; + +/** + * Callback that allows to {@link TransactionExecutor#cycleToPath} to {@link ON_PAUSE} or + * {@link ON_STOP} in {@link TransactionExecutor#executeCallbacks} for activity "refresh" flow + * that goes through "paused -> resumed" or "stopped -> resumed" cycle. + * + *

This is used in combination with {@link com.android.server.wm.DisplayRotationCompatPolicy} + * for camera compatibility treatment that handles orientation mismatch between camera buffers and + * an app window. This allows to clear cached values in apps (e.g. display or camera rotation) that + * influence camera preview and can lead to sideways or stretching issues. + * + * @hide + */ +public class RefreshCallbackItem extends ActivityTransactionItem { + + // Whether refresh should happen using the "stopped -> resumed" cycle or + // "paused -> resumed" cycle. + @LifecycleState + private int mPostExecutionState; + + @Override + public void execute(@NonNull ClientTransactionHandler client, + @NonNull ActivityClientRecord r, PendingTransactionActions pendingActions) {} + + @Override + public void postExecute(ClientTransactionHandler client, IBinder token, + PendingTransactionActions pendingActions) { + final ActivityClientRecord r = getActivityClientRecord(client, token); + client.reportRefresh(r); + } + + @Override + public int getPostExecutionState() { + return mPostExecutionState; + } + + @Override + boolean shouldHaveDefinedPreExecutionState() { + return false; + } + + // ObjectPoolItem implementation + + @Override + public void recycle() { + ObjectPool.recycle(this); + } + + /** + * Obtain an instance initialized with provided params. + * @param postExecutionState indicating whether refresh should happen using the + * "stopped -> resumed" cycle or "paused -> resumed" cycle. + */ + public static RefreshCallbackItem obtain(@LifecycleState int postExecutionState) { + if (postExecutionState != ON_STOP && postExecutionState != ON_PAUSE) { + throw new IllegalArgumentException( + "Only ON_STOP or ON_PAUSE are allowed as a post execution state for " + + "RefreshCallbackItem but got " + postExecutionState); + } + RefreshCallbackItem instance = + ObjectPool.obtain(RefreshCallbackItem.class); + if (instance == null) { + instance = new RefreshCallbackItem(); + } + instance.mPostExecutionState = postExecutionState; + return instance; + } + + private RefreshCallbackItem() {} + + // Parcelable implementation + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mPostExecutionState); + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final RefreshCallbackItem other = (RefreshCallbackItem) o; + return mPostExecutionState == other.mPostExecutionState; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + mPostExecutionState; + return result; + } + + @Override + public String toString() { + return "RefreshCallbackItem{mPostExecutionState=" + mPostExecutionState + "}"; + } + + private RefreshCallbackItem(Parcel in) { + mPostExecutionState = in.readInt(); + } + + public static final @NonNull Creator CREATOR = + new Creator() { + + public RefreshCallbackItem createFromParcel(Parcel in) { + return new RefreshCallbackItem(in); + } + + public RefreshCallbackItem[] newArray(int size) { + return new RefreshCallbackItem[size]; + } + }; +} diff --git a/core/java/android/app/servertransaction/TransactionExecutor.java b/core/java/android/app/servertransaction/TransactionExecutor.java index de1d38a64163f..1ff0b796fb1ec 100644 --- a/core/java/android/app/servertransaction/TransactionExecutor.java +++ b/core/java/android/app/servertransaction/TransactionExecutor.java @@ -126,10 +126,13 @@ public class TransactionExecutor { final ClientTransactionItem item = callbacks.get(i); if (DEBUG_RESOLVER) Slog.d(TAG, tId(transaction) + "Resolving callback: " + item); final int postExecutionState = item.getPostExecutionState(); - final int closestPreExecutionState = mHelper.getClosestPreExecutionState(r, - item.getPostExecutionState()); - if (closestPreExecutionState != UNDEFINED) { - cycleToPath(r, closestPreExecutionState, transaction); + + if (item.shouldHaveDefinedPreExecutionState()) { + final int closestPreExecutionState = mHelper.getClosestPreExecutionState(r, + item.getPostExecutionState()); + if (closestPreExecutionState != UNDEFINED) { + cycleToPath(r, closestPreExecutionState, transaction); + } } item.execute(mTransactionHandler, token, mPendingActions); diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index f55a91e9f1bff..f47d9c6e0c2d6 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -487,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", @@ -625,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", @@ -4225,6 +4237,12 @@ "group": "WM_DEBUG_REMOTE_ANIMATIONS", "at": "com\/android\/server\/wm\/RemoteAnimationController.java" }, + "1967643923": { + "message": "Refershing activity for camera compatibility treatment, activityRecord=%s", + "level": "VERBOSE", + "group": "WM_DEBUG_STATES", + "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java" + }, "1967975839": { "message": "Changing app %s visible=%b performLayout=%b", "level": "VERBOSE", diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java index da6e7e8436eb1..725254513519e 100644 --- a/services/core/java/com/android/server/wm/ActivityClientController.java +++ b/services/core/java/com/android/server/wm/ActivityClientController.java @@ -162,6 +162,15 @@ class ActivityClientController extends IActivityClientController.Stub { Binder.restoreCallingIdentity(origId); } + @Override + public void activityRefreshed(IBinder token) { + final long origId = Binder.clearCallingIdentity(); + synchronized (mGlobalLock) { + ActivityRecord.activityRefreshedLocked(token); + } + Binder.restoreCallingIdentity(origId); + } + @Override public void activityTopResumedStateLost() { final long origId = Binder.clearCallingIdentity(); diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 57eeb9a0a9055..50169b4ba6670 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -736,7 +736,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A */ private boolean mWillCloseOrEnterPip; - @VisibleForTesting final LetterboxUiController mLetterboxUiController; /** @@ -6111,6 +6110,19 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A r.mDisplayContent.mUnknownAppVisibilityController.notifyAppResumedFinished(r); } + static void activityRefreshedLocked(IBinder token) { + final ActivityRecord r = ActivityRecord.forTokenLocked(token); + ProtoLog.i(WM_DEBUG_STATES, "Refreshed activity: %s", r); + if (r == null) { + // In case the record on server side has been removed (e.g. destroy timeout) + // and the token could be null. + return; + } + if (r.mDisplayContent.mDisplayRotationCompatPolicy != null) { + r.mDisplayContent.mDisplayRotationCompatPolicy.onActivityRefreshed(r); + } + } + static void splashScreenAttachedLocked(IBinder token) { final ActivityRecord r = ActivityRecord.forTokenLocked(token); if (r == null) { @@ -9143,6 +9155,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } else { scheduleConfigurationChanged(newMergedOverrideConfig); } + notifyDisplayCompatPolicyAboutConfigurationChange( + mLastReportedConfiguration.getMergedConfiguration(), mTmpConfig); return true; } @@ -9211,11 +9225,24 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } else { scheduleConfigurationChanged(newMergedOverrideConfig); } + notifyDisplayCompatPolicyAboutConfigurationChange( + mLastReportedConfiguration.getMergedConfiguration(), mTmpConfig); + stopFreezingScreenLocked(false); return true; } + private void notifyDisplayCompatPolicyAboutConfigurationChange( + Configuration newConfig, Configuration lastReportedConfig) { + if (mDisplayContent.mDisplayRotationCompatPolicy == null + || !shouldBeResumed(/* activeActivity */ null)) { + return; + } + mDisplayContent.mDisplayRotationCompatPolicy.onActivityConfigurationChanging( + this, newConfig, lastReportedConfig); + } + /** Get process configuration, or global config if the process is not set. */ private Configuration getProcessGlobalConfiguration() { return app != null ? app.getConfiguration() : mAtmService.getGlobalConfiguration(); diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index c97d7a97d8863..36f86d10e2c5f 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -431,7 +431,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp private final DisplayMetrics mDisplayMetrics = new DisplayMetrics(); private final DisplayPolicy mDisplayPolicy; private final DisplayRotation mDisplayRotation; - @Nullable private final DisplayRotationCompatPolicy mDisplayRotationCompatPolicy; + @Nullable final DisplayRotationCompatPolicy mDisplayRotationCompatPolicy; DisplayFrames mDisplayFrames; private final RemoteCallbackList diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java index 34bdb7a2f4169..cf3a6880e7127 100644 --- a/services/core/java/com/android/server/wm/DisplayRotation.java +++ b/services/core/java/com/android/server/wm/DisplayRotation.java @@ -1805,6 +1805,7 @@ public class DisplayRotation { final int mHalfFoldSavedRotation; final boolean mInHalfFoldTransition; final DeviceStateController.FoldState mFoldState; + @Nullable final String mDisplayRotationCompatPolicySummary; Record(DisplayRotation dr, int fromRotation, int toRotation) { mFromRotation = fromRotation; @@ -1839,6 +1840,10 @@ public class DisplayRotation { mInHalfFoldTransition = false; mFoldState = DeviceStateController.FoldState.UNKNOWN; } + mDisplayRotationCompatPolicySummary = dc.mDisplayRotationCompatPolicy == null + ? null + : dc.mDisplayRotationCompatPolicy + .getSummaryForDisplayRotationHistoryRecord(); } void dump(String prefix, PrintWriter pw) { @@ -1861,6 +1866,9 @@ public class DisplayRotation { + " mInHalfFoldTransition=" + mInHalfFoldTransition + " mFoldState=" + mFoldState); } + if (mDisplayRotationCompatPolicySummary != null) { + pw.println(prefix + mDisplayRotationCompatPolicySummary); + } } } diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java index 0d3f784849bae..18c5c3b82b19b 100644 --- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java @@ -16,10 +16,13 @@ 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; @@ -27,12 +30,18 @@ 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; @@ -65,11 +74,15 @@ final class DisplayRotationCompatPolicy { 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 half CAMERA_CLOSED_ROTATION_UPDATE_DELAY_MS to avoid flickering when an app + // 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; @@ -99,6 +112,9 @@ final class DisplayRotationCompatPolicy { } }; + @ScreenOrientation + private int mLastReportedOrientation = SCREEN_ORIENTATION_UNSET; + DisplayRotationCompatPolicy(@NonNull DisplayContent displayContent) { this(displayContent, displayContent.mWmService.mH); } @@ -132,7 +148,13 @@ final class DisplayRotationCompatPolicy { * #shouldComputeCameraCompatOrientation} for conditions enabling the treatment. */ @ScreenOrientation - synchronized int getOrientation() { + int getOrientation() { + mLastReportedOrientation = getOrientationInternal(); + return mLastReportedOrientation; + } + + @ScreenOrientation + private synchronized int getOrientationInternal() { if (!isTreatmentEnabledForDisplay()) { return SCREEN_ORIENTATION_UNSPECIFIED; } @@ -168,6 +190,73 @@ final class DisplayRotationCompatPolicy { 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. * @@ -221,8 +310,6 @@ final class DisplayRotationCompatPolicy { mHandler.postDelayed( () -> delayedUpdateOrientationWithWmLock(cameraId, packageName), CAMERA_OPENED_ROTATION_UPDATE_DELAY_MS); - // TODO(b/218352945): Restart activity after forced rotation to avoid issues cased by - // in-app caching of pre-rotation display / camera properties. } private void updateOrientationWithWmLock() { @@ -251,8 +338,12 @@ final class DisplayRotationCompatPolicy { mScheduledToBeRemovedCameraIdSet.add(cameraId); // No need to update orientation for this camera if it's already closed. mScheduledOrientationUpdateCameraIdSet.remove(cameraId); - // Delay is needed to avoid rotation flickering when an app is flipping between front and - // rear cameras or when size compat mode is restarted. + 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); @@ -264,6 +355,15 @@ final class DisplayRotationCompatPolicy { // 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, @@ -272,6 +372,19 @@ final class DisplayRotationCompatPolicy { updateOrientationWithWmLock(); } + private boolean isActivityForCameraIdRefreshing(String cameraId) { + ActivityRecord topActivity = mDisplayContent.topRunningActivity( + /* considerKeyguardState= */ true); + if (!isTreatmentEnabledForActivity(topActivity)) { + return false; + } + String activeCameraId = mCameraIdPackageBiMap.getCameraId(topActivity.packageName); + if (activeCameraId == null || activeCameraId != cameraId) { + return false; + } + return topActivity.mLetterboxUiController.isRefreshAfterRotationRequested(); + } + private static class CameraIdPackageNameBiMap { private final Map mPackageToCameraIdMap = new ArrayMap<>(); @@ -290,6 +403,11 @@ final class DisplayRotationCompatPolicy { 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) { @@ -299,6 +417,10 @@ final class DisplayRotationCompatPolicy { mCameraIdToPackageMap.remove(cameraId, packageName); } + String getSummaryForDisplayRotationHistoryRecord() { + return "{ mPackageToCameraIdMap=" + mPackageToCameraIdMap + " }"; + } + private void removePackageName(String packageName) { String cameraId = mPackageToCameraIdMap.get(packageName); if (cameraId == null) { diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java index 793a352aeec67..a7bf595fa673c 100644 --- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java +++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java @@ -195,6 +195,16 @@ final class LetterboxConfiguration { // 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, @@ -973,4 +983,45 @@ final class LetterboxConfiguration { "enable_camera_compat_treatment", false); } + /** Whether camera compatibility refresh is enabled. */ + boolean isCameraCompatRefreshEnabled() { + return mIsCameraCompatTreatmentRefreshEnabled; + } + + /** Overrides whether camera compatibility treatment is enabled. */ + void setCameraCompatRefreshEnabled(boolean enabled) { + mIsCameraCompatTreatmentRefreshEnabled = enabled; + } + + /** + * Resets whether camera compatibility treatment is enabled to {@code true}. + */ + void resetCameraCompatRefreshEnabled() { + mIsCameraCompatTreatmentRefreshEnabled = true; + } + + /** + * Whether activity "refresh" in camera compatibility treatment should happen using the + * "stopped -> resumed" cycle rather than "paused -> resumed" cycle. + */ + boolean isCameraCompatRefreshCycleThroughStopEnabled() { + return mIsCameraCompatRefreshCycleThroughStopEnabled; + } + + /** + * Overrides whether activity "refresh" in camera compatibility treatment should happen using + * "stopped -> resumed" cycle rather than "paused -> resumed" cycle. + */ + void setCameraCompatRefreshCycleThroughStopEnabled(boolean enabled) { + mIsCameraCompatRefreshCycleThroughStopEnabled = enabled; + } + + /** + * Resets whether activity "refresh" in camera compatibility treatment should happen using + * "stopped -> resumed" cycle rather than "paused -> resumed" cycle to {@code true}. + */ + void resetCameraCompatRefreshCycleThroughStopEnabled() { + mIsCameraCompatRefreshCycleThroughStopEnabled = true; + } + } diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index 9cb94c68583ff..fd7e082beed4a 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -80,6 +80,7 @@ import java.io.PrintWriter; // SizeCompatTests and LetterboxTests but not all. // TODO(b/185264020): Consider making LetterboxUiController applicable to any level of the // hierarchy in addition to ActivityRecord (Task, DisplayArea, ...). +// TODO(b/263021211): Consider renaming to more generic CompatUIController. final class LetterboxUiController { private static final String TAG = TAG_WITH_CLASS_NAME ? "LetterboxUiController" : TAG_ATM; @@ -125,6 +126,11 @@ final class LetterboxUiController { @Nullable private Letterbox mLetterbox; + // Whether activity "refresh" was requested but not finished in + // ActivityRecord#activityResumedLocked following the camera compat force rotation in + // DisplayRotationCompatPolicy. + private boolean mIsRefreshAfterRotationRequested; + LetterboxUiController(WindowManagerService wmService, ActivityRecord activityRecord) { mLetterboxConfiguration = wmService.mLetterboxConfiguration; // Given activityRecord may not be fully constructed since LetterboxUiController @@ -147,6 +153,18 @@ final class LetterboxUiController { } } + /** + * Whether activity "refresh" was requested but not finished in {@link #activityResumedLocked} + * following the camera compat force rotation in {@link DisplayRotationCompatPolicy}. + */ + boolean isRefreshAfterRotationRequested() { + return mIsRefreshAfterRotationRequested; + } + + void setIsRefreshAfterRotationRequested(boolean isRequested) { + mIsRefreshAfterRotationRequested = isRequested; + } + boolean hasWallpaperBackgroundForLetterbox() { return mShowWallpaperForLetterboxBackground; } diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java index 554f271a31a74..aef6d1d155102 100644 --- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java +++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java @@ -959,6 +959,14 @@ public class WindowManagerShellCommand extends ShellCommand { 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( "Error: Unrecognized letterbox style option: " + arg); @@ -1025,6 +1033,13 @@ public class WindowManagerShellCommand extends ShellCommand { case "isTranslucentLetterboxingEnabled": mLetterboxConfiguration.resetTranslucentLetterboxingEnabled(); break; + case "isCameraCompatRefreshEnabled": + mLetterboxConfiguration.resetCameraCompatRefreshEnabled(); + break; + case "isCameraCompatRefreshCycleThroughStopEnabled": + mLetterboxConfiguration + .resetCameraCompatRefreshCycleThroughStopEnabled(); + break; default: getErrPrintWriter().println( "Error: Unrecognized letterbox style option: " + arg); @@ -1126,6 +1141,8 @@ public class WindowManagerShellCommand extends ShellCommand { mLetterboxConfiguration.resetIsEducationEnabled(); mLetterboxConfiguration.resetIsSplitScreenAspectRatioForUnresizableAppsEnabled(); mLetterboxConfiguration.resetTranslucentLetterboxingEnabled(); + mLetterboxConfiguration.resetCameraCompatRefreshEnabled(); + mLetterboxConfiguration.resetCameraCompatRefreshCycleThroughStopEnabled(); } } @@ -1171,6 +1188,11 @@ public class WindowManagerShellCommand extends ShellCommand { + 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())); @@ -1380,6 +1402,12 @@ public class WindowManagerShellCommand extends ShellCommand { pw.println(" unresizable apps."); pw.println(" --isTranslucentLetterboxingEnabled [true|1|false|0]"); pw.println(" Whether letterboxing for translucent activities is enabled."); + pw.println(" --isCameraCompatRefreshEnabled [true|1|false|0]"); + pw.println(" Whether camera compatibility refresh is enabled."); + pw.println(" --isCameraCompatRefreshCycleThroughStopEnabled [true|1|false|0]"); + pw.println(" Whether activity \"refresh\" in camera compatibility treatment should"); + pw.println(" happen using the \"stopped -> resumed\" cycle rather than"); + pw.println(" \"paused -> resumed\" cycle."); pw.println(" reset-letterbox-style [aspectRatio|cornerRadius|backgroundType"); pw.println(" |backgroundColor|wallpaperBlurRadius|wallpaperDarkScrimAlpha"); pw.println(" |horizontalPositionMultiplier|verticalPositionMultiplier"); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java index d66215051d625..d1234e3de81af 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java @@ -16,6 +16,8 @@ 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; @@ -23,6 +25,8 @@ 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; @@ -34,15 +38,23 @@ 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; @@ -85,6 +97,10 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled( /* checkDeviceConfig */ anyBoolean())) .thenReturn(true); + when(mLetterboxConfiguration.isCameraCompatRefreshEnabled()) + .thenReturn(true); + when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) + .thenReturn(true); mMockCameraManager = mock(CameraManager.class); doAnswer(invocation -> { @@ -112,35 +128,34 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { } @Test - public void testGetOrientation_treatmentNotEnabled_returnUnspecified() { + public void testTreatmentNotEnabled_noForceRotationOrRefresh() throws Exception { when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled( /* checkDeviceConfig */ anyBoolean())) .thenReturn(false); - mDisplayRotationCompatPolicy = new DisplayRotationCompatPolicy(mDisplayContent); configureActivity(SCREEN_ORIENTATION_PORTRAIT); mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); assertEquals(mDisplayRotationCompatPolicy.getOrientation(), SCREEN_ORIENTATION_UNSPECIFIED); + + assertNoForceRotationOrRefresh(); } @Test - public void testGetOrientation_treatmentDisabledViaDeviceConfig_returnUnspecified() { + public void testTreatmentDisabledViaDeviceConfig_noForceRotationOrRefresh() throws Exception { when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled( /* checkDeviceConfig */ true)) .thenReturn(false); - mDisplayRotationCompatPolicy = new DisplayRotationCompatPolicy(mDisplayContent); configureActivity(SCREEN_ORIENTATION_PORTRAIT); mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertEquals(mDisplayRotationCompatPolicy.getOrientation(), - SCREEN_ORIENTATION_UNSPECIFIED); + assertNoForceRotationOrRefresh(); } @Test - public void testGetOrientation_multiWindowMode_returnUnspecified() { + 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, @@ -149,53 +164,46 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); assertTrue(mActivity.inMultiWindowMode()); - assertEquals(mDisplayRotationCompatPolicy.getOrientation(), - SCREEN_ORIENTATION_UNSPECIFIED); + assertNoForceRotationOrRefresh(); } @Test - public void testGetOrientation_orientationUnspecified_returnUnspecified() { + public void testOrientationUnspecified_noForceRotationOrRefresh() throws Exception { configureActivity(SCREEN_ORIENTATION_UNSPECIFIED); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - - assertEquals(mDisplayRotationCompatPolicy.getOrientation(), - SCREEN_ORIENTATION_UNSPECIFIED); + assertNoForceRotationOrRefresh(); } @Test - public void testGetOrientation_orientationLocked_returnUnspecified() { + public void testOrientationLocked_noForceRotationOrRefresh() throws Exception { configureActivity(SCREEN_ORIENTATION_LOCKED); mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertEquals(mDisplayRotationCompatPolicy.getOrientation(), - SCREEN_ORIENTATION_UNSPECIFIED); + assertNoForceRotationOrRefresh(); } @Test - public void testGetOrientation_orientationNoSensor_returnUnspecified() { + public void testOrientationNoSensor_noForceRotationOrRefresh() throws Exception { configureActivity(SCREEN_ORIENTATION_NOSENSOR); mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertEquals(mDisplayRotationCompatPolicy.getOrientation(), - SCREEN_ORIENTATION_UNSPECIFIED); + assertNoForceRotationOrRefresh(); } @Test - public void testGetOrientation_ignoreOrientationRequestIsFalse_returnUnspecified() { + public void testIgnoreOrientationRequestIsFalse_noForceRotationOrRefresh() throws Exception { mDisplayContent.setIgnoreOrientationRequest(false); configureActivity(SCREEN_ORIENTATION_PORTRAIT); mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertEquals(mDisplayRotationCompatPolicy.getOrientation(), - SCREEN_ORIENTATION_UNSPECIFIED); + assertNoForceRotationOrRefresh(); } @Test - public void testGetOrientation_displayNotInternal_returnUnspecified() { + public void testDisplayNotInternal_noForceRotationOrRefresh() throws Exception { Display display = mDisplayContent.getDisplay(); spyOn(display); @@ -203,52 +211,51 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); when(display.getType()).thenReturn(Display.TYPE_EXTERNAL); - assertEquals(mDisplayRotationCompatPolicy.getOrientation(), - SCREEN_ORIENTATION_UNSPECIFIED); + assertNoForceRotationOrRefresh(); when(display.getType()).thenReturn(Display.TYPE_WIFI); - assertEquals(mDisplayRotationCompatPolicy.getOrientation(), - SCREEN_ORIENTATION_UNSPECIFIED); + assertNoForceRotationOrRefresh(); when(display.getType()).thenReturn(Display.TYPE_OVERLAY); - assertEquals(mDisplayRotationCompatPolicy.getOrientation(), - SCREEN_ORIENTATION_UNSPECIFIED); + assertNoForceRotationOrRefresh(); when(display.getType()).thenReturn(Display.TYPE_VIRTUAL); - assertEquals(mDisplayRotationCompatPolicy.getOrientation(), - SCREEN_ORIENTATION_UNSPECIFIED); + assertNoForceRotationOrRefresh(); } @Test - public void testGetOrientation_noCameraConnection_returnUnspecified() { + public void testNoCameraConnection_noForceRotationOrRefresh() throws Exception { configureActivity(SCREEN_ORIENTATION_PORTRAIT); - assertEquals(mDisplayRotationCompatPolicy.getOrientation(), - SCREEN_ORIENTATION_UNSPECIFIED); + assertNoForceRotationOrRefresh(); } @Test - public void testGetOrientation_cameraReconnected_returnNotUnspecified() { + 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 testGetOrientation_reconnectedToDifferentCamera_returnNotUnspecified() { + 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 @@ -267,13 +274,12 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { } @Test - public void testGetOrientation_cameraOpenedForDifferentPackage_returnUnspecified() { + public void testCameraOpenedForDifferentPackage_noForceRotationOrRefresh() throws Exception { configureActivity(SCREEN_ORIENTATION_PORTRAIT); mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_2); - assertEquals(mDisplayRotationCompatPolicy.getOrientation(), - SCREEN_ORIENTATION_UNSPECIFIED); + assertNoForceRotationOrRefresh(); } @Test @@ -320,6 +326,42 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { 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); } @@ -337,7 +379,50 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { .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; + } }