/*
 * Copyright 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.hardware.camera2.cts;

import static android.hardware.camera2.cts.CameraTestUtils.SessionConfigSupport;
import static android.hardware.camera2.cts.CameraTestUtils.SimpleCaptureCallback;
import static android.hardware.camera2.cts.CameraTestUtils.SimpleImageReaderListener;
import static android.hardware.camera2.cts.CameraTestUtils.StreamCombinationTargets;
import static android.hardware.camera2.cts.CameraTestUtils.assertEquals;
import static android.hardware.camera2.cts.CameraTestUtils.assertNotNull;
import static android.hardware.camera2.cts.CameraTestUtils.assertNull;
import static android.hardware.camera2.cts.CameraTestUtils.checkSessionConfigurationSupported;
import static android.hardware.camera2.cts.CameraTestUtils.checkSessionConfigurationWithSurfaces;
import static android.hardware.camera2.cts.CameraTestUtils.configureReprocessableCameraSession;
import static android.hardware.camera2.cts.CameraTestUtils.fail;
import static android.hardware.camera2.cts.CameraTestUtils.getUnavailablePhysicalCameras;
import static android.hardware.camera2.cts.CameraTestUtils.isSessionConfigSupported;
import static android.hardware.camera2.cts.CameraTestUtils.MaxStreamSizes;
import static android.hardware.camera2.cts.CameraTestUtils.MaxStreamSizes.JPEG;
import static android.hardware.camera2.cts.CameraTestUtils.MaxStreamSizes.MAXIMUM;
import static android.hardware.camera2.cts.CameraTestUtils.MaxStreamSizes.MAX_RES;
import static android.hardware.camera2.cts.CameraTestUtils.MaxStreamSizes.PREVIEW;
import static android.hardware.camera2.cts.CameraTestUtils.MaxStreamSizes.PRIV;
import static android.hardware.camera2.cts.CameraTestUtils.MaxStreamSizes.RAW;
import static android.hardware.camera2.cts.CameraTestUtils.MaxStreamSizes.RECORD;
import static android.hardware.camera2.cts.CameraTestUtils.MaxStreamSizes.S1440P_4_3;
import static android.hardware.camera2.cts.CameraTestUtils.MaxStreamSizes.S720P;
import static android.hardware.camera2.cts.CameraTestUtils.MaxStreamSizes.USE_CASE_PREVIEW;
import static android.hardware.camera2.cts.CameraTestUtils.MaxStreamSizes.USE_CASE_PREVIEW_VIDEO_STILL;
import static android.hardware.camera2.cts.CameraTestUtils.MaxStreamSizes.USE_CASE_STILL_CAPTURE;
import static android.hardware.camera2.cts.CameraTestUtils.MaxStreamSizes.USE_CASE_VIDEO_CALL;
import static android.hardware.camera2.cts.CameraTestUtils.MaxStreamSizes.USE_CASE_VIDEO_RECORD;
import static android.hardware.camera2.cts.CameraTestUtils.MaxStreamSizes.USE_CASE_CROPPED_RAW;
import static android.hardware.camera2.cts.CameraTestUtils.MaxStreamSizes.VGA;
import static android.hardware.camera2.cts.CameraTestUtils.MaxStreamSizes.YUV;

import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;

import static org.mockito.Mockito.any;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.isA;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;

import android.graphics.ImageFormat;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraMetadata;
import android.hardware.camera2.CaptureFailure;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult;
import android.hardware.camera2.cts.helpers.StaticMetadata;
import android.hardware.camera2.cts.testcases.Camera2AndroidTestCase;
import android.hardware.camera2.params.DynamicRangeProfiles;
import android.hardware.camera2.params.InputConfiguration;
import android.hardware.camera2.params.MandatoryStreamCombination;
import android.hardware.camera2.params.MandatoryStreamCombination.MandatoryStreamInformation;
import android.hardware.camera2.params.OisSample;
import android.hardware.camera2.params.OutputConfiguration;
import android.hardware.camera2.params.SessionConfiguration;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.Image;
import android.media.ImageReader;
import android.media.ImageWriter;
import android.util.Log;
import android.util.Pair;
import android.util.Size;
import android.view.Surface;

import com.android.ex.camera2.blocking.BlockingSessionCallback;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

/**
 * Tests exercising edge cases in camera setup, configuration, and usage.
 */

