[go: nahoru, domu]

[Concurrent Camera] Wire up CameraCoordinator to ProcessCameraProvider and CameraFactory

1. Add getAvailableConcurrentCameraInfos and isConcurrentCameraModeOn in ProcessCameraProvider
2. Add validation logic for bindToLifecycle in ProcessCameraProvider
   a) If single camera mode ON, binding concurrent camreas will throw UnsupportedOperationException, remains single mode
   b) If concurrent camera mode ON, binding single camera will throw UnsupportedOperationException, reamins concurrent mode
   c) If concurrent camera mode ON, binding different concurrent camera will throw UnsupportedOperationException, remains concurrent mode
   d) If concurrent camera mode ON, binding same concurrent camera will not no-op, remain concurrent mode

Bug: b/269146570
Test: Run unit and instrument tests
Change-Id: Id9ddfd88b6a0c255ec328ba4dc34db5e2cc7d50f
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt
index 56257d4..a6d6b1f 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt
@@ -91,7 +91,37 @@
 
     override fun getCameraCoordinator(): CameraCoordinator? {
         // TODO(b/262772650): camera-pipe support for concurrent camera.
-        return null
+        return object : CameraCoordinator {
+            override fun getConcurrentCameraSelectors(): MutableList<MutableList<CameraSelector>> {
+                return mutableListOf()
+            }
+
+            override fun getActiveConcurrentCameraSelectors(): MutableList<CameraSelector> {
+                return mutableListOf()
+            }
+
+            override fun setActiveConcurrentCameraSelectors(
+                cameraSelectors: MutableList<CameraSelector>
+            ) {
+            }
+
+            override fun getPairedConcurrentCameraId(cameraId: String): String? {
+                return null
+            }
+
+            override fun getCameraOperatingMode(): Int {
+                return CameraCoordinator.CAMERA_OPERATING_MODE_UNSPECIFIED
+            }
+
+            override fun setCameraOperatingMode(cameraOperatingMode: Int) {
+            }
+
+            override fun addListener(listener: CameraCoordinator.ConcurrentCameraModeListener) {
+            }
+
+            override fun removeListener(listener: CameraCoordinator.ConcurrentCameraModeListener) {
+            }
+        }
     }
 
     override fun getCameraManager(): Any? = appComponent
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/concurrent/Camera2CameraCoordinator.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/concurrent/Camera2CameraCoordinator.java
index e1f4760..d596e28 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/concurrent/Camera2CameraCoordinator.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/concurrent/Camera2CameraCoordinator.java
@@ -49,30 +49,20 @@
     private static final String TAG = "Camera2CameraCoordinator";
 
     @NonNull private final CameraManagerCompat mCameraManager;
-    @NonNull private Map<String, String> mConcurrentCameraIdMap;
-    @NonNull private Set<Set<String>> mConcurrentCameraIds;
     @NonNull private final List<ConcurrentCameraModeListener> mConcurrentCameraModeListeners;
+    @NonNull private final Map<String, String> mConcurrentCameraIdMap;
+    @NonNull private List<CameraSelector> mActiveConcurrentCameraSelectors;
+    @NonNull private Set<Set<String>> mConcurrentCameraIds;
 
-    private boolean mIsConcurrentCameraModeOn;
+    @CameraOperatingMode private int mCameraOperatingMode = CAMERA_OPERATING_MODE_UNSPECIFIED;
 
     public Camera2CameraCoordinator(@NonNull CameraManagerCompat cameraManager) {
         mCameraManager = cameraManager;
         mConcurrentCameraIdMap = new HashMap<>();
         mConcurrentCameraIds = new HashSet<>();
         mConcurrentCameraModeListeners = new ArrayList<>();
-    }
-
-    @Override
-    public void init() {
-        mConcurrentCameraIds = retrieveConcurrentCameraIds(mCameraManager);
-        for (Set<String> concurrentCameraIdList: mConcurrentCameraIds) {
-            List<String> cameraIdList = new ArrayList<>(concurrentCameraIdList);
-
-            // TODO(b/268531569): enumerate concurrent camera ids and convert to a map for
-            //  paired camera id lookup.
-            mConcurrentCameraIdMap.put(cameraIdList.get(0), cameraIdList.get(1));
-            mConcurrentCameraIdMap.put(cameraIdList.get(1), cameraIdList.get(0));
-        }
+        mActiveConcurrentCameraSelectors = new ArrayList<>();
+        retrieveConcurrentCameraIds();
     }
 
     @NonNull
@@ -89,6 +79,17 @@
         return concurrentCameraSelectorLists;
     }
 
+    @NonNull
+    @Override
+    public List<CameraSelector> getActiveConcurrentCameraSelectors() {
+        return mActiveConcurrentCameraSelectors;
+    }
+
+    @Override
+    public void setActiveConcurrentCameraSelectors(@NonNull List<CameraSelector> cameraSelectors) {
+        mActiveConcurrentCameraSelectors = cameraSelectors;
+    }
+
     @Nullable
     @Override
     public String getPairedConcurrentCameraId(@NonNull String cameraId) {
@@ -98,19 +99,20 @@
         return null;
     }
 
+    @CameraOperatingMode
     @Override
