Merge "Parameterized ImageAnalysisTest for Camera2/CameraPipe" into androidx-main
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/ImageAnalysisTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/ImageAnalysisTest.java
deleted file mode 100644
index 86c6f20..0000000
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/ImageAnalysisTest.java
+++ /dev/null
@@ -1,503 +0,0 @@
-/*
- * Copyright (C) 2019 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.camera2;
-
-import static androidx.camera.core.ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assume.assumeTrue;
-
-import android.app.Instrumentation;
-import android.content.Context;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.util.Size;
-import android.view.Surface;
-
-import androidx.annotation.GuardedBy;
-import androidx.camera.core.AspectRatio;
-import androidx.camera.core.CameraInfoUnavailableException;
-import androidx.camera.core.CameraSelector;
-import androidx.camera.core.CameraX;
-import androidx.camera.core.CameraXConfig;
-import androidx.camera.core.ImageAnalysis;
-import androidx.camera.core.ImageAnalysis.Analyzer;
-import androidx.camera.core.ImageAnalysis.BackpressureStrategy;
-import androidx.camera.core.ImageCapture;
-import androidx.camera.core.ImageProxy;
-import androidx.camera.core.impl.ImageOutputConfig;
-import androidx.camera.core.impl.UseCaseConfig;
-import androidx.camera.core.impl.utils.executor.CameraXExecutors;
-import androidx.camera.core.internal.CameraUseCaseAdapter;
-import androidx.camera.testing.CameraUtil;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.LargeTest;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestRule;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-public final class ImageAnalysisTest {
-    private static final Size GUARANTEED_RESOLUTION = new Size(640, 480);
-    private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
-    private final Object mAnalysisResultLock = new Object();
-    @GuardedBy("mAnalysisResultLock")
-    private Set<ImageProperties> mAnalysisResults;
-    private Analyzer mAnalyzer;
-    private HandlerThread mHandlerThread;
-    private Handler mHandler;
-    private Semaphore mAnalysisResultsSemaphore;
-    private CameraSelector mCameraSelector;
-    private Context mContext;
-    private CameraUseCaseAdapter mCamera;
-
-    @Rule
-    public TestRule mCameraRule = CameraUtil.grantCameraPermissionAndPreTest();
-
-    @Before
-    public void setUp() {
-        synchronized (mAnalysisResultLock) {
-            mAnalysisResults = new HashSet<>();
-        }
-        mAnalysisResultsSemaphore = new Semaphore(/*permits=*/ 0);
-        mAnalyzer =
-                (image) -> {
-                    synchronized (mAnalysisResultLock) {
-                        mAnalysisResults.add(new ImageProperties(image,
-                                image.getImageInfo().getRotationDegrees()));
-                    }
-                    mAnalysisResultsSemaphore.release();
-                    image.close();
-                };
-        mContext = ApplicationProvider.getApplicationContext();
-        CameraXConfig config = Camera2Config.defaultConfig();
-
-        CameraX.initialize(mContext, config);
-
-        mHandlerThread = new HandlerThread("AnalysisThread");
-        mHandlerThread.start();
-        mHandler = new Handler(mHandlerThread.getLooper());
-        mCameraSelector = new CameraSelector.Builder().requireLensFacing(
-                CameraSelector.LENS_FACING_BACK).build();
-    }
-
-    @After
-    public void tearDown() throws ExecutionException, InterruptedException, TimeoutException {
-        if (mCamera != null) {
-            mInstrumentation.runOnMainSync(() ->
-                    //TODO: The removeUseCases() call might be removed after clarifying the
-                    // abortCaptures() issue in b/162314023.
-                    mCamera.removeUseCases(mCamera.getUseCases())
-            );
-        }
-
-        CameraX.shutdown().get(10000, TimeUnit.MILLISECONDS);
-
-        if (mHandlerThread != null) {
-            mHandlerThread.quitSafely();
-        }
-    }
-
-    @Test
-    public void exceedMaxImagesWithoutClosing_doNotCrash() throws InterruptedException {
-        // Arrange.
-        int queueDepth = 3;
-        Semaphore semaphore = new Semaphore(0);
-        ImageAnalysis useCase = new ImageAnalysis.Builder()
-                .setBackpressureStrategy(ImageAnalysis.STRATEGY_BLOCK_PRODUCER)
-                .setImageQueueDepth(queueDepth)
-                .build();
-        List<ImageProxy> imageProxyList = new ArrayList<>();
-        useCase.setAnalyzer(CameraXExecutors.newHandlerExecutor(mHandler),
-                image -> {
-                    imageProxyList.add(image);
-                    semaphore.release();
-                });
-        // Act.
-        mCamera = CameraUtil.createCameraAndAttachUseCase(mContext,
-                CameraSelector.DEFAULT_FRONT_CAMERA, useCase);
-        // Assert: waiting for images does not crash.
-        assertThat(semaphore.tryAcquire(queueDepth + 1, /*timeout=*/1, TimeUnit.SECONDS)).isFalse();
-
-        // Clean it up.
-        useCase.clearAnalyzer();
-        for (ImageProxy image : imageProxyList) {
-            image.close();
-        }
-    }
-
-    @Test
-    public void canSupportGuaranteedSizeFront()
-            throws InterruptedException, CameraInfoUnavailableException {
-        // CameraSelector.LENS_FACING_FRONT/LENS_FACING_BACK are defined as constant int 0 and 1.
-        // Using for-loop to check both front and back device cameras can support the guaranteed
-        // 640x480 size.
-        assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_FRONT));
-        assumeTrue(!CameraUtil.requiresCorrectedAspectRatio(CameraSelector.LENS_FACING_FRONT));
-
-        // Checks camera device sensor degrees to set correct target rotation value to make sure
-        // the exactly matching result size 640x480 can be selected if the device supports it.
-        Integer sensorOrientation = CameraUtil.getSensorOrientation(
-                CameraSelector.LENS_FACING_FRONT);
-        boolean isRotateNeeded = (sensorOrientation % 180) != 0;
-        ImageAnalysis useCase = new ImageAnalysis.Builder().setTargetResolution(
-                GUARANTEED_RESOLUTION).setTargetRotation(
-                isRotateNeeded ? Surface.ROTATION_90 : Surface.ROTATION_0).build();
-        mCamera = CameraUtil.createCameraAndAttachUseCase(mContext,
-                CameraSelector.DEFAULT_FRONT_CAMERA, useCase);
-        useCase.setAnalyzer(CameraXExecutors.newHandlerExecutor(mHandler), mAnalyzer);
-        assertThat(mAnalysisResultsSemaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue();
-
-        synchronized (mAnalysisResultLock) {
-            // Check the analyzed image exactly matches 640x480 size. This test can also check
-            // whether the guaranteed resolution 640x480 is really supported for YUV_420_888
-            // format on the devices when running the test.
-            assertThat(GUARANTEED_RESOLUTION).isEqualTo(
-                    mAnalysisResults.iterator().next().mResolution);
-        }
-    }
-
-    @Test
-    public void canSupportGuaranteedSizeBack()
-            throws InterruptedException, CameraInfoUnavailableException {
-        // CameraSelector.LENS_FACING_FRONT/LENS_FACING_BACK are defined as constant int 0 and 1.
-        // Using for-loop to check both front and back device cameras can support the guaranteed
-        // 640x480 size.
-        assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK));
-        assumeTrue(!CameraUtil.requiresCorrectedAspectRatio(CameraSelector.LENS_FACING_BACK));
-
-        // Checks camera device sensor degrees to set correct target rotation value to make sure
-        // the exactly matching result size 640x480 can be selected if the device supports it.
-        Integer sensorOrientation = CameraUtil.getSensorOrientation(
-                CameraSelector.LENS_FACING_BACK);
-        boolean isRotateNeeded = (sensorOrientation % 180) != 0;
-        ImageAnalysis useCase = new ImageAnalysis.Builder().setTargetResolution(
-                GUARANTEED_RESOLUTION).setTargetRotation(
-                isRotateNeeded ? Surface.ROTATION_90 : Surface.ROTATION_0).build();
-
-        mCamera = CameraUtil.createCameraAndAttachUseCase(mContext,
-                CameraSelector.DEFAULT_BACK_CAMERA, useCase);
-        useCase.setAnalyzer(CameraXExecutors.newHandlerExecutor(mHandler), mAnalyzer);
-
-        assertThat(mAnalysisResultsSemaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue();
-
-        synchronized (mAnalysisResultLock) {
-            // Check the analyzed image exactly matches 640x480 size. This test can also check
-            // whether the guaranteed resolution 640x480 is really supported for YUV_420_888
-            // format on the devices when running the test.
-            assertThat(GUARANTEED_RESOLUTION).isEqualTo(
-                    mAnalysisResults.iterator().next().mResolution);
-        }
-    }
-
-    @Test
-    public void analyzesImages_withKEEP_ONLY_LATEST_whenCameraIsOpen()
-            throws InterruptedException {
-        analyzerAnalyzesImagesWithStrategy(STRATEGY_KEEP_ONLY_LATEST);
-    }
-
-    @Test
-    public void analyzesImages_withBLOCK_PRODUCER_whenCameraIsOpen()
-            throws InterruptedException {
-        analyzerAnalyzesImagesWithStrategy(ImageAnalysis.STRATEGY_BLOCK_PRODUCER);
-    }
-
-    private void analyzerAnalyzesImagesWithStrategy(@BackpressureStrategy int backpressureStrategy)
-            throws InterruptedException {
-        ImageAnalysis useCase = new ImageAnalysis.Builder().setBackpressureStrategy(
-                backpressureStrategy).build();
-        mCamera = CameraUtil.createCameraAndAttachUseCase(mContext, mCameraSelector, useCase);
-        useCase.setAnalyzer(CameraXExecutors.newHandlerExecutor(mHandler), mAnalyzer);
-
-        mAnalysisResultsSemaphore.tryAcquire(5, TimeUnit.SECONDS);
-
-        synchronized (mAnalysisResultLock) {
-            assertThat(mAnalysisResults).isNotEmpty();
-        }
-    }
-
-    @Test
-    public void analyzerDoesNotAnalyzeImages_whenCameraIsNotOpen() throws InterruptedException {
-        ImageAnalysis useCase = new ImageAnalysis.Builder().build();
-        // Bind but do not start lifecycle
-        mCamera = CameraUtil.createCameraAndAttachUseCase(mContext, mCameraSelector, useCase);
-        mCamera.detachUseCases();
-
-        useCase.setAnalyzer(CameraXExecutors.newHandlerExecutor(mHandler), mAnalyzer);
-        // Keep the lifecycle in an inactive state.
-        // Wait a little while for frames to be analyzed.
-        mAnalysisResultsSemaphore.tryAcquire(5, TimeUnit.SECONDS);
-
-        // No frames should have been analyzed.
-        synchronized (mAnalysisResultLock) {
-            assertThat(mAnalysisResults).isEmpty();
-        }
-    }
-
-    @Test
-    public void canObtainDefaultBackpressureStrategy() {
-        ImageAnalysis imageAnalysis = new ImageAnalysis.Builder().build();
-        assertThat(imageAnalysis.getBackpressureStrategy()).isEqualTo(STRATEGY_KEEP_ONLY_LATEST);
-    }
-
-    @Test
-    public void canObtainDefaultImageQueueDepth() {
-        ImageAnalysis imageAnalysis = new ImageAnalysis.Builder().build();
-
-        // Should not be less than 1
-        assertThat(imageAnalysis.getImageQueueDepth()).isAtLeast(1);
-    }
-
-    @Test
-    public void defaultAspectRatioWillBeSet_whenTargetResolutionIsNotSet() {
-        ImageAnalysis useCase = new ImageAnalysis.Builder().build();
-        mCamera = CameraUtil.createCameraAndAttachUseCase(mContext, mCameraSelector, useCase);
-        ImageOutputConfig config = (ImageOutputConfig) useCase.getCurrentConfig();
-        assertThat(config.getTargetAspectRatio()).isEqualTo(AspectRatio.RATIO_4_3);
-    }
-
-    @Test
-    public void defaultAspectRatioWontBeSet_whenTargetResolutionIsSet() {
-        assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK));
-        ImageAnalysis useCase = new ImageAnalysis.Builder().setTargetResolution(
-                GUARANTEED_RESOLUTION).build();
-
-        assertThat(useCase.getCurrentConfig().containsOption(
-                ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO)).isFalse();
-
-        mCamera = CameraUtil.createCameraAndAttachUseCase(mContext,
-                CameraSelector.DEFAULT_BACK_CAMERA, useCase);
-
-        assertThat(useCase.getCurrentConfig().containsOption(
-                ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO)).isFalse();
-    }
-
-    @Test
-    public void targetRotationCanBeUpdatedAfterUseCaseIsCreated() {
-        ImageAnalysis imageAnalysis =
-                new ImageAnalysis.Builder().setTargetRotation(Surface.ROTATION_0).build();
-        imageAnalysis.setTargetRotation(Surface.ROTATION_90);
-
-        assertThat(imageAnalysis.getTargetRotation()).isEqualTo(Surface.ROTATION_90);
-    }
-
-    @Test
-    public void targetResolutionIsUpdatedAfterTargetRotationIsUpdated() {
-        ImageAnalysis imageAnalysis = new ImageAnalysis.Builder().setTargetResolution(
-                GUARANTEED_RESOLUTION).setTargetRotation(Surface.ROTATION_0).build();
-        mCamera = CameraUtil.createCameraAndAttachUseCase(mContext, mCameraSelector, imageAnalysis);
-
-        // Updates target rotation from ROTATION_0 to ROTATION_90.
-        imageAnalysis.setTargetRotation(Surface.ROTATION_90);
-
-        ImageOutputConfig newConfig = (ImageOutputConfig) imageAnalysis.getCurrentConfig();
-        Size expectedTargetResolution = new Size(GUARANTEED_RESOLUTION.getHeight(),
-                GUARANTEED_RESOLUTION.getWidth());
-
-        // Expected targetResolution will be reversed from original target resolution.
-        assertThat(newConfig.getTargetResolution().equals(expectedTargetResolution)).isTrue();
-    }
-
-    // TODO(b/162298517): change the test to be deterministic instead of depend upon timing.
-    @Test
-    public void analyzerSetMultipleTimesInKeepOnlyLatestMode() throws Exception {
-        ImageAnalysis useCase = new ImageAnalysis.Builder().setBackpressureStrategy(
-                STRATEGY_KEEP_ONLY_LATEST).build();
-        mCamera = CameraUtil.createCameraAndAttachUseCase(mContext, mCameraSelector, useCase);
-
-        useCase.setAnalyzer(CameraXExecutors.newHandlerExecutor(mHandler), mAnalyzer);
-        mAnalysisResultsSemaphore.tryAcquire(5, TimeUnit.SECONDS);
-
-        Analyzer slowAnalyzer = image -> {
-            try {
-                Thread.sleep(200);
-                image.close();
-            } catch (Exception e) {
-            }
-        };
-        useCase.setAnalyzer(CameraXExecutors.newHandlerExecutor(mHandler), slowAnalyzer);
-
-        Thread.sleep(100);
-        useCase.setAnalyzer(CameraXExecutors.newHandlerExecutor(mHandler), slowAnalyzer);
-
-        Thread.sleep(100);
-        useCase.setAnalyzer(CameraXExecutors.newHandlerExecutor(mHandler), slowAnalyzer);
-
-        Thread.sleep(100);
-        useCase.setAnalyzer(CameraXExecutors.newHandlerExecutor(mHandler), slowAnalyzer);
-
-        Thread.sleep(100);
-        useCase.setAnalyzer(CameraXExecutors.newHandlerExecutor(mHandler), slowAnalyzer);
-
-        Thread.sleep(100);
-    }
-
-    @Test
-    public void useCaseConfigCanBeReset_afterUnbind() {
-        final ImageAnalysis useCase = new ImageAnalysis.Builder().build();
-        UseCaseConfig<?> initialConfig = useCase.getCurrentConfig();
-
-        mCamera = CameraUtil.createCameraAndAttachUseCase(mContext, mCameraSelector, useCase);
-
-        mInstrumentation.runOnMainSync(() -> {
-            mCamera.removeUseCases(Collections.singleton(useCase));
-        });
-
-        UseCaseConfig<?> configAfterUnbinding = useCase.getCurrentConfig();
-        assertThat(initialConfig.equals(configAfterUnbinding)).isTrue();
-    }
-
-    @Test
-    public void targetRotationIsRetained_whenUseCaseIsReused() {
-        ImageAnalysis useCase = new ImageAnalysis.Builder().build();
-
-        mCamera = CameraUtil.createCameraAndAttachUseCase(mContext, mCameraSelector, useCase);
-
-        // Generally, the device can't be rotated to Surface.ROTATION_180. Therefore,
-        // use it to do the test.
-        useCase.setTargetRotation(Surface.ROTATION_180);
-
-        mInstrumentation.runOnMainSync(() -> {
-            // Check the target rotation is kept when the use case is unbound.
-            mCamera.removeUseCases(Collections.singleton(useCase));
-            assertThat(useCase.getTargetRotation()).isEqualTo(Surface.ROTATION_180);
-        });
-
-        // Check the target rotation is kept when the use case is rebound to the
-        // lifecycle.
-        mCamera = CameraUtil.createCameraAndAttachUseCase(mContext, mCameraSelector, useCase);
-        assertThat(useCase.getTargetRotation()).isEqualTo(Surface.ROTATION_180);
-    }
-
-    @Test
-    public void useCaseCanBeReusedInSameCamera() throws InterruptedException {
-        ImageAnalysis useCase = new ImageAnalysis.Builder().build();
-
-        mCamera = CameraUtil.createCameraAndAttachUseCase(mContext, mCameraSelector, useCase);
-        useCase.setAnalyzer(CameraXExecutors.newHandlerExecutor(mHandler), mAnalyzer);
-
-        assertThat(mAnalysisResultsSemaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue();
-
-        mInstrumentation.runOnMainSync(() -> {
-            mCamera.removeUseCases(Collections.singleton(useCase));
-        });
-
-        mAnalysisResultsSemaphore = new Semaphore(/*permits=*/ 0);
-        // Rebind the use case to the same camera.
-        mCamera = CameraUtil.createCameraAndAttachUseCase(mContext, mCameraSelector, useCase);
-
-        assertThat(mAnalysisResultsSemaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue();
-    }
-
-    @Test
-    public void useCaseCanBeReusedInDifferentCamera() throws InterruptedException {
-        ImageAnalysis useCase = new ImageAnalysis.Builder().build();
-
-        mCamera = CameraUtil.createCameraAndAttachUseCase(mContext,
-                CameraSelector.DEFAULT_BACK_CAMERA, useCase);
-        useCase.setAnalyzer(CameraXExecutors.newHandlerExecutor(mHandler), mAnalyzer);
-
-        assertThat(mAnalysisResultsSemaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue();
-
-        mInstrumentation.runOnMainSync(() -> {
-            mCamera.removeUseCases(Collections.singleton(useCase));
-        });
-
-        mAnalysisResultsSemaphore = new Semaphore(/*permits=*/ 0);
-        // Rebind the use case to different camera.
-        mCamera = CameraUtil.createCameraAndAttachUseCase(mContext,
-                CameraSelector.DEFAULT_FRONT_CAMERA, useCase);
-
-        assertThat(mAnalysisResultsSemaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue();
-    }
-
-    @Test
-    public void returnValidTargetRotation_afterUseCaseIsCreated() {
-        ImageCapture imageCapture = new ImageCapture.Builder().build();
-        assertThat(imageCapture.getTargetRotation()).isNotEqualTo(
-                ImageOutputConfig.INVALID_ROTATION);
-    }
-
-    @Test
-    public void returnCorrectTargetRotation_afterUseCaseIsAttached() {
-        ImageAnalysis imageAnalysis = new ImageAnalysis.Builder().setTargetRotation(
-                Surface.ROTATION_180).build();
-        mCamera = CameraUtil.createCameraAndAttachUseCase(mContext, mCameraSelector, imageAnalysis);
-        assertThat(imageAnalysis.getTargetRotation()).isEqualTo(Surface.ROTATION_180);
-    }
-
-    private static class ImageProperties {
-        final Size mResolution;
-        final int mFormat;
-        final long mTimestamp;
-        final int mRotationDegrees;
-
-        ImageProperties(ImageProxy image, int rotationDegrees) {
-            this.mResolution = new Size(image.getWidth(), image.getHeight());
-            this.mFormat = image.getFormat();
-            this.mTimestamp = image.getImageInfo().getTimestamp();
-            this.mRotationDegrees = rotationDegrees;
-        }
-
-        @Override
-        public boolean equals(Object other) {
-            if (this == other) {
-                return true;
-            }
-            if (other == null) {
-                return false;
-            }
-            if (!(other instanceof ImageProperties)) {
-                return false;
-            }
-            ImageProperties otherProperties = (ImageProperties) other;
-            return mResolution.equals(otherProperties.mResolution)
-                    && mFormat == otherProperties.mFormat
-                    && otherProperties.mTimestamp == mTimestamp
-                    && otherProperties.mRotationDegrees == mRotationDegrees;
-        }
-
-        @Override
-        public int hashCode() {
-            int hash = 7;
-            hash = 31 * hash + mResolution.getWidth();
-            hash = 31 * hash + mResolution.getHeight();
-            hash = 31 * hash + mFormat;
-            hash = 31 * hash + (int) mTimestamp;
-            hash = 31 * hash + mRotationDegrees;
-            return hash;
-        }
-    }
-}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageAnalysisTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageAnalysisTest.kt
new file mode 100644
index 0000000..87aefea
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageAnalysisTest.kt
@@ -0,0 +1,459 @@
+/*
+ * Copyright (C) 2021 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.integration.core
+
+import android.content.Context
+import android.os.Handler
+import android.os.HandlerThread
+import android.util.Size
+import android.view.Surface
+import androidx.annotation.GuardedBy
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
+import androidx.camera.core.AspectRatio
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraX
+import androidx.camera.core.CameraXConfig
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.ImageAnalysis.BackpressureStrategy
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.ImageProxy
+import androidx.camera.core.impl.ImageOutputConfig
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.core.internal.CameraUseCaseAdapter
+import androidx.camera.testing.CameraUtil
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import java.util.concurrent.Semaphore
+import java.util.concurrent.TimeUnit
+
+private val DEFAULT_CAMERA_SELECTOR = CameraSelector.DEFAULT_BACK_CAMERA
+
+@LargeTest
+@RunWith(Parameterized::class)
+internal class ImageAnalysisTest(
+    private val implName: String,
+    private val cameraConfig: CameraXConfig
+) {
+
+    @get:Rule
+    val cameraRule = CameraUtil.grantCameraPermissionAndPreTest()
+
+    companion object {
+        private val GUARANTEED_RESOLUTION = Size(640, 480)
+
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun data() = listOf(
+            arrayOf(Camera2Config::class.simpleName, Camera2Config.defaultConfig()),
+            arrayOf(CameraPipeConfig::class.simpleName, CameraPipeConfig.defaultConfig())
+        )
+    }
+
+    private val analysisResultLock = Any()
+    private val context: Context = ApplicationProvider.getApplicationContext()
+
+    @GuardedBy("analysisResultLock")
+    private val analysisResults = mutableSetOf<ImageProperties>()
+    private val analyzer = ImageAnalysis.Analyzer { image ->
+        synchronized(analysisResultLock) {
+            analysisResults.add(ImageProperties(image))
+        }
+        analysisResultsSemaphore.release()
+        image.close()
+    }
+    private lateinit var analysisResultsSemaphore: Semaphore
+    private lateinit var handlerThread: HandlerThread
+    private lateinit var handler: Handler
+    private var camera: CameraUseCaseAdapter? = null
+
+    @Before
+    fun setUp(): Unit = runBlocking {
+        CameraX.initialize(context, cameraConfig).get(10, TimeUnit.SECONDS)
+        handlerThread = HandlerThread("AnalysisThread")
+        handlerThread.start()
+        handler = Handler(handlerThread.looper)
+        analysisResultsSemaphore = Semaphore(0)
+    }
+
+    @After
+    fun tearDown(): Unit = runBlocking {
+        camera?.let { camera ->
+            // TODO: The removeUseCases() call might be removed after clarifying the
+            //  abortCaptures() issue in b/162314023.
+            withContext(Dispatchers.Main) {
+                camera.removeUseCases(camera.useCases)
+            }
+        }
+        CameraX.shutdown().get(10, TimeUnit.SECONDS)
+        handlerThread.quitSafely()
+    }
+
+    @Test
+    fun exceedMaxImagesWithoutClosing_doNotCrash() {
+        // Arrange.
+        val queueDepth = 3
+        val semaphore = Semaphore(0)
+        val useCase = ImageAnalysis.Builder()
+            .setBackpressureStrategy(ImageAnalysis.STRATEGY_BLOCK_PRODUCER)
+            .setImageQueueDepth(queueDepth)
+            .build()
+        val imageProxyList = mutableListOf<ImageProxy>()
+        useCase.setAnalyzer(
+            CameraXExecutors.newHandlerExecutor(handler),
+            { image ->
+                imageProxyList.add(image)
+                semaphore.release()
+            }
+        )
+
+        // Act.
+        camera = CameraUtil.createCameraAndAttachUseCase(
+            context,
+            CameraSelector.DEFAULT_FRONT_CAMERA,
+            useCase
+        )
+
+        // Assert: waiting for images does not crash.
+        assertThat(semaphore.tryAcquire(queueDepth + 1, 1, TimeUnit.SECONDS)).isFalse()
+
+        // Clean it up.
+        useCase.clearAnalyzer()
+        for (image in imageProxyList) {
+            image.close()
+        }
+    }
+
+    @Ignore("TODO(b/183224022): Remove when resolution selection logic is ported to CameraPipe")
+    @Test
+    fun canSupportGuaranteedSizeFront() {
+        // CameraSelector.LENS_FACING_FRONT/LENS_FACING_BACK are defined as constant int 0 and 1.
+        // Using for-loop to check both front and back device cameras can support the guaranteed
+        // 640x480 size.
+        assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_FRONT))
+        assumeTrue(!CameraUtil.requiresCorrectedAspectRatio(CameraSelector.LENS_FACING_FRONT))
+
+        // Checks camera device sensor degrees to set correct target rotation value to make sure
+        // the exactly matching result size 640x480 can be selected if the device supports it.
+        val sensorOrientation = CameraUtil.getSensorOrientation(CameraSelector.LENS_FACING_FRONT)
+        val isRotateNeeded = sensorOrientation!! % 180 != 0
+        val useCase = ImageAnalysis.Builder()
+            .setTargetResolution(GUARANTEED_RESOLUTION)
+            .setTargetRotation(if (isRotateNeeded) Surface.ROTATION_90 else Surface.ROTATION_0)
+            .build()
+        camera = CameraUtil.createCameraAndAttachUseCase(
+            context,
+            CameraSelector.DEFAULT_FRONT_CAMERA,
+            useCase
+        )
+        useCase.setAnalyzer(CameraXExecutors.newHandlerExecutor(handler), analyzer)
+
+        assertThat(analysisResultsSemaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
+        synchronized(analysisResultLock) {
+            // Check the analyzed image exactly matches 640x480 size. This test can also check
+            // whether the guaranteed resolution 640x480 is really supported for YUV_420_888
+            // format on the devices when running the test.
+            assertThat(GUARANTEED_RESOLUTION).isEqualTo(
+                analysisResults.iterator().next().resolution
+            )
+        }
+    }
+
+    @Ignore("TODO(b/183224022): Remove when resolution selection logic is ported to CameraPipe")
+    @Test
+    fun canSupportGuaranteedSizeBack() {
+        // CameraSelector.LENS_FACING_FRONT/LENS_FACING_BACK are defined as constant int 0 and 1.
+        // Using for-loop to check both front and back device cameras can support the guaranteed
+        // 640x480 size.
+        assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK))
+        assumeTrue(!CameraUtil.requiresCorrectedAspectRatio(CameraSelector.LENS_FACING_BACK))
+
+        // Checks camera device sensor degrees to set correct target rotation value to make sure
+        // the exactly matching result size 640x480 can be selected if the device supports it.
+        val sensorOrientation = CameraUtil.getSensorOrientation(CameraSelector.LENS_FACING_BACK)
+        val isRotateNeeded = sensorOrientation!! % 180 != 0
+        val useCase = ImageAnalysis.Builder()
+            .setTargetResolution(GUARANTEED_RESOLUTION)
+            .setTargetRotation(if (isRotateNeeded) Surface.ROTATION_90 else Surface.ROTATION_0)
+            .build()
+        camera = CameraUtil.createCameraAndAttachUseCase(
+            context,
+            CameraSelector.DEFAULT_BACK_CAMERA,
+            useCase
+        )
+        useCase.setAnalyzer(CameraXExecutors.newHandlerExecutor(handler), analyzer)
+
+        assertThat(analysisResultsSemaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
+        synchronized(analysisResultLock) {
+            // Check the analyzed image exactly matches 640x480 size. This test can also check
+            // whether the guaranteed resolution 640x480 is really supported for YUV_420_888
+            // format on the devices when running the test.
+            assertThat(GUARANTEED_RESOLUTION).isEqualTo(
+                analysisResults.iterator().next().resolution
+            )
+        }
+    }
+
+    @Test
+    fun analyzesImages_withKEEP_ONLY_LATEST_whenCameraIsOpen() {
+        analyzerAnalyzesImagesWithStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
+    }
+
+    @Test
+    fun analyzesImages_withBLOCK_PRODUCER_whenCameraIsOpen() {
+        analyzerAnalyzesImagesWithStrategy(ImageAnalysis.STRATEGY_BLOCK_PRODUCER)
+    }
+
+    private fun analyzerAnalyzesImagesWithStrategy(@BackpressureStrategy strategy: Int) {
+        val useCase = ImageAnalysis.Builder()
+            .setBackpressureStrategy(strategy)
+            .build()
+        camera = CameraUtil.createCameraAndAttachUseCase(context, DEFAULT_CAMERA_SELECTOR, useCase)
+        useCase.setAnalyzer(CameraXExecutors.newHandlerExecutor(handler), analyzer)
+        analysisResultsSemaphore.tryAcquire(5, TimeUnit.SECONDS)
+        synchronized(analysisResultLock) { assertThat(analysisResults).isNotEmpty() }
+    }
+
+    @Test
+    fun analyzerDoesNotAnalyzeImages_whenCameraIsNotOpen() {
+        val useCase = ImageAnalysis.Builder().build()
+        // Bind but do not start lifecycle
+        camera = CameraUtil.createCameraAndAttachUseCase(context, DEFAULT_CAMERA_SELECTOR, useCase)
+        camera!!.detachUseCases()
+        useCase.setAnalyzer(CameraXExecutors.newHandlerExecutor(handler), analyzer)
+        // Keep the lifecycle in an inactive state.
+        // Wait a little while for frames to be analyzed.
+        analysisResultsSemaphore.tryAcquire(5, TimeUnit.SECONDS)
+
+        // No frames should have been analyzed.
+        synchronized(analysisResultLock) { assertThat(analysisResults).isEmpty() }
+    }
+
+    @Test
+    fun canObtainDefaultBackpressureStrategy() {
+        val imageAnalysis = ImageAnalysis.Builder().build()
+        assertThat(imageAnalysis.backpressureStrategy)
+            .isEqualTo(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
+    }
+
+    @Test
+    fun canObtainDefaultImageQueueDepth() {
+        val imageAnalysis = ImageAnalysis.Builder().build()
+
+        // Should not be less than 1
+        assertThat(imageAnalysis.imageQueueDepth).isAtLeast(1)
+    }
+
+    @Test
+    fun defaultAspectRatioWillBeSet_whenTargetResolutionIsNotSet() {
+        val useCase = ImageAnalysis.Builder().build()
+        camera = CameraUtil.createCameraAndAttachUseCase(context, DEFAULT_CAMERA_SELECTOR, useCase)
+        val config = useCase.currentConfig as ImageOutputConfig
+        assertThat(config.targetAspectRatio).isEqualTo(AspectRatio.RATIO_4_3)
+    }
+
+    @Test
+    fun defaultAspectRatioWontBeSet_whenTargetResolutionIsSet() {
+        assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK))
+        val useCase = ImageAnalysis.Builder()
+            .setTargetResolution(GUARANTEED_RESOLUTION)
+            .build()
+        assertThat(
+            useCase.currentConfig.containsOption(ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO)
+        ).isFalse()
+
+        camera = CameraUtil.createCameraAndAttachUseCase(
+            context,
+            CameraSelector.DEFAULT_BACK_CAMERA,
+            useCase
+        )
+        assertThat(
+            useCase.currentConfig.containsOption(ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO)
+        ).isFalse()
+    }
+
+    @Test
+    fun targetRotationCanBeUpdatedAfterUseCaseIsCreated() {
+        val imageAnalysis = ImageAnalysis.Builder().setTargetRotation(Surface.ROTATION_0).build()
+        imageAnalysis.targetRotation = Surface.ROTATION_90
+        assertThat(imageAnalysis.targetRotation).isEqualTo(Surface.ROTATION_90)
+    }
+
+    @Test
+    fun targetResolutionIsUpdatedAfterTargetRotationIsUpdated() {
+        val imageAnalysis = ImageAnalysis.Builder()
+            .setTargetResolution(GUARANTEED_RESOLUTION)
+            .setTargetRotation(Surface.ROTATION_0)
+            .build()
+        camera =
+            CameraUtil.createCameraAndAttachUseCase(context, DEFAULT_CAMERA_SELECTOR, imageAnalysis)
+
+        // Updates target rotation from ROTATION_0 to ROTATION_90.
+        imageAnalysis.targetRotation = Surface.ROTATION_90
+        val newConfig = imageAnalysis.currentConfig as ImageOutputConfig
+        val expectedTargetResolution = Size(
+            GUARANTEED_RESOLUTION.height,
+            GUARANTEED_RESOLUTION.width
+        )
+
+        // Expected targetResolution will be reversed from original target resolution.
+        assertThat(newConfig.targetResolution == expectedTargetResolution).isTrue()
+    }
+
+    // TODO(b/162298517): change the test to be deterministic instead of depend upon timing.
+    @Test
+    fun analyzerSetMultipleTimesInKeepOnlyLatestMode() {
+        val useCase = ImageAnalysis.Builder()
+            .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
+            .build()
+        camera = CameraUtil.createCameraAndAttachUseCase(context, DEFAULT_CAMERA_SELECTOR, useCase)
+        useCase.setAnalyzer(CameraXExecutors.newHandlerExecutor(handler), analyzer)
+        analysisResultsSemaphore.tryAcquire(5, TimeUnit.SECONDS)
+
+        val slowAnalyzer = ImageAnalysis.Analyzer { image ->
+            try {
+                Thread.sleep(200)
+                image.close()
+            } catch (e: Exception) {
+            }
+        }
+        useCase.setAnalyzer(CameraXExecutors.newHandlerExecutor(handler), slowAnalyzer)
+        Thread.sleep(100)
+
+        useCase.setAnalyzer(CameraXExecutors.newHandlerExecutor(handler), slowAnalyzer)
+        Thread.sleep(100)
+
+        useCase.setAnalyzer(CameraXExecutors.newHandlerExecutor(handler), slowAnalyzer)
+        Thread.sleep(100)
+
+        useCase.setAnalyzer(CameraXExecutors.newHandlerExecutor(handler), slowAnalyzer)
+        Thread.sleep(100)
+
+        useCase.setAnalyzer(CameraXExecutors.newHandlerExecutor(handler), slowAnalyzer)
+        Thread.sleep(100)
+    }
+
+    @Test
+    fun useCaseConfigCanBeReset_afterUnbind() = runBlocking {
+        val useCase = ImageAnalysis.Builder().build()
+        val initialConfig = useCase.currentConfig
+        camera = CameraUtil.createCameraAndAttachUseCase(context, DEFAULT_CAMERA_SELECTOR, useCase)
+
+        withContext(Dispatchers.Main) {
+            camera!!.removeUseCases(setOf(useCase))
+        }
+        val configAfterUnbinding = useCase.currentConfig
+        assertThat(initialConfig == configAfterUnbinding).isTrue()
+    }
+
+    @Test
+    fun targetRotationIsRetained_whenUseCaseIsReused() = runBlocking {
+        val useCase = ImageAnalysis.Builder().build()
+        camera = CameraUtil.createCameraAndAttachUseCase(context, DEFAULT_CAMERA_SELECTOR, useCase)
+
+        // Generally, the device can't be rotated to Surface.ROTATION_180. Therefore,
+        // use it to do the test.
+        useCase.targetRotation = Surface.ROTATION_180
+        withContext(Dispatchers.Main) {
+
+            // Check the target rotation is kept when the use case is unbound.
+            camera!!.removeUseCases(setOf(useCase))
+            assertThat(useCase.targetRotation).isEqualTo(Surface.ROTATION_180)
+        }
+
+        // Check the target rotation is kept when the use case is rebound to the
+        // lifecycle.
+        camera = CameraUtil.createCameraAndAttachUseCase(context, DEFAULT_CAMERA_SELECTOR, useCase)
+        assertThat(useCase.targetRotation).isEqualTo(Surface.ROTATION_180)
+    }
+
+    @Test
+    @Throws(InterruptedException::class)
+    fun useCaseCanBeReusedInSameCamera() = runBlocking {
+        val useCase = ImageAnalysis.Builder().build()
+        camera = CameraUtil.createCameraAndAttachUseCase(context, DEFAULT_CAMERA_SELECTOR, useCase)
+        useCase.setAnalyzer(CameraXExecutors.newHandlerExecutor(handler), analyzer)
+        assertThat(analysisResultsSemaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
+        withContext(Dispatchers.Main) { camera!!.removeUseCases(setOf(useCase)) }
+        analysisResultsSemaphore = Semaphore( /*permits=*/0)
+        // Rebind the use case to the same camera.
+        camera = CameraUtil.createCameraAndAttachUseCase(context, DEFAULT_CAMERA_SELECTOR, useCase)
+        assertThat(analysisResultsSemaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
+    }
+
+    @Test
+    @Throws(InterruptedException::class)
+    fun useCaseCanBeReusedInDifferentCamera() = runBlocking {
+        val useCase = ImageAnalysis.Builder().build()
+        camera = CameraUtil.createCameraAndAttachUseCase(
+            context,
+            CameraSelector.DEFAULT_BACK_CAMERA, useCase
+        )
+        useCase.setAnalyzer(CameraXExecutors.newHandlerExecutor(handler), analyzer)
+        assertThat(analysisResultsSemaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
+        withContext(Dispatchers.Main) { camera!!.removeUseCases(setOf(useCase)) }
+        analysisResultsSemaphore = Semaphore( /*permits=*/0)
+        // Rebind the use case to different camera.
+        camera = CameraUtil.createCameraAndAttachUseCase(
+            context,
+            CameraSelector.DEFAULT_FRONT_CAMERA, useCase
+        )
+        assertThat(analysisResultsSemaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
+    }
+
+    @Test
+    fun returnValidTargetRotation_afterUseCaseIsCreated() {
+        val imageCapture = ImageCapture.Builder().build()
+        assertThat(imageCapture.targetRotation).isNotEqualTo(ImageOutputConfig.INVALID_ROTATION)
+    }
+
+    @Test
+    fun returnCorrectTargetRotation_afterUseCaseIsAttached() {
+        val imageAnalysis = ImageAnalysis.Builder()
+            .setTargetRotation(Surface.ROTATION_180)
+            .build()
+        camera =
+            CameraUtil.createCameraAndAttachUseCase(context, DEFAULT_CAMERA_SELECTOR, imageAnalysis)
+        assertThat(imageAnalysis.targetRotation).isEqualTo(Surface.ROTATION_180)
+    }
+
+    private data class ImageProperties(
+        val resolution: Size,
+        val format: Int,
+        val timestamp: Long,
+        val rotationDegrees: Int
+    ) {
+
+        constructor(image: ImageProxy) : this(
+            Size(image.width, image.height),
+            image.format,
+            image.imageInfo.timestamp,
+            image.imageInfo.rotationDegrees
+        )
+    }
+}
\ No newline at end of file