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();
+                }
             }
         }