Camera2: Mandatory stream configuration combinations test

Also mark this test as a known failure, since currently it
freezes CTS runs on some devices.

Bug: 16899526

Change-Id: I0e7560b9df004d74546a594efc423d8460f28a31
diff --git a/tests/expectations/knownfailures.txt b/tests/expectations/knownfailures.txt
index 0ab6acb..23c7437 100644
--- a/tests/expectations/knownfailures.txt
+++ b/tests/expectations/knownfailures.txt
@@ -54,5 +54,11 @@
     "android.openglperf.cts.GlVboPerfTest#testVboWithVaryingIndexBufferNumbers"
   ],
   bug: 17394321
-}
+},
+{
+  description: "this test deadlocks on some devices, freezing CTS runs, marking known failure until resolved",
+  names: [
+    "android.hardware.camera2.cts.RobustnessTest#testMandatoryOutputCombinations"
+  ],
+  bug: 16899526
 ]
diff --git a/tests/tests/hardware/src/android/hardware/camera2/cts/RobustnessTest.java b/tests/tests/hardware/src/android/hardware/camera2/cts/RobustnessTest.java
index 5b0f0fd..c394b47 100644
--- a/tests/tests/hardware/src/android/hardware/camera2/cts/RobustnessTest.java
+++ b/tests/tests/hardware/src/android/hardware/camera2/cts/RobustnessTest.java
@@ -16,17 +16,31 @@
 
 package android.hardware.camera2.cts;
 
+import static android.hardware.camera2.cts.CameraTestUtils.*;
+import static android.hardware.camera2.cts.RobustnessTest.MaxOutputSizes.*;
+
+import android.graphics.ImageFormat;
 import android.graphics.SurfaceTexture;
 import android.hardware.camera2.CameraAccessException;
 import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CameraDevice;
 import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.TotalCaptureResult;
+import android.hardware.camera2.CaptureFailure;
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.hardware.camera2.cts.CameraTestUtils;
+import android.hardware.camera2.cts.helpers.StaticMetadata;
 import android.hardware.camera2.cts.testcases.Camera2AndroidTestCase;
+import android.media.CamcorderProfile;
+import android.media.ImageReader;
 import android.util.Log;
+import android.util.Size;
 import android.view.Surface;
 
 import com.android.ex.camera2.blocking.BlockingSessionCallback;
 
+import java.util.Arrays;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -78,4 +92,388 @@
             }
         }
     }
