Fix CaptureFailedRetryQuirk failing for previous request info

Relnote: "Fixed retry not triggering properly in case of capture failure in problematic devices mentioned in CaptureFailedRetryQuirk."

Bug: 288209032
Test: TakePictureManagerTest, ImageCaptureTest and other related tests
Change-Id: I7b589a19bf5ee8f316c009e6c69dd21b7cf6a0c3
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
index dba781b..6c21ada 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
@@ -49,6 +49,7 @@
 import androidx.camera.core.impl.UseCaseConfigFactory;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.camera.core.internal.CameraUseCaseAdapter;
+import androidx.camera.core.internal.compat.workaround.CaptureFailedRetryEnabler;
 import androidx.camera.testing.CoreAppTestUtil;
 import androidx.camera.testing.fakes.FakeCamera;
 import androidx.camera.testing.fakes.FakeCameraCaptureResult;
@@ -536,6 +537,9 @@
 
         // Act.
         // Complete the picture taken, then new flash mode should be applied.
+        CaptureFailedRetryEnabler retryEnabler = new CaptureFailedRetryEnabler();
+        // Because of retry in some devices, we may need to notify capture failures multiple times.
+        addExtraFailureNotificationsForRetry(fakeCameraControl, retryEnabler.getRetryCount());
         fakeCameraControl.notifyAllRequestsOnCaptureFailed();
 
         // Assert.
@@ -543,6 +547,16 @@
         assertThat(fakeCameraControl.getFlashMode()).isEqualTo(ImageCapture.FLASH_MODE_ON);
     }
 
+    private void addExtraFailureNotificationsForRetry(FakeCameraControl cameraControl,
+            int retryCount) {
+        if (retryCount > 0) {
+            cameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+                addExtraFailureNotificationsForRetry(cameraControl, retryCount - 1);
+                cameraControl.notifyAllRequestsOnCaptureFailed();
+            });
+        }
+    }
+
     @Test
     public void correctViewPortRectInResolutionInfo_withCropAspectRatioSetting() {
         ImageCapture imageCapture = new ImageCapture.Builder()
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 58ff19d..4903752 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
@@ -149,13 +149,19 @@
             // Fail silently if the request has been aborted.
             return;
         }
-        if (mTakePictureRequest.decrementRetryCounter()) {
-            mRetryControl.retryRequest(mTakePictureRequest);
-        } else {
+
+        boolean isRetryAllowed = mTakePictureRequest.decrementRetryCounter();
+        if (!isRetryAllowed) {
             onFailure(imageCaptureException);
         }
         markComplete();
         mCaptureCompleter.setException(imageCaptureException);
+
+        if (isRetryAllowed) {
+            // retry after all the cleaning up works are done via mCaptureCompleter.setException,
+            // e.g. removing previous request from CaptureNode, SingleBundlingNode etc.
+            mRetryControl.retryRequest(mTakePictureRequest);
+        }
     }
 
     @MainThread
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 3ba10d1..e2c6ae5 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
@@ -37,6 +37,7 @@
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.ImageCaptureException;
 import androidx.camera.core.ImageProxy;
+import androidx.camera.core.Logger;
 import androidx.camera.core.impl.utils.futures.FutureCallback;
 import androidx.camera.core.impl.utils.futures.Futures;
 import androidx.core.util.Pair;
