Merge "[Concurrent Camera] Add data models and bindToLifecycle API for concurrent camera" into androidx-main
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/concurrent/ConcurrentCamera.java b/camera/camera-core/src/main/java/androidx/camera/core/concurrent/ConcurrentCamera.java
new file mode 100644
index 0000000..864d033
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/concurrent/ConcurrentCamera.java
@@ -0,0 +1,79 @@
+/*
+ * 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.core.concurrent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.camera.core.Camera;
+
+import java.util.List;
+
+/**
+ * Concurrent camera instance.
+ */
+@RequiresApi(21)
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class ConcurrentCamera {
+
+    @NonNull
+    private List<Camera> mCameras;
+
+    ConcurrentCamera(@NonNull List<Camera> cameras) {
+        mCameras = cameras;
+    }
+
+    /**
+     * Gets the list of cameras.
+     */
+    @NonNull
+    public List<Camera> getCameras() {
+        return mCameras;
+    }
+
+    /**
+     * Builder for concurrent camera instance.
+     */
+    public static class Builder {
+
+        @NonNull
+        private List<Camera> mCameras;
+
+        public Builder() {}
+
+        /**
+         * Sets the list of cameras.
+         *
+         * @param cameras cameras
+         * @return {@link Builder}
+         */
+        @NonNull
+        public Builder setCameras(@NonNull List<Camera> cameras) {
+            mCameras = cameras;
+            return this;
+        }
+
+        /**
+         * Builds the {@link ConcurrentCamera}.
+         * @return {@link ConcurrentCamera}.
+         */
+        @NonNull
+        public ConcurrentCamera builder() {
+            return new ConcurrentCamera(mCameras);
+        }
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/concurrent/ConcurrentCameraConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/concurrent/ConcurrentCameraConfig.java
new file mode 100644
index 0000000..bf8f007
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/concurrent/ConcurrentCameraConfig.java
@@ -0,0 +1,78 @@
+/*
+ * 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.core.concurrent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+import java.util.List;
+
+/**
+ * Concurrent Camera Configuration.
+ */
+@RequiresApi(21)
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class ConcurrentCameraConfig {
+    @NonNull
+    private List<SingleCameraConfig> mSingleCameraConfigs;
+
+    ConcurrentCameraConfig(@NonNull List<SingleCameraConfig> singleCameraConfigs) {
+        mSingleCameraConfigs = singleCameraConfigs;
+    }
+
+    /**
+     * Returns single camera configs.
+     * @return list of single camera configs.
+     */
+    @NonNull
+    public List<SingleCameraConfig> getSingleCameraConfigs() {
+        return mSingleCameraConfigs;
+    }
+
+    /**
+     * Builder for concurrent camera config.
+     */
+    public static class Builder {
+
+        @NonNull
+        private List<SingleCameraConfig> mSingleCameraConfigs;
+
+        public Builder() {}
+
+        /**
+         * Sets the list of single camera configs.
+         * @param singleCameraConfigs list of single camera configs.
+         * @return {@link Builder}.
+         */
+        @NonNull
+        public Builder setCameraConfigs(@NonNull List<SingleCameraConfig> singleCameraConfigs) {
+            mSingleCameraConfigs = singleCameraConfigs;
+            return this;
+        }
+
+        /**
+         * Builds the {@link ConcurrentCameraConfig}.
+         * @return {@link ConcurrentCameraConfig}.
+         */
+        @NonNull
+        public ConcurrentCameraConfig build() {
+            return new ConcurrentCameraConfig(mSingleCameraConfigs);
+        }
+
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/concurrent/SingleCameraConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/concurrent/SingleCameraConfig.java
new file mode 100644
index 0000000..c46e856
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/concurrent/SingleCameraConfig.java
@@ -0,0 +1,119 @@
+/*
+ * 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.core.concurrent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.UseCaseGroup;
+import androidx.lifecycle.LifecycleOwner;
+
+/**
+ * Single Camera Configuration.
+ */
+@RequiresApi(21)
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class SingleCameraConfig {
+
+    @NonNull
+    private CameraSelector mCameraSelector;
+    @NonNull
+    private LifecycleOwner mLifecycleOwner;
+    @NonNull
+    private UseCaseGroup mUseCaseGroup;
+
+    SingleCameraConfig(
+            @NonNull CameraSelector cameraSelector,
+            @NonNull LifecycleOwner lifecycleOwner,
+            @NonNull UseCaseGroup useCaseGroup) {
+        this.mCameraSelector = cameraSelector;
+        this.mLifecycleOwner = lifecycleOwner;
+        this.mUseCaseGroup = useCaseGroup;
+    }
+
+    @NonNull
+    public CameraSelector getCameraSelector() {
+        return mCameraSelector;
+    }
+
+    @NonNull
+    public LifecycleOwner getLifecycleOwner() {
+        return mLifecycleOwner;
+    }
+
+    @NonNull
+    public UseCaseGroup getUseCaseGroup() {
+        return mUseCaseGroup;
+    }
+
+    /**
+     * Build for single camera config.
+     */
+    public static class Builder {
+        @NonNull
+        private CameraSelector mCameraSelector;
+        @NonNull
+        private LifecycleOwner mLifecycleOwner;
+        @NonNull
+        private UseCaseGroup mUseCaseGroup;
+
+        public Builder() {}
+
+        /**
+         * Sets {@link CameraSelector}.
+         * @param cameraSelector
+         * @return {@link Builder}.
+         */
+        @NonNull
+        public Builder setCameraSelector(@NonNull CameraSelector cameraSelector) {
+            mCameraSelector = cameraSelector;
+            return this;
+        }
+
+        /**
+         * Sets {@link LifecycleOwner}.
+         * @param lifecycleOwner
+         * @return {@link Builder}.
+         */
+        @NonNull
+        public Builder setLifecycleOwner(@NonNull LifecycleOwner lifecycleOwner) {
+            mLifecycleOwner = lifecycleOwner;
+            return this;
+        }
+
+        /**
+         * Sets {@link UseCaseGroup}.
+         * @param useCaseGroup
+         * @return {@link Builder}.
+         */
+        @NonNull
+        public Builder setUseCaseGroup(@NonNull UseCaseGroup useCaseGroup) {
+            mUseCaseGroup = useCaseGroup;
+            return this;
+        }
+
+        /**
+         * Builds the {@link SingleCameraConfig}.
+         * @return {@link SingleCameraConfig}.
+         */
+        @NonNull
+        public SingleCameraConfig build() {
+            return new SingleCameraConfig(mCameraSelector, mLifecycleOwner, mUseCaseGroup);
+        }
+    }
+}
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 38dd3a9..8cb0c37 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
@@ -26,6 +26,8 @@
 import androidx.camera.core.CameraXConfig
 import androidx.camera.core.Preview
 import androidx.camera.core.UseCaseGroup
+import androidx.camera.core.concurrent.ConcurrentCameraConfig
+import androidx.camera.core.concurrent.SingleCameraConfig
 import androidx.camera.core.impl.CameraFactory
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
 import androidx.camera.core.processing.SurfaceProcessorWithExecutor
@@ -628,6 +630,114 @@
         // Should not throw exception
         ProcessCameraProvider.configureInstance(FakeAppConfig.create())
     }
+
+    @Test
+    fun bindConcurrentCamera_isBound() {
+        ProcessCameraProvider.configureInstance(FakeAppConfig.create())
+
+        runBlocking(MainScope().coroutineContext) {
+            provider = ProcessCameraProvider.getInstance(context).await()
+            val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
+            val useCase1 = 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 concurrentCameraConfig = ConcurrentCameraConfig.Builder()
+                .setCameraConfigs(listOf(singleCameraConfig0, singleCameraConfig1))
+                .build()
+
+            val concurrentCamera = provider.bindToLifecycle(concurrentCameraConfig)
+
+            assertThat(concurrentCamera).isNotNull()
+            assertThat(concurrentCamera.cameras.size).isEqualTo(2)
+            assertThat(provider.isBound(useCase0)).isTrue()
+            assertThat(provider.isBound(useCase1)).isTrue()
+        }
+    }
+
+    @Test
+    fun bindConcurrentCamera_lessThanTwoSingleCameraConfigs() {
+        ProcessCameraProvider.configureInstance(FakeAppConfig.create())
+
+        runBlocking(MainScope().coroutineContext) {
+            provider = ProcessCameraProvider.getInstance(context).await()
+            val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
+
+            val singleCameraConfig0 = SingleCameraConfig.Builder()
+                .setCameraSelector(CameraSelector.DEFAULT_BACK_CAMERA)
+                .setUseCaseGroup(UseCaseGroup.Builder()
+                    .addUseCase(useCase0)
+                    .build())
+                .setLifecycleOwner(lifecycleOwner0)
+                .build()
+
+            val concurrentCameraConfig = ConcurrentCameraConfig.Builder()
+                .setCameraConfigs(listOf(singleCameraConfig0))
+                .build()
+
+            assertThrows<IllegalArgumentException> {
+                provider.bindToLifecycle(concurrentCameraConfig)
+            }
+        }
+    }
+
+    @Test
+    fun bindConcurrentCamera_moreThanTwoSingleCameraConfigs() {
+        ProcessCameraProvider.configureInstance(FakeAppConfig.create())
+
+        runBlocking(MainScope().coroutineContext) {
+            provider = ProcessCameraProvider.getInstance(context).await()
+            val useCase0 = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
+            val useCase1 = 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(useCase0)
+                    .build())
+                .setLifecycleOwner(lifecycleOwner1)
+                .build()
+
+            val concurrentCameraConfig = ConcurrentCameraConfig.Builder()
+                .setCameraConfigs(listOf(singleCameraConfig0,
+                    singleCameraConfig1,
+                    singleCameraConfig2))
+                .build()
+
+            assertThrows<UnsupportedOperationException> {
+                provider.bindToLifecycle(concurrentCameraConfig)
+            }
+        }
+    }
 }
 
 private class TestAppContextWrapper(base: Context, val app: Application? = null) :
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 ec82c85..7290be0 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
@@ -46,6 +46,9 @@
 import androidx.camera.core.UseCase;
 import androidx.camera.core.UseCaseGroup;
 import androidx.camera.core.ViewPort;
+import androidx.camera.core.concurrent.ConcurrentCamera;
+import androidx.camera.core.concurrent.ConcurrentCameraConfig;
+import androidx.camera.core.concurrent.SingleCameraConfig;
 import androidx.camera.core.impl.CameraConfig;
 import androidx.camera.core.impl.CameraInternal;
 import androidx.camera.core.impl.ExtendedCameraConfigProviderStore;
@@ -390,6 +393,51 @@
     }
 
     /**
+     * Binds a {@link ConcurrentCameraConfig} to {@link LifecycleOwner}.
+     *
+     * <p>The concurrent camera is only supporting two cameras currently. If the input
+     * {@link ConcurrentCameraConfig} has less or more than two {@link SingleCameraConfig},
+     * {@link IllegalArgumentException} will be thrown.
+     *
+     * @param concurrentCameraConfig input configuration for concurrent camera.
+     * @return output concurrent camera instance.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @SuppressWarnings({"lambdaLast"})
+    @MainThread
+    @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");
+        }
+
+        if (concurrentCameraConfig.getSingleCameraConfigs().size() > 2) {
+            throw new UnsupportedOperationException("Concurrent camera is only supporting two  "
+                    + "cameras at maximum.");
+        }
+
+        List<Camera> cameras = new ArrayList<>();
+        for (SingleCameraConfig config : concurrentCameraConfig.getSingleCameraConfigs()) {
+            Camera camera = bindToLifecycle(
+                    config.getLifecycleOwner(),
+                    config.getCameraSelector(),
+                    config.getUseCaseGroup().getViewPort(),
+                    config.getUseCaseGroup().getEffects(),
+                    config.getUseCaseGroup().getUseCases().toArray(new UseCase[0]));
+
+            cameras.add(camera);
+        }
+
+        return new ConcurrentCamera.Builder()
+                .setCameras(cameras)
+                .builder();
+    }
+
+    /**
      * Binds {@link ViewPort} and a collection of {@link UseCase} to a {@link LifecycleOwner}.
      *
      * <p>The state of the lifecycle will determine when the cameras are open, started, stopped