-    public boolean isConcurrentCameraModeOn() {
-        return mIsConcurrentCameraModeOn;
+    public int getCameraOperatingMode() {
+        return mCameraOperatingMode;
     }
 
     @Override
-    public void setConcurrentCameraMode(boolean isConcurrentCameraModeOn) {
-        if (isConcurrentCameraModeOn != mIsConcurrentCameraModeOn) {
+    public void setCameraOperatingMode(@CameraOperatingMode int cameraOperatingMode) {
+        if (cameraOperatingMode != mCameraOperatingMode) {
             for (ConcurrentCameraModeListener listener : mConcurrentCameraModeListeners) {
-                listener.notifyConcurrentCameraModeUpdated(isConcurrentCameraModeOn);
+                listener.notifyConcurrentCameraModeUpdated(cameraOperatingMode);
             }
         }
-        mIsConcurrentCameraModeOn = isConcurrentCameraModeOn;
+        mCameraOperatingMode = cameraOperatingMode;
     }
 
     @Override
@@ -123,16 +125,21 @@
         mConcurrentCameraModeListeners.remove(listener);
     }
 
-    @NonNull
-    private static Set<Set<String>> retrieveConcurrentCameraIds(
-            @NonNull CameraManagerCompat cameraManager) {
-        Set<Set<String>> map = new HashSet<>();
+    private void retrieveConcurrentCameraIds() {
         try {
-            map = cameraManager.getConcurrentCameraIds();
+            mConcurrentCameraIds = mCameraManager.getConcurrentCameraIds();
         } catch (CameraAccessExceptionCompat e) {
             Logger.e(TAG, "Failed to get concurrent camera ids");
         }
-        return map;
+
+        for (Set<String> concurrentCameraIdList: mConcurrentCameraIds) {
+            List<String> cameraIdList = new ArrayList<>(concurrentCameraIdList);
+
+            // TODO(b/268531569): enumerate concurrent camera ids and convert to a map for
+            //  paired camera id lookup.
+            mConcurrentCameraIdMap.put(cameraIdList.get(0), cameraIdList.get(1));
+            mConcurrentCameraIdMap.put(cameraIdList.get(1), cameraIdList.get(0));
+        }
     }
 
     @OptIn(markerClass = ExperimentalCamera2Interop.class)
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/concurrent/Camera2CameraCoordinatorTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/concurrent/Camera2CameraCoordinatorTest.kt
index 4eae589..908c5a9 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/concurrent/Camera2CameraCoordinatorTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/concurrent/Camera2CameraCoordinatorTest.kt
@@ -22,6 +22,9 @@
 import android.os.Build
 import androidx.camera.camera2.internal.compat.CameraManagerCompat
 import androidx.camera.core.concurrent.CameraCoordinator
+import androidx.camera.core.concurrent.CameraCoordinator.CAMERA_OPERATING_MODE_CONCURRENT
+import androidx.camera.core.concurrent.CameraCoordinator.CAMERA_OPERATING_MODE_SINGLE
+import androidx.camera.core.concurrent.CameraCoordinator.CAMERA_OPERATING_MODE_UNSPECIFIED
 import androidx.camera.core.impl.utils.MainThreadAsyncHandler
 import androidx.test.core.app.ApplicationProvider
 import com.google.common.truth.Truth.assertThat
@@ -60,11 +63,11 @@
         fakeCameraImpl.addCamera("0", cameraCharacteristics0)
         fakeCameraImpl.addCamera("1", cameraCharacteristics1)
         cameraCoordinator = Camera2CameraCoordinator(CameraManagerCompat.from(fakeCameraImpl))
-        cameraCoordinator.init()
     }
 
     @Test
     fun getConcurrentCameraSelectors() {
+        cameraCoordinator.cameraOperatingMode = CAMERA_OPERATING_MODE_CONCURRENT
         assertThat(cameraCoordinator.concurrentCameraSelectors).isNotEmpty()
         assertThat(cameraCoordinator.concurrentCameraSelectors[0]).isNotEmpty()
         assertThat(cameraCoordinator.concurrentCameraSelectors[0][0].lensFacing)
@@ -81,26 +84,30 @@
 
     @Test
     fun setAndIsConcurrentCameraMode() {
-        assertThat(cameraCoordinator.isConcurrentCameraModeOn).isFalse()
-        cameraCoordinator.setConcurrentCameraMode(true)
-        assertThat(cameraCoordinator.isConcurrentCameraModeOn).isTrue()
-        cameraCoordinator.setConcurrentCameraMode(false)
-        assertThat(cameraCoordinator.isConcurrentCameraModeOn).isFalse()
+        assertThat(cameraCoordinator.cameraOperatingMode).isEqualTo(
+            CAMERA_OPERATING_MODE_UNSPECIFIED)
+        cameraCoordinator.cameraOperatingMode = CAMERA_OPERATING_MODE_CONCURRENT
+        assertThat(cameraCoordinator.cameraOperatingMode).isEqualTo(
+            CAMERA_OPERATING_MODE_CONCURRENT)
+        cameraCoordinator.cameraOperatingMode = CAMERA_OPERATING_MODE_SINGLE
+        assertThat(cameraCoordinator.cameraOperatingMode).isEqualTo(
+            CAMERA_OPERATING_MODE_SINGLE)
     }
 
     @Test
     fun addAndRemoveListener() {
         val listener = mock(CameraCoordinator.ConcurrentCameraModeListener::class.java)
         cameraCoordinator.addListener(listener)
-        cameraCoordinator.setConcurrentCameraMode(true)
-        verify(listener).notifyConcurrentCameraModeUpdated(true)
-        cameraCoordinator.setConcurrentCameraMode(false)
-        verify(listener).notifyConcurrentCameraModeUpdated(false)
+        cameraCoordinator.cameraOperatingMode = CAMERA_OPERATING_MODE_CONCURRENT
+        verify(listener).notifyConcurrentCameraModeUpdated(CAMERA_OPERATING_MODE_CONCURRENT)
+        cameraCoordinator.cameraOperatingMode = CAMERA_OPERATING_MODE_SINGLE
+        verify(listener).notifyConcurrentCameraModeUpdated(CAMERA_OPERATING_MODE_SINGLE)
 
         reset(listener)
         cameraCoordinator.removeListener(listener)
-        cameraCoordinator.setConcurrentCameraMode(true)
-        verify(listener, never()).notifyConcurrentCameraModeUpdated(true)
+        cameraCoordinator.cameraOperatingMode = CAMERA_OPERATING_MODE_CONCURRENT
+        verify(listener, never()).notifyConcurrentCameraModeUpdated(
+            CAMERA_OPERATING_MODE_CONCURRENT)
     }
 
     private class FakeCameraManagerImpl : CameraManagerCompat.CameraManagerCompatImpl {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraProvider.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraProvider.java
index ffec29fc..79e1c4a 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraProvider.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraProvider.java
@@ -18,6 +18,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
 
 import java.util.List;
 
@@ -59,4 +60,29 @@
      */
     @NonNull
     List<CameraInfo> getAvailableCameraInfos();
+
+    /**
+     * Returns list of {@link CameraInfo} instances of the available concurrent cameras.
+     *
+     * <p>The available concurrent cameras include all combinations of cameras which could
+     * operate concurrently on the device. Each list maps to one combination of these camera's
+     * {@link CameraInfo}.
+     *
+     * @return list of combinations of {@link CameraInfo}.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    List<List<CameraInfo>> getAvailableConcurrentCameraInfos();
+
+    /**
+     * Returns concurrent camera mode.
+     *
+     * @return true if concurrent mode is enabled, otherwise false.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    boolean isConcurrentCameraModeOn();
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/concurrent/CameraCoordinator.java b/camera/camera-core/src/main/java/androidx/camera/core/concurrent/CameraCoordinator.java
index 7ade18a..31b6bfc 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/concurrent/CameraCoordinator.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/concurrent/CameraCoordinator.java
@@ -19,6 +19,7 @@
 
 import android.hardware.camera2.CameraManager;
 
+import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
@@ -26,6 +27,8 @@
 import androidx.camera.core.CameraSelector;
 import androidx.camera.core.impl.CameraStateRegistry;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.List;
 
 /**
@@ -41,10 +44,19 @@
 @RequiresApi(21)
 public interface CameraCoordinator {
 
-    /**
-     * Initializes the map for concurrent camera ids and convert camera ids to camera selectors.
-     */
-    void init();
+    int CAMERA_OPERATING_MODE_UNSPECIFIED = 0;
+
+    int CAMERA_OPERATING_MODE_SINGLE = 1;
+
+    int CAMERA_OPERATING_MODE_CONCURRENT = 2;
+
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @IntDef({CAMERA_OPERATING_MODE_UNSPECIFIED,
+            CAMERA_OPERATING_MODE_SINGLE,
+            CAMERA_OPERATING_MODE_CONCURRENT})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface CameraOperatingMode {
+    }
 
     /**
      * Returns concurrent camera selectors, which are converted from concurrent camera ids
@@ -59,6 +71,21 @@
     List<List<CameraSelector>> getConcurrentCameraSelectors();
 
     /**
+     * Gets active concurrent camera selectors.
+     *
+     * @return list of active concurrent camera selectors.
+     */
+    @NonNull
+    List<CameraSelector> getActiveConcurrentCameraSelectors();
+
+    /**
+     * Sets active concurrent camera selectors.
+     *
+     * @param cameraSelectors list of active concurrent camera selectors.
+     */
+    void setActiveConcurrentCameraSelectors(@NonNull List<CameraSelector> cameraSelectors);
+
+    /**
      * Returns paired camera id in concurrent mode.
      *
      * <p>The paired camera id dictionary is constructed when {@link CameraCoordinator#init()} is
@@ -73,11 +100,12 @@
     String getPairedConcurrentCameraId(@NonNull String cameraId);
 
     /**
-     * Returns concurrent camera mode.
+     * Returns camera operating mode.
      *
-     * @return true if concurrent mode is on, otherwise returns false.
+     * @return camera operating mode including unspecific, single or concurrent.
      */
-    boolean isConcurrentCameraModeOn();
+    @CameraOperatingMode
+    int getCameraOperatingMode();
 
     /**
      * Sets concurrent camera mode.
@@ -85,19 +113,19 @@
      * <p>This internal API will be called when user binds user cases to cameras, which will
      * enable or disable concurrent camera mode based on the input config.
      *
-     * @param enabled true if concurrent camera mode is enabled, otherwise false.
+     * @param cameraOperatingMode camera operating mode including unspecific, single or concurrent.
      */
-    void setConcurrentCameraMode(boolean enabled);
+    void setCameraOperatingMode(@CameraOperatingMode int cameraOperatingMode);
 
     /**
      * Adds listener for concurrent camera mode update.
-     * @param listener
+     * @param listener {@link ConcurrentCameraModeListener}.
      */
     void addListener(@NonNull ConcurrentCameraModeListener listener);
 
     /**
      * Removes listener for concurrent camera mode update.
-     * @param listener
+     * @param listener {@link ConcurrentCameraModeListener}.
      */
     void removeListener(@NonNull ConcurrentCameraModeListener listener);
 
@@ -110,6 +138,6 @@
      * allowed cameras if concurrent mode is set.
      */
     interface ConcurrentCameraModeListener {
-        void notifyConcurrentCameraModeUpdated(boolean isConcurrentCameraModeOn);
+        void notifyConcurrentCameraModeUpdated(@CameraOperatingMode int cameraOperatingMode);
     }
 }
diff --git a/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/LifecycleCameraRepositoryTest.java b/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/LifecycleCameraRepositoryTest.java
index 972d36c..2e8ba0a 100644
--- a/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/LifecycleCameraRepositoryTest.java
+++ b/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/LifecycleCameraRepositoryTest.java
@@ -16,13 +16,17 @@
 
 package androidx.camera.lifecycle;
 
+import static androidx.camera.core.concurrent.CameraCoordinator.CAMERA_OPERATING_MODE_CONCURRENT;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static java.util.Collections.emptyList;
 
+import androidx.camera.core.concurrent.CameraCoordinator;
 import androidx.camera.core.impl.CameraInternal;
 import androidx.camera.core.internal.CameraUseCaseAdapter;
 import androidx.camera.testing.fakes.FakeCamera;
+import androidx.camera.testing.fakes.FakeCameraCoordinator;
 import androidx.camera.testing.fakes.FakeCameraDeviceSurfaceManager;
 import androidx.camera.testing.fakes.FakeLifecycleOwner;
 import androidx.camera.testing.fakes.FakeUseCase;
@@ -47,6 +51,7 @@
 @SdkSuppress(minSdkVersion = 21)
 public final class LifecycleCameraRepositoryTest {
 
+    private CameraCoordinator mCameraCoordinator;
     private FakeLifecycleOwner mLifecycle;
     private LifecycleCameraRepository mRepository;
     private CameraUseCaseAdapter mCameraUseCaseAdapter;
@@ -55,6 +60,7 @@
 
     @Before
     public void setUp() {
+        mCameraCoordinator = new FakeCameraCoordinator();
         mLifecycle = new FakeLifecycleOwner();
         mRepository = new LifecycleCameraRepository();
         CameraInternal camera = new FakeCamera(String.valueOf(mCameraId));
@@ -119,7 +125,7 @@
         LifecycleCamera lifecycleCamera = mRepository.createLifecycleCamera(mLifecycle,
                 mCameraUseCaseAdapter);
         mRepository.bindToLifecycleCamera(lifecycleCamera, null, emptyList(),
-                Collections.singletonList(new FakeUseCase()));
+                Collections.singletonList(new FakeUseCase()), mCameraCoordinator);
         // LifecycleCamera is inactive before the lifecycle state becomes ON_START.
         assertThat(lifecycleCamera.isActive()).isFalse();
     }
@@ -129,7 +135,7 @@
         LifecycleCamera lifecycleCamera = mRepository.createLifecycleCamera(mLifecycle,
                 mCameraUseCaseAdapter);
         mRepository.bindToLifecycleCamera(lifecycleCamera, null, emptyList(),
-                Collections.singletonList(new FakeUseCase()));
+                Collections.singletonList(new FakeUseCase()), mCameraCoordinator);
         mLifecycle.start();
         // LifecycleCamera is active after the lifecycle state becomes ON_START.
         assertThat(lifecycleCamera.isActive()).isTrue();
@@ -141,7 +147,7 @@
                 mCameraUseCaseAdapter);
         mLifecycle.start();
         mRepository.bindToLifecycleCamera(lifecycleCamera, null, emptyList(),
-                Collections.singletonList(new FakeUseCase()));
+                Collections.singletonList(new FakeUseCase()), mCameraCoordinator);
 
         // LifecycleCamera is active after binding a use case when lifecycle state is ON_START.
         assertThat(lifecycleCamera.isActive()).isTrue();
