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
diff --git a/data/etc/core.protolog.pb b/data/etc/core.protolog.pb
index 97147a0..000f6ef 100644
--- a/data/etc/core.protolog.pb
+++ b/data/etc/core.protolog.pb
Binary files differ
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 483b693..01deb49 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.",
+ "-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"
},
- "-8746776274432739264": {
- "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"
- },
- "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 0000000..ea7edea
--- /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 95ec75c..f7e5dd8 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 @@
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 @@
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 @@
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 c8a7ef1..d5f8df3 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.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 @@
* 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 @@
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 @@
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 @@
// 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 @@
* 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 @@
+ (topActivity == null ? "null" : topActivity.shortComponentName)
+ " isTreatmentEnabledForActivity="
+ isTreatmentEnabledForActivity(topActivity)
- + " CameraIdPackageNameBiMap="
- + mCameraIdPackageBiMap.getSummaryForDisplayRotationHistoryRecord();
+ + "mCameraStateMonitor="
+ + mCameraStateMonitor.getSummary();
}
return "DisplayRotationCompatPolicy{"
+ " isTreatmentEnabledForDisplay=" + isTreatmentEnabledForDisplay()
@@ -373,67 +338,41 @@
// 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 @@
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 0000000..e468fd8
--- /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 0dd0239..507140d 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 @@
((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 b9fe074..64adff8 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 @@
if (dc.mDisplayRotationCompatPolicy != null) {
dc.mDisplayRotationCompatPolicy.dispose();
}
+ if (dc.mCameraStateMonitor != null) {
+ dc.mCameraStateMonitor.dispose();
+ }
}
}