@RunWith(Parameterized.class)
public class RobustnessTest extends Camera2AndroidTestCase {
    private static final String TAG = "RobustnessTest";
    private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);

    private static final int CONFIGURE_TIMEOUT = 5000; //ms
    private static final int CAPTURE_TIMEOUT = 1500; //ms

    // For testTriggerInteractions
    private static final int PREVIEW_WARMUP_FRAMES = 60;
    private static final int MAX_RESULT_STATE_CHANGE_WAIT_FRAMES = 100;
    private static final int MAX_TRIGGER_SEQUENCE_FRAMES = 180; // 6 sec at 30 fps
    private static final int MAX_RESULT_STATE_POSTCHANGE_WAIT_FRAMES = 10;

    /**
     * Test that a {@link CameraCaptureSession} can be configured with a {@link Surface} containing
     * a dimension other than one of the supported output dimensions.  The buffers produced into
     * this surface are expected have the dimensions of the closest possible buffer size in the
     * available stream configurations for a surface with this format.
     */
    @Test
    public void testBadSurfaceDimensions() throws Exception {
        for (String id : getCameraIdsUnderTest()) {
            try {
                Log.i(TAG, "Testing Camera " + id);
                openDevice(id);

                List<Size> testSizes = null;
                int format = mStaticInfo.isColorOutputSupported() ?
                    ImageFormat.YUV_420_888 : ImageFormat.DEPTH16;

                testSizes = CameraTestUtils.getSortedSizesForFormat(id, mCameraManager,
                        format, null);

                // Find some size not supported by the camera
                Size weirdSize = new Size(643, 577);
                int count = 0;
                while(testSizes.contains(weirdSize)) {
                    // Really, they can't all be supported...
                    weirdSize = new Size(weirdSize.getWidth() + 1, weirdSize.getHeight() + 1);
                    count++;
                    assertTrue("Too many exotic YUV_420_888 resolutions supported.", count < 100);
                }

                // Setup imageReader with invalid dimension
                ImageReader imageReader = ImageReader.newInstance(weirdSize.getWidth(),
                        weirdSize.getHeight(), format, 3);

                // Setup ImageReaderListener
                SimpleImageReaderListener imageListener = new SimpleImageReaderListener();
                imageReader.setOnImageAvailableListener(imageListener, mHandler);

                Surface surface = imageReader.getSurface();
                List<Surface> surfaces = new ArrayList<>();
                surfaces.add(surface);

                // Setup a capture request and listener
                CaptureRequest.Builder request =
                        mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
                request.addTarget(surface);

                // Check that correct session callback is hit.
                CameraCaptureSession.StateCallback sessionListener =
                        mock(CameraCaptureSession.StateCallback.class);
                CameraCaptureSession session = CameraTestUtils.configureCameraSession(mCamera,
                        surfaces, sessionListener, mHandler);

                verify(sessionListener, timeout(CONFIGURE_TIMEOUT).atLeastOnce()).
                        onConfigured(any(CameraCaptureSession.class));
                verify(sessionListener, timeout(CONFIGURE_TIMEOUT).atLeastOnce()).
                        onReady(any(CameraCaptureSession.class));
                verify(sessionListener, never()).onConfigureFailed(any(CameraCaptureSession.class));
                verify(sessionListener, never()).onActive(any(CameraCaptureSession.class));
                verify(sessionListener, never()).onClosed(any(CameraCaptureSession.class));

                CameraCaptureSession.CaptureCallback captureListener =
                        mock(CameraCaptureSession.CaptureCallback.class);
                session.capture(request.build(), captureListener, mHandler);

                verify(captureListener, timeout(CAPTURE_TIMEOUT).atLeastOnce()).
                        onCaptureCompleted(any(CameraCaptureSession.class),
                                any(CaptureRequest.class), any(TotalCaptureResult.class));
                verify(captureListener, never()).onCaptureFailed(any(CameraCaptureSession.class),
                        any(CaptureRequest.class), any(CaptureFailure.class));

                Image image = imageListener.getImage(CAPTURE_TIMEOUT);
                int imageWidth = image.getWidth();
                int imageHeight = image.getHeight();
                Size actualSize = new Size(imageWidth, imageHeight);

                assertTrue("Camera does not contain outputted image resolution " + actualSize,
                        testSizes.contains(actualSize));
                imageReader.close();
            } finally {
                closeDevice(id);
            }
        }
    }

    /**
     * Test for making sure the mandatory stream combinations work as expected.
     */
    @Test
    public void testMandatoryOutputCombinations() throws Exception {
        testMandatoryOutputCombinations(/*maxResolution*/false);
    }

    /**
     * Test for making sure the mandatory stream combinations work as expected.
     */
    private void testMandatoryOutputCombinations(boolean maxResolution) throws Exception {
        CameraCharacteristics.Key<MandatoryStreamCombination []> ck =
                CameraCharacteristics.SCALER_MANDATORY_STREAM_COMBINATIONS;

        if (maxResolution) {
            ck = CameraCharacteristics.SCALER_MANDATORY_MAXIMUM_RESOLUTION_STREAM_COMBINATIONS;
        }
        String[] cameraIdsUnderTest = getCameraIdsUnderTest();
        Set<Pair<String, String>> unavailablePhysicalCameras = getUnavailablePhysicalCameras(
                mCameraManager, mHandler);
        for (String id : cameraIdsUnderTest) {
            openDevice(id);
            MandatoryStreamCombination[] combinations = mStaticInfo.getCharacteristics().get(ck);

            if (combinations == null) {
                String maxResolutionStr = maxResolution ? " " : " maximum resolution ";
                Log.i(TAG, "No mandatory" + maxResolutionStr + "stream combinations for camera: " +
                        id + " skip test");
                closeDevice(id);
                continue;
            }

            try {
                for (MandatoryStreamCombination combination : combinations) {
                    if (!combination.isReprocessable()) {
                        if (maxResolution) {
                            testMandatoryStreamCombination(id, mStaticInfo,
                                    /*physicalCameraId*/ null, combination, /*substituteY8*/false,
                                    /*substituteHeic*/false, /*maxResolution*/true);
                        } else {
                            testMandatoryStreamCombination(id, mStaticInfo,
                                    null/*physicalCameraId*/, combination);
                        }
                    }
                }

                // Make sure mandatory stream combinations for each physical camera work
                // as expected.
                if (mStaticInfo.isLogicalMultiCamera()) {
                    Set<String> physicalCameraIds =
                            mStaticInfo.getCharacteristics().getPhysicalCameraIds();
                    for (String physicalId : physicalCameraIds) {
                        if (Arrays.asList(cameraIdsUnderTest).contains(physicalId)) {
                            // If physicalId is advertised in camera ID list, do not need to test
                            // its stream combination through logical camera.
                            continue;
                        }
                        if (unavailablePhysicalCameras.contains(new Pair<>(id, physicalId))) {
                            // If physicalId is unavailable, do not attempt to test its
                            // stream combinations.
                            continue;
                        }

                        StaticMetadata physicalStaticInfo = mAllStaticInfo.get(physicalId);

                        MandatoryStreamCombination[] phyCombinations =
                                physicalStaticInfo.getCharacteristics().get(ck);

                        if (phyCombinations == null) {
                            Log.i(TAG, "No mandatory stream combinations for physical camera device: " + id + " skip test");
                            continue;
                        }

                        for (MandatoryStreamCombination combination : phyCombinations) {
                            if (!combination.isReprocessable()) {
                                if (maxResolution) {
                                   testMandatoryStreamCombination(id, physicalStaticInfo,
                                           physicalId, combination, /*substituteY8*/false,
                                           /*substituteHeic*/false, /*maxResolution*/true);
                                } else {
                                    testMandatoryStreamCombination(id, physicalStaticInfo,
                                            physicalId, combination);
                                }
                            }
                        }
                    }
                }

            } finally {
                closeDevice(id);
            }
        }
    }


    /**
     * Test for making sure the mandatory stream combinations work as expected.
     */
    @Test
    public void testMandatoryMaximumResolutionOutputCombinations() throws Exception {
        testMandatoryOutputCombinations(/*maxResolution*/ true);
    }

    /**
     * Test for making sure the mandatory use case stream combinations work as expected.
     */
    @Test
    public void testMandatoryUseCaseOutputCombinations() throws Exception {
        for (String id : getCameraIdsUnderTest()) {
            StaticMetadata info = mAllStaticInfo.get(id);
            CameraCharacteristics chars = info.getCharacteristics();
            CameraCharacteristics.Key<MandatoryStreamCombination []> ck =
                    CameraCharacteristics.SCALER_MANDATORY_USE_CASE_STREAM_COMBINATIONS;
            MandatoryStreamCombination[] combinations = chars.get(ck);

            if (!info.isCapabilitySupported(
                    CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_STREAM_USE_CASE)) {
                assertNull(combinations);
                Log.i(TAG, "Camera id " + id + " doesn't support stream use case, skip test");
                continue;
            }

            assertNotNull(combinations);
            openDevice(id);

            try {
                Rect preCorrectionActiveArrayRect = info.getPreCorrectedActiveArraySizeChecked();
                for (MandatoryStreamCombination combination : combinations) {
                    Log.i(TAG, "Testing fixed mandatory output combination with stream use case: " +
                            combination.getDescription() + " on camera: " + id);
                    CaptureRequest.Builder requestBuilder =
                            mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
                    testMandatoryOutputCombinationWithPresetKeys(id, combination, requestBuilder,
                            preCorrectionActiveArrayRect);
                }
            } finally {
                closeDevice(id);
            }
        }
    }

    @Test
    public void testMandatoryPreviewStabilizationOutputCombinations() throws Exception {
        for (String id : getCameraIdsUnderTest()) {
            StaticMetadata info = mAllStaticInfo.get(id);
            boolean previewStabilizationSupported = isPreviewStabilizationSupported(info);
            CameraCharacteristics chars = info.getCharacteristics();
            CameraCharacteristics.Key<MandatoryStreamCombination []> ck =
                    CameraCharacteristics
                            .SCALER_MANDATORY_PREVIEW_STABILIZATION_OUTPUT_STREAM_COMBINATIONS;
            MandatoryStreamCombination[] combinations = chars.get(ck);

            if (combinations == null) {
                assertFalse("Preview stabilization supported by camera id: " + id
                        + " but null mandatory streams", previewStabilizationSupported);
                Log.i(TAG, "Camera id " + id + " doesn't support preview stabilization, skip test");
                continue;
            } else {
                assertTrue("Preview stabilization not supported by camera id: " + id
                        + " but non-null mandatory streams", previewStabilizationSupported);
            }

            openDevice(id);

            try {
                for (MandatoryStreamCombination combination : combinations) {
                    Log.i(TAG, "Testing fixed mandatory output combination with preview"
                            + "stabilization case: " + combination.getDescription() + " on camera: "
                                     + id);
                    CaptureRequest.Builder requestBuilder =
                            mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
                    requestBuilder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE,
                            CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION);
                    testMandatoryOutputCombinationWithPresetKeys(id, combination, requestBuilder,
                            /*preCorrectionActiveArrayRect*/null);
                }
            } finally {
                closeDevice(id);
            }
        }
    }

    private boolean isPreviewStabilizationSupported(StaticMetadata info) {
        int[] availableVideoStabilizationModes = info.getAvailableVideoStabilizationModesChecked();
        if (availableVideoStabilizationModes == null) {
            return false;
        }
        for (int mode : availableVideoStabilizationModes) {
            if (mode == CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION) {
                return true;
            }
        }
        return false;
    }

    private void testMandatoryOutputCombinationWithPresetKeys(String cameraId,
            MandatoryStreamCombination combination, CaptureRequest.Builder requestBuilderWithKeys,
            Rect preCorrectionActiveArrayRect) {
        final int TIMEOUT_FOR_RESULT_MS = 1000;
        final int MIN_RESULT_COUNT = 3;

        // Setup outputs
        List<OutputConfiguration> outputConfigs = new ArrayList<>();
        List<Surface> outputSurfaces = new ArrayList<Surface>();
        List<Surface> uhOutputSurfaces = new ArrayList<Surface>();
        StreamCombinationTargets targets = new StreamCombinationTargets();

        CameraTestUtils.setupConfigurationTargets(combination.getStreamsInformation(),
                targets, outputConfigs, outputSurfaces, uhOutputSurfaces, MIN_RESULT_COUNT,
                /*substituteY8*/ false, /*substituteHeic*/false,
                /*physicalCameraId*/ null,
                /*multiResStreamConfig*/null, mHandler,
                /*dynamicRangeProfiles*/ null);

        boolean haveSession = false;
        try {
            checkSessionConfigurationSupported(mCamera, mHandler, outputConfigs,
                    /*inputConfig*/ null, SessionConfiguration.SESSION_REGULAR,
                    mCameraManager, true/*defaultSupport*/,
                    String.format("Session configuration query from combination: %s failed",
                            combination.getDescription()));

            createSessionByConfigs(outputConfigs);
            haveSession = true;
            for (Surface s : outputSurfaces) {
                requestBuilderWithKeys.addTarget(s);
            }
            boolean croppedRawUseCase = false;
            for (OutputConfiguration c : outputConfigs) {
                if (c.getStreamUseCase() ==
                        CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_CROPPED_RAW) {
                    croppedRawUseCase = true;
                    break;
                }
            }

            CaptureRequest request = requestBuilderWithKeys.build();
            CameraTestUtils.SimpleCaptureCallback captureCallback =
                    new CameraTestUtils.SimpleCaptureCallback();


            mCameraSession.setRepeatingRequest(request, captureCallback, mHandler);

            for (int i = 0; i < MIN_RESULT_COUNT; i++) {
                // Makes sure that we received an onCaptureCompleted and not an onCaptureFailed.
                TotalCaptureResult result =
                        captureCallback.getTotalCaptureResultForRequest(request,
                                /*numResultsWait*/ 0);
                validateResultMandatoryConditions(result, croppedRawUseCase,
                    preCorrectionActiveArrayRect);
            }
            if (captureCallback.hasMoreFailures()) {
                mCollector.addMessage("No capture failures expected, but there was a failure");
            }

        } catch (Throwable e) {
            mCollector.addMessage(
                    String.format("Closing down for combination: %s failed due to: %s",
                            combination.getDescription(), e.getMessage()));
        }

        if (haveSession) {
            try {
                Log.i(TAG, String.format("Done with camera %s, combination: %s, closing session",
                                cameraId, combination.getDescription()));
                stopCapture(/*fast*/false);
            } catch (Throwable e) {
                mCollector.addMessage(
                    String.format("Closing down for combination: %s failed due to: %s",
                            combination.getDescription(), e.getMessage()));
            }
        }

        targets.close();
    }

    private void validateResultMandatoryConditions(TotalCaptureResult result,
            boolean croppedRawUseCase, Rect preCorrectionActiveArrayRect) {
        // validate more conditions here
        if (croppedRawUseCase) {
            Rect rawCropRegion = result.get(CaptureResult.SCALER_RAW_CROP_REGION);
            if (rawCropRegion == null) {
                mCollector.addMessage("SCALER_RAW_CROP_REGION should not be null " +
                        "when CROPPED_RAW stream use case is used.");
            }
            if (!(preCorrectionActiveArrayRect.width() >= rawCropRegion.width()
                    && preCorrectionActiveArrayRect.height() >= rawCropRegion.height())) {
                mCollector.addMessage("RAW_CROP_REGION dimensions should be <= pre correction"
                        + " array dimensions. SCALER_RAW_CROP_REGION : "
                        + rawCropRegion.flattenToString() + " pre correction active array is "
                        + preCorrectionActiveArrayRect.flattenToString());
            }
        }
    }

    private void testMandatoryStreamCombination(String cameraId, StaticMetadata staticInfo,
            String physicalCameraId, MandatoryStreamCombination combination) throws Exception {
        // Check whether substituting YUV_888 format with Y8 format
        boolean substituteY8 = false;
        if (staticInfo.isMonochromeWithY8()) {
            List<MandatoryStreamInformation> streamsInfo = combination.getStreamsInformation();
            for (MandatoryStreamInformation streamInfo : streamsInfo) {
                if (streamInfo.getFormat() == ImageFormat.YUV_420_888) {
                    substituteY8 = true;
                    break;
                }
            }
        }

        // Check whether substituting JPEG format with HEIC format
        boolean substituteHeic = false;
        if (staticInfo.isHeicSupported()) {
            List<MandatoryStreamInformation> streamsInfo = combination.getStreamsInformation();
            for (MandatoryStreamInformation streamInfo : streamsInfo) {
                if (streamInfo.getFormat() == ImageFormat.JPEG) {
                    substituteHeic = true;
                    break;
                }
            }
        }

        // Test camera output combination
        String log = "Testing mandatory stream combination: " + combination.getDescription() +
                " on camera: " + cameraId;
        if (physicalCameraId != null) {
            log += ", physical sub-camera: " + physicalCameraId;
        }
        Log.i(TAG, log);
        testMandatoryStreamCombination(cameraId, staticInfo, physicalCameraId, combination,
                /*substituteY8*/false, /*substituteHeic*/false, /*maxResolution*/false);

        if (substituteY8) {
            Log.i(TAG, log + " with Y8");
            testMandatoryStreamCombination(cameraId, staticInfo, physicalCameraId, combination,
                    /*substituteY8*/true, /*substituteHeic*/false, /*maxResolution*/false);
        }

        if (substituteHeic) {
            Log.i(TAG, log + " with HEIC");
            testMandatoryStreamCombination(cameraId, staticInfo, physicalCameraId, combination,
                    /*substituteY8*/false, /*substituteHeic*/true, /**maxResolution*/ false);
        }
    }

    private void testMandatoryStreamCombination(String cameraId,
            StaticMetadata staticInfo, String physicalCameraId,
            MandatoryStreamCombination combination,
            boolean substituteY8, boolean substituteHeic, boolean ultraHighResolution)
            throws Exception {
        // Timeout is relaxed by 1 second for LEGACY devices to reduce false positive rate in CTS
        // TODO: This needs to be adjusted based on feedback
        final int TIMEOUT_MULTIPLIER = ultraHighResolution ? 2 : 1;
        final int TIMEOUT_FOR_RESULT_MS =
                ((staticInfo.isHardwareLevelLegacy()) ? 2000 : 1000) * TIMEOUT_MULTIPLIER;
        final int MIN_RESULT_COUNT = 3;

        // Set up outputs
        List<OutputConfiguration> outputConfigs = new ArrayList<>();
        List<Surface> outputSurfaces = new ArrayList<Surface>();
        List<Surface> uhOutputSurfaces = new ArrayList<Surface>();
        StreamCombinationTargets targets = new StreamCombinationTargets();

        CameraTestUtils.setupConfigurationTargets(combination.getStreamsInformation(),
                targets, outputConfigs, outputSurfaces, uhOutputSurfaces, MIN_RESULT_COUNT,
                substituteY8, substituteHeic, physicalCameraId, /*multiResStreamConfig*/null,
                mHandler);

        boolean haveSession = false;
        try {
            CaptureRequest.Builder requestBuilder =
                    mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            CaptureRequest.Builder uhRequestBuilder =
                    mCamera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);

            for (Surface s : outputSurfaces) {
                requestBuilder.addTarget(s);
            }

            for (Surface s : uhOutputSurfaces) {
                uhRequestBuilder.addTarget(s);
            }
            // We need to explicitly set the sensor pixel mode to default since we're mixing default
            // and max resolution requests in the same capture session.
            requestBuilder.set(CaptureRequest.SENSOR_PIXEL_MODE,
                    CameraMetadata.SENSOR_PIXEL_MODE_DEFAULT);
            if (ultraHighResolution) {
                uhRequestBuilder.set(CaptureRequest.SENSOR_PIXEL_MODE,
                        CameraMetadata.SENSOR_PIXEL_MODE_MAXIMUM_RESOLUTION);
            }
            CameraCaptureSession.CaptureCallback mockCaptureCallback =
                    mock(CameraCaptureSession.CaptureCallback.class);

            if (physicalCameraId == null) {
                checkSessionConfigurationSupported(mCamera, mHandler, outputConfigs,
                        /*inputConfig*/ null, SessionConfiguration.SESSION_REGULAR,
                        mCameraManager, true/*defaultSupport*/, String.format(
                        "Session configuration query from combination: %s failed",
                        combination.getDescription()));
            } else {
                SessionConfigSupport sessionConfigSupport = isSessionConfigSupported(
                        mCamera, mHandler, outputConfigs, /*inputConfig*/ null,
                        SessionConfiguration.SESSION_REGULAR, mCameraManager,
                        false/*defaultSupport*/);
                assertTrue(
                        String.format("Session configuration query from combination: %s failed",
                        combination.getDescription()), !sessionConfigSupport.error);
                if (!sessionConfigSupport.callSupported) {
                    return;
                }
                assertTrue(
                        String.format("Session configuration must be supported for combination: " +
                        "%s", combination.getDescription()), sessionConfigSupport.configSupported);
            }

            createSessionByConfigs(outputConfigs);
            haveSession = true;
            CaptureRequest request = requestBuilder.build();
            CaptureRequest uhRequest = uhRequestBuilder.build();
            mCameraSession.setRepeatingRequest(request, mockCaptureCallback, mHandler);
            if (ultraHighResolution) {
                mCameraSession.capture(uhRequest, mockCaptureCallback, mHandler);
            }
            verify(mockCaptureCallback,
                    timeout(TIMEOUT_FOR_RESULT_MS * MIN_RESULT_COUNT).atLeast(MIN_RESULT_COUNT))
                    .onCaptureCompleted(
                        eq(mCameraSession),
                        eq(request),
                        isA(TotalCaptureResult.class));
            if (ultraHighResolution) {
                verify(mockCaptureCallback,
                        timeout(TIMEOUT_FOR_RESULT_MS).atLeast(1))
                        .onCaptureCompleted(
                            eq(mCameraSession),
                            eq(uhRequest),
                            isA(TotalCaptureResult.class));
            }

            verify(mockCaptureCallback, never()).
                    onCaptureFailed(
                        eq(mCameraSession),
                        eq(request),
                        isA(CaptureFailure.class));

        } catch (Throwable e) {
            mCollector.addMessage(String.format("Mandatory stream combination: %s failed due: %s",
                    combination.getDescription(), e.getMessage()));
        }
        if (haveSession) {
            try {
                Log.i(TAG, String.format("Done with camera %s, combination: %s, closing session",
                                cameraId, combination.getDescription()));
                stopCapture(/*fast*/false);
            } catch (Throwable e) {
                mCollector.addMessage(
                    String.format("Closing down for combination: %s failed due to: %s",
                            combination.getDescription(), e.getMessage()));
            }
        }

        targets.close();
    }

    /**
     * Test for making sure the required 10-bit stream combinations work as expected.
     * Since we have too many possible combinations between different 8-bit vs. 10-bit as well
     * as 10-bit dynamic profiles and in order to maximize the coverage within some reasonable
     * amount of iterations, the test case will configure 8-bit and 10-bit outputs randomly. In case
     * we have 10-bit output, then the dynamic range profile will also be randomly picked.
     */
    @Test
    public void testMandatory10BitStreamCombinations() throws Exception {
        for (String id : getCameraIdsUnderTest()) {
            openDevice(id);
            CameraCharacteristics chars = mStaticInfo.getCharacteristics();
            if (!CameraTestUtils.hasCapability(
                    chars, CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT)) {
                Log.i(TAG, "Camera id " + id + " doesn't support 10-bit output, skip test");
                closeDevice(id);
                continue;
            }
            CameraCharacteristics.Key<MandatoryStreamCombination []> ck =
                    CameraCharacteristics.SCALER_MANDATORY_TEN_BIT_OUTPUT_STREAM_COMBINATIONS;

            MandatoryStreamCombination[] combinations = chars.get(ck);
            assertNotNull(combinations);

            try {
                for (MandatoryStreamCombination combination : combinations) {
                    Log.i(TAG, "Testing fixed mandatory 10-bit output stream combination: " +
                            combination.getDescription() + " on camera: " + id);
                    DynamicRangeProfiles profiles = mStaticInfo.getCharacteristics().get(
                            CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES);
                    assertNotNull(profiles);

                    // First we want to make sure that a fixed set of 10-bit streams
                    // is functional
                    for (Long profile : profiles.getSupportedProfiles()) {
                        if (profile != DynamicRangeProfiles.STANDARD) {
                            ArrayList<Long> testProfiles = new ArrayList<Long>();
                            testProfiles.add(profile);
                            testMandatory10BitStreamCombination(id, combination, profiles,
                                    testProfiles);
                        }
                    }

                    Log.i(TAG, "Testing random mandatory 10-bit output stream combination: " +
                            combination.getDescription() + " on camera: " + id);
                    // Next try out a random mix of standard 8-bit and 10-bit profiles.
                    // The number of possible combinations is quite big and testing them
                    // all on physical hardware can become unfeasible.
                    ArrayList<Long> testProfiles = new ArrayList<>(
                            profiles.getSupportedProfiles());
                    testMandatory10BitStreamCombination(id, combination, profiles, testProfiles);
                }
            } finally {
                closeDevice(id);
            }
        }
    }

    private void testMandatory10BitStreamCombination(String cameraId,
            MandatoryStreamCombination combination, DynamicRangeProfiles profiles,
            List<Long> testProfiles) {
        final int TIMEOUT_FOR_RESULT_MS = 1000;
        final int MIN_RESULT_COUNT = 3;

        // Setup outputs
        List<OutputConfiguration> outputConfigs = new ArrayList<>();
        List<Surface> outputSurfaces = new ArrayList<Surface>();
        List<Surface> uhOutputSurfaces = new ArrayList<Surface>();
        StreamCombinationTargets targets = new StreamCombinationTargets();

        CameraTestUtils.setupConfigurationTargets(combination.getStreamsInformation(),
                targets, outputConfigs, outputSurfaces, uhOutputSurfaces, MIN_RESULT_COUNT,
                /*substituteY8*/ false, /*substituteHeic*/false,
                /*physicalCameraId*/ null,
                /*multiResStreamConfig*/null, mHandler,
                testProfiles);

        try {
            checkSessionConfigurationSupported(mCamera, mHandler, outputConfigs,
                    /*inputConfig*/ null, SessionConfiguration.SESSION_REGULAR,
                    mCameraManager, true/*defaultSupport*/,
                    String.format("Session configuration query from combination: %s failed",
                            combination.getDescription()));

            createSessionByConfigs(outputConfigs);

            boolean constraintPresent = false;
            List<Surface> constrainedOutputs = new ArrayList<>(outputSurfaces);

            while (!outputSurfaces.isEmpty()) {
                CaptureRequest.Builder requestBuilder =
                        mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
                // Check to see how many outputs can be combined in to a single request including
                // the first output surface and respecting the advertised constraints
                Iterator<OutputConfiguration> it = outputConfigs.iterator();
                OutputConfiguration config = it.next();
                HashSet<Long> currentProfiles = new HashSet<>();
                currentProfiles.add(config.getDynamicRangeProfile());
                requestBuilder.addTarget(config.getSurface());
                outputSurfaces.remove(config.getSurface());
                it.remove();
                while (it.hasNext()) {
                    config = it.next();
                    Long currentProfile = config.getDynamicRangeProfile();
                    Set<Long> newLimitations = profiles.getProfileCaptureRequestConstraints(
                            currentProfile);
                    if (newLimitations.isEmpty() || (newLimitations.containsAll(currentProfiles))) {
                        currentProfiles.add(currentProfile);
                        requestBuilder.addTarget(config.getSurface());
                        outputSurfaces.remove(config.getSurface());
                        it.remove();
                    } else if (!constraintPresent && !newLimitations.isEmpty() &&
                            !newLimitations.containsAll(currentProfiles)) {
                        constraintPresent = true;
                    }
                }

                CaptureRequest request = requestBuilder.build();
                CameraCaptureSession.CaptureCallback mockCaptureCallback =
                        mock(CameraCaptureSession.CaptureCallback.class);
                mCameraSession.capture(request, mockCaptureCallback, mHandler);
                verify(mockCaptureCallback,
                        timeout(TIMEOUT_FOR_RESULT_MS).atLeastOnce())
                        .onCaptureCompleted(
                                eq(mCameraSession),
                                eq(request),
                                isA(TotalCaptureResult.class));

                verify(mockCaptureCallback, never()).
                        onCaptureFailed(
                                eq(mCameraSession),
                                eq(request),
                                isA(CaptureFailure.class));
            }

            if (constraintPresent) {
                // Capture requests that include output surfaces with dynamic range profiles that
                // cannot be combined must throw a corresponding exception
                CaptureRequest.Builder requestBuilder =
                        mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
                for (Surface s : constrainedOutputs) {
                    requestBuilder.addTarget(s);
                }

                CaptureRequest request = requestBuilder.build();
                CameraCaptureSession.CaptureCallback mockCaptureCallback =
                        mock(CameraCaptureSession.CaptureCallback.class);
                try {
                    mCameraSession.capture(request, mockCaptureCallback, mHandler);
                    fail("Capture request to outputs with incompatible dynamic range profiles "
                            + "must always fail!");
                } catch (IllegalArgumentException e) {
                    // Expected
                }
            }

            Log.i(TAG, String.format("Done with camera %s, combination: %s, closing session",
                    cameraId, combination.getDescription()));
        } catch (Throwable e) {
            mCollector.addMessage(
                    String.format("Closing down for combination: %s failed due to: %s",
                            combination.getDescription(), e.getMessage()));
        }

        targets.close();
    }

    /**
     * Test for making sure the required reprocess input/output combinations for each hardware
     * level and capability work as expected.
     */
    @Test
    public void testMandatoryReprocessConfigurations() throws Exception {
        testMandatoryReprocessConfigurations(/*maxResolution*/false);
    }

    /**
     * Test for making sure the required reprocess input/output combinations for each hardware
     * level and capability work as expected.
     */
    @Test
    public void testMandatoryMaximumResolutionReprocessConfigurations() throws Exception {
        testMandatoryReprocessConfigurations(/*maxResolution*/true);
    }

    /**
     * Test for making sure the required reprocess input/output combinations for each hardware
     * level and capability work as expected.
     */
    public void testMandatoryReprocessConfigurations(boolean maxResolution) throws Exception {
        for (String id : getCameraIdsUnderTest()) {
            openDevice(id);
            CameraCharacteristics chars = mStaticInfo.getCharacteristics();
            if (maxResolution && !CameraTestUtils.hasCapability(
                  chars, CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_REMOSAIC_REPROCESSING)) {
                Log.i(TAG, "Camera id " + id + "doesn't support REMOSAIC_REPROCESSING, skip test");
                closeDevice(id);
                continue;
            }
            CameraCharacteristics.Key<MandatoryStreamCombination []> ck =
                    CameraCharacteristics.SCALER_MANDATORY_STREAM_COMBINATIONS;

            if (maxResolution) {
                ck = CameraCharacteristics.SCALER_MANDATORY_MAXIMUM_RESOLUTION_STREAM_COMBINATIONS;
            }

            MandatoryStreamCombination[] combinations = chars.get(ck);
            if (combinations == null) {
                Log.i(TAG, "No mandatory stream combinations for camera: " + id + " skip test");
                closeDevice(id);
                continue;
            }

            try {
                for (MandatoryStreamCombination combination : combinations) {
                    if (combination.isReprocessable()) {
                        Log.i(TAG, "Testing mandatory reprocessable stream combination: " +
                                combination.getDescription() + " on camera: " + id);
                        testMandatoryReprocessableStreamCombination(id, combination, maxResolution);
                    }
                }
            } finally {
                closeDevice(id);
            }
        }
    }

    private void testMandatoryReprocessableStreamCombination(String cameraId,
            MandatoryStreamCombination combination, boolean maxResolution)  throws Exception {
        // Test reprocess stream combination
        testMandatoryReprocessableStreamCombination(cameraId, combination,
                /*substituteY8*/false, /*substituteHeic*/false, maxResolution/*maxResolution*/);
        if (maxResolution) {
            // Maximum resolution mode doesn't guarantee HEIC and Y8 streams.
            return;
        }

        // Test substituting YUV_888 format with Y8 format in reprocess stream combination.
        if (mStaticInfo.isMonochromeWithY8()) {
            List<MandatoryStreamInformation> streamsInfo = combination.getStreamsInformation();
            boolean substituteY8 = false;
            for (MandatoryStreamInformation streamInfo : streamsInfo) {
                if (streamInfo.getFormat() == ImageFormat.YUV_420_888) {
                    substituteY8 = true;
                }
            }
            if (substituteY8) {
                testMandatoryReprocessableStreamCombination(cameraId, combination,
                        /*substituteY8*/true, /*substituteHeic*/false, false/*maxResolution*/);
            }
        }

        if (mStaticInfo.isHeicSupported()) {
            List<MandatoryStreamInformation> streamsInfo = combination.getStreamsInformation();
            boolean substituteHeic = false;
            for (MandatoryStreamInformation streamInfo : streamsInfo) {
                if (streamInfo.getFormat() == ImageFormat.JPEG) {
                    substituteHeic = true;
                }
            }
            if (substituteHeic) {
                testMandatoryReprocessableStreamCombination(cameraId, combination,
                        /*substituteY8*/false, /*substituteHeic*/true, false/*maxResolution*/);
            }
        }
    }

    private void testMandatoryReprocessableStreamCombination(String cameraId,
            MandatoryStreamCombination combination, boolean substituteY8,
            boolean substituteHeic, boolean maxResolution) throws Exception {

        final int TIMEOUT_MULTIPLIER = maxResolution ? 2 : 1;
        final int TIMEOUT_FOR_RESULT_MS = 5000 * TIMEOUT_MULTIPLIER;
        final int NUM_REPROCESS_CAPTURES_PER_CONFIG = 3;

        StreamCombinationTargets targets = new StreamCombinationTargets();
        ArrayList<Surface> defaultOutputSurfaces = new ArrayList<>();
        ArrayList<Surface> allOutputSurfaces = new ArrayList<>();
        List<OutputConfiguration> outputConfigs = new ArrayList<>();
        List<Surface> uhOutputSurfaces = new ArrayList<Surface>();
        ImageReader inputReader = null;
        ImageWriter inputWriter = null;
        SimpleImageReaderListener inputReaderListener = new SimpleImageReaderListener();
        SimpleCaptureCallback inputCaptureListener = new SimpleCaptureCallback();
        SimpleCaptureCallback reprocessOutputCaptureListener = new SimpleCaptureCallback();

        List<MandatoryStreamInformation> streamInfo = combination.getStreamsInformation();
        assertTrue("Reprocessable stream combinations should have at least 3 or more streams",
                (streamInfo != null) && (streamInfo.size() >= 3));

        assertTrue("The first mandatory stream information in a reprocessable combination must " +
                "always be input", streamInfo.get(0).isInput());

        List<Size> inputSizes = streamInfo.get(0).getAvailableSizes();
        int inputFormat = streamInfo.get(0).getFormat();
        if (substituteY8 && (inputFormat == ImageFormat.YUV_420_888)) {
            inputFormat = ImageFormat.Y8;
        }

        Log.i(TAG, "testMandatoryReprocessableStreamCombination: " +
                combination.getDescription() + ", substituteY8 = " + substituteY8 +
                ", substituteHeic = " + substituteHeic);
        try {
            // The second stream information entry is the ZSL stream, which is configured
            // separately.
            List<MandatoryStreamInformation> mandatoryStreamInfos = null;
            mandatoryStreamInfos = new ArrayList<MandatoryStreamInformation>();
            mandatoryStreamInfos = streamInfo.subList(2, streamInfo.size());
            CameraTestUtils.setupConfigurationTargets(mandatoryStreamInfos, targets,
                    outputConfigs, defaultOutputSurfaces, uhOutputSurfaces,
                    NUM_REPROCESS_CAPTURES_PER_CONFIG,
                    substituteY8, substituteHeic, null/*overridePhysicalCameraId*/,
                    /*multiResStreamConfig*/null, mHandler);
            allOutputSurfaces.addAll(defaultOutputSurfaces);
            allOutputSurfaces.addAll(uhOutputSurfaces);
            InputConfiguration inputConfig = new InputConfiguration(inputSizes.get(0).getWidth(),
                    inputSizes.get(0).getHeight(), inputFormat);

            // For each config, YUV and JPEG outputs will be tested. (For YUV/Y8 reprocessing,
            // the YUV/Y8 ImageReader for input is also used for output.)
            final boolean inputIsYuv = inputConfig.getFormat() == ImageFormat.YUV_420_888;
            final boolean inputIsY8 = inputConfig.getFormat() == ImageFormat.Y8;
            final boolean useYuv = inputIsYuv || targets.mYuvTargets.size() > 0;
            final boolean useY8 = inputIsY8 || targets.mY8Targets.size() > 0;
            final int totalNumReprocessCaptures =  NUM_REPROCESS_CAPTURES_PER_CONFIG *
                    (maxResolution ? 1 : (((inputIsYuv || inputIsY8) ? 1 : 0) +
                    (substituteHeic ? targets.mHeicTargets.size() : targets.mJpegTargets.size()) +
                    (useYuv ? targets.mYuvTargets.size() : targets.mY8Targets.size())));

            // It needs 1 input buffer for each reprocess capture + the number of buffers
            // that will be used as outputs.
            inputReader = ImageReader.newInstance(inputConfig.getWidth(), inputConfig.getHeight(),
                    inputConfig.getFormat(),
                    totalNumReprocessCaptures + NUM_REPROCESS_CAPTURES_PER_CONFIG);
            inputReader.setOnImageAvailableListener(inputReaderListener, mHandler);
            allOutputSurfaces.add(inputReader.getSurface());

            checkSessionConfigurationWithSurfaces(mCamera, mHandler, allOutputSurfaces,
                    inputConfig, SessionConfiguration.SESSION_REGULAR, mCameraManager,
                    /*defaultSupport*/ true, String.format("Session configuration query %s failed",
                    combination.getDescription()));

            // Verify we can create a reprocessable session with the input and all outputs.
            BlockingSessionCallback sessionListener = new BlockingSessionCallback();
            CameraCaptureSession session = configureReprocessableCameraSession(mCamera,
                    inputConfig, allOutputSurfaces, sessionListener, mHandler);
            inputWriter = ImageWriter.newInstance(session.getInputSurface(),
                    totalNumReprocessCaptures);

            // Prepare a request for reprocess input
            CaptureRequest.Builder builder = mCamera.createCaptureRequest(
                    CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG);
            builder.addTarget(inputReader.getSurface());
            if (maxResolution) {
                builder.set(CaptureRequest.SENSOR_PIXEL_MODE,
                        CameraMetadata.SENSOR_PIXEL_MODE_MAXIMUM_RESOLUTION);
            }

            for (int i = 0; i < totalNumReprocessCaptures; i++) {
                session.capture(builder.build(), inputCaptureListener, mHandler);
            }

            List<CaptureRequest> reprocessRequests = new ArrayList<>();
            List<Surface> reprocessOutputs = new ArrayList<>();

            if (maxResolution) {
                if (uhOutputSurfaces.size() == 0) { // RAW -> RAW reprocessing
                    reprocessOutputs.add(inputReader.getSurface());
                } else {
                    for (Surface surface : uhOutputSurfaces) {
                        reprocessOutputs.add(surface);
                    }
                }
            } else {
                if (inputIsYuv || inputIsY8) {
                    reprocessOutputs.add(inputReader.getSurface());
                }

                for (ImageReader reader : targets.mJpegTargets) {
                    reprocessOutputs.add(reader.getSurface());
                }

                for (ImageReader reader : targets.mHeicTargets) {
                    reprocessOutputs.add(reader.getSurface());
                }

                for (ImageReader reader : targets.mYuvTargets) {
                    reprocessOutputs.add(reader.getSurface());
                }

                for (ImageReader reader : targets.mY8Targets) {
                    reprocessOutputs.add(reader.getSurface());
                }
            }

            for (int i = 0; i < NUM_REPROCESS_CAPTURES_PER_CONFIG; i++) {
                for (Surface output : reprocessOutputs) {
                    TotalCaptureResult result = inputCaptureListener.getTotalCaptureResult(
                            TIMEOUT_FOR_RESULT_MS);
                    builder =  mCamera.createReprocessCaptureRequest(result);
                    inputWriter.queueInputImage(
                            inputReaderListener.getImage(TIMEOUT_FOR_RESULT_MS));
                    builder.addTarget(output);
                    reprocessRequests.add(builder.build());
                }
            }

            session.captureBurst(reprocessRequests, reprocessOutputCaptureListener, mHandler);

            for (int i = 0; i < reprocessOutputs.size() * NUM_REPROCESS_CAPTURES_PER_CONFIG; i++) {
                TotalCaptureResult result = reprocessOutputCaptureListener.getTotalCaptureResult(
                        TIMEOUT_FOR_RESULT_MS);
            }
        } catch (Throwable e) {
            mCollector.addMessage(String.format("Reprocess stream combination %s failed due to: %s",
                    combination.getDescription(), e.getMessage()));
        } finally {
            inputReaderListener.drain();
            reprocessOutputCaptureListener.drain();
            targets.close();

            if (inputReader != null) {
                inputReader.close();
            }

            if (inputWriter != null) {
                inputWriter.close();
            }
        }
    }

    @Test
    public void testBasicTriggerSequence() throws Exception {

        for (String id : getCameraIdsUnderTest()) {
            Log.i(TAG, String.format("Testing Camera %s", id));

            try {
                // Legacy devices do not support precapture trigger; don't test devices that
                // can't focus
                StaticMetadata staticInfo = mAllStaticInfo.get(id);
                if (staticInfo.isHardwareLevelLegacy() || !staticInfo.hasFocuser()) {
                    continue;
                }
                // Depth-only devices won't support AE
                if (!staticInfo.isColorOutputSupported()) {
                    Log.i(TAG, "Camera " + id + " does not support color outputs, skipping");
                    continue;
                }

                openDevice(id);
                int[] availableAfModes = mStaticInfo.getAfAvailableModesChecked();
                int[] availableAeModes = mStaticInfo.getAeAvailableModesChecked();

                for (int afMode : availableAfModes) {
                    if (afMode == CameraCharacteristics.CONTROL_AF_MODE_OFF ||
                            afMode == CameraCharacteristics.CONTROL_AF_MODE_EDOF) {
                        // Only test AF modes that have meaningful trigger behavior
                        continue;
                    }

                    for (int aeMode : availableAeModes) {
                        if (aeMode ==  CameraCharacteristics.CONTROL_AE_MODE_OFF) {
                            // Only test AE modes that have meaningful trigger behavior
                            continue;
                        }

                        SurfaceTexture preview = new SurfaceTexture(/*random int*/ 1);

                        CaptureRequest.Builder previewRequest =
                                prepareTriggerTestSession(preview, aeMode, afMode);

                        SimpleCaptureCallback captureListener =
                                new CameraTestUtils.SimpleCaptureCallback();

                        mCameraSession.setRepeatingRequest(previewRequest.build(), captureListener,
                                mHandler);

                        // Cancel triggers

                        cancelTriggersAndWait(previewRequest, captureListener, afMode);

                        //
                        // Standard sequence - AF trigger then AE trigger

                        if (VERBOSE) {
                            Log.v(TAG, String.format("Triggering AF"));
                        }

                        previewRequest.set(CaptureRequest.CONTROL_AF_TRIGGER,
                                CaptureRequest.CONTROL_AF_TRIGGER_START);
                        previewRequest.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
                                CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE);

                        CaptureRequest triggerRequest = previewRequest.build();
                        mCameraSession.capture(triggerRequest, captureListener, mHandler);

                        CaptureResult triggerResult = captureListener.getCaptureResultForRequest(
                                triggerRequest, MAX_RESULT_STATE_CHANGE_WAIT_FRAMES);
                        int afState = triggerResult.get(CaptureResult.CONTROL_AF_STATE);
                        boolean focusComplete = false;

                        for (int i = 0;
                             i < MAX_TRIGGER_SEQUENCE_FRAMES && !focusComplete;
                             i++) {

                            focusComplete = verifyAfSequence(afMode, afState, focusComplete);

                            CaptureResult focusResult = captureListener.getCaptureResult(
                                    CameraTestUtils.CAPTURE_RESULT_TIMEOUT_MS);
                            afState = focusResult.get(CaptureResult.CONTROL_AF_STATE);
                        }

                        assertTrue("Focusing never completed!", focusComplete);

                        // Standard sequence - Part 2 AE trigger

                        if (VERBOSE) {
                            Log.v(TAG, String.format("Triggering AE"));
                        }

                        previewRequest.set(CaptureRequest.CONTROL_AF_TRIGGER,
                                CaptureRequest.CONTROL_AF_TRIGGER_IDLE);
                        previewRequest.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
                                CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);

                        triggerRequest = previewRequest.build();
                        mCameraSession.capture(triggerRequest, captureListener, mHandler);

                        triggerResult = captureListener.getCaptureResultForRequest(
                                triggerRequest, MAX_RESULT_STATE_CHANGE_WAIT_FRAMES);

                        int aeState = triggerResult.get(CaptureResult.CONTROL_AE_STATE);

                        boolean precaptureComplete = false;

                        for (int i = 0;
                             i < MAX_TRIGGER_SEQUENCE_FRAMES && !precaptureComplete;
                             i++) {

                            precaptureComplete = verifyAeSequence(aeState, precaptureComplete);

                            CaptureResult precaptureResult = captureListener.getCaptureResult(
                                CameraTestUtils.CAPTURE_RESULT_TIMEOUT_MS);
                            aeState = precaptureResult.get(CaptureResult.CONTROL_AE_STATE);
                        }

                        assertTrue("Precapture sequence never completed!", precaptureComplete);

                        for (int i = 0; i < MAX_RESULT_STATE_POSTCHANGE_WAIT_FRAMES; i++) {
                            CaptureResult postPrecaptureResult = captureListener.getCaptureResult(
                                CameraTestUtils.CAPTURE_RESULT_TIMEOUT_MS);
                            aeState = postPrecaptureResult.get(CaptureResult.CONTROL_AE_STATE);
                            assertTrue("Late transition to PRECAPTURE state seen",
                                    aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE);
                        }

                        // Done

                        stopCapture(/*fast*/ false);
                        preview.release();
                    }

                }

            } finally {
                closeDevice(id);
            }
        }

    }

    @Test
    public void testSimultaneousTriggers() throws Exception {
        for (String id : getCameraIdsUnderTest()) {
            Log.i(TAG, String.format("Testing Camera %s", id));

            try {
                // Legacy devices do not support precapture trigger; don't test devices that
                // can't focus
                StaticMetadata staticInfo = mAllStaticInfo.get(id);
                if (staticInfo.isHardwareLevelLegacy() || !staticInfo.hasFocuser()) {
                    continue;
                }
                // Depth-only devices won't support AE
                if (!staticInfo.isColorOutputSupported()) {
                    Log.i(TAG, "Camera " + id + " does not support color outputs, skipping");
                    continue;
                }

                openDevice(id);
                int[] availableAfModes = mStaticInfo.getAfAvailableModesChecked();
                int[] availableAeModes = mStaticInfo.getAeAvailableModesChecked();

                for (int afMode : availableAfModes) {
                    if (afMode == CameraCharacteristics.CONTROL_AF_MODE_OFF ||
                            afMode == CameraCharacteristics.CONTROL_AF_MODE_EDOF) {
                        // Only test AF modes that have meaningful trigger behavior
                        continue;
                    }

                    for (int aeMode : availableAeModes) {
                        if (aeMode ==  CameraCharacteristics.CONTROL_AE_MODE_OFF) {
                            // Only test AE modes that have meaningful trigger behavior
                            continue;
                        }

                        SurfaceTexture preview = new SurfaceTexture(/*random int*/ 1);

                        CaptureRequest.Builder previewRequest =
                                prepareTriggerTestSession(preview, aeMode, afMode);

                        SimpleCaptureCallback captureListener =
                                new CameraTestUtils.SimpleCaptureCallback();

                        mCameraSession.setRepeatingRequest(previewRequest.build(), captureListener,
                                mHandler);

                        // Cancel triggers

                        cancelTriggersAndWait(previewRequest, captureListener, afMode);

                        //
                        // Trigger AF and AE together

                        if (VERBOSE) {
                            Log.v(TAG, String.format("Triggering AF and AE together"));
                        }

                        previewRequest.set(CaptureRequest.CONTROL_AF_TRIGGER,
                                CaptureRequest.CONTROL_AF_TRIGGER_START);
                        previewRequest.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
                                CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);

                        CaptureRequest triggerRequest = previewRequest.build();
                        mCameraSession.capture(triggerRequest, captureListener, mHandler);

                        CaptureResult triggerResult = captureListener.getCaptureResultForRequest(
                                triggerRequest, MAX_RESULT_STATE_CHANGE_WAIT_FRAMES);
                        int aeState = triggerResult.get(CaptureResult.CONTROL_AE_STATE);
                        int afState = triggerResult.get(CaptureResult.CONTROL_AF_STATE);

                        boolean precaptureComplete = false;
                        boolean focusComplete = false;

                        for (int i = 0;
                             i < MAX_TRIGGER_SEQUENCE_FRAMES &&
                                     !(focusComplete && precaptureComplete);
                             i++) {

                            focusComplete = verifyAfSequence(afMode, afState, focusComplete);
                            precaptureComplete = verifyAeSequence(aeState, precaptureComplete);

                            CaptureResult sequenceResult = captureListener.getCaptureResult(
                                    CameraTestUtils.CAPTURE_RESULT_TIMEOUT_MS);
                            afState = sequenceResult.get(CaptureResult.CONTROL_AF_STATE);
                            aeState = sequenceResult.get(CaptureResult.CONTROL_AE_STATE);
                        }

                        assertTrue("Precapture sequence never completed!", precaptureComplete);
                        assertTrue("Focus sequence never completed!", focusComplete);

                        // Done

                        stopCapture(/*fast*/ false);
                        preview.release();

                    }
                }
            } finally {
                closeDevice(id);
            }
        }
    }

    @Test
    public void testAfThenAeTrigger() throws Exception {
        for (String id : getCameraIdsUnderTest()) {
            Log.i(TAG, String.format("Testing Camera %s", id));

            try {
                // Legacy devices do not support precapture trigger; don't test devices that
                // can't focus
                StaticMetadata staticInfo = mAllStaticInfo.get(id);
                if (staticInfo.isHardwareLevelLegacy() || !staticInfo.hasFocuser()) {
                    continue;
                }
                // Depth-only devices won't support AE
                if (!staticInfo.isColorOutputSupported()) {
                    Log.i(TAG, "Camera " + id + " does not support color outputs, skipping");
                    continue;
                }

                openDevice(id);
                int[] availableAfModes = mStaticInfo.getAfAvailableModesChecked();
                int[] availableAeModes = mStaticInfo.getAeAvailableModesChecked();

                for (int afMode : availableAfModes) {
                    if (afMode == CameraCharacteristics.CONTROL_AF_MODE_OFF ||
                            afMode == CameraCharacteristics.CONTROL_AF_MODE_EDOF) {
                        // Only test AF modes that have meaningful trigger behavior
                        continue;
                    }

                    for (int aeMode : availableAeModes) {
                        if (aeMode ==  CameraCharacteristics.CONTROL_AE_MODE_OFF) {
                            // Only test AE modes that have meaningful trigger behavior
                            continue;
                        }

                        SurfaceTexture preview = new SurfaceTexture(/*random int*/ 1);

                        CaptureRequest.Builder previewRequest =
                                prepareTriggerTestSession(preview, aeMode, afMode);

                        SimpleCaptureCallback captureListener =
                                new CameraTestUtils.SimpleCaptureCallback();

                        mCameraSession.setRepeatingRequest(previewRequest.build(), captureListener,
                                mHandler);

                        // Cancel triggers

                        cancelTriggersAndWait(previewRequest, captureListener, afMode);

                        //
                        // AF with AE a request later

                        if (VERBOSE) {
                            Log.v(TAG, "Trigger AF, then AE trigger on next request");
                        }

                        previewRequest.set(CaptureRequest.CONTROL_AF_TRIGGER,
                                CaptureRequest.CONTROL_AF_TRIGGER_START);
                        previewRequest.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
                                CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE);

                        CaptureRequest triggerRequest = previewRequest.build();
                        mCameraSession.capture(triggerRequest, captureListener, mHandler);

                        previewRequest.set(CaptureRequest.CONTROL_AF_TRIGGER,
                                CaptureRequest.CONTROL_AF_TRIGGER_IDLE);
                        previewRequest.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
                                CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);

                        CaptureRequest triggerRequest2 = previewRequest.build();
                        mCameraSession.capture(triggerRequest2, captureListener, mHandler);

                        CaptureResult triggerResult = captureListener.getCaptureResultForRequest(
                                triggerRequest, MAX_RESULT_STATE_CHANGE_WAIT_FRAMES);
                        int afState = triggerResult.get(CaptureResult.CONTROL_AF_STATE);

                        boolean precaptureComplete = false;
                        boolean focusComplete = false;

                        focusComplete = verifyAfSequence(afMode, afState, focusComplete);

                        triggerResult = captureListener.getCaptureResultForRequest(
                                triggerRequest2, MAX_RESULT_STATE_CHANGE_WAIT_FRAMES);
                        afState = triggerResult.get(CaptureResult.CONTROL_AF_STATE);
                        int aeState = triggerResult.get(CaptureResult.CONTROL_AE_STATE);

                        for (int i = 0;
                             i < MAX_TRIGGER_SEQUENCE_FRAMES &&
                                     !(focusComplete && precaptureComplete);
                             i++) {

                            focusComplete = verifyAfSequence(afMode, afState, focusComplete);
                            precaptureComplete = verifyAeSequence(aeState, precaptureComplete);

                            CaptureResult sequenceResult = captureListener.getCaptureResult(
                                    CameraTestUtils.CAPTURE_RESULT_TIMEOUT_MS);
                            afState = sequenceResult.get(CaptureResult.CONTROL_AF_STATE);
                            aeState = sequenceResult.get(CaptureResult.CONTROL_AE_STATE);
                        }

                        assertTrue("Precapture sequence never completed!", precaptureComplete);
                        assertTrue("Focus sequence never completed!", focusComplete);

                        // Done

                        stopCapture(/*fast*/ false);
                        preview.release();

                    }
                }
            } finally {
                closeDevice(id);
            }
        }
    }

    @Test
    public void testAeThenAfTrigger() throws Exception {
        for (String id : getCameraIdsUnderTest()) {
            Log.i(TAG, String.format("Testing Camera %s", id));

            try {
                // Legacy devices do not support precapture trigger; don't test devices that
                // can't focus
                StaticMetadata staticInfo = mAllStaticInfo.get(id);
                if (staticInfo.isHardwareLevelLegacy() || !staticInfo.hasFocuser()) {
                    continue;
                }
                // Depth-only devices won't support AE
                if (!staticInfo.isColorOutputSupported()) {
                    Log.i(TAG, "Camera " + id + " does not support color outputs, skipping");
                    continue;
                }

                openDevice(id);
                int[] availableAfModes = mStaticInfo.getAfAvailableModesChecked();
                int[] availableAeModes = mStaticInfo.getAeAvailableModesChecked();

                for (int afMode : availableAfModes) {
                    if (afMode == CameraCharacteristics.CONTROL_AF_MODE_OFF ||
                            afMode == CameraCharacteristics.CONTROL_AF_MODE_EDOF) {
                        // Only test AF modes that have meaningful trigger behavior
                        continue;
                    }

                    for (int aeMode : availableAeModes) {
                        if (aeMode ==  CameraCharacteristics.CONTROL_AE_MODE_OFF) {
                            // Only test AE modes that have meaningful trigger behavior
                            continue;
                        }

                        SurfaceTexture preview = new SurfaceTexture(/*random int*/ 1);

                        CaptureRequest.Builder previewRequest =
                                prepareTriggerTestSession(preview, aeMode, afMode);

                        SimpleCaptureCallback captureListener =
                                new CameraTestUtils.SimpleCaptureCallback();

                        mCameraSession.setRepeatingRequest(previewRequest.build(), captureListener,
                                mHandler);

                        // Cancel triggers

                        cancelTriggersAndWait(previewRequest, captureListener, afMode);

                        //
                        // AE with AF a request later

                        if (VERBOSE) {
                            Log.v(TAG, "Trigger AE, then AF trigger on next request");
                        }

                        previewRequest.set(CaptureRequest.CONTROL_AF_TRIGGER,
                                CaptureRequest.CONTROL_AF_TRIGGER_IDLE);
                        previewRequest.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
                                CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);

                        CaptureRequest triggerRequest = previewRequest.build();
                        mCameraSession.capture(triggerRequest, captureListener, mHandler);

                        previewRequest.set(CaptureRequest.CONTROL_AF_TRIGGER,
                                CaptureRequest.CONTROL_AF_TRIGGER_START);
                        previewRequest.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
                                CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE);

                        CaptureRequest triggerRequest2 = previewRequest.build();
                        mCameraSession.capture(triggerRequest2, captureListener, mHandler);

                        CaptureResult triggerResult = captureListener.getCaptureResultForRequest(
                                triggerRequest, MAX_RESULT_STATE_CHANGE_WAIT_FRAMES);
                        int aeState = triggerResult.get(CaptureResult.CONTROL_AE_STATE);

                        boolean precaptureComplete = false;
                        boolean focusComplete = false;

                        precaptureComplete = verifyAeSequence(aeState, precaptureComplete);

                        triggerResult = captureListener.getCaptureResultForRequest(
                                triggerRequest2, MAX_RESULT_STATE_CHANGE_WAIT_FRAMES);
                        int afState = triggerResult.get(CaptureResult.CONTROL_AF_STATE);
                        aeState = triggerResult.get(CaptureResult.CONTROL_AE_STATE);

                        for (int i = 0;
                             i < MAX_TRIGGER_SEQUENCE_FRAMES &&
                                     !(focusComplete && precaptureComplete);
                             i++) {

                            focusComplete = verifyAfSequence(afMode, afState, focusComplete);
                            precaptureComplete = verifyAeSequence(aeState, precaptureComplete);

                            CaptureResult sequenceResult = captureListener.getCaptureResult(
                                    CameraTestUtils.CAPTURE_RESULT_TIMEOUT_MS);
                            afState = sequenceResult.get(CaptureResult.CONTROL_AF_STATE);
                            aeState = sequenceResult.get(CaptureResult.CONTROL_AE_STATE);
                        }

                        assertTrue("Precapture sequence never completed!", precaptureComplete);
                        assertTrue("Focus sequence never completed!", focusComplete);

                        // Done

                        stopCapture(/*fast*/ false);
                        preview.release();

                    }
                }
            } finally {
                closeDevice(id);
            }
        }
    }

    @Test
    public void testAeAndAfCausality() throws Exception {

        for (String id : getCameraIdsUnderTest()) {
            Log.i(TAG, String.format("Testing Camera %s", id));

            try {
                // Legacy devices do not support precapture trigger; don't test devices that
                // can't focus
                StaticMetadata staticInfo = mAllStaticInfo.get(id);
                if (staticInfo.isHardwareLevelLegacy() || !staticInfo.hasFocuser()) {
                    continue;
                }
                // Depth-only devices won't support AE
                if (!staticInfo.isColorOutputSupported()) {
                    Log.i(TAG, "Camera " + id + " does not support color outputs, skipping");
                    continue;
                }

                openDevice(id);
                int[] availableAfModes = mStaticInfo.getAfAvailableModesChecked();
                int[] availableAeModes = mStaticInfo.getAeAvailableModesChecked();
                final int maxPipelineDepth = mStaticInfo.getCharacteristics().get(
                        CameraCharacteristics.REQUEST_PIPELINE_MAX_DEPTH);

                for (int afMode : availableAfModes) {
                    if (afMode == CameraCharacteristics.CONTROL_AF_MODE_OFF ||
                            afMode == CameraCharacteristics.CONTROL_AF_MODE_EDOF) {
                        // Only test AF modes that have meaningful trigger behavior
                        continue;
                    }
                    for (int aeMode : availableAeModes) {
                        if (aeMode ==  CameraCharacteristics.CONTROL_AE_MODE_OFF) {
                            // Only test AE modes that have meaningful trigger behavior
                            continue;
                        }

                        SurfaceTexture preview = new SurfaceTexture(/*random int*/ 1);

                        CaptureRequest.Builder previewRequest =
                                prepareTriggerTestSession(preview, aeMode, afMode);

                        SimpleCaptureCallback captureListener =
                                new CameraTestUtils.SimpleCaptureCallback();

                        mCameraSession.setRepeatingRequest(previewRequest.build(), captureListener,
                                mHandler);

                        List<CaptureRequest> triggerRequests =
                                new ArrayList<CaptureRequest>(maxPipelineDepth+1);
                        for (int i = 0; i < maxPipelineDepth; i++) {
                            triggerRequests.add(previewRequest.build());
                        }

                        // Cancel triggers
                        cancelTriggersAndWait(previewRequest, captureListener, afMode);

                        //
                        // Standard sequence - Part 1 AF trigger

                        if (VERBOSE) {
                            Log.v(TAG, String.format("Triggering AF"));
                        }

                        previewRequest.set(CaptureRequest.CONTROL_AF_TRIGGER,
                                CaptureRequest.CONTROL_AF_TRIGGER_START);
                        previewRequest.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
                                CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE);
                        triggerRequests.add(previewRequest.build());

                        mCameraSession.captureBurst(triggerRequests, captureListener, mHandler);

                        TotalCaptureResult[] triggerResults =
                                captureListener.getTotalCaptureResultsForRequests(
                                triggerRequests, MAX_RESULT_STATE_CHANGE_WAIT_FRAMES);
                        for (int i = 0; i < maxPipelineDepth; i++) {
                            TotalCaptureResult triggerResult = triggerResults[i];
                            int afState = triggerResult.get(CaptureResult.CONTROL_AF_STATE);
                            int afTrigger = triggerResult.get(CaptureResult.CONTROL_AF_TRIGGER);

                            verifyStartingAfState(afMode, afState);
                            assertTrue(String.format("In AF mode %s, previous AF_TRIGGER must not "
                                    + "be START before TRIGGER_START",
                                    StaticMetadata.getAfModeName(afMode)),
                                    afTrigger != CaptureResult.CONTROL_AF_TRIGGER_START);
                        }

                        int afState =
                                triggerResults[maxPipelineDepth].get(CaptureResult.CONTROL_AF_STATE);
                        boolean focusComplete = false;
                        for (int i = 0;
                             i < MAX_TRIGGER_SEQUENCE_FRAMES && !focusComplete;
                             i++) {

                            focusComplete = verifyAfSequence(afMode, afState, focusComplete);

                            CaptureResult focusResult = captureListener.getCaptureResult(
                                    CameraTestUtils.CAPTURE_RESULT_TIMEOUT_MS);
                            afState = focusResult.get(CaptureResult.CONTROL_AF_STATE);
                        }

                        assertTrue("Focusing never completed!", focusComplete);

                        // Standard sequence - Part 2 AE trigger

                        if (VERBOSE) {
                            Log.v(TAG, String.format("Triggering AE"));
                        }
                        // Remove AF trigger request
                        triggerRequests.remove(maxPipelineDepth);

                        previewRequest.set(CaptureRequest.CONTROL_AF_TRIGGER,
                                CaptureRequest.CONTROL_AF_TRIGGER_IDLE);
                        previewRequest.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
                                CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);
                        triggerRequests.add(previewRequest.build());

                        mCameraSession.captureBurst(triggerRequests, captureListener, mHandler);

                        triggerResults = captureListener.getTotalCaptureResultsForRequests(
                                triggerRequests, MAX_RESULT_STATE_CHANGE_WAIT_FRAMES);

                        for (int i = 0; i < maxPipelineDepth; i++) {
                            TotalCaptureResult triggerResult = triggerResults[i];
                            int aeState = triggerResult.get(CaptureResult.CONTROL_AE_STATE);
                            int aeTrigger = triggerResult.get(
                                    CaptureResult.CONTROL_AE_PRECAPTURE_TRIGGER);

                            assertTrue(String.format("In AE mode %s, previous AE_TRIGGER must not "
                                    + "be START before TRIGGER_START",
                                    StaticMetadata.getAeModeName(aeMode)),
                                    aeTrigger != CaptureResult.CONTROL_AE_PRECAPTURE_TRIGGER_START);
                            assertTrue(String.format("In AE mode %s, previous AE_STATE must not be"
                                    + " PRECAPTURE_TRIGGER before TRIGGER_START",
                                    StaticMetadata.getAeModeName(aeMode)),
                                    aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE);
                        }

                        // Stand sequence - Part 3 Cancel AF trigger
                        if (VERBOSE) {
                            Log.v(TAG, String.format("Cancel AF trigger"));
                        }
                        // Remove AE trigger request
                        triggerRequests.remove(maxPipelineDepth);
                        previewRequest.set(CaptureRequest.CONTROL_AF_TRIGGER,
                                CaptureRequest.CONTROL_AF_TRIGGER_CANCEL);
                        triggerRequests.add(previewRequest.build());

                        mCameraSession.captureBurst(triggerRequests, captureListener, mHandler);
                        triggerResults = captureListener.getTotalCaptureResultsForRequests(
                                triggerRequests, MAX_RESULT_STATE_CHANGE_WAIT_FRAMES);
                        for (int i = 0; i < maxPipelineDepth; i++) {
                            TotalCaptureResult triggerResult = triggerResults[i];
                            afState = triggerResult.get(CaptureResult.CONTROL_AF_STATE);
                            int afTrigger = triggerResult.get(CaptureResult.CONTROL_AF_TRIGGER);

                            assertTrue(
                                    String.format("In AF mode %s, previous AF_TRIGGER must not " +
                                    "be CANCEL before TRIGGER_CANCEL",
                                    StaticMetadata.getAfModeName(afMode)),
                                    afTrigger != CaptureResult.CONTROL_AF_TRIGGER_CANCEL);
                            assertTrue(
                                    String.format("In AF mode %s, previous AF_STATE must be LOCKED"
                                    + " before CANCEL, but is %s",
                                    StaticMetadata.getAfModeName(afMode),
                                    StaticMetadata.AF_STATE_NAMES[afState]),
                                    afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED ||
                                    afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED);
                        }

                        stopCapture(/*fast*/ false);
                        preview.release();
                    }

                }

            } finally {
                closeDevice(id);
            }
        }

    }

    @Test
    public void testAbandonRepeatingRequestSurface() throws Exception {
        for (String id : getCameraIdsUnderTest()) {
            Log.i(TAG, String.format(
                    "Testing Camera %s for abandoning surface of a repeating request", id));

            StaticMetadata staticInfo = mAllStaticInfo.get(id);
            if (!staticInfo.isColorOutputSupported()) {
                Log.i(TAG, "Camera " + id + " does not support color output, skipping");
                continue;
            }

            openDevice(id);

            try {

                SurfaceTexture preview = new SurfaceTexture(/*random int*/ 1);
                Surface previewSurface = new Surface(preview);

                CaptureRequest.Builder previewRequest = preparePreviewTestSession(preview);
                SimpleCaptureCallback captureListener = new CameraTestUtils.SimpleCaptureCallback();

                int sequenceId = mCameraSession.setRepeatingRequest(previewRequest.build(),
                        captureListener, mHandler);

                for (int i = 0; i < PREVIEW_WARMUP_FRAMES; i++) {
                    captureListener.getTotalCaptureResult(CAPTURE_TIMEOUT);
                }

                // Abandon preview surface.
                preview.release();

                // Check onCaptureSequenceCompleted is received.
                long sequenceLastFrameNumber = captureListener.getCaptureSequenceLastFrameNumber(
                        sequenceId, CAPTURE_TIMEOUT);

                mCameraSession.stopRepeating();

                // Find the last frame number received in results and failures.
                long lastFrameNumber = -1;
                while (captureListener.hasMoreResults()) {
                    TotalCaptureResult result = captureListener.getTotalCaptureResult(
                            CAPTURE_TIMEOUT);
                    if (lastFrameNumber < result.getFrameNumber()) {
                        lastFrameNumber = result.getFrameNumber();
                    }
                }

                while (captureListener.hasMoreFailures()) {
                    ArrayList<CaptureFailure> failures = captureListener.getCaptureFailures(
                            /*maxNumFailures*/ 1);
                    for (CaptureFailure failure : failures) {
                        if (lastFrameNumber < failure.getFrameNumber()) {
                            lastFrameNumber = failure.getFrameNumber();
                        }
                    }
                }

                // Verify the last frame number received from capture sequence completed matches the
                // the last frame number of the results and failures.
                assertEquals(String.format("Last frame number from onCaptureSequenceCompleted " +
                        "(%d) doesn't match the last frame number received from " +
                        "results/failures (%d)", sequenceLastFrameNumber, lastFrameNumber),
                        sequenceLastFrameNumber, lastFrameNumber);
            } finally {
                closeDevice(id);
            }
        }
    }

    @Test
    public void testConfigureInvalidSensorPixelModes() throws Exception {
        for (String id : getCameraIdsUnderTest()) {
            // Go through given, stream configuration map, add the incorrect sensor pixel mode
            // to an OutputConfiguration, make sure the session configuration fails.
            CameraCharacteristics chars = mCameraManager.getCameraCharacteristics(id);
            StreamConfigurationMap defaultStreamConfigMap =
                    chars.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
            StreamConfigurationMap maxStreamConfigMap =
                    chars.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP_MAXIMUM_RESOLUTION);
            openDevice(id);
            try {
                verifyBasicSensorPixelModes(id, maxStreamConfigMap, defaultStreamConfigMap,
                        /*maxResolution*/ false);
                verifyBasicSensorPixelModes(id, maxStreamConfigMap, defaultStreamConfigMap,
                        /*maxResolution*/ true);
            } finally {
                closeDevice(id);
            }
        }
    }

    @Test
    public void testConfigureAbandonedSurface() throws Exception {
        for (String id : getCameraIdsUnderTest()) {
            Log.i(TAG, String.format(
                    "Testing Camera %s for configuring abandoned surface", id));

            openDevice(id);
            try {
                SurfaceTexture preview = new SurfaceTexture(/*random int*/ 1);
                Surface previewSurface = new Surface(preview);

                // Abandon preview SurfaceTexture.
                preview.release();

                try {
                    CaptureRequest.Builder previewRequest = preparePreviewTestSession(preview);
                    fail("Configuring abandoned surfaces must fail!");
                } catch (IllegalArgumentException e) {
                    // expected
                    Log.i(TAG, "normal session check passed");
                }

                // Try constrained high speed session/requests
                if (!mStaticInfo.isConstrainedHighSpeedVideoSupported()) {
                    continue;
                }

                List<Surface> surfaces = new ArrayList<>();
                surfaces.add(previewSurface);
                CameraCaptureSession.StateCallback sessionListener =
                        mock(CameraCaptureSession.StateCallback.class);

                try {
                    mCamera.createConstrainedHighSpeedCaptureSession(surfaces,
                            sessionListener, mHandler);
                    fail("Configuring abandoned surfaces in high speed session must fail!");
                } catch (IllegalArgumentException e) {
                    // expected
                    Log.i(TAG, "high speed session check 1 passed");
                }

                // Also try abandone the Surface directly
                previewSurface.release();

                try {
                    mCamera.createConstrainedHighSpeedCaptureSession(surfaces,
                            sessionListener, mHandler);
                    fail("Configuring abandoned surfaces in high speed session must fail!");
                } catch (IllegalArgumentException e) {
                    // expected
                    Log.i(TAG, "high speed session check 2 passed");
                }
            } finally {
                closeDevice(id);
            }
        }
    }

    @Test
    public void testAfSceneChange() throws Exception {
        final int NUM_FRAMES_VERIFIED = 3;

        for (String id : getCameraIdsUnderTest()) {
            Log.i(TAG, String.format("Testing Camera %s for AF scene change", id));

            StaticMetadata staticInfo =
                    new StaticMetadata(mCameraManager.getCameraCharacteristics(id));
            if (!staticInfo.isAfSceneChangeSupported()) {
                continue;
            }

            openDevice(id);

            try {
                SurfaceTexture preview = new SurfaceTexture(/*random int*/ 1);
                Surface previewSurface = new Surface(preview);

                CaptureRequest.Builder previewRequest = preparePreviewTestSession(preview);
                SimpleCaptureCallback previewListener = new CameraTestUtils.SimpleCaptureCallback();

                int[] availableAfModes = mStaticInfo.getAfAvailableModesChecked();

                // Test AF scene change in each AF mode.
                for (int afMode : availableAfModes) {
                    previewRequest.set(CaptureRequest.CONTROL_AF_MODE, afMode);

                    int sequenceId = mCameraSession.setRepeatingRequest(previewRequest.build(),
                            previewListener, mHandler);

                    // Verify that AF scene change is NOT_DETECTED or DETECTED.
                    for (int i = 0; i < NUM_FRAMES_VERIFIED; i++) {
                        TotalCaptureResult result =
                            previewListener.getTotalCaptureResult(CAPTURE_TIMEOUT);
                        mCollector.expectKeyValueIsIn(result,
                                CaptureResult.CONTROL_AF_SCENE_CHANGE,
                                CaptureResult.CONTROL_AF_SCENE_CHANGE_DETECTED,
                                CaptureResult.CONTROL_AF_SCENE_CHANGE_NOT_DETECTED);
                    }

                    mCameraSession.stopRepeating();
                    previewListener.getCaptureSequenceLastFrameNumber(sequenceId, CAPTURE_TIMEOUT);
                    previewListener.drain();
                }
            } finally {
                closeDevice(id);
            }
        }
    }

    @Test
    public void testOisDataMode() throws Exception {
        final int NUM_FRAMES_VERIFIED = 3;

        for (String id : getCameraIdsUnderTest()) {
            Log.i(TAG, String.format("Testing Camera %s for OIS mode", id));

            StaticMetadata staticInfo =
                    new StaticMetadata(mCameraManager.getCameraCharacteristics(id));
            if (!staticInfo.isOisDataModeSupported()) {
                continue;
            }

            openDevice(id);

            try {
                SurfaceTexture preview = new SurfaceTexture(/*random int*/ 1);
                Surface previewSurface = new Surface(preview);

                CaptureRequest.Builder previewRequest = preparePreviewTestSession(preview);
                SimpleCaptureCallback previewListener = new CameraTestUtils.SimpleCaptureCallback();

                int[] availableOisDataModes = staticInfo.getCharacteristics().get(
                        CameraCharacteristics.STATISTICS_INFO_AVAILABLE_OIS_DATA_MODES);

                // Test each OIS data mode
                for (int oisMode : availableOisDataModes) {
                    previewRequest.set(CaptureRequest.STATISTICS_OIS_DATA_MODE, oisMode);

                    int sequenceId = mCameraSession.setRepeatingRequest(previewRequest.build(),
                            previewListener, mHandler);

                    // Check OIS data in each mode.
                    for (int i = 0; i < NUM_FRAMES_VERIFIED; i++) {
                        TotalCaptureResult result =
                            previewListener.getTotalCaptureResult(CAPTURE_TIMEOUT);

                        OisSample[] oisSamples = result.get(CaptureResult.STATISTICS_OIS_SAMPLES);

                        boolean physicalDeviceSupportsOIS = true;
                        if (staticInfo.isLogicalMultiCamera() &&
                                staticInfo.isActivePhysicalCameraIdSupported()) {
                            String physicalId = result.get(
                                    CaptureResult.LOGICAL_MULTI_CAMERA_ACTIVE_PHYSICAL_ID);
                            assertNotNull(physicalId);
                            StaticMetadata physicalStaticInfo = mAllStaticInfo.get(physicalId);
                            physicalDeviceSupportsOIS = physicalStaticInfo.isOisDataModeSupported();
                        }

                        if (oisMode == CameraCharacteristics.STATISTICS_OIS_DATA_MODE_OFF ||
                                !physicalDeviceSupportsOIS) {
                            mCollector.expectKeyValueEquals(result,
                                    CaptureResult.STATISTICS_OIS_DATA_MODE,
                                    CaptureResult.STATISTICS_OIS_DATA_MODE_OFF);
                            mCollector.expectTrue("OIS samples reported in OIS_DATA_MODE_OFF",
                                    oisSamples == null || oisSamples.length == 0);

                        } else if (oisMode == CameraCharacteristics.STATISTICS_OIS_DATA_MODE_ON) {
                            mCollector.expectKeyValueEquals(result,
                                    CaptureResult.STATISTICS_OIS_DATA_MODE,
                                    CaptureResult.STATISTICS_OIS_DATA_MODE_ON);
                            mCollector.expectTrue("OIS samples not reported in OIS_DATA_MODE_ON",
                                    oisSamples != null && oisSamples.length != 0);
                        } else {
                            mCollector.addMessage(String.format("Invalid OIS mode: %d", oisMode));
                        }
                    }

                    mCameraSession.stopRepeating();
                    previewListener.getCaptureSequenceLastFrameNumber(sequenceId, CAPTURE_TIMEOUT);
                    previewListener.drain();
                }
            } finally {
                closeDevice(id);
            }
        }
    }

    private CaptureRequest.Builder preparePreviewTestSession(SurfaceTexture preview)
            throws Exception {
        Surface previewSurface = new Surface(preview);

        preview.setDefaultBufferSize(640, 480);

        ArrayList<Surface> sessionOutputs = new ArrayList<>();
        sessionOutputs.add(previewSurface);

        createSession(sessionOutputs);

        CaptureRequest.Builder previewRequest =
                mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);

        previewRequest.addTarget(previewSurface);

        return previewRequest;
    }

    private CaptureRequest.Builder prepareTriggerTestSession(
            SurfaceTexture preview, int aeMode, int afMode) throws Exception {
        Log.i(TAG, String.format("Testing AE mode %s, AF mode %s",
                        StaticMetadata.getAeModeName(aeMode),
                        StaticMetadata.getAfModeName(afMode)));

        CaptureRequest.Builder previewRequest = preparePreviewTestSession(preview);
        previewRequest.set(CaptureRequest.CONTROL_AE_MODE, aeMode);
        previewRequest.set(CaptureRequest.CONTROL_AF_MODE, afMode);

        return previewRequest;
    }

    private void cancelTriggersAndWait(CaptureRequest.Builder previewRequest,
            SimpleCaptureCallback captureListener, int afMode) throws Exception {
        previewRequest.set(CaptureRequest.CONTROL_AF_TRIGGER,
                CaptureRequest.CONTROL_AF_TRIGGER_CANCEL);
        previewRequest.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
                CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL);

        CaptureRequest triggerRequest = previewRequest.build();
        mCameraSession.capture(triggerRequest, captureListener, mHandler);

        // Wait for a few frames to initialize 3A

        CaptureResult previewResult = null;
        int afState;
        int aeState;

        for (int i = 0; i < PREVIEW_WARMUP_FRAMES; i++) {
            previewResult = captureListener.getCaptureResult(
                    CameraTestUtils.CAPTURE_RESULT_TIMEOUT_MS);
            if (VERBOSE) {
                afState = previewResult.get(CaptureResult.CONTROL_AF_STATE);
                aeState = previewResult.get(CaptureResult.CONTROL_AE_STATE);
                Log.v(TAG, String.format("AF state: %s, AE state: %s",
                                StaticMetadata.AF_STATE_NAMES[afState],
                                StaticMetadata.AE_STATE_NAMES[aeState]));
            }
        }

        // Verify starting states

        afState = previewResult.get(CaptureResult.CONTROL_AF_STATE);
        aeState = previewResult.get(CaptureResult.CONTROL_AE_STATE);

        verifyStartingAfState(afMode, afState);

        // After several frames, AE must no longer be in INACTIVE state
        assertTrue(String.format("AE state must be SEARCHING, CONVERGED, " +
                        "or FLASH_REQUIRED, is %s", StaticMetadata.AE_STATE_NAMES[aeState]),
                aeState == CaptureResult.CONTROL_AE_STATE_SEARCHING ||
                aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED ||
                aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED);
    }

    private boolean configsContain(StreamConfigurationMap configs, int format, Size size) {
        Size[] sizes = configs.getOutputSizes(format);
        if (sizes == null) {
            return false;
        }
        return Arrays.asList(sizes).contains(size);
    }

    private void verifyBasicSensorPixelModes(String id, StreamConfigurationMap maxResConfigs,
            StreamConfigurationMap defaultConfigs, boolean maxResolution) throws Exception {
        // Go through StreamConfiguration map, set up OutputConfiguration and add the opposite
        // sensorPixelMode.
        final int MIN_RESULT_COUNT = 3;
        assertTrue("Default stream config map must be present for id: " + id,
            defaultConfigs != null);
        if (maxResConfigs == null) {
            Log.i(TAG, "camera id " + id + " has no StreamConfigurationMap for max resolution " +
                ", skipping verifyBasicSensorPixelModes");
            return;
        }
        StreamConfigurationMap chosenConfigs = maxResolution ? maxResConfigs : defaultConfigs;
        StreamConfigurationMap otherConfigs = maxResolution ? defaultConfigs : maxResConfigs;
        OutputConfiguration outputConfig = null;
        for (int format : chosenConfigs.getOutputFormats()) {
            Size targetSize = CameraTestUtils.getMaxSize(chosenConfigs.getOutputSizes(format));
            if (configsContain(otherConfigs, format, targetSize)) {
                // Since both max res and default stream configuration maps contain this size,
                // both sensor pixel modes are valid.
                Log.v(TAG, "camera id " + id + " 'other' configs with maxResolution" +
                    maxResolution + " contains the format: " + format + " size: " + targetSize +
                    " skipping");
                continue;
            }
            // Create outputConfiguration with this size and format
            SimpleImageReaderListener imageListener = new SimpleImageReaderListener();
            SurfaceTexture textureTarget = null;
            ImageReader readerTarget = null;
            if (format == ImageFormat.PRIVATE) {
                textureTarget = new SurfaceTexture(1);
                textureTarget.setDefaultBufferSize(targetSize.getWidth(), targetSize.getHeight());
                outputConfig = new OutputConfiguration(new Surface(textureTarget));
            } else {
                readerTarget = ImageReader.newInstance(targetSize.getWidth(),
                        targetSize.getHeight(), format, MIN_RESULT_COUNT);
                readerTarget.setOnImageAvailableListener(imageListener, mHandler);
                outputConfig = new OutputConfiguration(readerTarget.getSurface());
            }
            try {
                int invalidSensorPixelMode =
                        maxResolution ? CameraMetadata.SENSOR_PIXEL_MODE_DEFAULT :
                                CameraMetadata.SENSOR_PIXEL_MODE_MAXIMUM_RESOLUTION;

                outputConfig.addSensorPixelModeUsed(invalidSensorPixelMode);
                CameraCaptureSession.StateCallback sessionListener =
                        mock(CameraCaptureSession.StateCallback.class);
                List<OutputConfiguration> outputs = new ArrayList<>();
                outputs.add(outputConfig);
                CameraCaptureSession session =
                        CameraTestUtils.configureCameraSessionWithConfig(mCamera, outputs,
                                sessionListener, mHandler);
                String desc = "verifyBasicSensorPixelModes : Format : " + format + " size: " +
                        targetSize.toString() + " maxResolution : " + maxResolution;
                verify(sessionListener, timeout(CONFIGURE_TIMEOUT).atLeastOnce().description(desc)).
                        onConfigureFailed(any(CameraCaptureSession.class));
                verify(sessionListener, never().description(desc)).
                        onConfigured(any(CameraCaptureSession.class));

                // Remove the invalid sensor pixel mode, session configuration should succeed
                sessionListener = mock(CameraCaptureSession.StateCallback.class);
                outputConfig.removeSensorPixelModeUsed(invalidSensorPixelMode);
                CameraTestUtils.configureCameraSessionWithConfig(mCamera, outputs,
                        sessionListener, mHandler);
                verify(sessionListener, timeout(CONFIGURE_TIMEOUT).atLeastOnce().description(desc)).
                        onConfigured(any(CameraCaptureSession.class));
                verify(sessionListener, never().description(desc)).
                        onConfigureFailed(any(CameraCaptureSession.class));
            } finally {
                if (textureTarget != null) {
                    textureTarget.release();
                }

                if (readerTarget != null) {
                    readerTarget.close();
                }
            }
        }
    }

    private void verifyStartingAfState(int afMode, int afState) {
        switch (afMode) {
            case CaptureResult.CONTROL_AF_MODE_AUTO:
            case CaptureResult.CONTROL_AF_MODE_MACRO:
                assertTrue(String.format("AF state not INACTIVE, is %s",
                                StaticMetadata.AF_STATE_NAMES[afState]),
                        afState == CaptureResult.CONTROL_AF_STATE_INACTIVE);
                break;
            case CaptureResult.CONTROL_AF_MODE_CONTINUOUS_PICTURE:
            case CaptureResult.CONTROL_AF_MODE_CONTINUOUS_VIDEO:
                // After several frames, AF must no longer be in INACTIVE state
                assertTrue(String.format("In AF mode %s, AF state not PASSIVE_SCAN" +
                                ", PASSIVE_FOCUSED, or PASSIVE_UNFOCUSED, is %s",
                                StaticMetadata.getAfModeName(afMode),
                                StaticMetadata.AF_STATE_NAMES[afState]),
                        afState == CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN ||
                        afState == CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED ||
                        afState == CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED);
                break;
            default:
                fail("unexpected af mode");
        }
    }

    private boolean verifyAfSequence(int afMode, int afState, boolean focusComplete) {
        if (focusComplete) {
            assertTrue(String.format("AF Mode %s: Focus lock lost after convergence: AF state: %s",
                            StaticMetadata.getAfModeName(afMode),
                            StaticMetadata.AF_STATE_NAMES[afState]),
                    afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED ||
                    afState ==CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED);
            return focusComplete;
        }
        if (VERBOSE) {
            Log.v(TAG, String.format("AF mode: %s, AF state: %s",
                            StaticMetadata.getAfModeName(afMode),
                            StaticMetadata.AF_STATE_NAMES[afState]));
        }
        switch (afMode) {
            case CaptureResult.CONTROL_AF_MODE_AUTO:
            case CaptureResult.CONTROL_AF_MODE_MACRO:
                assertTrue(String.format("AF mode %s: Unexpected AF state %s",
                                StaticMetadata.getAfModeName(afMode),
                                StaticMetadata.AF_STATE_NAMES[afState]),
                        afState == CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN ||
                        afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED ||
                        afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED);
                focusComplete =
                        (afState != CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN);
                break;
            case CaptureResult.CONTROL_AF_MODE_CONTINUOUS_PICTURE:
                assertTrue(String.format("AF mode %s: Unexpected AF state %s",
                                StaticMetadata.getAfModeName(afMode),
                                StaticMetadata.AF_STATE_NAMES[afState]),
                        afState == CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN ||
                        afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED ||
                        afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED);
                focusComplete =
                        (afState != CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN);
                break;
            case CaptureResult.CONTROL_AF_MODE_CONTINUOUS_VIDEO:
                assertTrue(String.format("AF mode %s: Unexpected AF state %s",
                                StaticMetadata.getAfModeName(afMode),
                                StaticMetadata.AF_STATE_NAMES[afState]),
                        afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED ||
                        afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED);
                focusComplete = true;
                break;
            default:
                fail("Unexpected AF mode: " + StaticMetadata.getAfModeName(afMode));
        }
        return focusComplete;
    }

    private boolean verifyAeSequence(int aeState, boolean precaptureComplete) {
        if (precaptureComplete) {
            assertTrue("Precapture state seen after convergence",
                    aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE);
            return precaptureComplete;
        }
        if (VERBOSE) {
            Log.v(TAG, String.format("AE state: %s", StaticMetadata.AE_STATE_NAMES[aeState]));
        }
        switch (aeState) {
            case CaptureResult.CONTROL_AE_STATE_PRECAPTURE:
                // scan still continuing
                break;
            case CaptureResult.CONTROL_AE_STATE_CONVERGED:
            case CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED:
                // completed
                precaptureComplete = true;
                break;
            default:
                fail(String.format("Precapture sequence transitioned to "
                                + "state %s incorrectly!", StaticMetadata.AE_STATE_NAMES[aeState]));
                break;
        }
        return precaptureComplete;
    }

    /**
     * Test for making sure that all expected mandatory stream combinations are present and
     * advertised accordingly.
     */
    @Test
    public void testVerifyMandatoryOutputCombinationTables() throws Exception {
        final int[][] legacyCombinations = {
            // Simple preview, GPU video processing, or no-preview video recording
            {PRIV, MAXIMUM},
            // No-viewfinder still image capture
            {JPEG, MAXIMUM},
            // In-application video/image processing
            {YUV,  MAXIMUM},
            // Standard still imaging.
            {PRIV, PREVIEW,  JPEG, MAXIMUM},
            // In-app processing plus still capture.
            {YUV,  PREVIEW,  JPEG, MAXIMUM},
            // Standard recording.
            {PRIV, PREVIEW,  PRIV, PREVIEW},
            // Preview plus in-app processing.
            {PRIV, PREVIEW,  YUV,  PREVIEW},
            // Still capture plus in-app processing.
            {PRIV, PREVIEW,  YUV,  PREVIEW,  JPEG, MAXIMUM}
        };

        final int[][] limitedCombinations = {
            // High-resolution video recording with preview.
            {PRIV, PREVIEW,  PRIV, RECORD },
            // High-resolution in-app video processing with preview.
            {PRIV, PREVIEW,  YUV , RECORD },
            // Two-input in-app video processing.
            {YUV , PREVIEW,  YUV , RECORD },
            // High-resolution recording with video snapshot.
            {PRIV, PREVIEW,  PRIV, RECORD,   JPEG, RECORD  },
            // High-resolution in-app processing with video snapshot.
            {PRIV, PREVIEW,  YUV,  RECORD,   JPEG, RECORD  },
            // Two-input in-app processing with still capture.
            {YUV , PREVIEW,  YUV,  PREVIEW,  JPEG, MAXIMUM }
        };

        final int[][] burstCombinations = {
            // Maximum-resolution GPU processing with preview.
            {PRIV, PREVIEW,  PRIV, MAXIMUM },
            // Maximum-resolution in-app processing with preview.
            {PRIV, PREVIEW,  YUV,  MAXIMUM },
            // Maximum-resolution two-input in-app processing.
            {YUV,  PREVIEW,  YUV,  MAXIMUM },
        };

        final int[][] fullCombinations = {
            // Video recording with maximum-size video snapshot.
            {PRIV, PREVIEW,  PRIV, PREVIEW,  JPEG, MAXIMUM },
            // Standard video recording plus maximum-resolution in-app processing.
            {YUV,  VGA,      PRIV, PREVIEW,  YUV,  MAXIMUM },
            // Preview plus two-input maximum-resolution in-app processing.
            {YUV,  VGA,      YUV,  PREVIEW,  YUV,  MAXIMUM }
        };

        final int[][] rawCombinations = {
            // No-preview DNG capture.
            {RAW,  MAXIMUM },
            // Standard DNG capture.
            {PRIV, PREVIEW,  RAW,  MAXIMUM },
            // In-app processing plus DNG capture.
            {YUV,  PREVIEW,  RAW,  MAXIMUM },
            // Video recording with DNG capture.
            {PRIV, PREVIEW,  PRIV, PREVIEW,  RAW, MAXIMUM},
            // Preview with in-app processing and DNG capture.
            {PRIV, PREVIEW,  YUV,  PREVIEW,  RAW, MAXIMUM},
            // Two-input in-app processing plus DNG capture.
            {YUV,  PREVIEW,  YUV,  PREVIEW,  RAW, MAXIMUM},
            // Still capture with simultaneous JPEG and DNG.
            {PRIV, PREVIEW,  JPEG, MAXIMUM,  RAW, MAXIMUM},
            // In-app processing with simultaneous JPEG and DNG.
            {YUV,  PREVIEW,  JPEG, MAXIMUM,  RAW, MAXIMUM}
        };

        final int[][] level3Combinations = {
            // In-app viewfinder analysis with dynamic selection of output format
            {PRIV, PREVIEW, PRIV, VGA, YUV, MAXIMUM, RAW, MAXIMUM},
            // In-app viewfinder analysis with dynamic selection of output format
            {PRIV, PREVIEW, PRIV, VGA, JPEG, MAXIMUM, RAW, MAXIMUM}
        };

        final int[][] concurrentStreamCombinations = {
            //In-app video / image processing.
            {YUV, S1440P_4_3},
            // In-app viewfinder analysis.
            {PRIV, S1440P_4_3},
            // No viewfinder still image capture.
            {JPEG, S1440P_4_3},
            // Standard still imaging.
            {YUV, S720P, JPEG, S1440P_4_3},
            {PRIV, S720P, JPEG, S1440P_4_3},
            // In-app video / processing with preview.
            {YUV, S720P, YUV, S1440P_4_3},
            {YUV, S720P, PRIV, S1440P_4_3},
            {PRIV, S720P, YUV, S1440P_4_3},
            {PRIV, S720P, PRIV, S1440P_4_3}
        };

        final int[][] ultraHighResolutionsCombinations = {
            // Ultra high res still image capture with preview.
            {YUV, MAX_RES, PRIV, PREVIEW},
            {YUV, MAX_RES, YUV, PREVIEW},
            {JPEG, MAX_RES, PRIV, PREVIEW},
            {JPEG, MAX_RES, YUV, PREVIEW},
            {RAW, MAX_RES, PRIV, PREVIEW},
            {RAW, MAX_RES, YUV, PREVIEW},
            // Ultra high res still capture with preview + app based RECORD size analysis.
            {YUV, MAX_RES, PRIV, PREVIEW, PRIV, RECORD},
            {YUV, MAX_RES, PRIV, PREVIEW, YUV, RECORD},
            {JPEG, MAX_RES, PRIV, PREVIEW, PRIV, RECORD},
            {JPEG, MAX_RES, PRIV, PREVIEW, YUV, RECORD},
            {RAW, MAX_RES, PRIV, PREVIEW, PRIV, RECORD},
            {RAW, MAX_RES, PRIV, PREVIEW, YUV, RECORD},
            // Ultra high res still image capture with preview + default sensor pixel mode analysis
            // stream
            {YUV, MAX_RES, PRIV, PREVIEW, JPEG, MAXIMUM},
            {YUV, MAX_RES, PRIV, PREVIEW, YUV, MAXIMUM},
            {YUV, MAX_RES, PRIV, PREVIEW, RAW, MAXIMUM},
            {JPEG, MAX_RES, PRIV, PREVIEW, JPEG, MAXIMUM},
            {JPEG, MAX_RES, PRIV, PREVIEW, YUV, MAXIMUM},
            {JPEG, MAX_RES, PRIV, PREVIEW, RAW, MAXIMUM},
            {RAW, MAX_RES, PRIV, PREVIEW, JPEG, MAXIMUM},
            {RAW, MAX_RES, PRIV, PREVIEW, YUV, MAXIMUM},
            {RAW, MAX_RES, PRIV, PREVIEW, RAW, MAXIMUM},
        };

        final int[][] tenBitOutputCombinations = {
            // Simple preview, GPU video processing, or no-preview video recording.
            {PRIV, MAXIMUM},
            // In-application video/image processing.
            {YUV, MAXIMUM},
            // Standard still imaging.
            {PRIV, PREVIEW, JPEG, MAXIMUM},
            // Maximum-resolution in-app processing with preview.
            {PRIV, PREVIEW, YUV, MAXIMUM},
            // Maximum-resolution two-input in-app processing.
            {YUV, PREVIEW, YUV, MAXIMUM},
            // High-resolution video recording with preview.
            {PRIV, PREVIEW, PRIV, RECORD},
            // High-resolution recording with in-app snapshot.
            {PRIV, PREVIEW, PRIV, RECORD, YUV, RECORD},
            // High-resolution recording with video snapshot.
            {PRIV, PREVIEW, PRIV, RECORD, JPEG, RECORD}
        };

        final int[][] streamUseCaseCombinations = {
            // Simple preview or in-app image processing.
            {YUV, PREVIEW, USE_CASE_PREVIEW},
            {PRIV, PREVIEW, USE_CASE_PREVIEW},
            // Simple video recording or in-app video processing.
            {YUV, RECORD, USE_CASE_VIDEO_RECORD},
            {PRIV, RECORD, USE_CASE_VIDEO_RECORD},
            // Simple JPEG or YUV still image capture.
            {YUV, MAXIMUM, USE_CASE_STILL_CAPTURE},
            {JPEG, MAXIMUM, USE_CASE_STILL_CAPTURE},
            // Multi-purpose stream for preview, video and still image capture.
            {YUV, S1440P_4_3, USE_CASE_PREVIEW_VIDEO_STILL},
            {PRIV, S1440P_4_3, USE_CASE_PREVIEW_VIDEO_STILL},
            // Simple video call.
            {YUV, S1440P_4_3, USE_CASE_VIDEO_CALL},
            {PRIV, S1440P_4_3, USE_CASE_VIDEO_CALL},
            // Preview with JPEG or YUV still image capture.
            {PRIV, PREVIEW, USE_CASE_PREVIEW, YUV, MAXIMUM, USE_CASE_STILL_CAPTURE},
            {PRIV, PREVIEW, USE_CASE_PREVIEW, JPEG, MAXIMUM, USE_CASE_STILL_CAPTURE},
            // Preview with video recording or in-app video processing.
            {PRIV, PREVIEW, USE_CASE_PREVIEW, YUV, RECORD, USE_CASE_VIDEO_RECORD},
            {PRIV, PREVIEW, USE_CASE_PREVIEW, PRIV, RECORD, USE_CASE_VIDEO_RECORD},
            // Preview with in-application image processing.
            {PRIV, PREVIEW, USE_CASE_PREVIEW, YUV, PREVIEW, USE_CASE_PREVIEW},
            // Preview with video call.
            {PRIV, PREVIEW, USE_CASE_PREVIEW, YUV, S1440P_4_3, USE_CASE_VIDEO_CALL},
            {PRIV, PREVIEW, USE_CASE_PREVIEW, PRIV, S1440P_4_3, USE_CASE_VIDEO_CALL},
            // {Multi-purpose stream with JPEG or YUV still capture.
            {YUV, S1440P_4_3, USE_CASE_PREVIEW_VIDEO_STILL, YUV, MAXIMUM, USE_CASE_STILL_CAPTURE},
            {YUV, S1440P_4_3, USE_CASE_PREVIEW_VIDEO_STILL, JPEG, MAXIMUM, USE_CASE_STILL_CAPTURE},
            {PRIV, S1440P_4_3, USE_CASE_PREVIEW_VIDEO_STILL, YUV, MAXIMUM, USE_CASE_STILL_CAPTURE},
            {PRIV, S1440P_4_3, USE_CASE_PREVIEW_VIDEO_STILL, JPEG, MAXIMUM, USE_CASE_STILL_CAPTURE},
            // YUV and JPEG concurrent still image capture (for testing).
            {YUV, PREVIEW, USE_CASE_STILL_CAPTURE, JPEG, MAXIMUM, USE_CASE_STILL_CAPTURE},
            // Preview, video record and JPEG video snapshot.
            {PRIV, PREVIEW, USE_CASE_PREVIEW, YUV, RECORD, USE_CASE_VIDEO_RECORD, JPEG, RECORD,
                    USE_CASE_STILL_CAPTURE},
            {PRIV, PREVIEW, USE_CASE_PREVIEW, PRIV, RECORD, USE_CASE_VIDEO_RECORD, JPEG, RECORD,
                    USE_CASE_STILL_CAPTURE},
            // Preview, in-application image processing, and JPEG still image capture.
            {PRIV, PREVIEW, USE_CASE_PREVIEW, YUV, PREVIEW, USE_CASE_PREVIEW, JPEG, MAXIMUM,
                    USE_CASE_STILL_CAPTURE},
        };

        final int[][] streamUseCaseCroppedRawCombinations = {
            // Cropped RAW still image capture without preview
            {RAW, MAXIMUM, USE_CASE_CROPPED_RAW},

            // Preview / In-app processing with cropped RAW still image capture
            {PRIV, PREVIEW, USE_CASE_PREVIEW, RAW, MAXIMUM, USE_CASE_CROPPED_RAW},
            {YUV, PREVIEW, USE_CASE_PREVIEW, RAW, MAXIMUM, USE_CASE_CROPPED_RAW},

            // Preview / In-app processing with YUV and cropped RAW still image capture
            {PRIV, PREVIEW, USE_CASE_PREVIEW, YUV, MAXIMUM, USE_CASE_STILL_CAPTURE, RAW, MAXIMUM,
              USE_CASE_CROPPED_RAW},
            {YUV, PREVIEW, USE_CASE_PREVIEW, YUV, MAXIMUM, USE_CASE_STILL_CAPTURE, RAW, MAXIMUM,
              USE_CASE_CROPPED_RAW},

            // Preview / In-app processing with JPEG and cropped RAW still image capture
            {PRIV, PREVIEW, USE_CASE_PREVIEW, JPEG, MAXIMUM, USE_CASE_STILL_CAPTURE, RAW, MAXIMUM,
              USE_CASE_CROPPED_RAW},
            {YUV, PREVIEW, USE_CASE_PREVIEW, JPEG, MAXIMUM, USE_CASE_STILL_CAPTURE, RAW, MAXIMUM,
              USE_CASE_CROPPED_RAW},

            // Preview with in-app processing / video recording and cropped RAW snapshot
            {PRIV, PREVIEW, USE_CASE_PREVIEW, PRIV, PREVIEW, USE_CASE_VIDEO_RECORD, RAW, MAXIMUM,
              USE_CASE_CROPPED_RAW},
            {PRIV, PREVIEW, USE_CASE_PREVIEW, YUV, PREVIEW, USE_CASE_PREVIEW, RAW, MAXIMUM,
              USE_CASE_CROPPED_RAW},

            // Two input in-app processing with RAW
            {YUV, PREVIEW, USE_CASE_PREVIEW, YUV, PREVIEW, USE_CASE_PREVIEW, RAW, MAXIMUM,
              USE_CASE_CROPPED_RAW},
        };


        final int[][] previewStabilizationCombinations = {
            // Stabilized preview, GPU video processing, or no-preview stabilized video recording.
            {PRIV, S1440P_4_3},
            {YUV, S1440P_4_3},
            // Standard still imaging with stabilized preview.
            {PRIV, S1440P_4_3, JPEG, MAXIMUM},
            {PRIV, S1440P_4_3, YUV, MAXIMUM},
            {YUV, S1440P_4_3, JPEG, MAXIMUM},
            {YUV, S1440P_4_3, YUV, MAXIMUM},
            // High-resolution recording with stabilized preview and recording stream.
            {PRIV, PREVIEW, PRIV, S1440P_4_3},
            {PRIV, PREVIEW, YUV, S1440P_4_3},
            {YUV, PREVIEW, PRIV, S1440P_4_3},
            {YUV, PREVIEW, YUV, S1440P_4_3},
        };

        final int[][][] tables =
                {legacyCombinations, limitedCombinations, burstCombinations, fullCombinations,
                 rawCombinations, level3Combinations, concurrentStreamCombinations,
                 ultraHighResolutionsCombinations, tenBitOutputCombinations,
                 previewStabilizationCombinations};

        final int[][][] useCaseTables = {streamUseCaseCombinations,
                streamUseCaseCroppedRawCombinations};

        validityCheckConfigurationTables(tables);
        validityCheckConfigurationTables(useCaseTables, /*useCaseSpecified*/ true);

        for (String id : getCameraIdsUnderTest()) {
            openDevice(id);
            MandatoryStreamCombination[] combinations =
                    mStaticInfo.getCharacteristics().get(
                            CameraCharacteristics.SCALER_MANDATORY_STREAM_COMBINATIONS);
            if ((combinations == null) || (combinations.length == 0)) {
                Log.i(TAG, "No mandatory stream combinations for camera: " + id + " skip test");
                closeDevice(id);
                continue;
            }

            MaxStreamSizes maxSizes = new MaxStreamSizes(mStaticInfo, id, mContext);
            try {
                if (mStaticInfo.isColorOutputSupported()) {
                    for (int[] c : legacyCombinations) {
                        assertTrue(String.format("Expected static stream combination: %s not "
                                    + "found among the available mandatory combinations",
                                    maxSizes.combinationToString(c)),
                                isMandatoryCombinationAvailable(c, maxSizes, combinations));
                    }
                }

                if (!mStaticInfo.isHardwareLevelLegacy()) {
                    if (mStaticInfo.isColorOutputSupported()) {
                        for (int[] c : limitedCombinations) {
                            assertTrue(String.format("Expected static stream combination: %s not "
                                        + "found among the available mandatory combinations",
                                        maxSizes.combinationToString(c)),
                                    isMandatoryCombinationAvailable(c, maxSizes, combinations));
                        }
                    }

                    if (mStaticInfo.isCapabilitySupported(
                            CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE)) {
                        for (int[] c : burstCombinations) {
                            assertTrue(String.format("Expected static stream combination: %s not "
                                        + "found among the available mandatory combinations",
                                        maxSizes.combinationToString(c)),
                                    isMandatoryCombinationAvailable(c, maxSizes, combinations));
                        }
                    }

                    if (mStaticInfo.isHardwareLevelAtLeastFull()) {
                        for (int[] c : fullCombinations) {
                            assertTrue(String.format("Expected static stream combination: %s not "
                                        + "found among the available mandatory combinations",
                                        maxSizes.combinationToString(c)),
                                    isMandatoryCombinationAvailable(c, maxSizes, combinations));
                        }
                    }

                    if (mStaticInfo.isCapabilitySupported(
                            CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW)) {
                        for (int[] c : rawCombinations) {
                            assertTrue(String.format("Expected static stream combination: %s not "
                                        + "found among the available mandatory combinations",
                                        maxSizes.combinationToString(c)),
                                    isMandatoryCombinationAvailable(c, maxSizes, combinations));
                        }
                    }

                    if (mStaticInfo.isHardwareLevelAtLeast(
                            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3)) {
                        for (int[] c: level3Combinations) {
                            assertTrue(String.format("Expected static stream combination: %s not "
                                        + "found among the available mandatory combinations ",
                                        maxSizes.combinationToString(c)),
                                    isMandatoryCombinationAvailable(c, maxSizes, combinations));
                        }
                    }
                }

                Set<Set<String>> concurrentCameraIdCombinations =
                        mCameraManager.getConcurrentCameraIds();
                boolean isConcurrentCamera = false;
                for (Set<String> concurrentCameraIdCombination : concurrentCameraIdCombinations) {
                    if (concurrentCameraIdCombination.contains(id)) {
                        isConcurrentCamera = true;
                        break;
                    }
                }

                if (isConcurrentCamera && mStaticInfo.isCapabilitySupported(
                        CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE)) {
                    MandatoryStreamCombination[] mandatoryConcurrentStreamCombinations =
                            mStaticInfo.getCharacteristics().get(
                                    CameraCharacteristics
                                            .SCALER_MANDATORY_CONCURRENT_STREAM_COMBINATIONS);
                    for (int[] c : concurrentStreamCombinations) {
                        assertTrue(String.format("Expected static stream combination: %s not "
                                    + "found among the available mandatory concurrent stream "
                                    + "combinations",
                                    maxSizes.combinationToString(c)),
                                isMandatoryCombinationAvailable(c, maxSizes,
                                        mandatoryConcurrentStreamCombinations));
                    }
                }

                if (mStaticInfo.isCapabilitySupported(
                        CameraCharacteristics
                                .REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR)) {
                    MandatoryStreamCombination[] maxResolutionStreamCombinations =
                        mStaticInfo.getCharacteristics().get(
                                CameraCharacteristics
                                        .SCALER_MANDATORY_MAXIMUM_RESOLUTION_STREAM_COMBINATIONS);
                    for (int[] c : ultraHighResolutionsCombinations) {
                        assertTrue(String.format("Expected static stream combination: %s not "
                                    + "found among the available mandatory max resolution stream "
                                    + "combinations",
                                    maxSizes.combinationToString(c)),
                                isMandatoryCombinationAvailable(c, maxSizes,
                                        maxResolutionStreamCombinations));
                    }
                }

                if (mStaticInfo.isCapabilitySupported(
                        CameraCharacteristics
                                .REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT)) {
                    MandatoryStreamCombination[] mandatoryTenBitOutputCombinations =
                        mStaticInfo.getCharacteristics().get(
                            CameraCharacteristics
                                    .SCALER_MANDATORY_TEN_BIT_OUTPUT_STREAM_COMBINATIONS);
                    for (int[] c : tenBitOutputCombinations) {
                        assertTrue(String.format("Expected static stream combination: %s not "
                                    + "found among the available mandatory 10 bit output "
                                    + "combinations",
                                    maxSizes.combinationToString(c)),
                                isMandatoryCombinationAvailable(c, maxSizes,
                                        mandatoryTenBitOutputCombinations));
                    }
                }

                if (mStaticInfo.isCapabilitySupported(
                        CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_STREAM_USE_CASE)) {
                    MandatoryStreamCombination[] mandatoryStreamUseCaseCombinations =
                        mStaticInfo.getCharacteristics().get(
                                CameraCharacteristics
                                        .SCALER_MANDATORY_USE_CASE_STREAM_COMBINATIONS);
                    for (int[] c : streamUseCaseCombinations) {
                        assertTrue(String.format("Expected static stream combination: %s not "
                                    + "found among the available mandatory stream use case "
                                    + "combinations",
                                    maxSizes.combinationToString(c, /*useCaseSpecified*/ true)),
                                isMandatoryCombinationAvailable(c, maxSizes,
                                        /*isInput*/ false,  mandatoryStreamUseCaseCombinations,
                                        /*useCaseSpecified*/ true));
                    }

                    if (mStaticInfo.isCroppedRawStreamUseCaseSupported()) {
                        for (int[] c : streamUseCaseCroppedRawCombinations) {
                            assertTrue(String.format("Expected static stream combination: %s not "
                                        + "found among the available mandatory cropped RAW stream"
                                        + " use case combinations",
                                        maxSizes.combinationToString(c, /*useCaseSpecified*/ true)),
                                    isMandatoryCombinationAvailable(c, maxSizes,
                                            /*isInput*/ false,  mandatoryStreamUseCaseCombinations,
                                            /*useCaseSpecified*/ true));
                        }
                    }
                }

                if (mStaticInfo.isPreviewStabilizationSupported()) {
                    MandatoryStreamCombination[] mandatoryPreviewStabilizationCombinations =
                        mStaticInfo.getCharacteristics().get(
                            CameraCharacteristics
                                .SCALER_MANDATORY_PREVIEW_STABILIZATION_OUTPUT_STREAM_COMBINATIONS);
                    for (int[] c : previewStabilizationCombinations) {
                        assertTrue(String.format("Expected static stream combination: %s not "
                                    + "found among the available mandatory preview stabilization"
                                    + "combinations",
                                    maxSizes.combinationToString(c)),
                                isMandatoryCombinationAvailable(c, maxSizes,
                                        mandatoryPreviewStabilizationCombinations));
                    }
                }
            } finally {
                closeDevice(id);
            }
        }
    }

    /**
     * Test for making sure that all expected reprocessable mandatory stream combinations are
     * present and advertised accordingly.
     */
    @Test
    public void testVerifyReprocessMandatoryOutputCombinationTables() throws Exception {
        final int[][] limitedCombinations = {
            // Input           Outputs
            {PRIV, MAXIMUM,    JPEG, MAXIMUM},
            {YUV , MAXIMUM,    JPEG, MAXIMUM},
            {PRIV, MAXIMUM,    PRIV, PREVIEW, JPEG, MAXIMUM},
            {YUV , MAXIMUM,    PRIV, PREVIEW, JPEG, MAXIMUM},
            {PRIV, MAXIMUM,    YUV , PREVIEW, JPEG, MAXIMUM},
            {YUV , MAXIMUM,    YUV , PREVIEW, JPEG, MAXIMUM},
            {PRIV, MAXIMUM,    YUV , PREVIEW, YUV , PREVIEW, JPEG, MAXIMUM},
            {YUV,  MAXIMUM,    YUV , PREVIEW, YUV , PREVIEW, JPEG, MAXIMUM},
        };

        final int[][] fullCombinations = {
            // Input           Outputs
            {YUV , MAXIMUM,    PRIV, PREVIEW},
            {YUV , MAXIMUM,    YUV , PREVIEW},
            {PRIV, MAXIMUM,    PRIV, PREVIEW, YUV , RECORD},
            {YUV , MAXIMUM,    PRIV, PREVIEW, YUV , RECORD},
            {PRIV, MAXIMUM,    PRIV, PREVIEW, YUV , MAXIMUM},
            {PRIV, MAXIMUM,    YUV , PREVIEW, YUV , MAXIMUM},
            {PRIV, MAXIMUM,    PRIV, PREVIEW, YUV , PREVIEW, JPEG, MAXIMUM},
            {YUV , MAXIMUM,    PRIV, PREVIEW, YUV , PREVIEW, JPEG, MAXIMUM},
        };

        final int[][] rawCombinations = {
            // Input           Outputs
            {PRIV, MAXIMUM,    YUV , PREVIEW, RAW , MAXIMUM},
            {YUV , MAXIMUM,    YUV , PREVIEW, RAW , MAXIMUM},
            {PRIV, MAXIMUM,    PRIV, PREVIEW, YUV , PREVIEW, RAW , MAXIMUM},
            {YUV , MAXIMUM,    PRIV, PREVIEW, YUV , PREVIEW, RAW , MAXIMUM},
            {PRIV, MAXIMUM,    YUV , PREVIEW, YUV , PREVIEW, RAW , MAXIMUM},
            {YUV , MAXIMUM,    YUV , PREVIEW, YUV , PREVIEW, RAW , MAXIMUM},
            {PRIV, MAXIMUM,    PRIV, PREVIEW, JPEG, MAXIMUM, RAW , MAXIMUM},
            {YUV , MAXIMUM,    PRIV, PREVIEW, JPEG, MAXIMUM, RAW , MAXIMUM},
            {PRIV, MAXIMUM,    YUV , PREVIEW, JPEG, MAXIMUM, RAW , MAXIMUM},
            {YUV , MAXIMUM,    YUV , PREVIEW, JPEG, MAXIMUM, RAW , MAXIMUM},
        };

        final int[][] level3Combinations = {
            // Input          Outputs
            // In-app viewfinder analysis with YUV->YUV ZSL and RAW
            {YUV , MAXIMUM,   PRIV, PREVIEW, PRIV, VGA, RAW, MAXIMUM},
            // In-app viewfinder analysis with PRIV->JPEG ZSL and RAW
            {PRIV, MAXIMUM,   PRIV, PREVIEW, PRIV, VGA, RAW, MAXIMUM, JPEG, MAXIMUM},
            // In-app viewfinder analysis with YUV->JPEG ZSL and RAW
            {YUV , MAXIMUM,   PRIV, PREVIEW, PRIV, VGA, RAW, MAXIMUM, JPEG, MAXIMUM},
        };

        final int[][] ultraHighResolutionCombinations = {
            // Input           Outputs
            // RAW remosaic reprocessing with separate preview
            {RAW, MAX_RES,     PRIV, PREVIEW},
            {RAW, MAX_RES,     YUV, PREVIEW},
            // Ultra high res RAW -> JPEG / YUV with separate preview
            {RAW, MAX_RES,     PRIV, PREVIEW, JPEG, MAX_RES},
            {RAW, MAX_RES,     PRIV, PREVIEW, YUV, MAX_RES},
            {RAW, MAX_RES,     YUV, PREVIEW, JPEG, MAX_RES},
            {RAW, MAX_RES,     YUV, PREVIEW, YUV, MAX_RES},
            // Ultra high res PRIV / YUV -> YUV / JPEG reprocessing with separate preview
            {YUV, MAX_RES,     YUV, PREVIEW, JPEG, MAX_RES},
            {YUV, MAX_RES,     PRIV, PREVIEW, JPEG, MAX_RES},
            {PRIV, MAX_RES,    YUV, PREVIEW, JPEG, MAX_RES},
            {PRIV, MAX_RES,    PRIV, PREVIEW, JPEG, MAX_RES},
        };

        final int[][][] TABLES =
                {limitedCombinations, fullCombinations, rawCombinations, level3Combinations,
                 ultraHighResolutionCombinations};

        validityCheckConfigurationTables(TABLES);

        for (String id : getCameraIdsUnderTest()) {
            openDevice(id);
            MandatoryStreamCombination[] cs = mStaticInfo.getCharacteristics().get(
                    CameraCharacteristics.SCALER_MANDATORY_STREAM_COMBINATIONS);
            if ((cs == null) || (cs.length == 0)) {
                Log.i(TAG, "No mandatory stream combinations for camera: " + id + " skip test");
                closeDevice(id);
                continue;
            }

            boolean supportYuvReprocess = mStaticInfo.isCapabilitySupported(
                    CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_YUV_REPROCESSING);
            boolean supportOpaqueReprocess = mStaticInfo.isCapabilitySupported(
                    CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_PRIVATE_REPROCESSING);
            if (!supportYuvReprocess && !supportOpaqueReprocess) {
                Log.i(TAG, "No reprocess support for camera: " + id + " skip test");
                closeDevice(id);
                continue;
            }

            MaxStreamSizes maxSizes = new MaxStreamSizes(mStaticInfo, id, mContext);
            try {
                for (int[] c : limitedCombinations) {
                    assertTrue(String.format("Expected static reprocessable stream combination:" +
                                "%s not found among the available mandatory combinations",
                                maxSizes.reprocessCombinationToString(c)),
                            isMandatoryCombinationAvailable(c, maxSizes, /*isInput*/ true, cs));
                }

                if (mStaticInfo.isHardwareLevelAtLeastFull()) {
                    for (int[] c : fullCombinations) {
                        assertTrue(String.format(
                                    "Expected static reprocessable stream combination:" +
                                    "%s not found among the available mandatory combinations",
                                    maxSizes.reprocessCombinationToString(c)),
                                isMandatoryCombinationAvailable(c, maxSizes, /*isInput*/ true, cs));
                    }
                }

                if (mStaticInfo.isCapabilitySupported(
                        CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW)) {
                    for (int[] c : rawCombinations) {
                        assertTrue(String.format(
                                    "Expected static reprocessable stream combination:" +
                                    "%s not found among the available mandatory combinations",
                                    maxSizes.reprocessCombinationToString(c)),
                                isMandatoryCombinationAvailable(c, maxSizes, /*isInput*/ true, cs));
                    }
                }

                if (mStaticInfo.isHardwareLevelAtLeast(
                            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3)) {
                    for (int[] c : level3Combinations) {
                        assertTrue(String.format(
                                    "Expected static reprocessable stream combination:" +
                                    "%s not found among the available mandatory combinations",
                                    maxSizes.reprocessCombinationToString(c)),
                                isMandatoryCombinationAvailable(c, maxSizes, /*isInput*/ true, cs));
                    }
                }

                if (mStaticInfo.isCapabilitySupported(
                        CameraCharacteristics
                                .REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR)) {
                    MandatoryStreamCombination[] maxResolutionCombinations =
                            mStaticInfo.getCharacteristics().get(
                                    CameraCharacteristics
                                        .SCALER_MANDATORY_MAXIMUM_RESOLUTION_STREAM_COMBINATIONS);
                    for (int[] c : ultraHighResolutionCombinations) {
                        assertTrue(String.format(
                                "Expected static reprocessable stream combination:"
                                    + "%s not found among the available mandatory max resolution"
                                    + "combinations",
                                    maxSizes.reprocessCombinationToString(c)),
                                isMandatoryCombinationAvailable(c, maxSizes, /*isInput*/ true,
                                        maxResolutionCombinations));
                    }
                }
            } finally {
                closeDevice(id);
            }
        }
    }

    private boolean isMandatoryCombinationAvailable(final int[] combination,
            final MaxStreamSizes maxSizes,
            final MandatoryStreamCombination[] availableCombinations) {
        return isMandatoryCombinationAvailable(combination, maxSizes, /*isInput*/ false,
                availableCombinations, /*useCaseSpecified*/ false);
    }

    private boolean isMandatoryCombinationAvailable(final int[] combination,
            final MaxStreamSizes maxSizes, boolean isInput,
            final MandatoryStreamCombination[] availableCombinations) {
        return isMandatoryCombinationAvailable(combination, maxSizes, isInput,
                availableCombinations, /*useCaseSpecified*/ false);
    }

    private boolean isMandatoryCombinationAvailable(final int[] combination,
            final MaxStreamSizes maxSizes, boolean isInput,
            final MandatoryStreamCombination[] availableCombinations, boolean useCaseSpecified) {
        boolean supportYuvReprocess = mStaticInfo.isCapabilitySupported(
                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_YUV_REPROCESSING);
        boolean supportOpaqueReprocess = mStaticInfo.isCapabilitySupported(
                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_PRIVATE_REPROCESSING);
        // Static combinations to be verified can be composed of multiple entries
        // that have the following layout (format, size). In case "isInput" is set,
        // the first stream configuration entry will contain the input format and size
        // as well as the first matching output.
        // For combinations that contain streamUseCase, the layout will be (format, size, useCase).
        int streamCount = useCaseSpecified ? combination.length / 3 : combination.length / 2;

        List<Pair<Pair<Integer, Boolean>, Size>> currentCombination =
                new ArrayList<Pair<Pair<Integer, Boolean>, Size>>(streamCount);
        List<Integer> streamUseCases = new ArrayList<Integer>(streamCount);
        int i = 0;
        while (i < combination.length) {
            if (isInput && (i == 0)) {
                // Skip the combination if the format is not supported for reprocessing.
                if ((combination[i] == YUV && !supportYuvReprocess) ||
                        (combination[i] == PRIV && !supportOpaqueReprocess)) {
                    return true;
                }
                // Skip the combination if for MAX_RES size, the maximum resolution stream config
                // map doesn't have the given format in getInputFormats().
                if (combination[i + 1] == MAX_RES) {
                    StreamConfigurationMap maxResolutionStreamConfigMap =
                            mStaticInfo.getCharacteristics().get(
                                    CameraCharacteristics
                                            .SCALER_STREAM_CONFIGURATION_MAP_MAXIMUM_RESOLUTION);
                    int[] inputFormats = maxResolutionStreamConfigMap.getInputFormats();
                    int type = combination[i];
                    if (!Arrays.stream(inputFormats).anyMatch(index -> index == type)) {
                        return true;
                    }
                }
                Size sz = maxSizes.getMaxInputSizeForFormat(combination[i], combination[i + 1]);
                currentCombination.add(Pair.create(Pair.create(new Integer(combination[i]),
                            new Boolean(true)), sz));
                currentCombination.add(Pair.create(Pair.create(new Integer(combination[i]),
                            new Boolean(false)), sz));
            } else {
                Size sz = maxSizes.getOutputSizeForFormat(combination[i], combination[i+1]);
                currentCombination.add(Pair.create(Pair.create(new Integer(combination[i]),
                            new Boolean(false)), sz));
                if (useCaseSpecified) {
                    streamUseCases.add(combination[i + 2]);
                }
            }
            i += 2;
            if (useCaseSpecified) {
                i += 1;
            }
        }

        for (MandatoryStreamCombination c : availableCombinations) {
            List<MandatoryStreamInformation> streamInfoList = c.getStreamsInformation();
            if ((streamInfoList.size() == currentCombination.size()) &&
                    (isInput == c.isReprocessable())) {
                ArrayList<Pair<Pair<Integer, Boolean>, Size>> expected =
                        new ArrayList<Pair<Pair<Integer, Boolean>, Size>>(currentCombination);
                ArrayList<Integer> expectedStreamUseCases = new ArrayList<Integer>(streamUseCases);

                for (MandatoryStreamInformation streamInfo : streamInfoList) {
                    Size maxSize = CameraTestUtils.getMaxSize(
                            streamInfo.getAvailableSizes().toArray(new Size[0]));
                    Pair p = Pair.create(Pair.create(new Integer(streamInfo.getFormat()),
                            new Boolean(streamInfo.isInput())), maxSize);
                    if (expected.contains(p)) {
                        expected.remove(p);
                    }
                    if (useCaseSpecified) {
                        int streamUseCase = (int) streamInfo.getStreamUseCase();
                        if (expectedStreamUseCases.contains(streamUseCase)) {
                            expectedStreamUseCases.remove(Integer.valueOf(streamUseCase));
                        }
                    }
                }

                if (expected.isEmpty() && (!useCaseSpecified || expectedStreamUseCases.isEmpty())) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Verify correctness of the configuration tables.
     */
    private void validityCheckConfigurationTables(final int[][][] tables) throws Exception {
        validityCheckConfigurationTables(tables, false);
    }

    private void validityCheckConfigurationTables(final int[][][] tables, boolean useCaseSpecified)
            throws Exception {
        int tableIdx = 0;
        for (int[][] table : tables) {
            int rowIdx = 0;
            for (int[] row : table) {
                if (!useCaseSpecified) {
                    assertTrue(String.format("Odd number of entries for table %d row %d: %s ",
                                    tableIdx, rowIdx, Arrays.toString(row)),
                            (row.length % 2) == 0);
                } else {
                    assertTrue(String.format("Incorrect number entries for table with use case "
                                             + "specified %d row %d: %s ",
                                    tableIdx, rowIdx, Arrays.toString(row)),
                            (row.length % 3) == 0);
                }

                int i = 0;
                while (i < row.length) {
                    int format = row[i];
                    int maxSize = row[i + 1];
                    assertTrue(String.format("table %d row %d index %d format not valid: %d",
                                    tableIdx, rowIdx, i, format),
                            format == PRIV || format == JPEG || format == YUV
                                    || format == RAW);
                    assertTrue(String.format("table %d row %d index %d max size not valid: %d",
                                    tableIdx, rowIdx, i + 1, maxSize),
                            maxSize == PREVIEW || maxSize == RECORD
                                    || maxSize == MAXIMUM || maxSize == VGA || maxSize == S720P
                                    || maxSize == S1440P_4_3 || maxSize == MAX_RES);
                    if (useCaseSpecified) {
                        int useCase = row[i + 2];
                        assertTrue(String.format("table %d row %d index %d use case not valid: %d",
                                        tableIdx, rowIdx, i + 2, useCase),
                                useCase == USE_CASE_PREVIEW
                                        || useCase == USE_CASE_PREVIEW_VIDEO_STILL
                                        || useCase == USE_CASE_STILL_CAPTURE
                                        || useCase == USE_CASE_VIDEO_CALL
                                        || useCase == USE_CASE_VIDEO_RECORD
                                        || useCase == USE_CASE_CROPPED_RAW);
                        i += 3;
                    } else {
                        i += 2;
                    }
                }
                rowIdx++;
            }
            tableIdx++;
        }
    }
}