@@ -153,13 +159,13 @@
         LifecycleCamera lifecycleCamera0 = mRepository.createLifecycleCamera(
                 mLifecycle, mCameraUseCaseAdapter);
         mRepository.bindToLifecycleCamera(lifecycleCamera0, null, emptyList(),
-                Collections.singletonList(new FakeUseCase()));
+                Collections.singletonList(new FakeUseCase()), mCameraCoordinator);
 
         // Creates second LifecycleCamera with use case bound to the same Lifecycle.
         LifecycleCamera lifecycleCamera1 = mRepository.createLifecycleCamera(mLifecycle,
                 createNewCameraUseCaseAdapter());
         mRepository.bindToLifecycleCamera(lifecycleCamera1, null, emptyList(),
-                Collections.singletonList(new FakeUseCase()));
+                Collections.singletonList(new FakeUseCase()), mCameraCoordinator);
     }
 
     @Test
@@ -170,7 +176,7 @@
         mLifecycle.start();
         FakeUseCase useCase = new FakeUseCase();
         mRepository.bindToLifecycleCamera(lifecycleCamera, null, emptyList(),
-                Collections.singletonList(useCase));
+                Collections.singletonList(useCase), mCameraCoordinator);
 
         // Unbinds the use case that was bound previously.
         mRepository.unbind(Collections.singletonList(useCase));
@@ -189,7 +195,7 @@
         FakeUseCase useCase0 = new FakeUseCase();
         FakeUseCase useCase1 = new FakeUseCase();
         mRepository.bindToLifecycleCamera(lifecycleCamera, null, emptyList(),
-                Arrays.asList(useCase0, useCase1));
+                Arrays.asList(useCase0, useCase1), mCameraCoordinator);
 
         // Only unbinds one use case but another one is kept in the LifecycleCamera.
         mRepository.unbind(Collections.singletonList(useCase0));
@@ -206,7 +212,7 @@
                 mCameraUseCaseAdapter);
         mLifecycle.start();
         mRepository.bindToLifecycleCamera(lifecycleCamera, null, emptyList(),
-                Collections.singletonList(new FakeUseCase()));
+                Collections.singletonList(new FakeUseCase()), mCameraCoordinator);
 
         // Unbinds all use cases from all LifecycleCamera by the unbindAll() API.
         mRepository.unbindAll();
@@ -222,7 +228,7 @@
                 mCameraUseCaseAdapter);
         mLifecycle.start();
         mRepository.bindToLifecycleCamera(lifecycleCamera0, null, emptyList(),
-                Collections.singletonList(new FakeUseCase()));
+                Collections.singletonList(new FakeUseCase()), mCameraCoordinator);
 
         // Starts second lifecycle with use case bound.
         FakeLifecycleOwner lifecycle1 = new FakeLifecycleOwner();
@@ -230,7 +236,7 @@
                 createNewCameraUseCaseAdapter());
         lifecycle1.start();
         mRepository.bindToLifecycleCamera(lifecycleCamera1, null, emptyList(),
-                Collections.singletonList(new FakeUseCase()));
+                Collections.singletonList(new FakeUseCase()), mCameraCoordinator);
 
         // The previous LifecycleCamera becomes inactive after new LifecycleCamera becomes active.
         assertThat(lifecycleCamera0.isActive()).isFalse();
@@ -245,7 +251,7 @@
                 mCameraUseCaseAdapter);
         mLifecycle.start();
         mRepository.bindToLifecycleCamera(lifecycleCamera0, null, emptyList(),
-                Collections.singletonList(new FakeUseCase()));
+                Collections.singletonList(new FakeUseCase()), mCameraCoordinator);
 
         // Starts second lifecycle with use case bound.
         FakeLifecycleOwner lifecycle1 = new FakeLifecycleOwner();
@@ -253,11 +259,11 @@
                 createNewCameraUseCaseAdapter());
         lifecycle1.start();
         mRepository.bindToLifecycleCamera(lifecycleCamera1, null, emptyList(),
-                Collections.singletonList(new FakeUseCase()));
+                Collections.singletonList(new FakeUseCase()), mCameraCoordinator);
 
         // Binds new use case to the next most recent active LifecycleCamera.
         mRepository.bindToLifecycleCamera(lifecycleCamera0, null, emptyList(),
-                Collections.singletonList(new FakeUseCase()));
+                Collections.singletonList(new FakeUseCase()), mCameraCoordinator);
 
         // The next most recent active LifecycleCamera becomes active after binding new use case.
         assertThat(lifecycleCamera0.isActive()).isTrue();
@@ -273,7 +279,7 @@
                 mCameraUseCaseAdapter);
         mLifecycle.start();
         mRepository.bindToLifecycleCamera(lifecycleCamera0, null, emptyList(),
-                Collections.singletonList(new FakeUseCase()));
+                Collections.singletonList(new FakeUseCase()), mCameraCoordinator);
 
         // Starts second lifecycle with use case bound.
         FakeLifecycleOwner lifecycle1 = new FakeLifecycleOwner();
@@ -282,7 +288,7 @@
         lifecycle1.start();
         FakeUseCase useCase = new FakeUseCase();
         mRepository.bindToLifecycleCamera(lifecycleCamera1, null, emptyList(),
-                Collections.singletonList(useCase));
+                Collections.singletonList(useCase), mCameraCoordinator);
 
         // Unbinds use case from the most recent active LifecycleCamera.
         mRepository.unbind(Collections.singletonList(useCase));
@@ -301,7 +307,7 @@
                 mLifecycle, mCameraUseCaseAdapter);
         FakeUseCase useCase = new FakeUseCase();
         mRepository.bindToLifecycleCamera(lifecycleCamera, null, emptyList(),
-                Collections.singletonList(useCase));
+                Collections.singletonList(useCase), mCameraCoordinator);
 
         assertThat(useCase.isDetached()).isFalse();
 
@@ -324,7 +330,7 @@
         LifecycleCamera firstLifecycleCamera = mRepository.createLifecycleCamera(
                 mLifecycle, mCameraUseCaseAdapter);
         mRepository.bindToLifecycleCamera(firstLifecycleCamera, null, emptyList(),
-                Collections.singletonList(new FakeUseCase()));
+                Collections.singletonList(new FakeUseCase()), mCameraCoordinator);
         mLifecycle.start();
         assertThat(firstLifecycleCamera.isActive()).isTrue();
 
