diff options
| author | 2022-12-22 06:22:05 +0000 | |
|---|---|---|
| committer | 2022-12-22 06:22:05 +0000 | |
| commit | 561fc8b41f3d9cf80b51f7dffc8043a8291b2bd9 (patch) | |
| tree | 22a178e7dbb263d25aec5f18d4eeb45db4d716e7 | |
| parent | 40b7fb2080f756349d912ec992c028dbdeb28a10 (diff) | |
| parent | a1b8de23173a53d867db89a5d423c290271f5cf4 (diff) | |
[1/n] Camera Compat: Force rotate activities am: 83c300c0f9 am: a1b8de2317
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/19601374
Change-Id: I6169f0c6eb1fe1f3844e7698534aa85ef12acf02
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
7 files changed, 708 insertions, 0 deletions
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 307707f6e152..35ff7e855b75 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -5366,6 +5366,12 @@ TODO(b/255532890) Enable when ignoreOrientationRequest is set --> <bool name="config_letterboxIsEnabledForTranslucentActivities">false</bool> + <!-- Whether camera compat treatment is enabled for issues caused by orientation mismatch + between camera buffers and an app window. This includes force rotation of fixed + orientation activities connected to the camera in fullscreen and showing a tooltip in + split screen. --> + <bool name="config_isWindowManagerCameraCompatTreatmentEnabled">false</bool> + <!-- Whether a camera compat controller is enabled to allow the user to apply or revert treatment for stretched issues in camera viewfinder. --> <bool name="config_isCameraCompatControlForStretchedIssuesEnabled">false</bool> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 923ef3245c21..c73f2f498355 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -4432,6 +4432,7 @@ <java-symbol type="bool" name="config_letterboxIsEducationEnabled" /> <java-symbol type="dimen" name="config_letterboxDefaultMinAspectRatioForUnresizableApps" /> <java-symbol type="bool" name="config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled" /> + <java-symbol type="bool" name="config_isWindowManagerCameraCompatTreatmentEnabled" /> <java-symbol type="bool" name="config_isCameraCompatControlForStretchedIssuesEnabled" /> <java-symbol type="bool" name="config_hideDisplayCutoutWithDisplayArea" /> diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 7425c95e9337..ffa847722ca6 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -313,6 +313,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", @@ -1375,6 +1381,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", @@ -1603,6 +1615,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", @@ -2161,6 +2179,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 e16ca0bbe1e1..0b03005af044 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 a8d26d6a6045..e26f6ca24066 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -449,6 +449,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 boolean mInTouchMode; @@ -1178,6 +1179,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(); @@ -2756,6 +2761,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)) { @@ -3318,6 +3331,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 000000000000..a19539d10e5e --- /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. + * + * <p>This includes force rotation of fixed orientation activities connected to the camera. + * + * <p>The treatment is enabled for internal displays that have {@code ignoreOrientationRequest} + * display setting enabled and when {@code + * R.bool.config_isWindowManagerCameraCompatTreatmentEnabled} is {@code true}. + */ + // TODO(b/261444714): Consider moving Camera-specific logic outside of the WM Core path +final class DisplayRotationCompatPolicy { + + // Delay for updating display rotation after Camera connection is closed. Needed to avoid + // rotation flickering when an app is flipping between front and rear cameras or when size + // compat mode is restarted. + // TODO(b/263114289): Consider associating this delay with a specific activity so that if + // the new non-camera activity started on top of the camer one we can rotate faster. + private static final int CAMERA_CLOSED_ROTATION_UPDATE_DELAY_MS = 2000; + // Delay for updating display rotation after Camera connection is opened. This delay is + // selected to be long enough to avoid conflicts with transitions on the app's side. + // Using 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<String> mScheduledToBeRemovedCameraIdSet = new ArraySet<>(); + @GuardedBy("this") + private final Set<String> mScheduledOrientationUpdateCameraIdSet = new ArraySet<>(); + + private final CameraManager.AvailabilityCallback mAvailabilityCallback = + new CameraManager.AvailabilityCallback() { + @Override + public void onCameraOpened(@NonNull String cameraId, @NonNull String packageId) { + notifyCameraOpened(cameraId, packageId); + } + + @Override + public void onCameraClosed(@NonNull String cameraId) { + notifyCameraClosed(cameraId); + } + }; + + 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. + * + * <p>The goal of this function is to compute a orientation which would align orientations of + * portrait app window and natural orientation of the device and set opposite to natural + * orientation for a landscape app window. This is one of the strongest assumptions that apps + * make when they implement camera previews. Since app and natural display orientations aren't + * guaranteed to match, the rotation can cause letterboxing. + * + * <p>If treatment isn't applicable returns {@link SCREEN_ORIENTATION_UNSPECIFIED}. See {@link + * #shouldComputeCameraCompatOrientation} for conditions enabling the treatment. + */ + @ScreenOrientation + 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. + * + * <p>Conditions that need to be met: + * <ul> + * <li>{@code R.bool.config_isWindowManagerCameraCompatTreatmentEnabled} is {@code true}. + * <li>Setting {@code ignoreOrientationRequest} is enabled for the display. + * <li>Associated {@link DisplayContent} is for internal display. See b/225928882 + * that tracks supporting external displays in the future. + * </ul> + */ + private boolean isTreatmentEnabledForDisplay() { + return 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. + * + * <p>Conditions that need to be met: + * <ul> + * <li>{@link #isCameraActiveForPackage} is {@code true} for the activity. + * <li>The activity is in fullscreen + * <li>The activity has fixed orientation but not "locked" or "nosensor" one. + * </ul> + */ + private boolean isTreatmentEnabledForActivity(@Nullable ActivityRecord activity) { + return activity != null && !activity.inMultiWindowMode() + && activity.getRequestedConfigurationOrientation() != ORIENTATION_UNDEFINED + // "locked" and "nosensor" values are often used by camera apps that can't + // handle dynamic changes so we shouldn't force rotate them. + && activity.getRequestedOrientation() != SCREEN_ORIENTATION_NOSENSOR + && activity.getRequestedOrientation() != SCREEN_ORIENTATION_LOCKED + && mCameraIdPackageBiMap.containsPackageName(activity.packageName); + } + + private synchronized void notifyCameraOpened( + @NonNull String cameraId, @NonNull String packageName) { + // If an activity is restarting or camera is flipping, the camera connection can be + // quickly closed and reopened. + mScheduledToBeRemovedCameraIdSet.remove(cameraId); + ProtoLog.v(WM_DEBUG_ORIENTATION, + "Display id=%d is notified that Camera %s is open for package %s", + mDisplayContent.mDisplayId, cameraId, packageName); + // Some apps can’t handle configuration changes coming at the same time with Camera setup + // so delaying orientation update to accomadate for that. + mScheduledOrientationUpdateCameraIdSet.add(cameraId); + mHandler.postDelayed( + () -> delayedUpdateOrientationWithWmLock(cameraId, packageName), + CAMERA_OPENED_ROTATION_UPDATE_DELAY_MS); + // 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<String, String> mPackageToCameraIdMap = new ArrayMap<>(); + private final Map<String, String> mCameraIdToPackageMap = new ArrayMap<>(); + + void put(String packageName, String cameraId) { + // Always using the last connected camera ID for the package even for the concurrent + // camera use case since we can't guess which camera is more important anyway. + removePackageName(packageName); + removeCameraId(cameraId); + mPackageToCameraIdMap.put(packageName, cameraId); + mCameraIdToPackageMap.put(cameraId, packageName); + } + + boolean containsPackageName(String packageName) { + return mPackageToCameraIdMap.containsKey(packageName); + } + + 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 000000000000..fda578da3235 --- /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(); + } +} |