Merge "Cancel capture request when aborted." into androidx-main
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/RequestWithCallback.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/RequestWithCallback.java
index dd26326..00c3ad3 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/RequestWithCallback.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/RequestWithCallback.java
@@ -19,10 +19,13 @@
 import static androidx.camera.core.impl.utils.Threads.checkMainThread;
 import static androidx.core.util.Preconditions.checkState;
 
+import static java.util.Objects.requireNonNull;
+
 import android.os.Build;
 
 import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.ImageCaptureException;
@@ -50,6 +53,8 @@
     // Flag tracks if the request has been aborted by the UseCase. Once aborted, this class stops
     // propagating callbacks to the app.
     private boolean mIsAborted = false;
+    @Nullable
+    private ListenableFuture<Void> mCaptureRequestFuture;
 
     RequestWithCallback(@NonNull TakePictureRequest takePictureRequest,
             @NonNull TakePictureRequest.RetryControl retryControl) {
@@ -67,6 +72,18 @@
                 });
     }
 
+    /**
+     * Sets the {@link ListenableFuture} associated with camera2 capture request.
+     *
+     * <p>Canceling this future should cancel the request sent to camera2.
+     */
+    @MainThread
+    public void setCaptureRequestFuture(@NonNull ListenableFuture<Void> captureRequestFuture) {
+        checkMainThread();
+        checkState(mCaptureRequestFuture == null, "CaptureRequestFuture can only be set once.");
+        mCaptureRequestFuture = captureRequestFuture;
+    }
+
     @MainThread
     @Override
     public void onImageCaptured() {
@@ -167,6 +184,8 @@
     private void abort() {
         checkMainThread();
         mIsAborted = true;
+        // Cancel the capture request sent to camera2.
+        requireNonNull(mCaptureRequestFuture).cancel(true);
         mCaptureCompleter.set(null);
         mCompleteCompleter.set(null);
     }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
index 49f346b..184c961 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
@@ -205,7 +205,9 @@
                 mImagePipeline.createRequests(request, requestWithCallback);
         CameraRequest cameraRequest = requireNonNull(requests.first);
         ProcessingRequest processingRequest = requireNonNull(requests.second);
-        submitCameraRequest(cameraRequest, () -> mImagePipeline.postProcess(processingRequest));
+        ListenableFuture<Void> captureRequestFuture = submitCameraRequest(cameraRequest,
+                () -> mImagePipeline.postProcess(processingRequest));
+        requestWithCallback.setCaptureRequestFuture(captureRequestFuture);
     }
 
     /**
@@ -234,14 +236,14 @@
      * <p>Flash is locked/unlocked during the flight of a {@link CameraRequest}.
      */
     @MainThread
