diff options
| author | 2024-04-24 12:20:38 +0000 | |
|---|---|---|
| committer | 2024-04-29 15:39:50 +0000 | |
| commit | f518b7194d28fc7a0caa8b842db0864b98a76f08 (patch) | |
| tree | 416e60948212cf0e2c1ad173d5ab7ee22b4df275 | |
| parent | 517493417ff57d82af278de8f596ac0b8882a5ee (diff) | |
Extract camera open/close logic in a separate class.
This allows the camera app state to be reused in other camera compat classes.
Bug: 314960895
Test: atest WmTests:DisplayRotationCompatPolicyTests
Test: atest WmTests:DisplayContentTests
Change-Id: Ie1692a59987267113c4d05bb332bdb28f6a929dd
8 files changed, 687 insertions, 178 deletions
diff --git a/data/etc/core.protolog.pb b/data/etc/core.protolog.pb Binary files differindex 97147a01b259..000f6ef46c2c 100644 --- a/data/etc/core.protolog.pb +++ b/data/etc/core.protolog.pb diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 483b6934ee8c..01deb4957cd3 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -1117,6 +1117,24 @@ "group": "WM_SHOW_SURFACE_ALLOC", "at": "com\/android\/server\/wm\/BlackFrame.java" }, + "5256889109971284149": { + "message": "CameraManager cannot be found.", + "level": "ERROR", + "group": "WM_DEBUG_STATES", + "at": "com\/android\/server\/wm\/CameraStateMonitor.java" + }, + "8116030277393789125": { + "message": "Display id=%d is notified that Camera %s is open for package %s", + "level": "VERBOSE", + "group": "WM_DEBUG_STATES", + "at": "com\/android\/server\/wm\/CameraStateMonitor.java" + }, + "-3774458166471278611": { + "message": "Display id=%d is notified that Camera %s is closed.", + "level": "VERBOSE", + "group": "WM_DEBUG_STATES", + "at": "com\/android\/server\/wm\/CameraStateMonitor.java" + }, "-74949168947384056": { "message": "Sending to proc %s new compat %s", "level": "VERBOSE", @@ -1771,32 +1789,20 @@ "group": "WM_DEBUG_ORIENTATION", "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java" }, - "-8302211458579221117": { - "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" - }, "-1534784331886673955": { "message": "DisplayRotationCompatPolicy: Multi-window toast not shown as package '%s' cannot be found.", "level": "ERROR", "group": "WM_DEBUG_ORIENTATION", "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java" }, - "1797195804376906831": { - "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" - }, - "-8746776274432739264": { - "message": "Display id=%d is notified that Camera %s is closed but activity is still refreshing. Rescheduling an update.", + "-5121743609317543819": { + "message": "Display id=%d is notified that camera is closed but activity is still refreshing. Rescheduling an update.", "level": "VERBOSE", "group": "WM_DEBUG_ORIENTATION", "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java" }, - "3622181214422515679": { - "message": "Display id=%d is notified that Camera %s is closed, updating rotation.", + "1769752961776628557": { + "message": "Display id=%d is notified that Camera is closed, updating rotation.", "level": "VERBOSE", "group": "WM_DEBUG_ORIENTATION", "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java" diff --git a/services/core/java/com/android/server/wm/CameraStateMonitor.java b/services/core/java/com/android/server/wm/CameraStateMonitor.java new file mode 100644 index 000000000000..ea7edea7d4b3 --- /dev/null +++ b/services/core/java/com/android/server/wm/CameraStateMonitor.java @@ -0,0 +1,287 @@ +/* + * Copyright (C) 2024 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 com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES; +import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; +import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.hardware.camera2.CameraManager; +import android.os.Handler; +import android.util.ArraySet; +import android.util.Slog; + +import com.android.internal.protolog.common.ProtoLog; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * Class that listens to camera open/closed signals, keeps track of the current apps using camera, + * and notifies listeners. + */ +class CameraStateMonitor { + private static final String TAG = TAG_WITH_CLASS_NAME ? "CameraStateMonitor" : TAG_WM; + + // Delay for updating letterbox after Camera connection is closed. Needed to avoid flickering + // when an app is flipping between front and rear cameras or when size compat mode is restarted. + // TODO(b/330148095): Investigate flickering without using delays, remove them if possible. + private static final int CAMERA_CLOSED_LETTERBOX_UPDATE_DELAY_MS = 2000; + // Delay for updating letterboxing after Camera connection is opened. This delay is selected to + // be long enough to avoid conflicts with transitions on the app's side. + // 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. + // TODO(b/330148095): Investigate flickering without using delays, remove them if possible. + private static final int CAMERA_OPENED_LETTERBOX_UPDATE_DELAY_MS = + CAMERA_CLOSED_LETTERBOX_UPDATE_DELAY_MS / 2; + + @NonNull + private final DisplayContent mDisplayContent; + @NonNull + private final WindowManagerService mWmService; + @Nullable + private final CameraManager mCameraManager; + @NonNull + private final Handler mHandler; + + @Nullable + private ActivityRecord mCameraActivity; + + // Bi-directional map between package names and active camera IDs since we need to 1) get a + // camera id by a package name when resizing the window; 2) get a package name by a camera id + // when camera connection is closed and we need to clean up our records. + private final CameraIdPackageNameBiMapping mCameraIdPackageBiMapping = + new CameraIdPackageNameBiMapping(); + private final Set<String> mScheduledToBeRemovedCameraIdSet = new ArraySet<>(); + + // TODO(b/336474959): should/can this go in the compat listeners? + private final Set<String> mScheduledCompatModeUpdateCameraIdSet = new ArraySet<>(); + + private final ArrayList<CameraCompatStateListener> mCameraStateListeners = new ArrayList<>(); + + /** + * {@link CameraCompatStateListener} which returned {@code true} on the last {@link + * CameraCompatStateListener#onCameraOpened(ActivityRecord, String)}, if any. + * + * <p>This allows the {@link CameraStateMonitor} to notify a particular listener when camera + * closes, so they can revert any changes. + */ + @Nullable + private CameraCompatStateListener mCurrentListenerForCameraActivity; + + private final CameraManager.AvailabilityCallback mAvailabilityCallback = + new CameraManager.AvailabilityCallback() { + @Override + public void onCameraOpened(@NonNull String cameraId, @NonNull String packageId) { + synchronized (mWmService.mGlobalLock) { + notifyCameraOpened(cameraId, packageId); + } + } + @Override + public void onCameraClosed(@NonNull String cameraId) { + synchronized (mWmService.mGlobalLock) { + notifyCameraClosed(cameraId); + } + } + }; + + CameraStateMonitor(@NonNull DisplayContent displayContent, @NonNull 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; + mCameraManager = mWmService.mContext.getSystemService(CameraManager.class); + } + + void startListeningToCameraState() { + mCameraManager.registerAvailabilityCallback( + mWmService.mContext.getMainExecutor(), mAvailabilityCallback); + } + + /** Releases camera callback listener. */ + void dispose() { + if (mCameraManager != null) { + mCameraManager.unregisterAvailabilityCallback(mAvailabilityCallback); + } + } + + void addCameraStateListener(CameraCompatStateListener listener) { + mCameraStateListeners.add(listener); + } + + void removeCameraStateListener(CameraCompatStateListener listener) { + mCameraStateListeners.remove(listener); + } + + private 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_STATES, + "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 accommodate for that. + mScheduledCompatModeUpdateCameraIdSet.add(cameraId); + mHandler.postDelayed( + () -> { + synchronized (mWmService.mGlobalLock) { + if (!mScheduledCompatModeUpdateCameraIdSet.remove(cameraId)) { + // Camera compat mode update has happened already or was cancelled + // because camera was closed. + return; + } + mCameraIdPackageBiMapping.put(packageName, cameraId); + mCameraActivity = findCameraActivity(packageName); + if (mCameraActivity == null || mCameraActivity.getTask() == null) { + return; + } + notifyListenersCameraOpened(mCameraActivity, cameraId); + } + }, + CAMERA_OPENED_LETTERBOX_UPDATE_DELAY_MS); + } + + private void notifyListenersCameraOpened(@NonNull ActivityRecord cameraActivity, + @NonNull String cameraId) { + for (int i = 0; i < mCameraStateListeners.size(); i++) { + CameraCompatStateListener listener = mCameraStateListeners.get(i); + boolean activeCameraTreatment = listener.onCameraOpened( + cameraActivity, cameraId); + if (activeCameraTreatment) { + mCurrentListenerForCameraActivity = listener; + break; + } + } + } + + private void notifyCameraClosed(@NonNull String cameraId) { + ProtoLog.v(WM_DEBUG_STATES, + "Display id=%d is notified that Camera %s is closed.", + mDisplayContent.mDisplayId, cameraId); + mScheduledToBeRemovedCameraIdSet.add(cameraId); + // No need to update window size for this camera if it's already closed. + mScheduledCompatModeUpdateCameraIdSet.remove(cameraId); + scheduleRemoveCameraId(cameraId); + } + + boolean isCameraRunningForActivity(@NonNull ActivityRecord activity) { + return getCameraIdForActivity(activity) != null; + } + + // TODO(b/336474959): try to decouple `cameraId` from the listeners. + boolean isCameraWithIdRunningForActivity(@NonNull ActivityRecord activity, String cameraId) { + return cameraId.equals(getCameraIdForActivity(activity)); + } + + void rescheduleRemoveCameraActivity(@NonNull String cameraId) { + mScheduledToBeRemovedCameraIdSet.add(cameraId); + scheduleRemoveCameraId(cameraId); + } + + @Nullable + private String getCameraIdForActivity(@NonNull ActivityRecord activity) { + return mCameraIdPackageBiMapping.getCameraId(activity.packageName); + } + + // 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_LETTERBOX_UPDATE_DELAY_MS); + } + + private void removeCameraId(@NonNull String cameraId) { + synchronized (mWmService.mGlobalLock) { + if (!mScheduledToBeRemovedCameraIdSet.remove(cameraId)) { + // Already reconnected to this camera, no need to clean up. + return; + } + if (mCameraActivity != null && mCurrentListenerForCameraActivity != null) { + boolean closeSuccessful = + mCurrentListenerForCameraActivity.onCameraClosed(mCameraActivity, cameraId); + if (closeSuccessful) { + mCameraIdPackageBiMapping.removeCameraId(cameraId); + mCurrentListenerForCameraActivity = null; + } else { + rescheduleRemoveCameraActivity(cameraId); + } + } + } + } + + // TODO(b/335165310): verify that this works in multi instance and permission dialogs. + @Nullable + private ActivityRecord findCameraActivity(@NonNull String packageName) { + final ActivityRecord topActivity = mDisplayContent.topRunningActivity( + /* considerKeyguardState= */ true); + if (topActivity != null && topActivity.packageName.equals(packageName)) { + return topActivity; + } + + final List<ActivityRecord> activitiesOfPackageWhichOpenedCamera = new ArrayList<>(); + mDisplayContent.forAllActivities(activityRecord -> { + if (activityRecord.isVisibleRequested() + && activityRecord.packageName.equals(packageName)) { + activitiesOfPackageWhichOpenedCamera.add(activityRecord); + } + }); + + if (activitiesOfPackageWhichOpenedCamera.isEmpty()) { + Slog.w(TAG, "Cannot find camera activity."); + return null; + } + + if (activitiesOfPackageWhichOpenedCamera.size() == 1) { + return activitiesOfPackageWhichOpenedCamera.getFirst(); + } + + // Return null if we cannot determine which activity opened camera. This is preferred to + // applying treatment to the wrong activity. + Slog.w(TAG, "Cannot determine which activity opened camera."); + return null; + } + + String getSummary() { + return " CameraIdPackageNameBiMapping=" + + mCameraIdPackageBiMapping + .getSummaryForDisplayRotationHistoryRecord(); + } + + interface CameraCompatStateListener { + /** + * Notifies the compat listener that an activity has opened camera. + * + * @return true if the treatment has been applied. + */ + // TODO(b/336474959): try to decouple `cameraId` from the listeners. + boolean onCameraOpened(@NonNull ActivityRecord cameraActivity, @NonNull String cameraId); + /** + * Notifies the compat listener that an activity has closed the camera. + * + * @return true if cleanup has been successful - the notifier might try again if false. + */ + // TODO(b/336474959): try to decouple `cameraId` from the listeners. + boolean onCameraClosed(@NonNull ActivityRecord cameraActivity, @NonNull String cameraId); + } +} diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 95ec75ca172c..f7e5dd8632fc 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -473,7 +473,12 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp private final DisplayMetrics mDisplayMetrics = new DisplayMetrics(); private final DisplayPolicy mDisplayPolicy; private final DisplayRotation mDisplayRotation; - @Nullable final DisplayRotationCompatPolicy mDisplayRotationCompatPolicy; + + @Nullable + final DisplayRotationCompatPolicy mDisplayRotationCompatPolicy; + @Nullable + final CameraStateMonitor mCameraStateMonitor; + DisplayFrames mDisplayFrames; final DisplayUpdater mDisplayUpdater; @@ -1247,11 +1252,23 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp onDisplayChanged(this); updateDisplayAreaOrganizers(); - mDisplayRotationCompatPolicy = - // Not checking DeviceConfig value here to allow enabling via DeviceConfig - // without the need to restart the device. - mWmService.mLetterboxConfiguration.isCameraCompatTreatmentEnabledAtBuildTime() - ? new DisplayRotationCompatPolicy(this) : null; + // Not checking DeviceConfig value here to allow enabling via DeviceConfig + // without the need to restart the device. + final boolean shouldCreateDisplayRotationCompatPolicy = + mWmService.mLetterboxConfiguration.isCameraCompatTreatmentEnabledAtBuildTime(); + if (shouldCreateDisplayRotationCompatPolicy) { + mCameraStateMonitor = new CameraStateMonitor(this, mWmService.mH); + mDisplayRotationCompatPolicy = new DisplayRotationCompatPolicy( + this, mWmService.mH, mCameraStateMonitor); + + mCameraStateMonitor.startListeningToCameraState(); + } else { + // These are to satisfy the `final` check. + mCameraStateMonitor = null; + mDisplayRotationCompatPolicy = null; + } + + mRotationReversionController = new DisplayRotationReversionController(this); mInputMonitor = new InputMonitor(mWmService, this); @@ -3453,6 +3470,9 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp if (mDisplayRotationCompatPolicy != null) { mDisplayRotationCompatPolicy.dispose(); } + if (mCameraStateMonitor != null) { + mCameraStateMonitor.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 index c8a7ef11a700..d5f8df35d96f 100644 --- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java @@ -43,20 +43,15 @@ import android.app.servertransaction.ResumeActivityItem; import android.content.pm.ActivityInfo.ScreenOrientation; import android.content.pm.PackageManager; import android.content.res.Configuration; -import android.hardware.camera2.CameraManager; import android.os.Handler; import android.os.RemoteException; -import android.util.ArraySet; import android.widget.Toast; 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 com.android.server.UiThread; -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 @@ -69,7 +64,7 @@ import java.util.Set; * R.bool.config_isWindowManagerCameraCompatTreatmentEnabled} is {@code true}. */ // TODO(b/261444714): Consider moving Camera-specific logic outside of the WM Core path -final class DisplayRotationCompatPolicy { +final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraCompatStateListener { // 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 @@ -91,54 +86,26 @@ final class DisplayRotationCompatPolicy { private final DisplayContent mDisplayContent; private final WindowManagerService mWmService; - private final CameraManager mCameraManager; + private final CameraStateMonitor mCameraStateMonitor; private final Handler mHandler; - // 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 CameraIdPackageNameBiMapping mCameraIdPackageBiMap = - new CameraIdPackageNameBiMapping(); - @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); - } - }; - @ScreenOrientation private int mLastReportedOrientation = SCREEN_ORIENTATION_UNSET; - DisplayRotationCompatPolicy(@NonNull DisplayContent displayContent) { - this(displayContent, displayContent.mWmService.mH); - } - - @VisibleForTesting - DisplayRotationCompatPolicy(@NonNull DisplayContent displayContent, Handler handler) { + DisplayRotationCompatPolicy(@NonNull DisplayContent displayContent, Handler handler, + @NonNull CameraStateMonitor cameraStateMonitor) { // 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; - mCameraManager = mWmService.mContext.getSystemService(CameraManager.class); - mCameraManager.registerAvailabilityCallback( - mWmService.mContext.getMainExecutor(), mAvailabilityCallback); + mCameraStateMonitor = cameraStateMonitor; + mCameraStateMonitor.addCameraStateListener(this); } + /** Releases camera state listener. */ void dispose() { - mCameraManager.unregisterAvailabilityCallback(mAvailabilityCallback); + mCameraStateMonitor.removeCameraStateListener(this); } /** @@ -169,7 +136,7 @@ final class DisplayRotationCompatPolicy { if (!isTreatmentEnabledForDisplay()) { return SCREEN_ORIENTATION_UNSPECIFIED; } - ActivityRecord topActivity = mDisplayContent.topRunningActivity( + final ActivityRecord topActivity = mDisplayContent.topRunningActivity( /* considerKeyguardState= */ true); if (!isTreatmentEnabledForActivity(topActivity)) { return SCREEN_ORIENTATION_UNSPECIFIED; @@ -188,7 +155,7 @@ final class DisplayRotationCompatPolicy { // 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) + final int orientation = (isPortraitActivity && isNaturalDisplayOrientationPortrait) || (!isPortraitActivity && !isNaturalDisplayOrientationPortrait) ? SCREEN_ORIENTATION_PORTRAIT : SCREEN_ORIENTATION_LANDSCAPE; @@ -249,12 +216,10 @@ final class DisplayRotationCompatPolicy { * reason with the {@link Toast}. */ void onScreenRotationAnimationFinished() { - if (!isTreatmentEnabledForDisplay() || mCameraIdPackageBiMap.isEmpty()) { - return; - } - ActivityRecord topActivity = mDisplayContent.topRunningActivity( - /* considerKeyguardState= */ true); - if (!isTreatmentEnabledForActivity(topActivity)) { + final ActivityRecord topActivity = mDisplayContent.topRunningActivity( + /* considerKeyguardState= */ true); + if (!isTreatmentEnabledForDisplay() + || !isTreatmentEnabledForActivity(topActivity)) { return; } showToast(R.string.display_rotation_camera_compat_toast_after_rotation); @@ -272,8 +237,8 @@ final class DisplayRotationCompatPolicy { + (topActivity == null ? "null" : topActivity.shortComponentName) + " isTreatmentEnabledForActivity=" + isTreatmentEnabledForActivity(topActivity) - + " CameraIdPackageNameBiMap=" - + mCameraIdPackageBiMap.getSummaryForDisplayRotationHistoryRecord(); + + "mCameraStateMonitor=" + + mCameraStateMonitor.getSummary(); } return "DisplayRotationCompatPolicy{" + " isTreatmentEnabledForDisplay=" + isTreatmentEnabledForDisplay() @@ -373,67 +338,41 @@ final class DisplayRotationCompatPolicy { // Checking windowing mode on activity level because we don't want to // apply treatment in case of activity embedding. return (!mustBeFullscreen || !activity.inMultiWindowMode()) - && mCameraIdPackageBiMap.containsPackageName(activity.packageName) + && mCameraStateMonitor.isCameraRunningForActivity(activity) && activity.mLetterboxUiController.shouldForceRotateForCameraCompat(); } - 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); - } - - 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); + @Override + public boolean onCameraOpened(@NonNull ActivityRecord cameraActivity, + @NonNull String cameraId) { + // Checking whether an activity in fullscreen rather than the task as this camera + // compat treatment doesn't cover activity embedding. + if (cameraActivity.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { + cameraActivity.mLetterboxUiController.recomputeConfigurationForCameraCompatIfNeeded(); + mDisplayContent.updateOrientation(); + return true; } - synchronized (mWmService.mGlobalLock) { - ActivityRecord topActivity = mDisplayContent.topRunningActivity( - /* considerKeyguardState= */ true); - if (topActivity == null || topActivity.getTask() == null) { - return; - } - // Checking whether an activity in fullscreen rather than the task as this camera - // compat treatment doesn't cover activity embedding. - if (topActivity.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { - topActivity.mLetterboxUiController.recomputeConfigurationForCameraCompatIfNeeded(); - mDisplayContent.updateOrientation(); - return; - } - // Checking that the whole app is in multi-window mode as we shouldn't show toast - // for the activity embedding case. - if (topActivity.getTask().getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW - && isTreatmentEnabledForActivity(topActivity, /* mustBeFullscreen */ false)) { - final PackageManager packageManager = mWmService.mContext.getPackageManager(); - try { - showToast( - R.string.display_rotation_camera_compat_toast_in_multi_window, - (String) packageManager.getApplicationLabel( - packageManager.getApplicationInfo(packageName, /* flags */ 0))); - } catch (PackageManager.NameNotFoundException e) { - ProtoLog.e(WM_DEBUG_ORIENTATION, - "DisplayRotationCompatPolicy: Multi-window toast not shown as " - + "package '%s' cannot be found.", - packageName); - } + // Checking that the whole app is in multi-window mode as we shouldn't show toast + // for the activity embedding case. + if (cameraActivity.getTask().getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW + && isTreatmentEnabledForActivity( + cameraActivity, /* mustBeFullscreen */ false)) { + final PackageManager packageManager = mWmService.mContext.getPackageManager(); + try { + showToast( + R.string.display_rotation_camera_compat_toast_in_multi_window, + (String) packageManager.getApplicationLabel( + packageManager.getApplicationInfo(cameraActivity.packageName, + /* flags */ 0))); + return true; + } catch (PackageManager.NameNotFoundException e) { + ProtoLog.e(WM_DEBUG_ORIENTATION, + "DisplayRotationCompatPolicy: Multi-window toast not shown as " + + "package '%s' cannot be found.", + cameraActivity.packageName); } } + return false; } @VisibleForTesting @@ -451,66 +390,42 @@ final class DisplayRotationCompatPolicy { Toast.LENGTH_LONG).show()); } - 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); - 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); - } - - private void removeCameraId(String cameraId) { + @Override + public boolean onCameraClosed(@NonNull ActivityRecord cameraActivity, + @NonNull String cameraId) { synchronized (this) { - if (!mScheduledToBeRemovedCameraIdSet.remove(cameraId)) { - // Already reconnected to this camera, no need to clean up. - return; - } + // TODO(b/336474959): Once refresh is implemented in `CameraCompatFreeformPolicy`, + // consider checking this in CameraStateMonitor before notifying the listeners (this). if (isActivityForCameraIdRefreshing(cameraId)) { ProtoLog.v(WM_DEBUG_ORIENTATION, - "Display id=%d is notified that Camera %s is closed but activity is" + "Display id=%d is notified that camera is closed but activity is" + " still refreshing. Rescheduling an update.", - mDisplayContent.mDisplayId, cameraId); - mScheduledToBeRemovedCameraIdSet.add(cameraId); - scheduleRemoveCameraId(cameraId); - return; + mDisplayContent.mDisplayId); + return false; } - mCameraIdPackageBiMap.removeCameraId(cameraId); } ProtoLog.v(WM_DEBUG_ORIENTATION, - "Display id=%d is notified that Camera %s is closed, updating rotation.", - mDisplayContent.mDisplayId, cameraId); - synchronized (mWmService.mGlobalLock) { - ActivityRecord topActivity = mDisplayContent.topRunningActivity( - /* considerKeyguardState= */ true); - if (topActivity == null - // Checking whether an activity in fullscreen rather than the task as this - // camera compat treatment doesn't cover activity embedding. - || topActivity.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) { - return; - } - topActivity.mLetterboxUiController.recomputeConfigurationForCameraCompatIfNeeded(); - mDisplayContent.updateOrientation(); + "Display id=%d is notified that Camera is closed, updating rotation.", + mDisplayContent.mDisplayId); + final ActivityRecord topActivity = mDisplayContent.topRunningActivity( + /* considerKeyguardState= */ true); + if (topActivity == null + // Checking whether an activity in fullscreen rather than the task as this + // camera compat treatment doesn't cover activity embedding. + || topActivity.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) { + return true; } + topActivity.mLetterboxUiController.recomputeConfigurationForCameraCompatIfNeeded(); + mDisplayContent.updateOrientation(); + return true; } + // TODO(b/336474959): Do we need cameraId here? private boolean isActivityForCameraIdRefreshing(String cameraId) { - ActivityRecord topActivity = mDisplayContent.topRunningActivity( + final ActivityRecord topActivity = mDisplayContent.topRunningActivity( /* considerKeyguardState= */ true); - if (!isTreatmentEnabledForActivity(topActivity)) { - return false; - } - String activeCameraId = mCameraIdPackageBiMap.getCameraId(topActivity.packageName); - if (activeCameraId == null || activeCameraId != cameraId) { + if (!isTreatmentEnabledForActivity(topActivity) + || !mCameraStateMonitor.isCameraWithIdRunningForActivity(topActivity, cameraId)) { return false; } return topActivity.mLetterboxUiController.isRefreshAfterRotationRequested(); diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraStateMonitorTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraStateMonitorTests.java new file mode 100644 index 000000000000..e468fd8ee495 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/CameraStateMonitorTests.java @@ -0,0 +1,274 @@ +/* + * 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 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.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.hardware.camera2.CameraManager; +import android.os.Handler; +import android.platform.test.annotations.Presubmit; + +import androidx.annotation.NonNull; +import androidx.test.filters.SmallTest; + +import org.junit.After; +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 CameraStateMonitorTests 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 static final String TEST_PACKAGE_1_LABEL = "testPackage1"; + private CameraManager mMockCameraManager; + private Handler mMockHandler; + private LetterboxConfiguration mLetterboxConfiguration; + + private CameraStateMonitor mCameraStateMonitor; + private CameraManager.AvailabilityCallback mCameraAvailabilityCallback; + + private ActivityRecord mActivity; + private Task mTask; + + // Simulates a listener which will not react to the change on a particular activity. + private final FakeCameraCompatStateListener mNotInterestedListener = + new FakeCameraCompatStateListener( + /*onCameraOpenedReturnValue=*/ false, + /*simulateUnsuccessfulCloseOnce=*/ false); + // Simulates a listener which will react to the change on a particular activity - for example + // put the activity in a camera compat mode. + private final FakeCameraCompatStateListener mInterestedListener = + new FakeCameraCompatStateListener( + /*onCameraOpenedReturnValue=*/ true, + /*simulateUnsuccessfulCloseOnce=*/ false); + // Simulates a listener which for some reason cannot process `onCameraClosed` event once it + // first arrives - this means that the update needs to be postponed. + private final FakeCameraCompatStateListener mListenerCannotClose = + new FakeCameraCompatStateListener( + /*onCameraOpenedReturnValue=*/ true, + /*simulateUnsuccessfulCloseOnce=*/ true); + + @Before + public void setUp() throws Exception { + mLetterboxConfiguration = mDisplayContent.mWmService.mLetterboxConfiguration; + spyOn(mLetterboxConfiguration); + when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled()) + .thenReturn(true); + when(mLetterboxConfiguration.isCameraCompatRefreshEnabled()) + .thenReturn(true); + when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) + .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; + }); + mCameraStateMonitor = + new CameraStateMonitor(mDisplayContent, mMockHandler); + configureActivity(TEST_PACKAGE_1); + configureActivity(TEST_PACKAGE_2); + + mCameraStateMonitor.startListeningToCameraState(); + } + + @After + public void tearDown() { + // Remove all listeners. + mCameraStateMonitor.removeCameraStateListener(mNotInterestedListener); + mCameraStateMonitor.removeCameraStateListener(mInterestedListener); + mCameraStateMonitor.removeCameraStateListener(mListenerCannotClose); + + // Reset the listener's state. + mNotInterestedListener.resetCounters(); + mInterestedListener.resetCounters(); + mListenerCannotClose.resetCounters(); + } + + @Test + public void testOnCameraOpened_listenerAdded_notifiesCameraOpened() { + mCameraStateMonitor.addCameraStateListener(mNotInterestedListener); + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + assertEquals(1, mNotInterestedListener.mOnCameraOpenedCounter); + } + + @Test + public void testOnCameraOpened_listenerReturnsFalse_doesNotNotifyCameraClosed() { + mCameraStateMonitor.addCameraStateListener(mNotInterestedListener); + // Listener returns false on `onCameraOpened`. + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1); + + assertEquals(0, mNotInterestedListener.mOnCameraClosedCounter); + } + + @Test + public void testOnCameraOpened_listenerReturnsTrue_notifyCameraClosed() { + mCameraStateMonitor.addCameraStateListener(mInterestedListener); + // Listener returns true on `onCameraOpened`. + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1); + + assertEquals(1, mInterestedListener.mOnCameraClosedCounter); + } + + @Test + public void testOnCameraOpened_listenerCannotCloseYet_notifyCameraClosedAgain() { + mCameraStateMonitor.addCameraStateListener(mListenerCannotClose); + // Listener returns true on `onCameraOpened`. + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1); + + assertEquals(2, mListenerCannotClose.mOnCameraClosedCounter); + } + + @Test + public void testReconnectedToDifferentCamera_notifiesListener() { + mCameraStateMonitor.addCameraStateListener(mInterestedListener); + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1); + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_2, TEST_PACKAGE_1); + + assertEquals(2, mInterestedListener.mOnCameraOpenedCounter); + } + + @Test + public void testDifferentAppConnectedToCamera_notifiesListener() { + mCameraStateMonitor.addCameraStateListener(mInterestedListener); + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1); + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_2); + + assertEquals(2, mInterestedListener.mOnCameraOpenedCounter); + } + + @Test + public void testCameraAlreadyClosed_notifiesListenerOnce() { + mCameraStateMonitor.addCameraStateListener(mInterestedListener); + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1); + mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1); + + assertEquals(1, mInterestedListener.mOnCameraClosedCounter); + } + + private void configureActivity(@NonNull String packageName) { + mTask = new TaskBuilder(mSupervisor) + .setDisplay(mDisplayContent) + .build(); + + mActivity = new ActivityBuilder(mAtm) + .setComponent(new ComponentName(packageName, ".TestActivity")) + .setTask(mTask) + .build(); + + spyOn(mActivity.mAtmService.getLifecycleManager()); + spyOn(mActivity.mLetterboxUiController); + + doReturn(mActivity).when(mDisplayContent).topRunningActivity(anyBoolean()); + } + + private class FakeCameraCompatStateListener implements + CameraStateMonitor.CameraCompatStateListener { + + int mOnCameraOpenedCounter = 0; + int mOnCameraClosedCounter = 0; + + boolean mOnCameraOpenedReturnValue = true; + private boolean mOnCameraClosedReturnValue = true; + + /** + * @param simulateUnsuccessfulCloseOnce When false, returns `true` on every + * `onCameraClosed`. When true, returns `false` on the + * first `onCameraClosed` callback, and `true on the + * subsequent calls. This fake implementation tests the + * retry mechanism in {@link CameraStateMonitor}. + */ + FakeCameraCompatStateListener(boolean onCameraOpenedReturnValue, + boolean simulateUnsuccessfulCloseOnce) { + mOnCameraOpenedReturnValue = onCameraOpenedReturnValue; + mOnCameraClosedReturnValue = !simulateUnsuccessfulCloseOnce; + } + + @Override + public boolean onCameraOpened(@NonNull ActivityRecord cameraActivity, + @NonNull String cameraId) { + mOnCameraOpenedCounter++; + return mOnCameraOpenedReturnValue; + } + + @Override + public boolean onCameraClosed(@NonNull ActivityRecord cameraActivity, + @NonNull String cameraId) { + mOnCameraClosedCounter++; + boolean returnValue = mOnCameraClosedReturnValue; + // If false, return false only the first time, so it doesn't fall in the infinite retry + // loop. + mOnCameraClosedReturnValue = true; + return returnValue; + } + + void resetCounters() { + mOnCameraOpenedCounter = 0; + mOnCameraClosedCounter = 0; + } + } +} 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 0dd02394d424..507140d573ea 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java @@ -129,13 +129,17 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { ((Runnable) invocation.getArgument(0)).run(); return null; }); - mDisplayRotationCompatPolicy = new DisplayRotationCompatPolicy( - mDisplayContent, mMockHandler); + CameraStateMonitor cameraStateMonitor = + new CameraStateMonitor(mDisplayContent, mMockHandler); + mDisplayRotationCompatPolicy = + new DisplayRotationCompatPolicy(mDisplayContent, mMockHandler, cameraStateMonitor); // Do not show the real toast. spyOn(mDisplayRotationCompatPolicy); doNothing().when(mDisplayRotationCompatPolicy).showToast(anyInt()); doNothing().when(mDisplayRotationCompatPolicy).showToast(anyInt(), anyString()); + + cameraStateMonitor.startListeningToCameraState(); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java index b9fe0745ef4c..64adff80f442 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java +++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java @@ -425,6 +425,9 @@ public class SystemServicesTestRule implements TestRule { if (dc.mDisplayRotationCompatPolicy != null) { dc.mDisplayRotationCompatPolicy.dispose(); } + if (dc.mCameraStateMonitor != null) { + dc.mCameraStateMonitor.dispose(); + } } } |