Camera2: Add reprocess timestamps test

Verify that reprocess request's shutter timestamp, result's
sensor timestamp, and output image's timestamp all match
the reprocess input's timestamp.

Bug: 21112186
Change-Id: I81fa5ecda53b5059ff4ed92cb0b325dceaba52d2
diff --git a/tests/tests/hardware/src/android/hardware/camera2/cts/CameraTestUtils.java b/tests/tests/hardware/src/android/hardware/camera2/cts/CameraTestUtils.java
index f062545..423577e 100644
--- a/tests/tests/hardware/src/android/hardware/camera2/cts/CameraTestUtils.java
+++ b/tests/tests/hardware/src/android/hardware/camera2/cts/CameraTestUtils.java
@@ -32,7 +32,6 @@
 import android.hardware.camera2.params.InputConfiguration;
 import android.hardware.camera2.TotalCaptureResult;
 import android.hardware.cts.helpers.CameraUtils;
-import android.util.Size;
 import android.hardware.camera2.params.MeteringRectangle;
 import android.hardware.camera2.params.StreamConfigurationMap;
 import android.media.Image;
@@ -41,6 +40,8 @@
 import android.media.Image.Plane;
 import android.os.Handler;
 import android.util.Log;
+import android.util.Pair;
+import android.util.Size;
 import android.view.Surface;
 
 import com.android.ex.camera2.blocking.BlockingCameraManager;
@@ -315,13 +316,21 @@
                 new LinkedBlockingQueue<TotalCaptureResult>();
         private final LinkedBlockingQueue<CaptureFailure> mFailureQueue =
                 new LinkedBlockingQueue<>();
+        // Pair<CaptureRequest, Long> is a pair of capture request and timestamp.
+        private final LinkedBlockingQueue<Pair<CaptureRequest, Long>> mCaptureStartQueue =
+                new LinkedBlockingQueue<>();
 
         private AtomicLong mNumFramesArrived = new AtomicLong(0);
 
         @Override
         public void onCaptureStarted(CameraCaptureSession session, CaptureRequest request,
-                long timestamp, long frameNumber)
-        {
+                long timestamp, long frameNumber) {
+            try {
+                mCaptureStartQueue.put(new Pair(request, timestamp));
+            } catch (InterruptedException e) {
+                throw new UnsupportedOperationException(
+                        "Can't handle InterruptedException in onCaptureStarted");
+            }
         }
 
         @Override
@@ -519,6 +528,37 @@
             return failures;
         }
 
+        /**
+         * Wait until the capture start of a request and expected timestamp arrives or it times
+         * out after a number of capture starts.
+         *
+         * @param request The request for the capture start to wait for.
+         * @param timestamp The timestamp for the capture start to wait for.
+         * @param numCaptureStartsWait The number of capture start events to wait for before timing
+         *                             out.
+         */
+        public void waitForCaptureStart(CaptureRequest request, Long timestamp,
+                int numCaptureStartsWait) throws Exception {
+            Pair<CaptureRequest, Long> expectedShutter = new Pair<>(request, timestamp);
+
+            int i = 0;
+            do {
+                Pair<CaptureRequest, Long> shutter = mCaptureStartQueue.poll(
+                        CAPTURE_RESULT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+
+                if (shutter == null) {
+                    throw new TimeoutRuntimeException("Unable to get any more capture start " +
+                            "event after waiting for " + CAPTURE_RESULT_TIMEOUT_MS + " ms.");
+                } else if (expectedShutter.equals(shutter)) {
+                    return;
+                }
+
+            } while (i++ < numCaptureStartsWait);
+
+            throw new TimeoutRuntimeException("Unable to get the expected capture start " +
+                    "event after waiting for " + numCaptureStartsWait + " capture starts");
+        }
+
         public boolean hasMoreResults()
         {
             return mQueue.isEmpty();
@@ -528,6 +568,7 @@
             mQueue.clear();
             mNumFramesArrived.getAndSet(0);
             mFailureQueue.clear();
+            mCaptureStartQueue.clear();
         }
     }
 
diff --git a/tests/tests/hardware/src/android/hardware/camera2/cts/ReprocessCaptureTest.java b/tests/tests/hardware/src/android/hardware/camera2/cts/ReprocessCaptureTest.java
index 4061412..a115fbe 100644
--- a/tests/tests/hardware/src/android/hardware/camera2/cts/ReprocessCaptureTest.java
+++ b/tests/tests/hardware/src/android/hardware/camera2/cts/ReprocessCaptureTest.java
@@ -77,7 +77,8 @@
         SINGLE_SHOT,
         BURST,
         MIXED_BURST,
