1/*
2 * Copyright 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.mediaframeworktest.stress;
18
19import com.android.ex.camera2.blocking.BlockingSessionCallback;
20import com.android.mediaframeworktest.Camera2SurfaceViewTestCase;
21import com.android.mediaframeworktest.helpers.CameraTestUtils;
22import com.android.mediaframeworktest.helpers.StaticMetadata;
23
24import android.graphics.ImageFormat;
25import android.hardware.camera2.CameraDevice;
26import android.hardware.camera2.CaptureFailure;
27import android.hardware.camera2.CaptureRequest;
28import android.hardware.camera2.CaptureResult;
29import android.hardware.camera2.TotalCaptureResult;
30import android.hardware.camera2.params.InputConfiguration;
31import android.media.Image;
32import android.media.ImageReader;
33import android.media.ImageWriter;
34import android.util.Log;
35import android.util.Size;
36import android.view.Surface;
37
38import java.util.ArrayList;
39import java.util.Arrays;
40import java.util.List;
41
42import static com.android.mediaframeworktest.helpers.CameraTestUtils.EXIF_TEST_DATA;
43import static com.android.mediaframeworktest.helpers.CameraTestUtils.SESSION_CLOSE_TIMEOUT_MS;
44import static com.android.mediaframeworktest.helpers.CameraTestUtils.SimpleCaptureCallback;
45import static com.android.mediaframeworktest.helpers.CameraTestUtils.SimpleImageReaderListener;
46import static com.android.mediaframeworktest.helpers.CameraTestUtils.SimpleImageWriterListener;
47import static com.android.mediaframeworktest.helpers.CameraTestUtils.configureReprocessableCameraSession;
48import static com.android.mediaframeworktest.helpers.CameraTestUtils.dumpFile;
49import static com.android.mediaframeworktest.helpers.CameraTestUtils.getAscendingOrderSizes;
50import static com.android.mediaframeworktest.helpers.CameraTestUtils.getDataFromImage;
51import static com.android.mediaframeworktest.helpers.CameraTestUtils.makeImageReader;
52import static com.android.mediaframeworktest.helpers.CameraTestUtils.setJpegKeys;
53import static com.android.mediaframeworktest.helpers.CameraTestUtils.verifyJpegKeys;
54
55/**
56 * <p>Tests for Reprocess API.</p>
57 *
58 * adb shell am instrument \
59 *    -e class \
60 *    com.android.mediaframeworktest.stress.Camera2StillCaptureTest#Camera2ReprocessCaptureTest \
61 *    -e iterations 1 \
62 *    -e waitIntervalMs 1000 \
63 *    -e resultToFile false \
64 *    -r -w com.android.mediaframeworktest/.Camera2InstrumentationTestRunner
65 */
66public class Camera2ReprocessCaptureTest extends Camera2SurfaceViewTestCase  {
67    private static final String TAG = "ReprocessCaptureTest";
68    private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
69    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
70    private static final int CAPTURE_TIMEOUT_FRAMES = 100;
71    private static final int CAPTURE_TIMEOUT_MS = 3000;
72    private static final int WAIT_FOR_SURFACE_CHANGE_TIMEOUT_MS = 1000;
73    private static final int CAPTURE_TEMPLATE = CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG;
74    private static final int ZSL_TEMPLATE = CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG;
75    private static final int NUM_REPROCESS_TEST_LOOP = 3;
76    private static final int NUM_REPROCESS_CAPTURES = 3;
77    private static final int NUM_REPROCESS_BURST = 3;
78    private int mDumpFrameCount = 0;
79
80    // The image reader for the first regular capture
81    private ImageReader mFirstImageReader;
82    // The image reader for the reprocess capture
83    private ImageReader mSecondImageReader;
84    // A flag indicating whether the regular capture and the reprocess capture share the same image
85    // reader. If it's true, mFirstImageReader should be used for regular and reprocess outputs.
86    private boolean mShareOneImageReader;
87    private SimpleImageReaderListener mFirstImageReaderListener;
88    private SimpleImageReaderListener mSecondImageReaderListener;
89    private Surface mInputSurface;
90    private ImageWriter mImageWriter;
91    private SimpleImageWriterListener mImageWriterListener;
92
93    private enum CaptureTestCase {
94        SINGLE_SHOT,
95        BURST,
96        MIXED_BURST,
97        ABORT_CAPTURE,
98        TIMESTAMPS,
99        JPEG_EXIF,
100        REQUEST_KEYS,
101    }
102
103    /**
104     * Test YUV_420_888 -> JPEG with maximal supported sizes
105     */
106    public void testBasicYuvToJpegReprocessing() throws Exception {
107        for (String id : mCameraIds) {
108            if (!isYuvReprocessSupported(id)) {
109                continue;
110            }
111
112            // Test iteration starts...
113            for (int iteration = 0; iteration < getIterationCount(); ++iteration) {
114                Log.v(TAG, String.format("Reprocessing YUV to JPEG: %d/%d", iteration + 1,
115                        getIterationCount()));
116                // YUV_420_888 -> JPEG must be supported.
117                testBasicReprocessing(id, ImageFormat.YUV_420_888, ImageFormat.JPEG);
118                getResultPrinter().printStatus(getIterationCount(), iteration + 1, id);
119                Thread.sleep(getTestWaitIntervalMs());
120            }
121        }
122    }
123
124    /**
125     * Test OPAQUE -> JPEG with maximal supported sizes
126     */
127    public void testBasicOpaqueToJpegReprocessing() throws Exception {
128        for (String id : mCameraIds) {
129            if (!isOpaqueReprocessSupported(id)) {
130                continue;
131            }
132
133            // Test iteration starts...
134            for (int iteration = 0; iteration < getIterationCount(); ++iteration) {
135                Log.v(TAG, String.format("Reprocessing OPAQUE to JPEG: %d/%d", iteration + 1,
136                        getIterationCount()));
137                // OPAQUE -> JPEG must be supported.
138                testBasicReprocessing(id, ImageFormat.PRIVATE, ImageFormat.JPEG);
139                getResultPrinter().printStatus(getIterationCount(), iteration + 1, id);
140                Thread.sleep(getTestWaitIntervalMs());
141            }
142
143        }
144    }
145
146    /**
147     * Test all supported size and format combinations with preview.
148     */
149    public void testReprocessingSizeFormatWithPreview() throws Exception {
150        for (String id : mCameraIds) {
151            if (!isYuvReprocessSupported(id) && !isOpaqueReprocessSupported(id)) {
152                continue;
153            }
154
155            try {
156                // open Camera device
157                openDevice(id);
158
159                // Test iteration starts...
160                for (int iteration = 0; iteration < getIterationCount(); ++iteration) {
161                    Log.v(TAG, String.format("Reprocessing size format with preview: %d/%d",
162                            iteration + 1, getIterationCount()));
163                    testReprocessingAllCombinations(id, mOrderedPreviewSizes.get(0),
164                            CaptureTestCase.SINGLE_SHOT);
165                    getResultPrinter().printStatus(getIterationCount(), iteration + 1, id);
166                    Thread.sleep(getTestWaitIntervalMs());
167                }
168            } finally {
169                closeDevice();
170            }
171        }
172    }
173
174    /**
175     * Test burst captures mixed with regular and reprocess captures with and without preview.
176     */
177    public void testMixedBurstReprocessing() throws Exception {
178        for (String id : mCameraIds) {
179            if (!isYuvReprocessSupported(id) && !isOpaqueReprocessSupported(id)) {
180                continue;
181            }
182
183            try {
184                // open Camera device
185                openDevice(id);
186
187                // Test iteration starts...
188                for (int iteration = 0; iteration < getIterationCount(); ++iteration) {
189                    Log.v(TAG, String.format("Reprocessing mixed burst with or without preview: "
190                            + "%d/%d", iteration + 1, getIterationCount()));
191                    // no preview
192                    testReprocessingAllCombinations(id, /*previewSize*/null,
193                            CaptureTestCase.MIXED_BURST);
194                    // with preview
195                    testReprocessingAllCombinations(id, mOrderedPreviewSizes.get(0),
196                            CaptureTestCase.MIXED_BURST);
197                    getResultPrinter().printStatus(getIterationCount(), iteration + 1, id);
198                    Thread.sleep(getTestWaitIntervalMs());
199                }
200            } finally {
201                closeDevice();
202            }
203        }
204    }
205
206    /**
207     * Test the input format and output format with the largest input and output sizes.
208     */
209    private void testBasicReprocessing(String cameraId, int inputFormat,
210            int reprocessOutputFormat) throws Exception {
211        try {
212            openDevice(cameraId);
213
214            testReprocessingMaxSizes(cameraId, inputFormat, reprocessOutputFormat,
215                    /* previewSize */null, CaptureTestCase.SINGLE_SHOT);
216        } finally {
217            closeDevice();
218        }
219    }
220
221    /**
222     * Test the input format and output format with the largest input and output sizes for a
223     * certain test case.
224     */
225    private void testReprocessingMaxSizes(String cameraId, int inputFormat,
226            int reprocessOutputFormat, Size previewSize, CaptureTestCase captureTestCase)
227            throws Exception {
228        Size maxInputSize = getMaxSize(inputFormat, StaticMetadata.StreamDirection.Input);
229        Size maxReprocessOutputSize =
230                getMaxSize(reprocessOutputFormat, StaticMetadata.StreamDirection.Output);
231
232        switch (captureTestCase) {
233            case SINGLE_SHOT:
234                testReprocess(cameraId, maxInputSize, inputFormat, maxReprocessOutputSize,
235                        reprocessOutputFormat, previewSize, NUM_REPROCESS_CAPTURES);
236                break;
237            case ABORT_CAPTURE:
238                testReprocessAbort(cameraId, maxInputSize, inputFormat, maxReprocessOutputSize,
239                        reprocessOutputFormat);
240                break;
241            case TIMESTAMPS:
242                testReprocessTimestamps(cameraId, maxInputSize, inputFormat, maxReprocessOutputSize,
243                        reprocessOutputFormat);
244                break;
245            case JPEG_EXIF:
246                testReprocessJpegExif(cameraId, maxInputSize, inputFormat, maxReprocessOutputSize);
247                break;
248            case REQUEST_KEYS:
249                testReprocessRequestKeys(cameraId, maxInputSize, inputFormat,
250                        maxReprocessOutputSize, reprocessOutputFormat);
251                break;
252            default:
253                throw new IllegalArgumentException("Invalid test case");
254        }
255    }
256
257    /**
258     * Test all input format, input size, output format, and output size combinations.
259     */
260    private void testReprocessingAllCombinations(String cameraId, Size previewSize,
261            CaptureTestCase captureTestCase) throws Exception {
262
263        int[] supportedInputFormats =
264                mStaticInfo.getAvailableFormats(StaticMetadata.StreamDirection.Input);
265        for (int inputFormat : supportedInputFormats) {
266            Size[] supportedInputSizes =
267                    mStaticInfo.getAvailableSizesForFormatChecked(inputFormat,
268                    StaticMetadata.StreamDirection.Input);
269
270            for (Size inputSize : supportedInputSizes) {
271                int[] supportedReprocessOutputFormats =
272                        mStaticInfo.getValidOutputFormatsForInput(inputFormat);
273
274                for (int reprocessOutputFormat : supportedReprocessOutputFormats) {
275                    Size[] supportedReprocessOutputSizes =
276                            mStaticInfo.getAvailableSizesForFormatChecked(reprocessOutputFormat,
277                            StaticMetadata.StreamDirection.Output);
278
279                    for (Size reprocessOutputSize : supportedReprocessOutputSizes) {
280                        switch (captureTestCase) {
281                            case SINGLE_SHOT:
282                                testReprocess(cameraId, inputSize, inputFormat,
283                                        reprocessOutputSize, reprocessOutputFormat, previewSize,
284                                        NUM_REPROCESS_CAPTURES);
285                                break;
286                            case BURST:
287                                testReprocessBurst(cameraId, inputSize, inputFormat,
288                                        reprocessOutputSize, reprocessOutputFormat, previewSize,
289                                        NUM_REPROCESS_BURST);
290                                break;
291                            case MIXED_BURST:
292                                testReprocessMixedBurst(cameraId, inputSize, inputFormat,
293                                        reprocessOutputSize, reprocessOutputFormat, previewSize,
294                                        NUM_REPROCESS_BURST);
295                                break;
296                            default:
297                                throw new IllegalArgumentException("Invalid test case");
298                        }
299                    }
300                }
301            }
302        }
303    }
304
305    /**
306     * Test burst that is mixed with regular and reprocess capture requests.
307     */
308    private void testReprocessMixedBurst(String cameraId, Size inputSize, int inputFormat,
309            Size reprocessOutputSize, int reprocessOutputFormat, Size previewSize,
310            int numBurst) throws Exception {
311        if (VERBOSE) {
312            Log.v(TAG, "testReprocessMixedBurst: cameraId: " + cameraId + " inputSize: " +
313                    inputSize + " inputFormat: " + inputFormat + " reprocessOutputSize: " +
314                    reprocessOutputSize + " reprocessOutputFormat: " + reprocessOutputFormat +
315                    " previewSize: " + previewSize + " numBurst: " + numBurst);
316        }
317
318        boolean enablePreview = (previewSize != null);
319        ImageResultHolder[] imageResultHolders = new ImageResultHolder[0];
320
321        try {
322            // totalNumBurst = number of regular burst + number of reprocess burst.
323            int totalNumBurst = numBurst * 2;
324
325            if (enablePreview) {
326                updatePreviewSurface(previewSize);
327            } else {
328                mPreviewSurface = null;
329            }
330
331            setupImageReaders(inputSize, inputFormat, reprocessOutputSize, reprocessOutputFormat,
332                totalNumBurst);
333            setupReprocessableSession(mPreviewSurface, /*numImageWriterImages*/numBurst);
334
335            if (enablePreview) {
336                startPreview(mPreviewSurface);
337            }
338
339            // Prepare an array of booleans indicating each capture's type (regular or reprocess)
340            boolean[] isReprocessCaptures = new boolean[totalNumBurst];
341            for (int i = 0; i < totalNumBurst; i++) {
342                if ((i & 1) == 0) {
343                    isReprocessCaptures[i] = true;
344                } else {
345                    isReprocessCaptures[i] = false;
346                }
347            }
348
349            imageResultHolders = doMixedReprocessBurstCapture(isReprocessCaptures);
350            for (ImageResultHolder holder : imageResultHolders) {
351                Image reprocessedImage = holder.getImage();
352                TotalCaptureResult result = holder.getTotalCaptureResult();
353
354                mCollector.expectImageProperties("testReprocessMixedBurst", reprocessedImage,
355                            reprocessOutputFormat, reprocessOutputSize,
356                            result.get(CaptureResult.SENSOR_TIMESTAMP));
357
358                if (DEBUG) {
359                    Log.d(TAG, String.format("camera %s in %dx%d %d out %dx%d %d",
360                            cameraId, inputSize.getWidth(), inputSize.getHeight(), inputFormat,
361                            reprocessOutputSize.getWidth(), reprocessOutputSize.getHeight(),
362                            reprocessOutputFormat));
363                    dumpImage(reprocessedImage,
364                            "/testReprocessMixedBurst_camera" + cameraId + "_" + mDumpFrameCount);
365                    mDumpFrameCount++;
366                }
367            }
368        } finally {
369            for (ImageResultHolder holder : imageResultHolders) {
370                holder.getImage().close();
371            }
372            closeReprossibleSession();
373            closeImageReaders();
374        }
375    }
376
377    /**
378     * Test burst of reprocess capture requests.
379     */
380    private void testReprocessBurst(String cameraId, Size inputSize, int inputFormat,
381            Size reprocessOutputSize, int reprocessOutputFormat, Size previewSize,
382            int numBurst) throws Exception {
383        if (VERBOSE) {
384            Log.v(TAG, "testReprocessBurst: cameraId: " + cameraId + " inputSize: " +
385                    inputSize + " inputFormat: " + inputFormat + " reprocessOutputSize: " +
386                    reprocessOutputSize + " reprocessOutputFormat: " + reprocessOutputFormat +
387                    " previewSize: " + previewSize + " numBurst: " + numBurst);
388        }
389
390        boolean enablePreview = (previewSize != null);
391        ImageResultHolder[] imageResultHolders = new ImageResultHolder[0];
392
393        try {
394            if (enablePreview) {
395                updatePreviewSurface(previewSize);
396            } else {
397                mPreviewSurface = null;
398            }
399
400            setupImageReaders(inputSize, inputFormat, reprocessOutputSize, reprocessOutputFormat,
401                numBurst);
402            setupReprocessableSession(mPreviewSurface, numBurst);
403
404            if (enablePreview) {
405                startPreview(mPreviewSurface);
406            }
407
408            imageResultHolders = doReprocessBurstCapture(numBurst);
409            for (ImageResultHolder holder : imageResultHolders) {
410                Image reprocessedImage = holder.getImage();
411                TotalCaptureResult result = holder.getTotalCaptureResult();
412
413                mCollector.expectImageProperties("testReprocessBurst", reprocessedImage,
414                            reprocessOutputFormat, reprocessOutputSize,
415                            result.get(CaptureResult.SENSOR_TIMESTAMP));
416
417                if (DEBUG) {
418                    Log.d(TAG, String.format("camera %s in %dx%d %d out %dx%d %d",
419                            cameraId, inputSize.getWidth(), inputSize.getHeight(), inputFormat,
420                            reprocessOutputSize.getWidth(), reprocessOutputSize.getHeight(),
421                            reprocessOutputFormat));
422                    dumpImage(reprocessedImage,
423                            "/testReprocessBurst_camera" + cameraId + "_" + mDumpFrameCount);
424                    mDumpFrameCount++;
425                }
426            }
427        } finally {
428            for (ImageResultHolder holder : imageResultHolders) {
429                holder.getImage().close();
430            }
431            closeReprossibleSession();
432            closeImageReaders();
433        }
434    }
435
436    /**
437     * Test a sequences of reprocess capture requests.
438     */
439    private void testReprocess(String cameraId, Size inputSize, int inputFormat,
440            Size reprocessOutputSize, int reprocessOutputFormat, Size previewSize,
441            int numReprocessCaptures) throws Exception {
442        if (VERBOSE) {
443            Log.v(TAG, "testReprocess: cameraId: " + cameraId + " inputSize: " +
444                    inputSize + " inputFormat: " + inputFormat + " reprocessOutputSize: " +
445                    reprocessOutputSize + " reprocessOutputFormat: " + reprocessOutputFormat +
446                    " previewSize: " + previewSize);
447        }
448
449        boolean enablePreview = (previewSize != null);
450
451        try {
452            if (enablePreview) {
453                updatePreviewSurface(previewSize);
454            } else {
455                mPreviewSurface = null;
456            }
457
458            setupImageReaders(inputSize, inputFormat, reprocessOutputSize, reprocessOutputFormat,
459                    /*maxImages*/1);
460            setupReprocessableSession(mPreviewSurface, /*numImageWriterImages*/1);
461
462            if (enablePreview) {
463                startPreview(mPreviewSurface);
464            }
465
466            for (int i = 0; i < numReprocessCaptures; i++) {
467                ImageResultHolder imageResultHolder = null;
468
469                try {
470                    imageResultHolder = doReprocessCapture();
471                    Image reprocessedImage = imageResultHolder.getImage();
472                    TotalCaptureResult result = imageResultHolder.getTotalCaptureResult();
473
474                    mCollector.expectImageProperties("testReprocess", reprocessedImage,
475                            reprocessOutputFormat, reprocessOutputSize,
476                            result.get(CaptureResult.SENSOR_TIMESTAMP));
477
478                    if (DEBUG) {
479                        Log.d(TAG, String.format("camera %s in %dx%d %d out %dx%d %d",
480                                cameraId, inputSize.getWidth(), inputSize.getHeight(), inputFormat,
481                                reprocessOutputSize.getWidth(), reprocessOutputSize.getHeight(),
482                                reprocessOutputFormat));
483
484                        dumpImage(reprocessedImage,
485                                "/testReprocess_camera" + cameraId + "_" + mDumpFrameCount);
486                        mDumpFrameCount++;
487                    }
488                } finally {
489                    if (imageResultHolder != null) {
490                        imageResultHolder.getImage().close();
491                    }
492                }
493            }
494        } finally {
495            closeReprossibleSession();
496            closeImageReaders();
497        }
498    }
499
500    /**
501     * Test aborting a burst reprocess capture and multiple single reprocess captures.
502     */
503    private void testReprocessAbort(String cameraId, Size inputSize, int inputFormat,
504            Size reprocessOutputSize, int reprocessOutputFormat) throws Exception {
505        if (VERBOSE) {
506            Log.v(TAG, "testReprocessAbort: cameraId: " + cameraId + " inputSize: " +
507                    inputSize + " inputFormat: " + inputFormat + " reprocessOutputSize: " +
508                    reprocessOutputSize + " reprocessOutputFormat: " + reprocessOutputFormat);
509        }
510
511        try {
512            setupImageReaders(inputSize, inputFormat, reprocessOutputSize, reprocessOutputFormat,
513                    NUM_REPROCESS_CAPTURES);
514            setupReprocessableSession(/*previewSurface*/null, NUM_REPROCESS_CAPTURES);
515
516            // Test two cases: submitting reprocess requests one by one and in a burst.
517            boolean submitInBursts[] = {false, true};
518            for (boolean submitInBurst : submitInBursts) {
519                // Prepare reprocess capture requests.
520                ArrayList<CaptureRequest> reprocessRequests =
521                        new ArrayList<>(NUM_REPROCESS_CAPTURES);
522
523                for (int i = 0; i < NUM_REPROCESS_CAPTURES; i++) {
524                    TotalCaptureResult result = submitCaptureRequest(mFirstImageReader.getSurface(),
525                            /*inputResult*/null);
526
527                    mImageWriter.queueInputImage(
528                            mFirstImageReaderListener.getImage(CAPTURE_TIMEOUT_MS));
529                    CaptureRequest.Builder builder = mCamera.createReprocessCaptureRequest(result);
530                    builder.addTarget(getReprocessOutputImageReader().getSurface());
531                    reprocessRequests.add(builder.build());
532                }
533
534                SimpleCaptureCallback captureCallback = new SimpleCaptureCallback();
535
536                // Submit reprocess capture requests.
537                if (submitInBurst) {
538                    mSession.captureBurst(reprocessRequests, captureCallback, mHandler);
539                } else {
540                    for (CaptureRequest request : reprocessRequests) {
541                        mSession.capture(request, captureCallback, mHandler);
542                    }
543                }
544
545                // Abort after getting the first result
546                TotalCaptureResult reprocessResult =
547                        captureCallback.getTotalCaptureResultForRequest(reprocessRequests.get(0),
548                        CAPTURE_TIMEOUT_FRAMES);
549                mSession.abortCaptures();
550
551                // Wait until the session is ready again.
552                mSessionListener.getStateWaiter().waitForState(
553                        BlockingSessionCallback.SESSION_READY, SESSION_CLOSE_TIMEOUT_MS);
554
555                // Gather all failed requests.
556                ArrayList<CaptureFailure> failures =
557                        captureCallback.getCaptureFailures(NUM_REPROCESS_CAPTURES - 1);
558                ArrayList<CaptureRequest> failedRequests = new ArrayList<>();
559                for (CaptureFailure failure : failures) {
560                    failedRequests.add(failure.getRequest());
561                }
562
563                // For each request that didn't fail must have a valid result.
564                for (int i = 1; i < reprocessRequests.size(); i++) {
565                    CaptureRequest request = reprocessRequests.get(i);
566                    if (!failedRequests.contains(request)) {
567                        captureCallback.getTotalCaptureResultForRequest(request,
568                                CAPTURE_TIMEOUT_FRAMES);
569                    }
570                }
571
572                // Drain the image reader listeners.
573                mFirstImageReaderListener.drain();
574                if (!mShareOneImageReader) {
575                    mSecondImageReaderListener.drain();
576                }
577
578                // Make sure all input surfaces are released.
579                for (int i = 0; i < NUM_REPROCESS_CAPTURES; i++) {
580                    mImageWriterListener.waitForImageReleased(CAPTURE_TIMEOUT_MS);
581                }
582            }
583        } finally {
584            closeReprossibleSession();
585            closeImageReaders();
586        }
587    }
588
589    /**
590     * Test timestamps for reprocess requests. Reprocess request's shutter timestamp, result's
591     * sensor timestamp, and output image's timestamp should match the reprocess input's timestamp.
592     */
593    private void testReprocessTimestamps(String cameraId, Size inputSize, int inputFormat,
594            Size reprocessOutputSize, int reprocessOutputFormat) throws Exception {
595        if (VERBOSE) {
596            Log.v(TAG, "testReprocessTimestamps: cameraId: " + cameraId + " inputSize: " +
597                    inputSize + " inputFormat: " + inputFormat + " reprocessOutputSize: " +
598                    reprocessOutputSize + " reprocessOutputFormat: " + reprocessOutputFormat);
599        }
600
601        try {
602            setupImageReaders(inputSize, inputFormat, reprocessOutputSize, reprocessOutputFormat,
603                    NUM_REPROCESS_CAPTURES);
604            setupReprocessableSession(/*previewSurface*/null, NUM_REPROCESS_CAPTURES);
605
606            // Prepare reprocess capture requests.
607            ArrayList<CaptureRequest> reprocessRequests = new ArrayList<>(NUM_REPROCESS_CAPTURES);
608            ArrayList<Long> expectedTimestamps = new ArrayList<>(NUM_REPROCESS_CAPTURES);
609
610            for (int i = 0; i < NUM_REPROCESS_CAPTURES; i++) {
611                TotalCaptureResult result = submitCaptureRequest(mFirstImageReader.getSurface(),
612                        /*inputResult*/null);
613
614                mImageWriter.queueInputImage(
615                        mFirstImageReaderListener.getImage(CAPTURE_TIMEOUT_MS));
616                CaptureRequest.Builder builder = mCamera.createReprocessCaptureRequest(result);
617                builder.addTarget(getReprocessOutputImageReader().getSurface());
618                reprocessRequests.add(builder.build());
619                // Reprocess result's timestamp should match input image's timestamp.
620                expectedTimestamps.add(result.get(CaptureResult.SENSOR_TIMESTAMP));
621            }
622
623            // Submit reprocess requests.
624            SimpleCaptureCallback captureCallback = new SimpleCaptureCallback();
625            mSession.captureBurst(reprocessRequests, captureCallback, mHandler);
626
627            // Verify we get the expected timestamps.
628            for (int i = 0; i < reprocessRequests.size(); i++) {
629                captureCallback.waitForCaptureStart(reprocessRequests.get(i),
630                        expectedTimestamps.get(i), CAPTURE_TIMEOUT_FRAMES);
631            }
632
633            TotalCaptureResult[] reprocessResults =
634                    captureCallback.getTotalCaptureResultsForRequests(reprocessRequests,
635                    CAPTURE_TIMEOUT_FRAMES);
636
637            for (int i = 0; i < expectedTimestamps.size(); i++) {
638                // Verify the result timestamps match the input image's timestamps.
639                long expected = expectedTimestamps.get(i);
640                long timestamp = reprocessResults[i].get(CaptureResult.SENSOR_TIMESTAMP);
641                assertEquals("Reprocess result timestamp (" + timestamp + ") doesn't match input " +
642                        "image's timestamp (" + expected + ")", expected, timestamp);
643
644                // Verify the reprocess output image timestamps match the input image's timestamps.
645                Image image = getReprocessOutputImageReaderListener().getImage(CAPTURE_TIMEOUT_MS);
646                timestamp = image.getTimestamp();
647                image.close();
648
649                assertEquals("Reprocess output timestamp (" + timestamp + ") doesn't match input " +
650                        "image's timestamp (" + expected + ")", expected, timestamp);
651            }
652
653            // Make sure all input surfaces are released.
654            for (int i = 0; i < NUM_REPROCESS_CAPTURES; i++) {
655                mImageWriterListener.waitForImageReleased(CAPTURE_TIMEOUT_MS);
656            }
657        } finally {
658            closeReprossibleSession();
659            closeImageReaders();
660        }
661    }
662
663    /**
664     * Test JPEG tags for reprocess requests. Reprocess result's JPEG tags and JPEG image's tags
665     * match reprocess request's JPEG tags.
666     */
667    private void testReprocessJpegExif(String cameraId, Size inputSize, int inputFormat,
668            Size reprocessOutputSize) throws Exception {
669        if (VERBOSE) {
670            Log.v(TAG, "testReprocessJpegExif: cameraId: " + cameraId + " inputSize: " +
671                    inputSize + " inputFormat: " + inputFormat + " reprocessOutputSize: " +
672                    reprocessOutputSize);
673        }
674
675        Size[] thumbnailSizes = mStaticInfo.getAvailableThumbnailSizesChecked();
676        Size[] testThumbnailSizes = new Size[EXIF_TEST_DATA.length];
677        Arrays.fill(testThumbnailSizes, thumbnailSizes[thumbnailSizes.length - 1]);
678        // Make sure thumbnail size (0, 0) is covered.
679        testThumbnailSizes[0] = new Size(0, 0);
680
681        try {
682            setupImageReaders(inputSize, inputFormat, reprocessOutputSize, ImageFormat.JPEG,
683                    EXIF_TEST_DATA.length);
684            setupReprocessableSession(/*previewSurface*/null, EXIF_TEST_DATA.length);
685
686            // Prepare reprocess capture requests.
687            ArrayList<CaptureRequest> reprocessRequests = new ArrayList<>(EXIF_TEST_DATA.length);
688
689            for (int i = 0; i < EXIF_TEST_DATA.length; i++) {
690                TotalCaptureResult result = submitCaptureRequest(mFirstImageReader.getSurface(),
691                        /*inputResult*/null);
692                mImageWriter.queueInputImage(
693                        mFirstImageReaderListener.getImage(CAPTURE_TIMEOUT_MS));
694
695                CaptureRequest.Builder builder = mCamera.createReprocessCaptureRequest(result);
696                builder.addTarget(getReprocessOutputImageReader().getSurface());
697
698                // set jpeg keys
699                setJpegKeys(builder, EXIF_TEST_DATA[i], testThumbnailSizes[i], mCollector);
700                reprocessRequests.add(builder.build());
701            }
702
703            // Submit reprocess requests.
704            SimpleCaptureCallback captureCallback = new SimpleCaptureCallback();
705            mSession.captureBurst(reprocessRequests, captureCallback, mHandler);
706
707            TotalCaptureResult[] reprocessResults =
708                    captureCallback.getTotalCaptureResultsForRequests(reprocessRequests,
709                    CAPTURE_TIMEOUT_FRAMES);
710
711            for (int i = 0; i < EXIF_TEST_DATA.length; i++) {
712                // Verify output image's and result's JPEG EXIF data.
713                Image image = getReprocessOutputImageReaderListener().getImage(CAPTURE_TIMEOUT_MS);
714                verifyJpegKeys(image, reprocessResults[i], reprocessOutputSize,
715                        testThumbnailSizes[i], EXIF_TEST_DATA[i], mStaticInfo, mCollector);
716                image.close();
717
718            }
719        } finally {
720            closeReprossibleSession();
721            closeImageReaders();
722        }
723    }
724
725
726
727    /**
728     * Test the following keys in reprocess results match the keys in reprocess requests:
729     *   1. EDGE_MODE
730     *   2. NOISE_REDUCTION_MODE
731     *   3. REPROCESS_EFFECTIVE_EXPOSURE_FACTOR (only for YUV reprocess)
732     */
733    private void testReprocessRequestKeys(String cameraId, Size inputSize, int inputFormat,
734            Size reprocessOutputSize, int reprocessOutputFormat) throws Exception {
735        if (VERBOSE) {
736            Log.v(TAG, "testReprocessRequestKeys: cameraId: " + cameraId + " inputSize: " +
737                    inputSize + " inputFormat: " + inputFormat + " reprocessOutputSize: " +
738                    reprocessOutputSize + " reprocessOutputFormat: " + reprocessOutputFormat);
739        }
740
741        final Integer[] EDGE_MODES = {CaptureRequest.EDGE_MODE_FAST,
742                CaptureRequest.EDGE_MODE_HIGH_QUALITY, CaptureRequest.EDGE_MODE_OFF,
743                CaptureRequest.EDGE_MODE_ZERO_SHUTTER_LAG};
744        final Integer[] NR_MODES = {CaptureRequest.NOISE_REDUCTION_MODE_HIGH_QUALITY,
745                CaptureRequest.NOISE_REDUCTION_MODE_OFF,
746                CaptureRequest.NOISE_REDUCTION_MODE_ZERO_SHUTTER_LAG,
747                CaptureRequest.NOISE_REDUCTION_MODE_FAST};
748        final Float[] EFFECTIVE_EXP_FACTORS = {null, 1.0f, 2.5f, 4.0f};
749        int numFrames = EDGE_MODES.length;
750
751        try {
752            setupImageReaders(inputSize, inputFormat, reprocessOutputSize, reprocessOutputFormat,
753                    numFrames);
754            setupReprocessableSession(/*previewSurface*/null, numFrames);
755
756            // Prepare reprocess capture requests.
757            ArrayList<CaptureRequest> reprocessRequests = new ArrayList<>(numFrames);
758
759            for (int i = 0; i < numFrames; i++) {
760                TotalCaptureResult result = submitCaptureRequest(mFirstImageReader.getSurface(),
761                        /*inputResult*/null);
762                mImageWriter.queueInputImage(
763                        mFirstImageReaderListener.getImage(CAPTURE_TIMEOUT_MS));
764
765                CaptureRequest.Builder builder = mCamera.createReprocessCaptureRequest(result);
766                builder.addTarget(getReprocessOutputImageReader().getSurface());
767
768                // Set reprocess request keys
769                builder.set(CaptureRequest.EDGE_MODE, EDGE_MODES[i]);
770                builder.set(CaptureRequest.NOISE_REDUCTION_MODE, NR_MODES[i]);
771                if (inputFormat == ImageFormat.YUV_420_888) {
772                    builder.set(CaptureRequest.REPROCESS_EFFECTIVE_EXPOSURE_FACTOR,
773                            EFFECTIVE_EXP_FACTORS[i]);
774                }
775                reprocessRequests.add(builder.build());
776            }
777
778            // Submit reprocess requests.
779            SimpleCaptureCallback captureCallback = new SimpleCaptureCallback();
780            mSession.captureBurst(reprocessRequests, captureCallback, mHandler);
781
782            TotalCaptureResult[] reprocessResults =
783                    captureCallback.getTotalCaptureResultsForRequests(reprocessRequests,
784                    CAPTURE_TIMEOUT_FRAMES);
785
786            for (int i = 0; i < numFrames; i++) {
787                // Verify result's keys
788                Integer resultEdgeMode = reprocessResults[i].get(CaptureResult.EDGE_MODE);
789                Integer resultNoiseReductionMode =
790                        reprocessResults[i].get(CaptureResult.NOISE_REDUCTION_MODE);
791
792                assertEquals("Reprocess result edge mode (" + resultEdgeMode +
793                        ") doesn't match requested edge mode (" + EDGE_MODES[i] + ")",
794                        resultEdgeMode, EDGE_MODES[i]);
795                assertEquals("Reprocess result noise reduction mode (" + resultNoiseReductionMode +
796                        ") doesn't match requested noise reduction mode (" +
797                        NR_MODES[i] + ")", resultNoiseReductionMode,
798                        NR_MODES[i]);
799
800                if (inputFormat == ImageFormat.YUV_420_888) {
801                    Float resultEffectiveExposureFactor = reprocessResults[i].get(
802                            CaptureResult.REPROCESS_EFFECTIVE_EXPOSURE_FACTOR);
803                    assertEquals("Reprocess effective exposure factor (" +
804                            resultEffectiveExposureFactor + ") doesn't match requested " +
805                            "effective exposure factor (" + EFFECTIVE_EXP_FACTORS[i] + ")",
806                            resultEffectiveExposureFactor, EFFECTIVE_EXP_FACTORS[i]);
807                }
808            }
809        } finally {
810            closeReprossibleSession();
811            closeImageReaders();
812        }
813    }
814
815    /**
816     * Set up two image readers: one for regular capture (used for reprocess input) and one for
817     * reprocess capture.
818     */
819    private void setupImageReaders(Size inputSize, int inputFormat, Size reprocessOutputSize,
820            int reprocessOutputFormat, int maxImages) {
821
822        mShareOneImageReader = false;
823        // If the regular output and reprocess output have the same size and format,
824        // they can share one image reader.
825        if (inputFormat == reprocessOutputFormat &&
826                inputSize.equals(reprocessOutputSize)) {
827            maxImages *= 2;
828            mShareOneImageReader = true;
829        }
830        // create an ImageReader for the regular capture
831        mFirstImageReaderListener = new SimpleImageReaderListener();
832        mFirstImageReader = makeImageReader(inputSize, inputFormat, maxImages,
833                mFirstImageReaderListener, mHandler);
834
835        if (!mShareOneImageReader) {
836            // create an ImageReader for the reprocess capture
837            mSecondImageReaderListener = new SimpleImageReaderListener();
838            mSecondImageReader = makeImageReader(reprocessOutputSize, reprocessOutputFormat,
839                    maxImages, mSecondImageReaderListener, mHandler);
840        }
841    }
842
843    /**
844     * Close two image readers.
845     */
846    private void closeImageReaders() {
847        CameraTestUtils.closeImageReader(mFirstImageReader);
848        mFirstImageReader = null;
849        CameraTestUtils.closeImageReader(mSecondImageReader);
850        mSecondImageReader = null;
851    }
852
853    /**
854     * Get the ImageReader for reprocess output.
855     */
856    private ImageReader getReprocessOutputImageReader() {
857        if (mShareOneImageReader) {
858            return mFirstImageReader;
859        } else {
860            return mSecondImageReader;
861        }
862    }
863
864    private SimpleImageReaderListener getReprocessOutputImageReaderListener() {
865        if (mShareOneImageReader) {
866            return mFirstImageReaderListener;
867        } else {
868            return mSecondImageReaderListener;
869        }
870    }
871
872    /**
873     * Set up a reprocessable session and create an ImageWriter with the sessoin's input surface.
874     */
875    private void setupReprocessableSession(Surface previewSurface, int numImageWriterImages)
876            throws Exception {
877        // create a reprocessable capture session
878        List<Surface> outSurfaces = new ArrayList<Surface>();
879        outSurfaces.add(mFirstImageReader.getSurface());
880        if (!mShareOneImageReader) {
881            outSurfaces.add(mSecondImageReader.getSurface());
882        }
883        if (previewSurface != null) {
884            outSurfaces.add(previewSurface);
885        }
886
887        InputConfiguration inputConfig = new InputConfiguration(mFirstImageReader.getWidth(),
888                mFirstImageReader.getHeight(), mFirstImageReader.getImageFormat());
889        String inputConfigString = inputConfig.toString();
890        if (VERBOSE) {
891            Log.v(TAG, "InputConfiguration: " + inputConfigString);
892        }
893        assertTrue(String.format("inputConfig is wrong: %dx%d format %d. Expect %dx%d format %d",
894                inputConfig.getWidth(), inputConfig.getHeight(), inputConfig.getFormat(),
895                mFirstImageReader.getWidth(), mFirstImageReader.getHeight(),
896                mFirstImageReader.getImageFormat()),
897                inputConfig.getWidth() == mFirstImageReader.getWidth() &&
898                inputConfig.getHeight() == mFirstImageReader.getHeight() &&
899                inputConfig.getFormat() == mFirstImageReader.getImageFormat());
900
901        mSessionListener = new BlockingSessionCallback();
902        mSession = configureReprocessableCameraSession(mCamera, inputConfig, outSurfaces,
903                mSessionListener, mHandler);
904
905        // create an ImageWriter
906        mInputSurface = mSession.getInputSurface();
907        mImageWriter = ImageWriter.newInstance(mInputSurface,
908                numImageWriterImages);
909
910        mImageWriterListener = new SimpleImageWriterListener(mImageWriter);
911        mImageWriter.setOnImageReleasedListener(mImageWriterListener, mHandler);
912    }
913
914    /**
915     * Close the reprocessable session and ImageWriter.
916     */
917    private void closeReprossibleSession() {
918        mInputSurface = null;
919
920        if (mSession != null) {
921            mSession.close();
922            mSession = null;
923        }
924
925        if (mImageWriter != null) {
926            mImageWriter.close();
927            mImageWriter = null;
928        }
929    }
930
931    /**
932     * Do one reprocess capture.
933     */
934    private ImageResultHolder doReprocessCapture() throws Exception {
935        return doReprocessBurstCapture(/*numBurst*/1)[0];
936    }
937
938    /**
939     * Do a burst of reprocess captures.
940     */
941    private ImageResultHolder[] doReprocessBurstCapture(int numBurst) throws Exception {
942        boolean[] isReprocessCaptures = new boolean[numBurst];
943        for (int i = 0; i < numBurst; i++) {
944            isReprocessCaptures[i] = true;
945        }
946
947        return doMixedReprocessBurstCapture(isReprocessCaptures);
948    }
949
950    /**
951     * Do a burst of captures that are mixed with regular and reprocess captures.
952     *
953     * @param isReprocessCaptures An array whose elements indicate whether it's a reprocess capture
954     *                            request. If the element is true, it represents a reprocess capture
955     *                            request. If the element is false, it represents a regular capture
956     *                            request. The size of the array is the number of capture requests
957     *                            in the burst.
958     */
959    private ImageResultHolder[] doMixedReprocessBurstCapture(boolean[] isReprocessCaptures)
960            throws Exception {
961        if (isReprocessCaptures == null || isReprocessCaptures.length <= 0) {
962            throw new IllegalArgumentException("isReprocessCaptures must have at least 1 capture.");
963        }
964
965        boolean hasReprocessRequest = false;
966        boolean hasRegularRequest = false;
967
968        TotalCaptureResult[] results = new TotalCaptureResult[isReprocessCaptures.length];
969        for (int i = 0; i < isReprocessCaptures.length; i++) {
970            // submit a capture and get the result if this entry is a reprocess capture.
971            if (isReprocessCaptures[i]) {
972                results[i] = submitCaptureRequest(mFirstImageReader.getSurface(),
973                        /*inputResult*/null);
974                mImageWriter.queueInputImage(
975                        mFirstImageReaderListener.getImage(CAPTURE_TIMEOUT_MS));
976                hasReprocessRequest = true;
977            } else {
978                hasRegularRequest = true;
979            }
980        }
981
982        Surface[] outputSurfaces = new Surface[isReprocessCaptures.length];
983        for (int i = 0; i < isReprocessCaptures.length; i++) {
984            outputSurfaces[i] = getReprocessOutputImageReader().getSurface();
985        }
986
987        TotalCaptureResult[] finalResults = submitMixedCaptureBurstRequest(outputSurfaces, results);
988
989        ImageResultHolder[] holders = new ImageResultHolder[isReprocessCaptures.length];
990        for (int i = 0; i < isReprocessCaptures.length; i++) {
991            Image image = getReprocessOutputImageReaderListener().getImage(CAPTURE_TIMEOUT_MS);
992            if (hasReprocessRequest && hasRegularRequest) {
993                // If there are mixed requests, images and results may not be in the same order.
994                for (int j = 0; j < finalResults.length; j++) {
995                    if (finalResults[j] != null &&
996                            finalResults[j].get(CaptureResult.SENSOR_TIMESTAMP) ==
997                            image.getTimestamp()) {
998                        holders[i] = new ImageResultHolder(image, finalResults[j]);
999                        finalResults[j] = null;
1000                        break;
1001                    }
1002                }
1003
1004                assertNotNull("Cannot find a result matching output image's timestamp: " +
1005                        image.getTimestamp(), holders[i]);
1006            } else {
1007                // If no mixed requests, images and results should be in the same order.
1008                holders[i] = new ImageResultHolder(image, finalResults[i]);
1009            }
1010        }
1011
1012        return holders;
1013    }
1014
1015    /**
1016     * Start preview without a listener.
1017     */
1018    private void startPreview(Surface previewSurface) throws Exception {
1019        CaptureRequest.Builder builder = mCamera.createCaptureRequest(ZSL_TEMPLATE);
1020        builder.addTarget(previewSurface);
1021        mSession.setRepeatingRequest(builder.build(), null, mHandler);
1022    }
1023
1024    /**
1025     * Issue a capture request and return the result. If inputResult is null, it's a regular
1026     * request. Otherwise, it's a reprocess request.
1027     */
1028    private TotalCaptureResult submitCaptureRequest(Surface output,
1029            TotalCaptureResult inputResult) throws Exception {
1030        Surface[] outputs = new Surface[1];
1031        outputs[0] = output;
1032        TotalCaptureResult[] inputResults = new TotalCaptureResult[1];
1033        inputResults[0] = inputResult;
1034
1035        return submitMixedCaptureBurstRequest(outputs, inputResults)[0];
1036    }
1037
1038    /**
1039     * Submit a burst request mixed with regular and reprocess requests.
1040     *
1041     * @param outputs An array of output surfaces. One output surface will be used in one request
1042     *                so the length of the array is the number of requests in a burst request.
1043     * @param inputResults An array of input results. If it's null, all requests are regular
1044     *                     requests. If an element is null, that element represents a regular
1045     *                     request. If an element if not null, that element represents a reprocess
1046     *                     request.
1047     *
1048     */
1049    private TotalCaptureResult[] submitMixedCaptureBurstRequest(Surface[] outputs,
1050            TotalCaptureResult[] inputResults) throws Exception {
1051        if (outputs == null || outputs.length <= 0) {
1052            throw new IllegalArgumentException("outputs must have at least 1 surface");
1053        } else if (inputResults != null && inputResults.length != outputs.length) {
1054            throw new IllegalArgumentException("The lengths of outputs and inputResults " +
1055                    "don't match");
1056        }
1057
1058        int numReprocessCaptures = 0;
1059        SimpleCaptureCallback captureCallback = new SimpleCaptureCallback();
1060        ArrayList<CaptureRequest> captureRequests = new ArrayList<>(outputs.length);
1061
1062        // Prepare a list of capture requests. Whether it's a regular or reprocess capture request
1063        // is based on inputResults array.
1064        for (int i = 0; i < outputs.length; i++) {
1065            CaptureRequest.Builder builder;
1066            boolean isReprocess = (inputResults != null && inputResults[i] != null);
1067            if (isReprocess) {
1068                builder = mCamera.createReprocessCaptureRequest(inputResults[i]);
1069                numReprocessCaptures++;
1070            } else {
1071                builder = mCamera.createCaptureRequest(CAPTURE_TEMPLATE);
1072            }
1073            builder.addTarget(outputs[i]);
1074            CaptureRequest request = builder.build();
1075            assertTrue("Capture request reprocess type " + request.isReprocess() + " is wrong.",
1076                request.isReprocess() == isReprocess);
1077
1078            captureRequests.add(request);
1079        }
1080
1081        if (captureRequests.size() == 1) {
1082            mSession.capture(captureRequests.get(0), captureCallback, mHandler);
1083        } else {
1084            mSession.captureBurst(captureRequests, captureCallback, mHandler);
1085        }
1086
1087        TotalCaptureResult[] results;
1088        if (numReprocessCaptures == 0 || numReprocessCaptures == outputs.length) {
1089            results = new TotalCaptureResult[outputs.length];
1090            // If the requests are not mixed, they should come in order.
1091            for (int i = 0; i < results.length; i++){
1092                results[i] = captureCallback.getTotalCaptureResultForRequest(
1093                        captureRequests.get(i), CAPTURE_TIMEOUT_FRAMES);
1094            }
1095        } else {
1096            // If the requests are mixed, they may not come in order.
1097            results = captureCallback.getTotalCaptureResultsForRequests(
1098                    captureRequests, CAPTURE_TIMEOUT_FRAMES * captureRequests.size());
1099        }
1100
1101        // make sure all input surfaces are released.
1102        for (int i = 0; i < numReprocessCaptures; i++) {
1103            mImageWriterListener.waitForImageReleased(CAPTURE_TIMEOUT_MS);
1104        }
1105
1106        return results;
1107    }
1108
1109    private Size getMaxSize(int format, StaticMetadata.StreamDirection direction) {
1110        Size[] sizes = mStaticInfo.getAvailableSizesForFormatChecked(format, direction);
1111        return getAscendingOrderSizes(Arrays.asList(sizes), /*ascending*/false).get(0);
1112    }
1113
1114    private boolean isYuvReprocessSupported(String cameraId) throws Exception {
1115        return isReprocessSupported(cameraId, ImageFormat.YUV_420_888);
1116    }
1117
1118    private boolean isOpaqueReprocessSupported(String cameraId) throws Exception {
1119        return isReprocessSupported(cameraId, ImageFormat.PRIVATE);
1120    }
1121
1122    private void dumpImage(Image image, String name) {
1123        String filename = DEBUG_FILE_NAME_BASE + name;
1124        switch(image.getFormat()) {
1125            case ImageFormat.JPEG:
1126                filename += ".jpg";
1127                break;
1128            case ImageFormat.NV16:
1129            case ImageFormat.NV21:
1130            case ImageFormat.YUV_420_888:
1131                filename += ".yuv";
1132                break;
1133            default:
1134                filename += "." + image.getFormat();
1135                break;
1136        }
1137
1138        Log.d(TAG, "dumping an image to " + filename);
1139        dumpFile(filename , getDataFromImage(image));
1140    }
1141
1142    /**
1143     * A class that holds an Image and a TotalCaptureResult.
1144     */
1145    private static class ImageResultHolder {
1146        private final Image mImage;
1147        private final TotalCaptureResult mResult;
1148
1149        public ImageResultHolder(Image image, TotalCaptureResult result) {
1150            mImage = image;
1151            mResult = result;
1152        }
1153
1154        public Image getImage() {
1155            return mImage;
1156        }
1157
1158        public TotalCaptureResult getTotalCaptureResult() {
1159            return mResult;
1160        }
1161    }
1162}
1163