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