@@ -333,7 +339,7 @@
         LifecycleCamera secondLifecycleCamera = mRepository.createLifecycleCamera(secondLifecycle,
                 createNewCameraUseCaseAdapter());
         mRepository.bindToLifecycleCamera(secondLifecycleCamera, null, emptyList(),
-                Collections.singletonList(new FakeUseCase()));
+                Collections.singletonList(new FakeUseCase()), mCameraCoordinator);
         secondLifecycle.start();
         assertThat(secondLifecycleCamera.isActive()).isTrue();
         assertThat(firstLifecycleCamera.isActive()).isFalse();
@@ -345,7 +351,7 @@
         LifecycleCamera firstLifecycleCamera = mRepository.createLifecycleCamera(
                 mLifecycle, mCameraUseCaseAdapter);
         mRepository.bindToLifecycleCamera(firstLifecycleCamera, null, emptyList(),
-                Collections.singletonList(new FakeUseCase()));
+                Collections.singletonList(new FakeUseCase()), mCameraCoordinator);
         mLifecycle.start();
         assertThat(firstLifecycleCamera.isActive()).isTrue();
 
@@ -354,7 +360,7 @@
         LifecycleCamera secondLifecycleCamera = mRepository.createLifecycleCamera(secondLifecycle,
                 createNewCameraUseCaseAdapter());
         mRepository.bindToLifecycleCamera(secondLifecycleCamera, null, emptyList(),
-                Collections.singletonList(new FakeUseCase()));
+                Collections.singletonList(new FakeUseCase()), mCameraCoordinator);
         secondLifecycle.start();
         assertThat(secondLifecycleCamera.isActive()).isTrue();
         assertThat(firstLifecycleCamera.isActive()).isFalse();
@@ -371,7 +377,7 @@
         LifecycleCamera firstLifecycleCamera = mRepository.createLifecycleCamera(
                 mLifecycle, mCameraUseCaseAdapter);
         mRepository.bindToLifecycleCamera(firstLifecycleCamera, null, emptyList(),
-                Collections.singletonList(new FakeUseCase()));
+                Collections.singletonList(new FakeUseCase()), mCameraCoordinator);
         mLifecycle.start();
         assertThat(firstLifecycleCamera.isActive()).isTrue();
 
@@ -398,7 +404,7 @@
         LifecycleCamera lifecycleCamera1 = mRepository.createLifecycleCamera(
                 mLifecycle, createNewCameraUseCaseAdapter());
         mRepository.bindToLifecycleCamera(lifecycleCamera1, null, emptyList(),
-                Collections.singletonList(new FakeUseCase()));
+                Collections.singletonList(new FakeUseCase()), mCameraCoordinator);
 
         // Starts third LifecycleCamera with no use case bound to the same Lifecycle.
         LifecycleCamera lifecycleCamera2 = mRepository.createLifecycleCamera(
@@ -453,7 +459,7 @@
         LifecycleCamera lifecycleCamera = mRepository.createLifecycleCamera(
                 mLifecycle, mCameraUseCaseAdapter);
         mRepository.bindToLifecycleCamera(lifecycleCamera, null, emptyList(),
-                Collections.singletonList(new FakeUseCase()));
+                Collections.singletonList(new FakeUseCase()), mCameraCoordinator);
         mLifecycle.start();
         assertThat(lifecycleCamera.isActive()).isTrue();
 
@@ -464,6 +470,113 @@
         mRepository.setInactive(mLifecycle);
     }
 
+    @Test
+    public void concurrentModeOn_twoLifecycleCamerasControlledByOneLifecycle_start() {
+        mCameraCoordinator.setCameraOperatingMode(CAMERA_OPERATING_MODE_CONCURRENT);
+
+        // Starts first lifecycle camera
+        LifecycleCamera lifecycleCamera0 = mRepository.createLifecycleCamera(mLifecycle,
+                mCameraUseCaseAdapter);
+        mRepository.bindToLifecycleCamera(lifecycleCamera0, null, emptyList(),
+                Collections.singletonList(new FakeUseCase()), mCameraCoordinator);
+
+        // Starts second lifecycle camera
+        LifecycleCamera lifecycleCamera1 = mRepository.createLifecycleCamera(mLifecycle,
+                createNewCameraUseCaseAdapter());
+        mRepository.bindToLifecycleCamera(lifecycleCamera1, null, emptyList(),
+                Collections.singletonList(new FakeUseCase()), mCameraCoordinator);
+
+        // Starts lifecycle
+        mLifecycle.start();
+
+        // Both cameras are active in concurrent mode
+        assertThat(lifecycleCamera0.isActive()).isTrue();
+        assertThat(lifecycleCamera1.isActive()).isTrue();
+    }
+
+    @Test
+    public void concurrentModeOn_twoLifecycleCamerasControlledByTwoLifecycles_start() {
+        mCameraCoordinator.setCameraOperatingMode(CAMERA_OPERATING_MODE_CONCURRENT);
+
+        // Starts first lifecycle camera
+        LifecycleCamera lifecycleCamera0 = mRepository.createLifecycleCamera(mLifecycle,
+                mCameraUseCaseAdapter);
+        mRepository.bindToLifecycleCamera(lifecycleCamera0, null, emptyList(),
+                Collections.singletonList(new FakeUseCase()), mCameraCoordinator);
+
+        // Starts lifecycle
+        mLifecycle.start();
+
+        // Starts second lifecycle camera
+        FakeLifecycleOwner lifecycle1 = new FakeLifecycleOwner();
+        LifecycleCamera lifecycleCamera1 = mRepository.createLifecycleCamera(lifecycle1,
+                createNewCameraUseCaseAdapter());
+        mRepository.bindToLifecycleCamera(lifecycleCamera1, null, emptyList(),
+                Collections.singletonList(new FakeUseCase()), mCameraCoordinator);
+
+        // Starts lifecycle1
+        lifecycle1.start();
+
+        // Both cameras are active in concurrent mode
+        assertThat(lifecycleCamera0.isActive()).isTrue();
+        assertThat(lifecycleCamera1.isActive()).isTrue();
+    }
+
+    @Test
+    public void concurrentModeOn_twoLifecycleCamerasControlledByOneLifecycle_stop() {
+        mCameraCoordinator.setCameraOperatingMode(CAMERA_OPERATING_MODE_CONCURRENT);
+
+        // Starts first lifecycle camera
+        LifecycleCamera firstLifecycleCamera = mRepository.createLifecycleCamera(
+                mLifecycle, mCameraUseCaseAdapter);
+        mRepository.bindToLifecycleCamera(firstLifecycleCamera, null, emptyList(),
+                Collections.singletonList(new FakeUseCase()), mCameraCoordinator);
+
+        // Starts second lifecycle camera
+        LifecycleCamera secondLifecycleCamera = mRepository.createLifecycleCamera(mLifecycle,
+                createNewCameraUseCaseAdapter());
+        mRepository.bindToLifecycleCamera(secondLifecycleCamera, null, emptyList(),
+                Collections.singletonList(new FakeUseCase()), mCameraCoordinator);
+
+        // Starts lifecycle
+        mLifecycle.start();
+        assertThat(secondLifecycleCamera.isActive()).isTrue();
+        assertThat(firstLifecycleCamera.isActive()).isTrue();
+
+        // Stops lifecycle
+        mLifecycle.stop();
+        assertThat(secondLifecycleCamera.isActive()).isFalse();
+        assertThat(firstLifecycleCamera.isActive()).isFalse();
+    }
+
+    @Test
+    public void concurrentModeOn_twoLifecycleCamerasControlledByTwoLifecycles_stop() {
+        mCameraCoordinator.setCameraOperatingMode(CAMERA_OPERATING_MODE_CONCURRENT);
+
+        // Starts first lifecycle camera
+        LifecycleCamera firstLifecycleCamera = mRepository.createLifecycleCamera(
+                mLifecycle, mCameraUseCaseAdapter);
+        mRepository.bindToLifecycleCamera(firstLifecycleCamera, null, emptyList(),
+                Collections.singletonList(new FakeUseCase()), mCameraCoordinator);
+        mLifecycle.start();
+        assertThat(firstLifecycleCamera.isActive()).isTrue();
+
+        // Starts second lifecycle camera
+        FakeLifecycleOwner secondLifecycle = new FakeLifecycleOwner();
+        LifecycleCamera secondLifecycleCamera = mRepository.createLifecycleCamera(secondLifecycle,
+                createNewCameraUseCaseAdapter());
+        mRepository.bindToLifecycleCamera(secondLifecycleCamera, null, emptyList(),
+                Collections.singletonList(new FakeUseCase()), mCameraCoordinator);
+        secondLifecycle.start();
+        assertThat(secondLifecycleCamera.isActive()).isTrue();
+        assertThat(firstLifecycleCamera.isActive()).isTrue();
+
+        // Stops lifecycle
+        secondLifecycle.stop();
+        assertThat(secondLifecycleCamera.isActive()).isFalse();
+        assertThat(firstLifecycleCamera.isActive()).isTrue();
+    }
+
     private CameraUseCaseAdapter createNewCameraUseCaseAdapter() {
         String cameraId = String.valueOf(++mCameraId);
         CameraInternal fakeCamera = new FakeCamera(cameraId);
diff --git a/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt b/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
index 0c38d7a..1bf7e70 100644
--- a/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
+++ b/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
@@ -23,6 +23,8 @@
 import androidx.annotation.OptIn
 import androidx.annotation.RequiresApi
 import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraSelector.LENS_FACING_BACK
+import androidx.camera.core.CameraSelector.LENS_FACING_FRONT
 import androidx.camera.core.CameraXConfig
 import androidx.camera.core.Preview
 import androidx.camera.core.UseCaseGroup
@@ -32,6 +34,7 @@
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
 import androidx.camera.testing.fakes.FakeAppConfig
 import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.fakes.FakeCameraCoordinator
 import androidx.camera.testing.fakes.FakeCameraDeviceSurfaceManager
 import androidx.camera.testing.fakes.FakeCameraFactory
 import androidx.camera.testing.fakes.FakeCameraInfoInternal
@@ -97,6 +100,7 @@
 
             // Assert.
             assertThat(preview.effect).isEqualTo(effect)
+            assertThat(provider.isConcurrentCameraModeOn).isFalse()
         }
     }
 
