Cancel capture request when aborted.
When requests are aborted, we should fail-fast the capture request sent for camera2. This matches the behavior of the old pipeline.
Bug: 225204995
Test: manual test and ./gradlew bOS
Change-Id: Id3c1d218a4548245c92a094d68b883dc7d21e1d4
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)