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