CCA: Skip calling getUserMedia on conflicting device until released

If all the cameras failed to open, the watchdog will keep trying to open
the cameras. In this case, the notification in the tool bar is
annoying. This CL adds a private api IsDeviceInUse() which can only be
called by CCA to let CCA get if a device is in use. If NotReadableError
is thrown and the device is in use, it means that the device is used by
Lacros. In this case, the watchdog skips to call getUserMedia on the
failing device until it is not in use.

BUG: b:289773559
Test: Manually by open Meet, then open CCA, finally close Meet and check
the console.

Change-Id: Id45c4c115d1bf79e0509beced5fd041bc9f620c3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4688637
Commit-Queue: Sean Li <seannli@google.com>
Reviewed-by: Pi-Hsun Shih <pihsun@chromium.org>
Reviewed-by: Takashi Toyoshima <toyoshim@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1174725}
diff --git a/ash/webui/camera_app_ui/resources/js/device/camera_manager.ts b/ash/webui/camera_app_ui/resources/js/device/camera_manager.ts
index 942a9db..c244be7 100644
--- a/ash/webui/camera_app_ui/resources/js/device/camera_manager.ts
+++ b/ash/webui/camera_app_ui/resources/js/device/camera_manager.ts
@@ -544,7 +544,7 @@
       // reconfigure result which may not reflect the setting before calling it.
       // Thus still fallthrough here to start another reconfigure.
     }
-
+    this.scheduler.reconfigurer.resetFailedDevices();
     return this.doReconfigure();
   }
 
diff --git a/ash/webui/camera_app_ui/resources/js/device/camera_operation.ts b/ash/webui/camera_app_ui/resources/js/device/camera_operation.ts
index 6694831..d1a6ab4 100644
--- a/ash/webui/camera_app_ui/resources/js/device/camera_operation.ts
+++ b/ash/webui/camera_app_ui/resources/js/device/camera_operation.ts
@@ -71,6 +71,8 @@
 
   readonly capturePreferrer = new CaptureCandidatePreferrer();
 