@@ -120,8 +121,11 @@
     @Override
     public void retryRequest(@NonNull TakePictureRequest request) {
         checkMainThread();
+        Logger.d(TAG, "Add a new request for retrying.");
         // Insert the request to the front of the queue.
         mNewRequests.addFirst(request);
+        // Try to issue the newly added request in case condition allows.
+        issueNextRequest();
     }
 
     /**
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 d0b5089..85043d9 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
@@ -39,11 +39,17 @@
     // Flip this flag to return a custom result using pendingResultCompleter.
     var shouldUsePendingResult = false
     lateinit var pendingResultCompleter: CallbackToFutureAdapter.Completer<Void>
-    var pendingResult = CallbackToFutureAdapter.getFuture { completer ->
+    var pendingResult = createPendingResult()
+
+    private fun createPendingResult() = CallbackToFutureAdapter.getFuture { completer ->
         pendingResultCompleter = completer
         "FakeImageCaptureControl's pendingResult"
     }
 
+    fun resetPendingResult() {
+        pendingResult = createPendingResult()
+    }
+
     override fun lockFlashMode() {
         actions.add(Action.LOCK_FLASH)
     }
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 2a6cf328..2f8d74f 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
@@ -24,10 +24,14 @@
 import androidx.camera.core.ImageCapture.ERROR_CAPTURE_FAILED
 import androidx.camera.core.ImageCapture.OutputFileResults
 import androidx.camera.core.ImageCaptureException
+import androidx.camera.core.imagecapture.FakeImageCaptureControl.Action.SUBMIT_REQUESTS
 import androidx.camera.core.impl.CaptureConfig
+import androidx.camera.core.internal.compat.quirk.CaptureFailedRetryQuirk
+import androidx.camera.core.internal.compat.quirk.DeviceQuirks
 import androidx.camera.testing.fakes.FakeImageInfo
 import androidx.camera.testing.fakes.FakeImageProxy
 import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
 import org.junit.After
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -35,6 +39,7 @@
 import org.robolectric.Shadows.shadowOf
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadows.ShadowBuild
 
 /**
  * Unit tests for [TakePictureManager].
@@ -133,7 +138,7 @@
         // Assert: one request is sent.
         assertThat(imageCaptureControl.actions).containsExactly(
             FakeImageCaptureControl.Action.LOCK_FLASH,
-            FakeImageCaptureControl.Action.SUBMIT_REQUESTS,
+            SUBMIT_REQUESTS,
             FakeImageCaptureControl.Action.UNLOCK_FLASH,
         ).inOrder()
         // Both request are aborted.
@@ -184,7 +189,7 @@
         // Assert: only one request is sent.
         assertThat(imageCaptureControl.actions).containsExactly(
             FakeImageCaptureControl.Action.LOCK_FLASH,
-            FakeImageCaptureControl.Action.SUBMIT_REQUESTS,
+            SUBMIT_REQUESTS,
             FakeImageCaptureControl.Action.UNLOCK_FLASH,
         ).inOrder()
 
@@ -195,10 +200,10 @@
         // Assert: 2nd request is sent too.
         assertThat(imageCaptureControl.actions).containsExactly(
             FakeImageCaptureControl.Action.LOCK_FLASH,
-            FakeImageCaptureControl.Action.SUBMIT_REQUESTS,
+            SUBMIT_REQUESTS,
             FakeImageCaptureControl.Action.UNLOCK_FLASH,
             FakeImageCaptureControl.Action.LOCK_FLASH,
-            FakeImageCaptureControl.Action.SUBMIT_REQUESTS,
+            SUBMIT_REQUESTS,
             FakeImageCaptureControl.Action.UNLOCK_FLASH,
         ).inOrder()
     }
@@ -275,7 +280,7 @@
         // Assert:
         assertThat(imageCaptureControl.actions).containsExactly(
             FakeImageCaptureControl.Action.LOCK_FLASH,
-            FakeImageCaptureControl.Action.SUBMIT_REQUESTS,
+            SUBMIT_REQUESTS,
             FakeImageCaptureControl.Action.UNLOCK_FLASH,
         ).inOrder()
         assertThat(imageCaptureControl.latestCaptureConfigs).isEqualTo(response1)
@@ -288,10 +293,10 @@
         // Assert: imageCaptureControl was invoked in the exact given order.
         assertThat(imageCaptureControl.actions).containsExactly(
             FakeImageCaptureControl.Action.LOCK_FLASH,
-            FakeImageCaptureControl.Action.SUBMIT_REQUESTS,
+            SUBMIT_REQUESTS,
             FakeImageCaptureControl.Action.UNLOCK_FLASH,
             FakeImageCaptureControl.Action.LOCK_FLASH,
-            FakeImageCaptureControl.Action.SUBMIT_REQUESTS,
+            SUBMIT_REQUESTS,
             FakeImageCaptureControl.Action.UNLOCK_FLASH,
         ).inOrder()
         assertThat(imageCaptureControl.latestCaptureConfigs).isEqualTo(response2)
@@ -399,7 +404,7 @@
         assertThat(takePictureManager.mNewRequests.size).isEqualTo(0)
         assertThat(imageCaptureControl.actions).containsExactly(
             FakeImageCaptureControl.Action.LOCK_FLASH,
-            FakeImageCaptureControl.Action.SUBMIT_REQUESTS,
+            SUBMIT_REQUESTS,
             FakeImageCaptureControl.Action.UNLOCK_FLASH,
         ).inOrder()
     }
@@ -443,4 +448,74 @@
         // Assert. new request can be issued after the capture failure of the first request
         takePictureManager.offerRequest(request2)
     }
-}
\ No newline at end of file
+
+    @Test
+    fun requestFailure_failureReportedIfQuirkDisabled() {
+        // Arrange: use the real ImagePipeline implementation to do the test
+        takePictureManager.mImagePipeline =
+            ImagePipeline(Utils.createEmptyImageCaptureConfig(), Size(640, 480))
+
+        // Create a request and offer it to the manager.
+        imageCaptureControl.shouldUsePendingResult = true
+        val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
+        takePictureManager.offerRequest(request)
+
+        // Act: make the request fail once.
+        imageCaptureControl.pendingResultCompleter.setException(
+            ImageCaptureException(
+                ERROR_CAPTURE_FAILED, "", null
+            )
+        )
+        shadowOf(getMainLooper()).idle()
+
+        // Assert: failure exception received and no more retry possible.
+        assertThat((request.exceptionReceived as ImageCaptureException).imageCaptureError)
+            .isEqualTo(ERROR_CAPTURE_FAILED)
+        assertThat(request.remainingRetries).isEqualTo(0)
+        // Only 1 request submitted to camera
+        assertThat(imageCaptureControl.actions.count { it == SUBMIT_REQUESTS }).isEqualTo(1)
+    }
+
+    @Test
+    fun requestFailure_retriedIfQuirkEnabled() {
+        // Arrange: enable retry related quirk.
+        ShadowBuild.setBrand("SAMSUNG")
+        ShadowBuild.setModel("SM-G981U1")
+
+        val captureFailedRetryQuirk = DeviceQuirks.get(CaptureFailedRetryQuirk::class.java)
+
+        assertWithMessage("CaptureFailedRetryQuirk not enabled!")
+            .that(captureFailedRetryQuirk).isNotNull()
+
+        // Use the real ImagePipeline implementation to do the test
+        takePictureManager.mImagePipeline =
+            ImagePipeline(Utils.createEmptyImageCaptureConfig(), Size(640, 480))
+
+        // Create a request and offer it to the manager.
+        imageCaptureControl.shouldUsePendingResult = true
+        val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
+        takePictureManager.offerRequest(request)
+
+        // Act: make the request fail once and then successful if retried later.
+        imageCaptureControl.pendingResultCompleter.setException(
+            ImageCaptureException(
+                ERROR_CAPTURE_FAILED, "", null
+            )
+        )
+        imageCaptureControl.resetPendingResult()
+
+        // complete the new capture successfully
+        imageCaptureControl.pendingResultCompleter.set(null)
+        shadowOf(getMainLooper()).idle()
+
+        // Assert: retry count decremented without any failure.
+        assertThat(request.remainingRetries).isEqualTo(captureFailedRetryQuirk!!.retryCount - 1)
+        assertThat(request.exceptionReceived).isNull()
+        // 2 requests submitted to camera
+        assertThat(imageCaptureControl.actions.count { it == SUBMIT_REQUESTS }).isEqualTo(2)
+
+        // Clean-up brands and models set for enabling quirk.
+        ShadowBuild.setBrand("")
+        ShadowBuild.setModel("")
+    }
+}