@@ -201,6 +205,7 @@
             val camera =
                 provider.bindToLifecycle(lifecycleOwner0, CameraSelector.DEFAULT_BACK_CAMERA)
             assertThat(camera).isNotNull()
+            assertThat(provider.isConcurrentCameraModeOn).isFalse()
         }
     }
 
@@ -218,6 +223,7 @@
             )
 
             assertThat(provider.isBound(useCase)).isTrue()
+            assertThat(provider.isConcurrentCameraModeOn).isFalse()
         }
     }
 
@@ -246,6 +252,7 @@
             assertThat(provider.isBound(useCase0)).isTrue()
             assertThat(useCase1.camera).isNotNull()
             assertThat(provider.isBound(useCase1)).isTrue()
+            assertThat(provider.isConcurrentCameraModeOn).isFalse()
         }
     }
 
@@ -267,6 +274,7 @@
             provider.unbind(useCase)
 
             assertThat(provider.isBound(useCase)).isFalse()
+            assertThat(provider.isConcurrentCameraModeOn).isFalse()
         }
     }
 
@@ -291,6 +299,7 @@
             assertThat(provider.isBound(useCase0)).isFalse()
             assertThat(useCase1.camera).isNotNull()
             assertThat(provider.isBound(useCase1)).isTrue()
+            assertThat(provider.isConcurrentCameraModeOn).isFalse()
         }
     }
 
@@ -311,6 +320,7 @@
 
             assertThat(useCase.camera).isNull()
             assertThat(provider.isBound(useCase)).isFalse()
+            assertThat(provider.isConcurrentCameraModeOn).isFalse()
         }
     }
 
@@ -330,6 +340,7 @@
 
             assertThat(provider.isBound(useCase0)).isTrue()
             assertThat(provider.isBound(useCase1)).isTrue()
+            assertThat(provider.isConcurrentCameraModeOn).isFalse()
         }
     }
 
@@ -353,6 +364,7 @@
             )
 
             assertThat(camera0).isNotEqualTo(camera1)
+            assertThat(provider.isConcurrentCameraModeOn).isFalse()
         }
     }
 
@@ -368,6 +380,7 @@
             assertThrows<IllegalArgumentException> {
                 provider.bindToLifecycle(lifecycleOwner0, CameraSelector.DEFAULT_BACK_CAMERA)
             }
+            assertThat(provider.isConcurrentCameraModeOn).isFalse()
         }
     }
 
@@ -394,6 +407,7 @@
             )
 
             assertThat(camera0).isSameInstanceAs(camera1)
+            assertThat(provider.isConcurrentCameraModeOn).isFalse()
         }
     }
 
@@ -416,6 +430,7 @@
                     useCase1
                 )
             }
+            assertThat(provider.isConcurrentCameraModeOn).isFalse()
         }
     }
 
@@ -444,6 +459,7 @@
             )
 
             assertThat(camera0).isNotEqualTo(camera1)
+            assertThat(provider.isConcurrentCameraModeOn).isFalse()
         }
     }
 
@@ -464,6 +480,7 @@
                         )
                     )
                 }
+                cameraFactory.cameraCoordinator = FakeCameraCoordinator()
                 cameraFactory
             }
 
@@ -488,6 +505,7 @@
                     useCase
                 )
             }
+            assertThat(provider.isConcurrentCameraModeOn).isFalse()
         }
     }
 
@@ -501,6 +519,7 @@
                     LifecycleCamera
             lifecycleOwner0.startAndResume()
             assertThat(camera.isActive).isFalse()
+            assertThat(provider.isConcurrentCameraModeOn).isFalse()
         }
     }
 
@@ -514,6 +533,7 @@
                 provider.bindToLifecycle(lifecycleOwner0, CameraSelector.DEFAULT_BACK_CAMERA) as
                     LifecycleCamera
             assertThat(camera.isActive).isFalse()
+            assertThat(provider.isConcurrentCameraModeOn).isFalse()
         }
     }
 
@@ -530,6 +550,7 @@
                 ) as LifecycleCamera
             lifecycleOwner0.startAndResume()
             assertThat(camera.isActive).isTrue()
+            assertThat(provider.isConcurrentCameraModeOn).isFalse()
         }
     }
 
@@ -546,6 +567,7 @@
                     useCase
                 ) as LifecycleCamera
             assertThat(camera.isActive).isTrue()
+            assertThat(provider.isConcurrentCameraModeOn).isFalse()
         }
     }
 
@@ -564,6 +586,7 @@
             assertThat(camera.isActive).isTrue()
             provider.unbind(useCase)
             assertThat(camera.isActive).isFalse()
+            assertThat(provider.isConcurrentCameraModeOn).isFalse()
         }
     }
 
@@ -582,6 +605,7 @@
             assertThat(camera.isActive).isTrue()
             provider.unbindAll()
             assertThat(camera.isActive).isFalse()
+            assertThat(provider.isConcurrentCameraModeOn).isFalse()
         }
     }
 
@@ -611,6 +635,17 @@
     }
 
     @Test
