Check for duplicate effects target in UseCaseGroup
Throws IllegalArgumentException if there are duplicates or it's not in a supported list.
Bug: 240608986
Test: manual test and ./gradlew bOS
Change-Id: Ic48722cde4ae7202beef8090bfa0e618a424676d
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/UseCaseGroup.java b/camera/camera-core/src/main/java/androidx/camera/core/UseCaseGroup.java
index 3848aa5..5008b5c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/UseCaseGroup.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/UseCaseGroup.java
@@ -16,8 +16,13 @@
package androidx.camera.core;
+import static androidx.camera.core.CameraEffect.IMAGE_CAPTURE;
+import static androidx.camera.core.CameraEffect.PREVIEW;
+import static androidx.camera.core.CameraEffect.VIDEO_CAPTURE;
import static androidx.core.util.Preconditions.checkArgument;
+import static java.util.Objects.requireNonNull;
+
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
@@ -25,7 +30,11 @@
import androidx.lifecycle.Lifecycle;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
+import java.util.Map;
/**
* Represents a collection of {@link UseCase}.
@@ -82,10 +91,17 @@
* A builder for generating {@link UseCaseGroup}.
*/
public static final class Builder {
+
+ // Allow-list effect targets supported by CameraX.
+ private static final List<Integer> SUPPORTED_TARGETS = Arrays.asList(
+ PREVIEW,
+ IMAGE_CAPTURE);
+
private ViewPort mViewPort;
private final List<UseCase> mUseCases;
private final List<CameraEffect> mEffects;
+
public Builder() {
mUseCases = new ArrayList<>();
mEffects = new ArrayList<>();
@@ -101,7 +117,13 @@
}
/**
- * Adds a {@link CameraEffect} to the collection
+ * Adds a {@link CameraEffect} to the collection.
+ *
+ * <p>The value of {@link CameraEffect#getTargets()} must be unique and must be one of
+ * the supported values below:
+ * <ul>
+ * <li>{@link CameraEffect#PREVIEW}
+ * </ul>
*
* <p>Once added, CameraX will use the {@link CameraEffect}s to process the outputs of
* the {@link UseCase}s.
@@ -116,6 +138,57 @@
}
/**
+ * Checks effect targets and throw {@link IllegalArgumentException}.
+ *
+ * <p>Throws exception if the effects 1) contains duplicate targets or 2) contains
+ * effects that is not in the allowlist.
+ */
+ private void checkEffectTargets() {
+ Map<Integer, CameraEffect> targetEffectMap = new HashMap<>();
+ for (CameraEffect effect : mEffects) {
+ int targets = effect.getTargets();
+ if (!SUPPORTED_TARGETS.contains(targets)) {
+ throw new IllegalArgumentException(String.format(Locale.US,
+ "Target %s is not in the supported list %s.",
+ getHumanReadableTargets(targets),
+ getHumanReadableSupportedTargets()));
+ }
+ if (targetEffectMap.containsKey(effect.getTargets())) {
+ throw new IllegalArgumentException(String.format(Locale.US,
+ "%s and %s contain duplicate targets %s.",
+ requireNonNull(
+ targetEffectMap.get(effect.getTargets())).getClass().getName(),
+ effect.getClass().getName(),
+ getHumanReadableTargets(targets)));
+ }
+ targetEffectMap.put(effect.getTargets(), effect);
+ }
+ }
+
+ static String getHumanReadableSupportedTargets() {
+ List<String> targetNameList = new ArrayList<>();
+ for (Integer targets : SUPPORTED_TARGETS) {
+ targetNameList.add(getHumanReadableTargets(targets));
+ }
+ return "[" + String.join(", ", targetNameList) + "]";
+ }
+
+ static String getHumanReadableTargets(int targets) {
+ List<String> names = new ArrayList<>();
+ if ((targets & IMAGE_CAPTURE) != 0) {
+ names.add("IMAGE_CAPTURE");
+ }
+ if ((targets & PREVIEW) != 0) {
+ names.add("PREVIEW");
+ }
+
+ if ((targets & VIDEO_CAPTURE) != 0) {
+ names.add("VIDEO_CAPTURE");
+ }
+ return String.join("|", names);
+ }
+
+ /**
* Adds {@link UseCase} to the collection.
*/
@NonNull
@@ -130,6 +203,7 @@
@NonNull
public UseCaseGroup build() {
checkArgument(!mUseCases.isEmpty(), "UseCase must not be empty.");
+ checkEffectTargets();
return new UseCaseGroup(mViewPort, mUseCases, mEffects);
}
}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/UseCaseGroupTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/UseCaseGroupTest.kt
new file mode 100644
index 0000000..a4eae79
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/UseCaseGroupTest.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core
+
+import android.os.Build
+import androidx.camera.core.CameraEffect.IMAGE_CAPTURE
+import androidx.camera.core.CameraEffect.PREVIEW
+import androidx.camera.core.CameraEffect.VIDEO_CAPTURE
+import androidx.camera.core.UseCaseGroup.Builder.getHumanReadableTargets
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.testing.fakes.FakePreviewEffect
+import androidx.camera.testing.fakes.FakeSurfaceProcessor
+import androidx.camera.testing.fakes.FakeUseCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+/**
+ * Unit tests for [UseCaseGroup].
+ */
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class UseCaseGroupTest {
+
+ @Test
+ fun duplicateTargets_throwsException() {
+ // Arrange.
+ val previewEffect = FakePreviewEffect(
+ CameraXExecutors.mainThreadExecutor(),
+ FakeSurfaceProcessor(CameraXExecutors.mainThreadExecutor())
+ )
+ val builder = UseCaseGroup.Builder().addUseCase(FakeUseCase())
+ .addEffect(previewEffect)
+ .addEffect(previewEffect)
+
+ // Act.
+ var message: String? = null
+ try {
+ builder.build()
+ } catch (e: IllegalArgumentException) {
+ message = e.message
+ }
+
+ // Assert.
+ assertThat(message).isEqualTo(
+ "androidx.camera.testing.fakes.FakePreviewEffect " +
+ "and androidx.camera.testing.fakes.FakePreviewEffect " +
+ "contain duplicate targets PREVIEW."
+ )
+ }
+
+ @Test
+ fun verifyHumanReadableTargetsNames() {
+ assertThat(getHumanReadableTargets(PREVIEW)).isEqualTo("PREVIEW")
+ assertThat(getHumanReadableTargets(PREVIEW or VIDEO_CAPTURE))
+ .isEqualTo("PREVIEW|VIDEO_CAPTURE")
+ assertThat(getHumanReadableTargets(PREVIEW or VIDEO_CAPTURE or IMAGE_CAPTURE))
+ .isEqualTo("IMAGE_CAPTURE|PREVIEW|VIDEO_CAPTURE")
+ }
+}
\ No newline at end of file