-        ABORT_CAPTURE
+        ABORT_CAPTURE,
+        TIMESTAMPS
     }
 
     /**
@@ -366,6 +367,35 @@
     }
 
     /**
+     * Test reprocess timestamps for the largest input and output sizes for each supported format.
+     */
+    public void testReprocessTimestamps() throws Exception {
+        for (String id : mCameraIds) {
+            if (!isYuvReprocessSupported(id) && !isOpaqueReprocessSupported(id)) {
+                continue;
+            }
+
+            try {
+                // open Camera device
+                openDevice(id);
+
+                int[] supportedInputFormats =
+                    mStaticInfo.getAvailableFormats(StaticMetadata.StreamDirection.Input);
+                for (int inputFormat : supportedInputFormats) {
+                    int[] supportedReprocessOutputFormats =
+                            mStaticInfo.getValidOutputFormatsForInput(inputFormat);
+                    for (int reprocessOutputFormat : supportedReprocessOutputFormats) {
+                        testReprocessingMaxSizes(id, inputFormat, reprocessOutputFormat,
+                                /*previewSize*/null, CaptureTestCase.TIMESTAMPS);
+                    }
+                }
+            } finally {
+                closeDevice();
+            }
+        }
+    }
+
+    /**
      * Test the input format and output format with the largest input and output sizes.
      */
     private void testBasicReprocessing(String cameraId, int inputFormat,
@@ -400,6 +430,10 @@
                 testReprocessAbort(cameraId, maxInputSize, inputFormat, maxReprocessOutputSize,
                         reprocessOutputFormat);
                 break;
+            case TIMESTAMPS:
+                testReprocessTimestamps(cameraId, maxInputSize, inputFormat, maxReprocessOutputSize,
+                        reprocessOutputFormat);
+                break;
             default:
                 throw new IllegalArgumentException("Invalid test case");
         }
@@ -742,6 +776,89 @@
     }
 
     /**
+     * Test timestamps for reprocess requests. Reprocess request's shutter timestamp, result's
+     * sensor timestamp, and output image's timestamp should match the reprocess input's timestamp.
+     */
+    private void testReprocessTimestamps(String cameraId, Size inputSize, int inputFormat,
+            Size reprocessOutputSize, int reprocessOutputFormat) throws Exception {
+        if (VERBOSE) {
+            Log.v(TAG, "testReprocessTimestamps: cameraId: " + cameraId + " inputSize: " +
+                    inputSize + " inputFormat: " + inputFormat + " reprocessOutputSize: " +
+                    reprocessOutputSize + " reprocessOutputFormat: " + reprocessOutputFormat);
+        }
+
+        try {
+            setupImageReaders(inputSize, inputFormat, reprocessOutputSize, reprocessOutputFormat,
+                    NUM_REPROCESS_CAPTURES);
+            setupReprocessableSession(/*previewSurface*/null, NUM_REPROCESS_CAPTURES);
+
+            // Prepare reprocess capture requests.
+            ArrayList<CaptureRequest> reprocessRequests = new ArrayList<>(NUM_REPROCESS_CAPTURES);
+            ArrayList<Long> expectedTimestamps = new ArrayList<>(NUM_REPROCESS_CAPTURES);
+
+            for (int i = 0; i < NUM_REPROCESS_CAPTURES; i++) {
+                TotalCaptureResult result = submitCaptureRequest(mFirstImageReader.getSurface(),
+                        /*inputResult*/null);
+
+                mImageWriter.queueInputImage(
+                        mFirstImageReaderListener.getImage(CAPTURE_TIMEOUT_MS));
+                CaptureRequest.Builder builder = mCamera.createReprocessCaptureRequest(result);
+                if (mShareOneImageReader) {
+                    builder.addTarget(mFirstImageReader.getSurface());
+                } else {
+                    builder.addTarget(mSecondImageReader.getSurface());
+                }
+                reprocessRequests.add(builder.build());
+                // Reprocess result's timestamp should match input image's timestamp.
+                expectedTimestamps.add(result.get(CaptureResult.SENSOR_TIMESTAMP));
+            }
+
+            // Submit reprocess requests.
+            SimpleCaptureCallback captureCallback = new SimpleCaptureCallback();
+            mSession.captureBurst(reprocessRequests, captureCallback, mHandler);
+
+            // Verify we get the expected timestamps.
+            for (int i = 0; i < reprocessRequests.size(); i++) {
+                captureCallback.waitForCaptureStart(reprocessRequests.get(i),
+                        expectedTimestamps.get(i), CAPTURE_TIMEOUT_FRAMES);
+            }
+
+            TotalCaptureResult[] reprocessResults =
+                    captureCallback.getTotalCaptureResultsForRequests(reprocessRequests,
+                    CAPTURE_TIMEOUT_FRAMES);
+
+            for (int i = 0; i < expectedTimestamps.size(); i++) {
+                // Verify the result timestamps match the input image's timestamps.
+                long expected = expectedTimestamps.get(i);
+                long timestamp = reprocessResults[i].get(CaptureResult.SENSOR_TIMESTAMP);
+                assertEquals("Reprocess result timestamp (" + timestamp + ") doesn't match input " +
+                        "image's timestamp (" + expected + ")", expected, timestamp);
+
+                // Verify the reprocess output image timestamps match the input image's timestamps.
+                Image image;
+                if (mShareOneImageReader) {
+                    image = mFirstImageReaderListener.getImage(CAPTURE_TIMEOUT_MS);
+                } else {
+                    image = mSecondImageReaderListener.getImage(CAPTURE_TIMEOUT_MS);
+                }
+                timestamp = image.getTimestamp();
+                image.close();
+
+                assertEquals("Reprocess output timestamp (" + timestamp + ") doesn't match input " +
+                        "image's timestamp (" + expected + ")", expected, timestamp);
+            }
+
+            // Make sure all input surfaces are released.
+            for (int i = 0; i < NUM_REPROCESS_CAPTURES; i++) {
+                mImageWriterListener.waitForImageReleased(CAPTURE_TIMEOUT_MS);
+            }
+        } finally {
+            closeReprossibleSession();
+            closeImageReaders();
+        }
+    }
+
+    /**
      * Set up two image readers: one for regular capture (used for reprocess input) and one for
      * reprocess capture.
      */