+  private readonly failedDevices = new Set<string>();
+
   constructor(
       private readonly preview: Preview,
       private readonly modes: Modes,
@@ -210,6 +212,14 @@
   }
 
   /**
+   * Reset the failed devices list so the next reconfiguration
+   * will try to open those devices.
+   */
+  resetFailedDevices(): void {
+    this.failedDevices.clear();
+  }
+
+  /**
    * @return If the configuration finished successfully.
    */
   async startConfigure(cameraInfo: CameraInfo): Promise<boolean> {
@@ -224,7 +234,17 @@
       if (this.shouldSuspend) {
         return false;
       }
-
+      if (this.failedDevices.has(c.deviceId)) {
+        // Check if the devices is released from other apps. If not,
+        // we skip using it as a constraint to open a stream.
+        const deviceOperator = DeviceOperator.getInstance();
+        if (deviceOperator !== null) {
+          const inUse = await deviceOperator.isDeviceInUse(c.deviceId);
+          if (inUse) {
+            continue;
+          }
+        }
+      }
       let facing = c.deviceId !== null ?
           cameraInfo.getCamera3DeviceInfo(c.deviceId)?.facing ?? null :
           null;
@@ -286,6 +306,17 @@
           if (e.name === 'NotReadableError') {
             // TODO(b/187879603): Remove this hacked once we understand more
             // about such error.
+            const deviceOperator = DeviceOperator.getInstance();
+            if (deviceOperator !== null) {
+              // If 'NotReadableError' is thrown while the device is in use,
+              // it means that the devices is used by Lacros.
+              // In this case, we add it into `failedDevices` and skip using
+              // it to open a stream until it is not in use.
+              const inUse = await deviceOperator.isDeviceInUse(c.deviceId);
+              if (inUse) {
+                this.failedDevices.add(c.deviceId);
+              }
+            }
             // We cannot get the camera facing from stream since it might
             // not be successfully opened. Therefore, we asked the camera
             // facing via Mojo API.
diff --git a/ash/webui/camera_app_ui/resources/js/mojo/device_operator.ts b/ash/webui/camera_app_ui/resources/js/mojo/device_operator.ts
index ebfccbc9..f16173f1 100644
--- a/ash/webui/camera_app_ui/resources/js/mojo/device_operator.ts
+++ b/ash/webui/camera_app_ui/resources/js/mojo/device_operator.ts
@@ -205,6 +205,17 @@
   }
 
   /**
+   * Check if the device is in use.
+   *
+   * @param deviceId The id of target camera device.
+   */
+  async isDeviceInUse(deviceId: string): Promise<boolean> {
+    assert(this.deviceProvider !== null);
+    const {inUse} = await this.deviceProvider.isDeviceInUse(deviceId);
+    return inUse;
+  }
+
+  /**
    * Gets corresponding device remote by given |deviceId|.
    *
    * @throws Thrown when given |deviceId| is invalid.
diff --git a/media/capture/video/chromeos/camera_app_device_bridge_impl.cc b/media/capture/video/chromeos/camera_app_device_bridge_impl.cc
index 7b2ade7..94aca9c4 100644
--- a/media/capture/video/chromeos/camera_app_device_bridge_impl.cc
+++ b/media/capture/video/chromeos/camera_app_device_bridge_impl.cc
@@ -238,4 +238,24 @@
   std::move(callback).Run(true);
 }
 
+void CameraAppDeviceBridgeImpl::SetDeviceInUse(const std::string& device_id,
+                                               bool in_use) {
+  base::AutoLock lock(devices_in_use_lock_);
+  if (in_use) {
+    devices_in_use_.insert(device_id);
+  } else {
+    devices_in_use_.erase(device_id);
+  }
+}
+
+void CameraAppDeviceBridgeImpl::IsDeviceInUse(const std::string& device_id,
+                                              IsDeviceInUseCallback callback) {
+  bool in_use;
+  {
+    base::AutoLock lock(devices_in_use_lock_);
+    in_use = devices_in_use_.contains(device_id);
+  }
+  std::move(callback).Run(in_use);
+}
+
 }  // namespace media
diff --git a/media/capture/video/chromeos/camera_app_device_bridge_impl.h b/media/capture/video/chromeos/camera_app_device_bridge_impl.h
index c761390..271c831 100644
--- a/media/capture/video/chromeos/camera_app_device_bridge_impl.h
+++ b/media/capture/video/chromeos/camera_app_device_bridge_impl.h
@@ -82,6 +82,11 @@
       bool enabled,
       SetVirtualDeviceEnabledCallback callback) override;
 
+  void SetDeviceInUse(const std::string& device_id, bool in_use);
+
+  void IsDeviceInUse(const std::string& device_id,
+                     IsDeviceInUseCallback callback) override;
+
  private:
   friend struct base::DefaultSingletonTraits<CameraAppDeviceBridgeImpl>;
 
@@ -106,6 +111,9 @@
   base::Lock task_runner_map_lock_;
   base::flat_map<std::string, scoped_refptr<base::SingleThreadTaskRunner>>
       ipc_task_runners_ GUARDED_BY(task_runner_map_lock_);
+
+  base::Lock devices_in_use_lock_;
+  base::flat_set<std::string> devices_in_use_ GUARDED_BY(devices_in_use_lock_);
 };
 
 }  // namespace media
diff --git a/media/capture/video/chromeos/camera_app_device_provider_impl.cc b/media/capture/video/chromeos/camera_app_device_provider_impl.cc
index 54eae6d..3eef8da 100644
--- a/media/capture/video/chromeos/camera_app_device_provider_impl.cc
+++ b/media/capture/video/chromeos/camera_app_device_provider_impl.cc
@@ -77,4 +77,23 @@
   bridge_->SetVirtualDeviceEnabled(*device_id, enabled, std::move(callback));
 }
 
+void CameraAppDeviceProviderImpl::IsDeviceInUse(
+    const std::string& source_id,
+    IsDeviceInUseCallback callback) {
+  mapping_callback_.Run(
+      source_id, base::BindPostTaskToCurrentDefault(base::BindOnce(
+                     &CameraAppDeviceProviderImpl::IsDeviceInUseWithDeviceId,
+                     weak_ptr_factory_.GetWeakPtr(), std::move(callback))));
+}
+
+void CameraAppDeviceProviderImpl::IsDeviceInUseWithDeviceId(
+    IsDeviceInUseCallback callback,
+    const absl::optional<std::string>& device_id) {
+  if (!device_id.has_value()) {
+    std::move(callback).Run(false);
+    return;
+  }
+  bridge_->IsDeviceInUse(*device_id, std::move(callback));
+}
+
 }  // namespace media
diff --git a/media/capture/video/chromeos/camera_app_device_provider_impl.h b/media/capture/video/chromeos/camera_app_device_provider_impl.h
index 179110a..004a3cb 100644
--- a/media/capture/video/chromeos/camera_app_device_provider_impl.h
+++ b/media/capture/video/chromeos/camera_app_device_provider_impl.h
@@ -42,6 +42,8 @@
       const std::string& device_id,
       bool enabled,
       SetVirtualDeviceEnabledCallback callback) override;
+  void IsDeviceInUse(const std::string& source_id,
+                     IsDeviceInUseCallback callback) override;
 
  private:
   void GetCameraAppDeviceWithDeviceId(
@@ -52,6 +54,8 @@
       bool enable,
       SetVirtualDeviceEnabledCallback callback,
       const absl::optional<std::string>& device_id);
+  void IsDeviceInUseWithDeviceId(IsDeviceInUseCallback callback,
+                                 const absl::optional<std::string>& device_id);
 
   mojo::Remote<cros::mojom::CameraAppDeviceBridge> bridge_;
 
diff --git a/media/capture/video/chromeos/camera_device_delegate.cc b/media/capture/video/chromeos/camera_device_delegate.cc
index cb66ad5..7f56827 100644
--- a/media/capture/video/chromeos/camera_device_delegate.cc
+++ b/media/capture/video/chromeos/camera_device_delegate.cc
@@ -327,6 +327,8 @@
   if (camera_app_device) {
     camera_app_device->SetCameraDeviceContext(device_context_);
   }
+  CameraAppDeviceBridgeImpl::GetInstance()->SetDeviceInUse(
+      device_descriptor_.device_id, true);
 
   auto camera_info = camera_hal_delegate_->GetCameraInfoFromDeviceId(
       device_descriptor_.device_id);
@@ -397,6 +399,8 @@
   if (camera_app_device) {
     camera_app_device->SetCameraDeviceContext(nullptr);
   }
+  CameraAppDeviceBridgeImpl::GetInstance()->SetDeviceInUse(
+      device_descriptor_.device_id, false);
 
   device_close_callback_ = std::move(device_close_callback);
   device_context_->SetState(CameraDeviceContext::State::kStopping);
diff --git a/media/capture/video/chromeos/mojom/camera_app.mojom b/media/capture/video/chromeos/mojom/camera_app.mojom
index e823a091..897aaf9 100644
--- a/media/capture/video/chromeos/mojom/camera_app.mojom
+++ b/media/capture/video/chromeos/mojom/camera_app.mojom
@@ -59,6 +59,9 @@
   // The virtual device has the same config as |device_id| except facing
   // attribute.
   SetVirtualDeviceEnabled(string device_id, bool enabled) => (bool success);
+
+  // Check if the device with |source_id| is in use.
+  IsDeviceInUse(string source_id) => (bool in_use);
 };
 
 // Inner interface that used to communicate between browser process (Remote) and
@@ -79,6 +82,9 @@
   // The virtual device has the same config as |device_id| except facing
   // attribute.
   SetVirtualDeviceEnabled(string device_id, bool enabled) => (bool success);
+
+  // Check if the device with |device_id| is in use.
+  IsDeviceInUse(string device_id) => (bool in_use);
 };
 
 // Interface for communication from the Chrome Camera App (Remote) to the camera