+    fun getAvailableConcurrentCameraInfos() {
+        ProcessCameraProvider.configureInstance(createConcurrentCameraAppConfig())
+        runBlocking {
+            provider = ProcessCameraProvider.getInstance(context).await()
+            assertThat(provider.availableConcurrentCameraInfos.size).isEqualTo(2)
+            assertThat(provider.availableConcurrentCameraInfos[0].size).isEqualTo(2)
+            assertThat(provider.availableConcurrentCameraInfos[1].size).isEqualTo(2)
+        }
+    }
+
+    @Test
     fun cannotConfigureTwice() {
         ProcessCameraProvider.configureInstance(FakeAppConfig.create())
         assertThrows<IllegalStateException> {
@@ -634,7 +669,7 @@
 
     @Test
     fun bindConcurrentCamera_isBound() {
-        ProcessCameraProvider.configureInstance(FakeAppConfig.create())
+        ProcessCameraProvider.configureInstance(createConcurrentCameraAppConfig())
 
         runBlocking(MainScope().coroutineContext) {
             provider = ProcessCameraProvider.getInstance(context).await()
@@ -666,12 +701,72 @@
             assertThat(concurrentCamera.cameras.size).isEqualTo(2)
             assertThat(provider.isBound(useCase0)).isTrue()
             assertThat(provider.isBound(useCase1)).isTrue()
+            assertThat(provider.isConcurrentCameraModeOn).isTrue()
+        }
+    }
+
+    @Test
+    fun bindConcurrentCameraTwice_isBound() {
+        ProcessCameraProvider.configureInstance(createConcurrentCameraAppConfig())
+
+        runBlocking(MainScope().coroutineContext) {
+            provider = ProcessCameraProvider.getInstance(context).await()
+            val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
+            val useCase1 = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
+            val useCase2 = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
+
+            val singleCameraConfig0 = SingleCameraConfig.Builder()
+                .setCameraSelector(CameraSelector.DEFAULT_BACK_CAMERA)
+                .setUseCaseGroup(UseCaseGroup.Builder()
+                    .addUseCase(useCase0)
+                    .build())
+                .setLifecycleOwner(lifecycleOwner0)
+                .build()
+            val singleCameraConfig1 = SingleCameraConfig.Builder()
+                .setCameraSelector(CameraSelector.DEFAULT_FRONT_CAMERA)
+                .setUseCaseGroup(UseCaseGroup.Builder()
+                    .addUseCase(useCase1)
+                    .build())
+                .setLifecycleOwner(lifecycleOwner1)
+                .build()
+
+            val singleCameraConfig2 = SingleCameraConfig.Builder()
+                .setCameraSelector(CameraSelector.DEFAULT_FRONT_CAMERA)
+                .setUseCaseGroup(UseCaseGroup.Builder()
+                    .addUseCase(useCase2)
+                    .build())
+                .setLifecycleOwner(lifecycleOwner1)
+                .build()
+
+            val concurrentCameraConfig0 = ConcurrentCameraConfig.Builder()
+                .setCameraConfigs(listOf(singleCameraConfig0, singleCameraConfig1))
+                .build()
+
+            val concurrentCamera0 = provider.bindToLifecycle(concurrentCameraConfig0)
+
+            assertThat(concurrentCamera0).isNotNull()
+            assertThat(concurrentCamera0.cameras.size).isEqualTo(2)
+            assertThat(provider.isBound(useCase0)).isTrue()
+            assertThat(provider.isBound(useCase1)).isTrue()
+            assertThat(provider.isConcurrentCameraModeOn).isTrue()
+
+            val concurrentCameraConfig1 = ConcurrentCameraConfig.Builder()
+                .setCameraConfigs(listOf(singleCameraConfig0, singleCameraConfig2))
+                .build()
+
+            val concurrentCamera1 = provider.bindToLifecycle(concurrentCameraConfig1)
+
+            assertThat(concurrentCamera1).isNotNull()
+            assertThat(concurrentCamera1.cameras.size).isEqualTo(2)
+            assertThat(provider.isBound(useCase0)).isTrue()
+            assertThat(provider.isBound(useCase2)).isTrue()
+            assertThat(provider.isConcurrentCameraModeOn).isTrue()
         }
     }
 
     @Test
     fun bindConcurrentCamera_lessThanTwoSingleCameraConfigs() {
-        ProcessCameraProvider.configureInstance(FakeAppConfig.create())
+        ProcessCameraProvider.configureInstance(createConcurrentCameraAppConfig())
 
         runBlocking(MainScope().coroutineContext) {
             provider = ProcessCameraProvider.getInstance(context).await()
@@ -692,12 +787,13 @@
             assertThrows<IllegalArgumentException> {
                 provider.bindToLifecycle(concurrentCameraConfig)
             }
+            assertThat(provider.isConcurrentCameraModeOn).isFalse()
         }
     }
 
     @Test
     fun bindConcurrentCamera_moreThanTwoSingleCameraConfigs() {
-        ProcessCameraProvider.configureInstance(FakeAppConfig.create())
+        ProcessCameraProvider.configureInstance(createConcurrentCameraAppConfig())
 
         runBlocking(MainScope().coroutineContext) {
             provider = ProcessCameraProvider.getInstance(context).await()
@@ -737,8 +833,70 @@
             assertThrows<UnsupportedOperationException> {
                 provider.bindToLifecycle(concurrentCameraConfig)
             }
+            assertThat(provider.isConcurrentCameraModeOn).isFalse()
         }
     }
+
+    private fun createConcurrentCameraAppConfig(): CameraXConfig {
+        val cameraCoordinator = FakeCameraCoordinator()
+        val combination0 = mapOf(
+            "0" to CameraSelector.Builder().requireLensFacing(LENS_FACING_BACK).build(),
+            "1" to CameraSelector.Builder().requireLensFacing(LENS_FACING_FRONT).build())
+        val combination1 = mapOf(
+            "0" to CameraSelector.Builder().requireLensFacing(LENS_FACING_BACK).build(),
+            "2" to CameraSelector.Builder().requireLensFacing(LENS_FACING_FRONT).build())
+
+        cameraCoordinator.addConcurrentCameraIdsAndCameraSelectors(combination0)
+        cameraCoordinator.addConcurrentCameraIdsAndCameraSelectors(combination1)
+        val cameraFactoryProvider =
+            CameraFactory.Provider { _, _, _ ->
+                val cameraFactory = FakeCameraFactory()
+                cameraFactory.insertCamera(
+                    CameraSelector.LENS_FACING_BACK,
+                    "0"
+                ) {
+                    FakeCamera(
+                        "0", null,
+                        FakeCameraInfoInternal(
+                            "0", 0,
+                            CameraSelector.LENS_FACING_BACK
+                        )
+                    )
+                }
+                cameraFactory.insertCamera(
+                    CameraSelector.LENS_FACING_FRONT,
+                    "1"
+                ) {
+                    FakeCamera(
+                        "1", null,
+                        FakeCameraInfoInternal(
+                            "1", 0,
+                            CameraSelector.LENS_FACING_FRONT
+                        )
+                    )
+                }
+                cameraFactory.insertCamera(
+                    CameraSelector.LENS_FACING_FRONT,
+                    "2"
+                ) {
+                    FakeCamera(
+                        "2", null,
+                        FakeCameraInfoInternal(
+                            "2", 0,
+                            CameraSelector.LENS_FACING_FRONT
+                        )
+                    )
+                }
+                cameraFactory.cameraCoordinator = cameraCoordinator
+                cameraFactory
+            }
+        val appConfigBuilder = CameraXConfig.Builder()
+            .setCameraFactoryProvider(cameraFactoryProvider)
+            .setDeviceSurfaceManagerProvider { _, _, _ -> FakeCameraDeviceSurfaceManager() }
+            .setUseCaseConfigFactoryProvider { FakeUseCaseConfigFactory() }
+
+        return appConfigBuilder.build()
+    }
 }
 
 private class TestAppContextWrapper(base: Context, val app: Application? = null) :
diff --git a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCameraRepository.java b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCameraRepository.java
index 706c842..3b8fae9 100644
--- a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCameraRepository.java
+++ b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCameraRepository.java
@@ -23,6 +23,7 @@
 import androidx.camera.core.CameraEffect;
 import androidx.camera.core.UseCase;
 import androidx.camera.core.ViewPort;
+import androidx.camera.core.concurrent.CameraCoordinator;
 import androidx.camera.core.impl.CameraInternal;
 import androidx.camera.core.internal.CameraUseCaseAdapter;
 import androidx.core.util.Preconditions;
@@ -81,6 +82,9 @@
     @GuardedBy("mLock")
     private final ArrayDeque<LifecycleOwner> mActiveLifecycleOwners = new ArrayDeque<>();
 
+    @GuardedBy("mLock")
+    @Nullable CameraCoordinator mCameraCoordinator;
+
     /**
      * Create a new {@link LifecycleCamera} associated with the given {@link LifecycleOwner}.
      *
@@ -253,15 +257,21 @@
      * @param viewPort The viewport which represents the visible camera sensor rect.
      * @param effects The effects applied to the camera outputs.
      * @param useCases The use cases to bind to a lifecycle.
+     * @param cameraCoordinator The {@link CameraCoordinator} for concurrent camera mode.
+     *
      * @throws IllegalArgumentException If multiple LifecycleCameras with use cases are
      * registered to the same LifecycleOwner. Or all use cases will exceed the capability of the
      * camera after binding them to the LifecycleCamera.
      */
-    void bindToLifecycleCamera(@NonNull LifecycleCamera lifecycleCamera,
-            @Nullable ViewPort viewPort, @NonNull List<CameraEffect> effects,
-            @NonNull Collection<UseCase> useCases) {
+    void bindToLifecycleCamera(
+            @NonNull LifecycleCamera lifecycleCamera,
+            @Nullable ViewPort viewPort,
+            @NonNull List<CameraEffect> effects,
+            @NonNull Collection<UseCase> useCases,
+            @Nullable CameraCoordinator cameraCoordinator) {
         synchronized (mLock) {
             Preconditions.checkArgument(!useCases.isEmpty());
+            mCameraCoordinator = cameraCoordinator;
             LifecycleOwner lifecycleOwner = lifecycleCamera.getLifecycleOwner();
             // Disallow multiple LifecycleCameras with use cases to be registered to the same
             // LifecycleOwner.
@@ -269,11 +279,18 @@
                     getLifecycleCameraRepositoryObserver(lifecycleOwner);
             Set<Key> lifecycleCameraKeySet = mLifecycleObserverMap.get(observer);
 
-            for (Key key : lifecycleCameraKeySet) {
-                LifecycleCamera camera = Preconditions.checkNotNull(mCameraMap.get(key));
-                if (!camera.equals(lifecycleCamera) && !camera.getUseCases().isEmpty()) {
-                    throw new IllegalArgumentException("Multiple LifecycleCameras with use cases "
-                            + "are registered to the same LifecycleOwner.");
+            // Bypass the use cases lifecycle owner validation when concurrent camera mode is on.
+            // In concurrent camera mode, we allow multiple cameras registered to the same
+            // lifecycle owner.
+            if (mCameraCoordinator == null || (mCameraCoordinator.getCameraOperatingMode()
+                    != CameraCoordinator.CAMERA_OPERATING_MODE_CONCURRENT)) {
+                for (Key key : lifecycleCameraKeySet) {
+                    LifecycleCamera camera = Preconditions.checkNotNull(mCameraMap.get(key));
+                    if (!camera.equals(lifecycleCamera) && !camera.getUseCases().isEmpty()) {
+                        throw new IllegalArgumentException(
+                                "Multiple LifecycleCameras with use cases "
+                                        + "are registered to the same LifecycleOwner.");
+                    }
                 }
             }
 
@@ -366,12 +383,18 @@
             if (mActiveLifecycleOwners.isEmpty()) {
                 mActiveLifecycleOwners.push(lifecycleOwner);
             } else {
-                LifecycleOwner currentActiveLifecycleOwner = mActiveLifecycleOwners.peek();
-                if (!lifecycleOwner.equals(currentActiveLifecycleOwner)) {
-                    suspendUseCases(currentActiveLifecycleOwner);
+                // Bypass the use cases suspending when concurrent camera mode is on.
+                // In concurrent camera mode, we allow multiple cameras registered to the same
+                // lifecycle owner.
+                if (mCameraCoordinator == null || (mCameraCoordinator.getCameraOperatingMode()
+                        != CameraCoordinator.CAMERA_OPERATING_MODE_CONCURRENT)) {
+                    LifecycleOwner currentActiveLifecycleOwner = mActiveLifecycleOwners.peek();
+                    if (!lifecycleOwner.equals(currentActiveLifecycleOwner)) {
+                        suspendUseCases(currentActiveLifecycleOwner);
 
-                    mActiveLifecycleOwners.remove(lifecycleOwner);
-                    mActiveLifecycleOwners.push(lifecycleOwner);
+                        mActiveLifecycleOwners.remove(lifecycleOwner);
+                        mActiveLifecycleOwners.push(lifecycleOwner);
+                    }
                 }
             }
 
diff --git a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.java b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.java
index 7290be0..782dfca 100644
--- a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.java
+++ b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.java
@@ -16,9 +16,13 @@
 
 package androidx.camera.lifecycle;
 
+import static androidx.camera.core.concurrent.CameraCoordinator.CAMERA_OPERATING_MODE_CONCURRENT;
+import static androidx.camera.core.concurrent.CameraCoordinator.CAMERA_OPERATING_MODE_SINGLE;
+import static androidx.camera.core.concurrent.CameraCoordinator.CAMERA_OPERATING_MODE_UNSPECIFIED;
 import static androidx.camera.core.impl.utils.Threads.runOnMainSync;
 
 import static java.util.Collections.emptyList;
+import static java.util.Objects.requireNonNull;
 
 import android.app.Application;
 import android.content.Context;
@@ -46,6 +50,7 @@
 import androidx.camera.core.UseCase;
 import androidx.camera.core.UseCaseGroup;
 import androidx.camera.core.ViewPort;
+import androidx.camera.core.concurrent.CameraCoordinator.CameraOperatingMode;
 import androidx.camera.core.concurrent.ConcurrentCamera;
 import androidx.camera.core.concurrent.ConcurrentCameraConfig;
 import androidx.camera.core.concurrent.SingleCameraConfig;
@@ -366,7 +371,15 @@
     public Camera bindToLifecycle(@NonNull LifecycleOwner lifecycleOwner,
             @NonNull CameraSelector cameraSelector,
             @NonNull UseCase... useCases) {
-        return bindToLifecycle(lifecycleOwner, cameraSelector, null, emptyList(), useCases);
+        if (getCameraOperatingMode() == CAMERA_OPERATING_MODE_CONCURRENT) {
+            throw new UnsupportedOperationException("bindToLifecycle for single camera is not "
+                    + "supported in concurrent camera mode, call unbindAll() first");
+        }
+
+        Camera camera = bindToLifecycle(lifecycleOwner, cameraSelector, null, emptyList(),
+                useCases);
+        setCameraOperatingMode(CAMERA_OPERATING_MODE_SINGLE);
+        return camera;
     }
 
     /**
@@ -387,9 +400,16 @@
     public Camera bindToLifecycle(@NonNull LifecycleOwner lifecycleOwner,
             @NonNull CameraSelector cameraSelector,
             @NonNull UseCaseGroup useCaseGroup) {
-        return bindToLifecycle(lifecycleOwner, cameraSelector,
+        if (getCameraOperatingMode() == CAMERA_OPERATING_MODE_CONCURRENT) {
+            throw new UnsupportedOperationException("bindToLifecycle for single camera is not "
+                    + "supported in concurrent camera mode, call unbindAll() first");
+        }
+
+        Camera camera = bindToLifecycle(lifecycleOwner, cameraSelector,
                 useCaseGroup.getViewPort(), useCaseGroup.getEffects(),
                 useCaseGroup.getUseCases().toArray(new UseCase[0]));
+        setCameraOperatingMode(CAMERA_OPERATING_MODE_SINGLE);
+        return camera;
     }
 
     /**
@@ -402,6 +422,10 @@
      * @param concurrentCameraConfig input configuration for concurrent camera.
      * @return output concurrent camera instance.
      *
+     * @throws IllegalArgumentException If less than two camera configs are provided.
+     * @throws UnsupportedOperationException If more than two camera configs are provides or
+     * there is single camera already running.
+     *
      * @hide
      */
     @RestrictTo(Scope.LIBRARY_GROUP)
@@ -410,7 +434,6 @@
     @NonNull
     public ConcurrentCamera bindToLifecycle(
             @NonNull ConcurrentCameraConfig concurrentCameraConfig) {
-        // TODO(b/268347532): enable concurrent mode in camera coordinator
         if (concurrentCameraConfig.getSingleCameraConfigs().size() < 2) {
             throw new IllegalArgumentException("Concurrent camera needs two camera configs");
         }
@@ -420,6 +443,20 @@
                     + "cameras at maximum.");
         }
 
+        if (getCameraOperatingMode() == CAMERA_OPERATING_MODE_SINGLE) {
+            throw new UnsupportedOperationException("Camera is already running, call "
+                    + "unbindAll() before binding more cameras");
+        }
+
+        List<CameraSelector> cameraSelectorsToBind = Arrays.asList(
+                concurrentCameraConfig.getSingleCameraConfigs().get(0).getCameraSelector(),
+                concurrentCameraConfig.getSingleCameraConfigs().get(1).getCameraSelector());
+        if (!getActiveConcurrentCameraSelectors().isEmpty()
+                && !cameraSelectorsToBind.equals(getActiveConcurrentCameraSelectors())) {
+            throw new UnsupportedOperationException("Cameras are already running, call "
+                    + "unbindAll() before binding more cameras");
+        }
+
         List<Camera> cameras = new ArrayList<>();
         for (SingleCameraConfig config : concurrentCameraConfig.getSingleCameraConfigs()) {
             Camera camera = bindToLifecycle(
@@ -428,10 +465,12 @@
                     config.getUseCaseGroup().getViewPort(),
                     config.getUseCaseGroup().getEffects(),
                     config.getUseCaseGroup().getUseCases().toArray(new UseCase[0]));
-
             cameras.add(camera);
         }
 
+        setActiveConcurrentCameraSelectors(cameraSelectorsToBind);
+        setCameraOperatingMode(CAMERA_OPERATING_MODE_CONCURRENT);
+
         return new ConcurrentCamera.Builder()
                 .setCameras(cameras)
                 .builder();
@@ -586,8 +625,12 @@
             return lifecycleCameraToBind;
         }
 
-        mLifecycleCameraRepository.bindToLifecycleCamera(lifecycleCameraToBind, viewPort,
-                effects, Arrays.asList(useCases));
+        mLifecycleCameraRepository.bindToLifecycleCamera(
+                lifecycleCameraToBind,
+                viewPort,
+                effects,
+                Arrays.asList(useCases),
+                mCameraX.getCameraFactory().getCameraCoordinator());
 
         return lifecycleCameraToBind;
     }
@@ -624,11 +667,18 @@
      *
      * @param useCases The collection of use cases to remove.
      * @throws IllegalStateException If not called on main thread.
+     * @throws UnsupportedOperationException If called in concurrent mode.
      */
     @MainThread
     @Override
     public void unbind(@NonNull UseCase... useCases) {
         Threads.checkMainThread();
+
+        if (getCameraOperatingMode() == CAMERA_OPERATING_MODE_CONCURRENT) {
+            throw new UnsupportedOperationException("unbind usecase is not "
+                    + "supported in concurrent camera mode, call unbindAll() first");
+        }
+
         mLifecycleCameraRepository.unbind(Arrays.asList(useCases));
     }
 
@@ -644,6 +694,9 @@
     public void unbindAll() {
         Threads.checkMainThread();
         mLifecycleCameraRepository.unbindAll();
+
+        // Reset camera operating mode.
+        setCameraOperatingMode(CAMERA_OPERATING_MODE_UNSPECIFIED);
     }
 
     /** {@inheritDoc} */
@@ -684,6 +737,82 @@
         return availableCameraInfos;
     }
 
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    @Override
+    public List<List<CameraInfo>> getAvailableConcurrentCameraInfos() {
+        requireNonNull(mCameraX);
+        requireNonNull(mCameraX.getCameraFactory().getCameraCoordinator());
+        List<List<CameraSelector>> concurrentCameraSelectorLists =
+                mCameraX.getCameraFactory().getCameraCoordinator().getConcurrentCameraSelectors();
+        List<CameraInfo> availableCameraInfos = getAvailableCameraInfos();
+
+        List<List<CameraInfo>> availableConcurrentCameraInfos = new ArrayList<>();
+        for (final List<CameraSelector> cameraSelectors : concurrentCameraSelectorLists) {
+            List<CameraInfo> cameraInfos = new ArrayList<>();
+            for (CameraSelector cameraSelector : cameraSelectors) {
+                for (CameraInfo cameraInfo : availableCameraInfos) {
+                    if (cameraSelector.getLensFacing()
+                            == cameraInfo.getLensFacing()) {
+                        cameraInfos.add(cameraInfo);
+                        break;
+                    }
+                }
+            }
+            availableConcurrentCameraInfos.add(cameraInfos);
+        }
+        return availableConcurrentCameraInfos;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Override
+    public boolean isConcurrentCameraModeOn() {
+        return getCameraOperatingMode() == CAMERA_OPERATING_MODE_CONCURRENT;
+    }
+
+    @CameraOperatingMode
+    private int getCameraOperatingMode() {
+        if (mCameraX == null || mCameraX.getCameraFactory().getCameraCoordinator() == null) {
+            return CAMERA_OPERATING_MODE_UNSPECIFIED;
+        }
+        return mCameraX.getCameraFactory().getCameraCoordinator().getCameraOperatingMode();
+    }
+
+    private void setCameraOperatingMode(@CameraOperatingMode int cameraOperatingMode) {
+        if (mCameraX == null || mCameraX.getCameraFactory().getCameraCoordinator() == null) {
+            return;
+        }
+        mCameraX.getCameraFactory().getCameraCoordinator()
+                .setCameraOperatingMode(cameraOperatingMode);
+    }
+
+    @NonNull
+    private List<CameraSelector> getActiveConcurrentCameraSelectors() {
+        if (mCameraX == null || mCameraX.getCameraFactory().getCameraCoordinator() == null) {
+            return new ArrayList<>();
+        }
+        return mCameraX.getCameraFactory().getCameraCoordinator()
+                .getActiveConcurrentCameraSelectors();
+    }
+
+    private void setActiveConcurrentCameraSelectors(@NonNull List<CameraSelector> cameraSelectors) {
+        if (mCameraX == null || mCameraX.getCameraFactory().getCameraCoordinator() == null) {
+            return;
+        }
+        mCameraX.getCameraFactory().getCameraCoordinator()
+                .setActiveConcurrentCameraSelectors(cameraSelectors);
+    }
+
     private ProcessCameraProvider() {
     }
 }
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
index a6004bc..e608dcd 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
@@ -22,6 +22,7 @@
 import androidx.annotation.RestrictTo;
 import androidx.camera.core.CameraSelector;
 import androidx.camera.core.CameraXConfig;
+import androidx.camera.core.concurrent.CameraCoordinator;
 import androidx.camera.core.impl.CameraDeviceSurfaceManager;
 import androidx.camera.core.impl.CameraFactory;
 
@@ -60,6 +61,8 @@
                     () -> new FakeCamera(CAMERA_ID_1, null,
                             new FakeCameraInfoInternal(CAMERA_ID_1, 0,
                                     CameraSelector.LENS_FACING_FRONT)));
+            final CameraCoordinator cameraCoordinator = new FakeCameraCoordinator();
+            cameraFactory.setCameraCoordinator(cameraCoordinator);
             return cameraFactory;
         };
 
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraCoordinator.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraCoordinator.java
new file mode 100644
index 0000000..b4ce627
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraCoordinator.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2023 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 androidx.camera.testing.fakes;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.concurrent.CameraCoordinator;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A {@link CameraCoordinator} implementation that contains concurrent camera mode and camera id
+ * information.
+ *
+ * @hide
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class FakeCameraCoordinator implements CameraCoordinator {
+
+    @NonNull private Map<String, String> mConcurrentCameraIdMap;
+    @NonNull private List<List<String>> mConcurrentCameraIds;
+    @NonNull private List<List<CameraSelector>> mConcurrentCameraSelectors;
+    @NonNull private List<CameraSelector> mActiveConcurrentCameraSelectors;
+    @NonNull private final List<ConcurrentCameraModeListener> mConcurrentCameraModeListeners;
+
+    @CameraOperatingMode private int mCameraOperatingMode;
+
+    public FakeCameraCoordinator() {
+        mConcurrentCameraIdMap = new HashMap<>();
+        mConcurrentCameraIds = new ArrayList<>();
+        mConcurrentCameraSelectors = new ArrayList<>();
+        mActiveConcurrentCameraSelectors = new ArrayList<>();
+        mConcurrentCameraModeListeners = new ArrayList<>();
+    }
+
+    /**
+     * Adds concurrent camera id and camera selectors.
+     *
+     * @param cameraIdAndSelectors combinations of camera id and selector.
+     */
+    public void addConcurrentCameraIdsAndCameraSelectors(
+            @NonNull Map<String, CameraSelector> cameraIdAndSelectors) {
+        mConcurrentCameraIds.add(new ArrayList<>(cameraIdAndSelectors.keySet()));
+        mConcurrentCameraSelectors.add(new ArrayList<>(cameraIdAndSelectors.values()));
+
+        for (List<String> concurrentCameraIdList: mConcurrentCameraIds) {
+            List<String> cameraIdList = new ArrayList<>(concurrentCameraIdList);
+            mConcurrentCameraIdMap.put(cameraIdList.get(0), cameraIdList.get(1));
+            mConcurrentCameraIdMap.put(cameraIdList.get(1), cameraIdList.get(0));
+        }
+    }
+
+    @NonNull
+    @Override
+    public List<List<CameraSelector>> getConcurrentCameraSelectors() {
+        return mConcurrentCameraSelectors;
+    }
+
+    @Override
+    public void setActiveConcurrentCameraSelectors(@NonNull List<CameraSelector> cameraSelectors) {
+        mActiveConcurrentCameraSelectors = cameraSelectors;
+    }
+
+    @NonNull
+    @Override
+    public List<CameraSelector> getActiveConcurrentCameraSelectors() {
+        return mActiveConcurrentCameraSelectors;
+    }
+
+    @Nullable
+    @Override
+    public String getPairedConcurrentCameraId(@NonNull String cameraId) {
+        if (mConcurrentCameraIdMap.containsKey(cameraId)) {
+            return mConcurrentCameraIdMap.get(cameraId);
+        }
+        return null;
+    }
+
+    @CameraOperatingMode
+    @Override
+    public int getCameraOperatingMode() {
+        return mCameraOperatingMode;
+    }
+
+    @Override
+    public void setCameraOperatingMode(@CameraOperatingMode int cameraOperatingMode) {
+        if (cameraOperatingMode != mCameraOperatingMode) {
+            for (ConcurrentCameraModeListener listener : mConcurrentCameraModeListeners) {
+                listener.notifyConcurrentCameraModeUpdated(cameraOperatingMode);
+            }
+        }
+
+        mCameraOperatingMode = cameraOperatingMode;
+    }
+
+    @Override
+    public void addListener(@NonNull ConcurrentCameraModeListener listener) {
+        mConcurrentCameraModeListeners.add(listener);
+    }
+
+    @Override
+    public void removeListener(@NonNull ConcurrentCameraModeListener listener) {
+        mConcurrentCameraModeListeners.remove(listener);
+    }
+}