-    private void submitCameraRequest(
+    private ListenableFuture<Void> submitCameraRequest(
             @NonNull CameraRequest cameraRequest,
             @NonNull Runnable successRunnable) {
         checkMainThread();
         mImageCaptureControl.lockFlashMode();
-        ListenableFuture<Void> submitRequestFuture =
+        ListenableFuture<Void> captureRequestFuture =
                 mImageCaptureControl.submitStillCaptureRequests(cameraRequest.getCaptureConfigs());
-        Futures.addCallback(submitRequestFuture, new FutureCallback<Void>() {
+        Futures.addCallback(captureRequestFuture, new FutureCallback<Void>() {
             @Override
             public void onSuccess(@Nullable Void result) {
                 successRunnable.run();
@@ -261,6 +263,7 @@
                 mImageCaptureControl.unlockFlashMode();
             }
         }, mainThreadExecutor());
+        return captureRequestFuture;
     }
 
     @VisibleForTesting
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeImageCaptureControl.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeImageCaptureControl.kt
index 26bba9a..ee31c80 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeImageCaptureControl.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeImageCaptureControl.kt
@@ -61,6 +61,11 @@
         return IMMEDIATE_RESULT
     }
 
+    fun clear() {
+        // Cancel pending futures.
+        pendingResult.cancel(true)
+    }
+
     enum class Action {
         LOCK_FLASH,
         UNLOCK_FLASH,
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/RequestWithCallbackTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/RequestWithCallbackTest.kt
index acdc78e..425cdef 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/RequestWithCallbackTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/RequestWithCallbackTest.kt
@@ -25,7 +25,10 @@
 import androidx.camera.core.ImageProxy
 import androidx.camera.testing.fakes.FakeImageInfo
 import androidx.camera.testing.fakes.FakeImageProxy
+import androidx.concurrent.futures.CallbackToFutureAdapter
 import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.ListenableFuture
+import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -47,6 +50,7 @@
     private lateinit var imageResult: ImageProxy
     private lateinit var fileResult: ImageCapture.OutputFileResults
     private lateinit var retryControl: FakeRetryControl
+    private lateinit var captureRequestFuture: ListenableFuture<Void>
 
     @Before
     fun setUp() {
@@ -55,6 +59,12 @@
         imageResult = FakeImageProxy(FakeImageInfo())
         fileResult = ImageCapture.OutputFileResults(null)
         retryControl = FakeRetryControl()
+        captureRequestFuture = CallbackToFutureAdapter.getFuture { "captureRequestFuture" }
+    }
+
+    @After
+    fun tearDown() {
+        captureRequestFuture.cancel(true)
     }
 
     @Test
@@ -126,6 +136,7 @@
         // Arrange.
         val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
         val callback = RequestWithCallback(request, retryControl)
+        callback.setCaptureRequestFuture(captureRequestFuture)
         // Act.
         callback.abortAndSendErrorToApp(abortError)
         callback.onCaptureFailure(otherError)
@@ -133,6 +144,7 @@
         shadowOf(getMainLooper()).idle()
         // Assert.
         assertThat(request.exceptionReceived).isEqualTo(abortError)
+        assertThat(captureRequestFuture.isCancelled).isTrue()
     }
 
     @Test
@@ -153,12 +165,14 @@
         // Arrange.
         val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
         val callback = RequestWithCallback(request, retryControl)
+        callback.setCaptureRequestFuture(captureRequestFuture)
         // Act.
         callback.abortAndSendErrorToApp(abortError)
         callback.onFinalResult(imageResult)
         shadowOf(getMainLooper()).idle()
         // Assert.
         assertThat(request.imageReceived).isNull()
+        assertThat(captureRequestFuture.isCancelled).isTrue()
     }
 
     @Test
@@ -179,11 +193,13 @@
         // Arrange.
         val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.ON_DISK)
         val callback = RequestWithCallback(request, retryControl)
+        callback.setCaptureRequestFuture(captureRequestFuture)
         // Act.
         callback.abortAndSendErrorToApp(abortError)
         callback.onFinalResult(imageResult)
         shadowOf(getMainLooper()).idle()
         // Assert.
         assertThat(request.imageReceived).isNull()
+        assertThat(captureRequestFuture.isCancelled).isTrue()
     }
 }
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
index 5ed2782..c49618db 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
@@ -46,12 +46,13 @@
     private val imagePipeline = FakeImagePipeline()
     private val imageCaptureControl = FakeImageCaptureControl()
     private val takePictureManager =
-        TakePictureManager(imageCaptureControl).also { it.setImagePipeline(imagePipeline) }
+        TakePictureManager(imageCaptureControl).also { it.imagePipeline = imagePipeline }
     private val exception = ImageCaptureException(ImageCapture.ERROR_UNKNOWN, "", null)
 
     @After
     fun tearDown() {
         imagePipeline.close()
+        imageCaptureControl.clear()
     }
 
     @Test
@@ -74,6 +75,23 @@
     }
 
     @Test
+    fun abort_captureRequestFutureIsCanceled() {
+        // Arrange: configure ImageCaptureControl to not return immediately.
+        val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
+        imageCaptureControl.shouldUsePendingResult = true
+
+        // Act: offer request then abort.
+        takePictureManager.offerRequest(request)
+        takePictureManager.abortRequests()
+        shadowOf(getMainLooper()).idle()
+
+        // Assert: that the app receives exception and the capture future is canceled.
+        assertThat((request.exceptionReceived as ImageCaptureException).imageCaptureError)
+            .isEqualTo(ERROR_CAMERA_CLOSED)
+        assertThat(imageCaptureControl.pendingResult.isCancelled).isTrue()
+    }
+
+    @Test
     fun abortPostProcessingRequests_receiveErrorCallback() {
         // Arrange: setup a request that is captured but not processed.
         val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)