+
+    /**
+     * Test for making sure the required output combinations for each hardware level and capability
+     * work as expected.
+     */
+    public void testMandatoryOutputCombinations() throws Exception {
+        /**
+         * Tables for maximum sizes to try for each hardware level and capability.
+         *
+         * Keep in sync with the tables in
+         * frameworks/base/core/java/android/hardware/camera2/CameraDevice.java#createCaptureSession
+         *
+         * Each row of the table is a set of (format, max resolution) pairs, using the below consts
+         */
+
+        // Enum values are defined in MaxOutputSizes
+        final int[][] LEGACY_COMBINATIONS = {
+            {PRIV, MAXIMUM}, // Simple preview, GPU video processing, or no-preview video recording
+            {JPEG, MAXIMUM}, // No-viewfinder still image capture
+            {YUV,  MAXIMUM}, // In-application video/image processing
+            {PRIV, PREVIEW,  JPEG, MAXIMUM}, // Standard still imaging.
+            {YUV,  PREVIEW,  JPEG, MAXIMUM}, // In-app processing plus still capture.
+            {PRIV, PREVIEW,  PRIV, PREVIEW}, // Standard recording.
+            {PRIV, PREVIEW,  YUV,  PREVIEW}, // Preview plus in-app processing.
+            {PRIV, PREVIEW,  YUV,  PREVIEW,  JPEG, MAXIMUM} // Still capture plus in-app processing.
+        };
+
+        final int[][] LIMITED_COMBINATIONS = {
+            {PRIV, PREVIEW,  PRIV, RECORD }, // High-resolution video recording with preview.
+            {PRIV, PREVIEW,  YUV , RECORD }, // High-resolution in-app video processing with preview.
+            {YUV , PREVIEW,  YUV , RECORD }, // Two-input in-app video processing.
+            {PRIV, PREVIEW,  PRIV, RECORD,   JPEG, RECORD  }, // High-resolution recording with video snapshot.
+            {PRIV, PREVIEW,  YUV,  RECORD,   JPEG, RECORD  }, // High-resolution in-app processing with video snapshot.
+            {YUV , PREVIEW,  YUV,  PREVIEW,  JPEG, MAXIMUM }  // Two-input in-app processing with still capture.
+        };
+
+        final int[][] FULL_COMBINATIONS = {
+            {PRIV, PREVIEW,  PRIV, MAXIMUM }, // Maximum-resolution GPU processing with preview.
+            {PRIV, PREVIEW,  YUV,  MAXIMUM }, // Maximum-resolution in-app processing with preview.
+            {YUV,  PREVIEW,  YUV,  MAXIMUM }, // Maximum-resolution two-input in-app processsing.
+            {PRIV, PREVIEW,  PRIV, PREVIEW,  JPEG, MAXIMUM }, //Video recording with maximum-size video snapshot.
+            {YUV,  VGA,      PRIV, PREVIEW,  YUV,  MAXIMUM }, // Standard video recording plus maximum-resolution in-app processing.
+            {YUV,  VGA,      YUV,  PREVIEW,  YUV,  MAXIMUM } // Preview plus two-input maximum-resolution in-app processing.
+        };
+
+        final int[][] RAW_COMBINATIONS = {
+            {RAW,  MAXIMUM }, // No-preview DNG capture.
+            {PRIV, PREVIEW,  RAW,  MAXIMUM }, // Standard DNG capture.
+            {YUV,  PREVIEW,  RAW,  MAXIMUM }, // In-app processing plus DNG capture.
+            {PRIV, PREVIEW,  PRIV, PREVIEW,  RAW, MAXIMUM}, // Video recording with DNG capture.
+            {PRIV, PREVIEW,  YUV,  PREVIEW,  RAW, MAXIMUM}, // Preview with in-app processing and DNG capture.
+            {YUV,  PREVIEW,  YUV,  PREVIEW,  RAW, MAXIMUM}, // Two-input in-app processing plus DNG capture.
+            {PRIV, PREVIEW,  JPEG, MAXIMUM,  RAW, MAXIMUM}, // Still capture with simultaneous JPEG and DNG.
+            {YUV,  PREVIEW,  JPEG, MAXIMUM,  RAW, MAXIMUM}  // In-app processing with simultaneous JPEG and DNG.
+        };
+
+        final int[][][] TABLES =
+                { LEGACY_COMBINATIONS, LIMITED_COMBINATIONS, FULL_COMBINATIONS, RAW_COMBINATIONS };
+
+        // Sanity check the tables
+        int tableIdx = 0;
+        for (int[][] table : TABLES) {
+            int rowIdx = 0;
+            for (int[] row : table) {
+                assertTrue(String.format("Odd number of entries for table %d row %d: %s ",
+                                tableIdx, rowIdx, Arrays.toString(row)),
+                        (row.length % 2) == 0);
+                for (int i = 0; i < row.length; i += 2) {
+                    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);
+                }
+                rowIdx++;
+            }
+            tableIdx++;
+        }
+
+        for (String id : mCameraIds) {
+
+            // Find the concrete max sizes for each format/resolution combination
+
+            CameraCharacteristics cc = mCameraManager.getCameraCharacteristics(id);
+
+            MaxOutputSizes maxSizes = new MaxOutputSizes(cc, id);
+
+            final StaticMetadata staticInfo = new StaticMetadata(cc);
+
+            openDevice(id);
+
+            // Always run legacy-level tests
+
+            for (int[] config : LEGACY_COMBINATIONS) {
+                testOutputCombination(id, config, maxSizes);
+            }
+
+            // Then run higher-level tests if applicable
+
+            if (!staticInfo.isHardwareLevelLegacy()) {
+
+                // If not legacy, at least limited, so run limited-level tests
+
+                for (int[] config : LIMITED_COMBINATIONS) {
+                    testOutputCombination(id, config, maxSizes);
+                }
+
+                // Check for FULL and RAW and run those if appropriate
+
+                if (staticInfo.isHardwareLevelFull()) {
+                    for (int[] config : FULL_COMBINATIONS) {
+                        testOutputCombination(id, config, maxSizes);
+                    }
+                }
+
+                if (staticInfo.isCapabilitySupported(
+                        CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW)) {
+                    for (int[] config : RAW_COMBINATIONS) {
+                        testOutputCombination(id, config, maxSizes);
+                    }
+                }
+            }
+
+            closeDevice(id);
+        }
+    }
+
+    /**
+     * Simple holder for resolutions to use for different camera outputs and size limits.
+     */
+    static class MaxOutputSizes {
+        // Format shorthands
+        static final int PRIV = -1;
+        static final int JPEG = ImageFormat.JPEG;
+        static final int YUV  = ImageFormat.YUV_420_888;
+        static final int RAW  = ImageFormat.RAW_SENSOR;
+
+        // Max resolution indices
+        static final int PREVIEW = 0;
+        static final int RECORD  = 1;
+        static final int MAXIMUM = 2;
+        static final int VGA = 3;
+        static final int RESOLUTION_COUNT = 4;
+
+        public MaxOutputSizes(CameraCharacteristics cc, String cameraId) {
+            StreamConfigurationMap configs =
+                    cc.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
+            Size[] privSizes = configs.getOutputSizes(SurfaceTexture.class);
+            Size[] yuvSizes = configs.getOutputSizes(ImageFormat.YUV_420_888);
+            Size[] jpegSizes = configs.getOutputSizes(ImageFormat.JPEG);
+            Size[] rawSizes = configs.getOutputSizes(ImageFormat.RAW_SENSOR);
+
+            maxRawSize = (rawSizes != null) ? CameraTestUtils.getMaxSize(rawSizes) : null;
+
+            maxPrivSizes[PREVIEW] = getMaxSize(privSizes, PREVIEW_SIZE_BOUND);
+            maxYuvSizes[PREVIEW]  = getMaxSize(yuvSizes, PREVIEW_SIZE_BOUND);
+            maxJpegSizes[PREVIEW] = getMaxSize(jpegSizes, PREVIEW_SIZE_BOUND);
+
+            maxPrivSizes[RECORD] = getMaxRecordingSize(cameraId);
+            maxYuvSizes[RECORD]  = getMaxRecordingSize(cameraId);
+            maxJpegSizes[RECORD] = getMaxRecordingSize(cameraId);
+
+            maxPrivSizes[MAXIMUM] = CameraTestUtils.getMaxSize(privSizes);
+            maxYuvSizes[MAXIMUM] = CameraTestUtils.getMaxSize(yuvSizes);
+            maxJpegSizes[MAXIMUM] = CameraTestUtils.getMaxSize(jpegSizes);
+
+            // Must always be supported, add unconditionally
+            final Size vgaSize = new Size(640, 480);
+            maxPrivSizes[VGA] = vgaSize;
+            maxJpegSizes[VGA] = vgaSize;
+            maxYuvSizes[VGA] = vgaSize;
+        }
+
+        public final Size[] maxPrivSizes = new Size[RESOLUTION_COUNT];
+        public final Size[] maxJpegSizes = new Size[RESOLUTION_COUNT];
+        public final Size[] maxYuvSizes = new Size[RESOLUTION_COUNT];
+        public final Size maxRawSize;
+
+        static public String configToString(int[] config) {
+            StringBuilder b = new StringBuilder("{ ");
+            for (int i = 0; i < config.length; i += 2) {
+                int format = config[i];
+                int sizeLimit = config[i + 1];
+                switch (format) {
+                    case PRIV:
+                        b.append("[PRIV, ");
+                        break;
+                    case JPEG:
+                        b.append("[JPEG, ");
+                        break;
+                    case YUV:
+                        b.append("[YUV, ");
+                        break;
+                    case RAW:
+                        b.append("[RAW, ");
+                        break;
+                    default:
+                        b.append("[UNK, ");
+                        break;
+                }
+                switch (sizeLimit) {
+                    case PREVIEW:
+                        b.append("PREVIEW] ");
+                        break;
+                    case RECORD:
+                        b.append("RECORD] ");
+                        break;
+                    case MAXIMUM:
+                        b.append("MAXIMUM] ");
+                        break;
+                    case VGA:
+                        b.append("VGA] ");
+                        break;
+                    default:
+                        b.append("UNK] ");
+                        break;
+                }
+            }
+            b.append("}");
+            return b.toString();
+        }
+    }
+
+    private void testOutputCombination(String cameraId, int[] config, MaxOutputSizes maxSizes)
+            throws Exception {
+
+        Log.i(TAG, String.format("Testing Camera %s, config %s",
+                        cameraId, MaxOutputSizes.configToString(config)));
+
+        final int TIMEOUT_FOR_RESULT_MS = 1000;
+        final int MIN_RESULT_COUNT = 3;
+
+        // Set up outputs
+        List<Object> outputTargets = new ArrayList<>();
+        List<Surface> outputSurfaces = new ArrayList<>();
+        for (int i = 0; i < config.length; i += 2) {
+            int format = config[i];
+            int sizeLimit = config[i + 1];
+
+            switch (format) {
+                case PRIV: {
+                    Size targetSize = maxSizes.maxPrivSizes[sizeLimit];
+                    SurfaceTexture target = new SurfaceTexture(/*random int*/1);
+                    target.setDefaultBufferSize(targetSize.getWidth(), targetSize.getHeight());
+                    outputTargets.add(target);
+                    outputSurfaces.add(new Surface(target));
+                    break;
+                }
+                case JPEG: {
+                    Size targetSize = maxSizes.maxJpegSizes[sizeLimit];
+                    ImageReader target = ImageReader.newInstance(
+                        targetSize.getWidth(), targetSize.getHeight(), JPEG, MIN_RESULT_COUNT);
+                    outputTargets.add(target);
+                    outputSurfaces.add(target.getSurface());
+                    break;
+                }
+                case YUV: {
+                    Size targetSize = maxSizes.maxYuvSizes[sizeLimit];
+                    ImageReader target = ImageReader.newInstance(
+                        targetSize.getWidth(), targetSize.getHeight(), YUV, MIN_RESULT_COUNT);
+                    outputTargets.add(target);
+                    outputSurfaces.add(target.getSurface());
+                    break;
+                }
+                case RAW: {
+                    Size targetSize = maxSizes.maxRawSize;
+                    ImageReader target = ImageReader.newInstance(
+                        targetSize.getWidth(), targetSize.getHeight(), RAW, MIN_RESULT_COUNT);
+                    outputTargets.add(target);
+                    outputSurfaces.add(target.getSurface());
+                    break;
+                }
+                default:
+                    fail("Unknown output format " + format);
+            }
+        }
+
+        boolean haveSession = false;
+        try {
+            CaptureRequest.Builder requestBuilder =
+                    mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
+
+            for (Surface s : outputSurfaces) {
+                requestBuilder.addTarget(s);
+            }
+
+            CameraCaptureSession.CaptureCallback mockCaptureCallback =
+                    mock(CameraCaptureSession.CaptureCallback.class);
+
+            createSession(outputSurfaces);
+            haveSession = true;
+            CaptureRequest request = requestBuilder.build();
+            mCameraSession.setRepeatingRequest(request, mockCaptureCallback, mHandler);
+
+            verify(mockCaptureCallback,
+                    timeout(TIMEOUT_FOR_RESULT_MS * MIN_RESULT_COUNT).atLeast(MIN_RESULT_COUNT))
+                    .onCaptureCompleted(
+                        eq(mCameraSession),
+                        eq(request),
+                        isA(TotalCaptureResult.class));
+            verify(mockCaptureCallback, never()).
+                    onCaptureFailed(
+                        eq(mCameraSession),
+                        eq(request),
+                        isA(CaptureFailure.class));
+
+        } catch (Throwable e) {
+            mCollector.addMessage(String.format("Output combination %s failed due to: %s",
+                    MaxOutputSizes.configToString(config), e.getMessage()));
+        }
+        if (haveSession) {
+            try {
+                Log.i(TAG, String.format("Done with camera %s, config %s, closing session",
+                                cameraId, MaxOutputSizes.configToString(config)));
+                stopCapture(/*fast*/false);
+            } catch (Throwable e) {
+                mCollector.addMessage(
+                    String.format("Closing down for output combination %s failed due to: %s",
+                            MaxOutputSizes.configToString(config), e.getMessage()));
+            }
+        }
+    }
+
+    private static Size getMaxRecordingSize(String cameraId) {
+        int id = Integer.valueOf(cameraId);
+
+        int quality =
+                CamcorderProfile.hasProfile(id, CamcorderProfile.QUALITY_2160P) ?
+                    CamcorderProfile.QUALITY_2160P :
+                CamcorderProfile.hasProfile(id, CamcorderProfile.QUALITY_1080P) ?
+                    CamcorderProfile.QUALITY_1080P :
+                CamcorderProfile.hasProfile(id, CamcorderProfile.QUALITY_720P) ?
+                    CamcorderProfile.QUALITY_720P :
+                CamcorderProfile.hasProfile(id, CamcorderProfile.QUALITY_480P) ?
+                    CamcorderProfile.QUALITY_480P :
+                CamcorderProfile.hasProfile(id, CamcorderProfile.QUALITY_QVGA) ?
+                    CamcorderProfile.QUALITY_QVGA :
+                CamcorderProfile.hasProfile(id, CamcorderProfile.QUALITY_CIF) ?
+                    CamcorderProfile.QUALITY_CIF :
+                CamcorderProfile.hasProfile(id, CamcorderProfile.QUALITY_QCIF) ?
+                    CamcorderProfile.QUALITY_QCIF :
+                    -1;
+
+        assertTrue("No recording supported for camera id " + cameraId, quality != -1);
+
+        CamcorderProfile maxProfile = CamcorderProfile.get(id, quality);
+        return new Size(maxProfile.videoFrameWidth, maxProfile.videoFrameHeight);
+    }
+
+    /**
+     * Get maximum size in list that's equal or smaller to than the bound.
+     * Returns null if no size is smaller than or equal to the bound.
+     */
+    private static Size getMaxSize(Size[] sizes, Size bound) {
+        if (sizes == null || sizes.length == 0) {
+            throw new IllegalArgumentException("sizes was empty");
+        }
+
+        Size sz = null;
+        for (Size size : sizes) {
+            if (size.getWidth() <= bound.getWidth() && size.getHeight() <= bound.getHeight()) {
+
+                if (sz == null) {
+                    sz = size;
+                } else {
+                    long curArea = sz.getWidth() * (long) sz.getHeight();
+                    long newArea = size.getWidth() * (long) size.getHeight();
+                    if ( newArea > curArea ) {
+                        sz = size;
+                    }
+                }
+            }
+        }
+
+        assertTrue("No size under bound found: " + Arrays.toString(sizes) + " bound " + bound,
+                sz != null);
+
+        return sz;
+    }
+
 }