Merge "CamcorderProfile: guard unknown profile qualities" into lmp-sprout-dev
diff --git a/apps/CameraITS/tests/scene1/test_ae_precapture_trigger.py b/apps/CameraITS/tests/scene1/test_ae_precapture_trigger.py
index b1f51f3..563cebd 100644
--- a/apps/CameraITS/tests/scene1/test_ae_precapture_trigger.py
+++ b/apps/CameraITS/tests/scene1/test_ae_precapture_trigger.py
@@ -17,6 +17,9 @@
 import its.objects
 import its.target
 
+# AE must converge within this number of auto requests under scene1
+THRESH_AE_CONVERGE = 8
+
 def main():
     """Test the AE state machine when using the precapture trigger.
     """
@@ -68,7 +71,7 @@
 
         # Capture some more auto requests, and AE should converge.
         auto_req['android.control.aePrecaptureTrigger'] = 0
-        caps = cam.do_capture([auto_req]*5, fmt)
+        caps = cam.do_capture([auto_req] * THRESH_AE_CONVERGE, fmt)
         state = caps[-1]['metadata']['android.control.aeState']
         print "AE state after auto request:", state
         assert(state == CONVERGED)
diff --git a/tests/tests/hardware/src/android/hardware/camera2/cts/StaticMetadataCollectionTest.java b/tests/tests/hardware/src/android/hardware/camera2/cts/StaticMetadataCollectionTest.java
new file mode 100644
index 0000000..283f09b
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/camera2/cts/StaticMetadataCollectionTest.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 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 android.content.pm.PackageManager;
+import android.cts.util.DeviceReportLog;
+import android.hardware.camera2.cts.testcases.Camera2SurfaceViewTestCase;
+import android.hardware.camera2.cts.helpers.CameraMetadataGetter;
+import android.util.Log;
+
+import com.android.cts.util.ResultType;
+import com.android.cts.util.ResultUnit;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.Iterator;
+
+/**
+ * This test collects camera2 API static metadata and reports to device report.
+ *
+ */
+public class StaticMetadataCollectionTest extends Camera2SurfaceViewTestCase {
+    private static final String TAG = "StaticMetadataCollectionTest";
+
+    private DeviceReportLog mReportLog;
+
+    @Override
+    protected void setUp() throws Exception {
+        mReportLog = new DeviceReportLog();
+        super.setUp();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        // Deliver the report to host will automatically clear the report log.
+        mReportLog.deliverReportToHost(getInstrumentation());
+        super.tearDown();
+    }
+
+    public void testDataCollection() {
+        if (hasCameraFeature()) {
+            CameraMetadataGetter cameraInfoGetter = new CameraMetadataGetter(mCameraManager);
+            for (String id : mCameraIds) {
+                // Gather camera info
+                JSONObject cameraInfo = cameraInfoGetter.getCameraInfo(id);
+                dumpJsonObjectAsCtsResult(String.format("camera2_id%s_static_info", id), cameraInfo);
+                dumpDoubleAsCtsResult(String.format("camera2_id%s_static_info:", id)
+                        + cameraInfo.toString(), 0);
+
+                JSONObject[] templates = cameraInfoGetter.getCaptureRequestTemplates(id);
+                for (int i = 0; i < templates.length; i++) {
+                    dumpJsonObjectAsCtsResult(String.format("camera2_id%s_capture_template%d",
+                            id, CameraMetadataGetter.TEMPLATE_IDS[i]), templates[i]);
+                    if (templates[i] != null) {
+                        dumpDoubleAsCtsResult(String.format("camera2_id%s_capture_template%d:",
+                                id, CameraMetadataGetter.TEMPLATE_IDS[i])
+                                + templates[i].toString(), 0);
+                    }
+                }
+            }
+
+            try {
+                cameraInfoGetter.close();
+            } catch (Exception e) {
+                Log.e(TAG, "Unable to close camera info getter " + e.getMessage());
+            }
+
+            mReportLog.printSummary("Camera data collection for static info and capture request"
+                    + " templates",
+                    0.0, ResultType.NEUTRAL, ResultUnit.NONE);
+        }
+
+    }
+
+    private void dumpDoubleAsCtsResult(String name, double value) {
+        mReportLog.printValue(name, value, ResultType.NEUTRAL, ResultUnit.NONE);
+    }
+
+    public void dumpDoubleArrayAsCtsResult(String name, double[] values) {
+        mReportLog.printArray(name, values, ResultType.NEUTRAL, ResultUnit.NONE);
+    }
+
+    private double getJsonValueAsDouble(String name, Object obj) throws Exception {
+        if (obj == null) {
+            Log.e(TAG, "Null value: " + name);
+            throw new Exception();
+        } else if (obj instanceof Double) {
+            return ((Double)obj).doubleValue();
+        } else if (obj instanceof Float) {
+            return ((Float)obj).floatValue();
+        } else if (obj instanceof Long) {
+            return ((Long)obj).longValue();
+        } else if (obj instanceof Integer) {
+            return ((Integer)obj).intValue();
+        } else if (obj instanceof Byte) {
+            return ((Byte)obj).intValue();
+        } else if (obj instanceof Short) {
+            return ((Short)obj).intValue();
+        } else if (obj instanceof Boolean) {
+            return ((Boolean)obj) ? 1 : 0;
+        } else {
+            Log.e(TAG, "Unsupported value type: " + name);
+            throw new Exception();
+        }
+    }
+
+    private void dumpJsonArrayAsCtsResult(String name, JSONArray arr) throws Exception {
+        if (arr == null || arr.length() == 0) {
+            dumpDoubleAsCtsResult(name + "[]", 0);
+        } else if (arr.get(0) instanceof JSONObject) {
+            for (int i = 0; i < arr.length(); i++) {
+                dumpJsonObjectAsCtsResult(name+String.format("[%04d]",i),(JSONObject)arr.get(i));
+            }
+        } else if (arr.get(0) instanceof JSONArray) {
+            for (int i = 0; i < arr.length(); i++) {
+                dumpJsonArrayAsCtsResult(name+String.format("[%04d]",i),(JSONArray)arr.get(i));
+            }
+        } else if (!(arr.get(0) instanceof String)) {
+            double[] values = new double[arr.length()];
+            for (int i = 0; i < arr.length(); i++) {
+                values[i] = getJsonValueAsDouble(name + "[]", arr.get(i));
+            }
+            dumpDoubleArrayAsCtsResult(name + "[]", values);
+        } else if (arr.get(0) instanceof String) {
+            for (int i = 0; i < arr.length(); i++) {
+                dumpDoubleAsCtsResult(
+                        name+String.format("[%04d]",i)+" = "+(String)arr.get(i), 0);
+            }
+        } else {
+            Log.e(TAG, "Unsupported array value type: " + name);
+            throw new Exception();
+        }
+    }
+
+    private void dumpJsonObjectAsCtsResult(String name, JSONObject obj) {
+        if (obj == null) {
+            dumpDoubleAsCtsResult(name + "{}", 0);
+            return;
+        }
+        Iterator<?> keys = obj.keys();
+        while (keys.hasNext()) {
+            try {
+                String key = (String)keys.next();
+                if (obj.get(key) instanceof JSONObject) {
+                    dumpJsonObjectAsCtsResult(name+"."+key, (JSONObject)obj.get(key));
+                } else if (obj.get(key) instanceof JSONArray) {
+                    dumpJsonArrayAsCtsResult(name+"."+key, (JSONArray)obj.get(key));
+                } else if (!(obj.get(key) instanceof String)) {
+                    dumpDoubleAsCtsResult(name+"."+key,
+                            getJsonValueAsDouble(name+"."+key, obj.get(key)));
+                } else if (obj.get(key) instanceof String) {
+                    dumpDoubleAsCtsResult(name+"."+key + " = " + (String)obj.get(key), 0);
+                } else {
+                    Log.e(TAG, "Unsupported object field type: " + name + "." + key);
+                }
+            } catch (Exception e) {
+                // Swallow
+            }
+        }
+    }
+
+    private boolean hasCameraFeature() {
+        PackageManager packageManager = getActivity().getPackageManager();
+        return packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
+    }
+}
diff --git a/tests/tests/hardware/src/android/hardware/camera2/cts/StillCaptureTest.java b/tests/tests/hardware/src/android/hardware/camera2/cts/StillCaptureTest.java
index f18a1cf..e816659 100644
--- a/tests/tests/hardware/src/android/hardware/camera2/cts/StillCaptureTest.java
+++ b/tests/tests/hardware/src/android/hardware/camera2/cts/StillCaptureTest.java
@@ -291,6 +291,12 @@
             try {
                 Log.i(TAG, "Testing AE compensation for Camera " + id);
                 openDevice(id);
+
+                if (mStaticInfo.isHardwareLevelLegacy()) {
+                    Log.i(TAG, "Skipping test on legacy devices");
+                    continue;
+                }
+
                 aeCompensationTestByCamera();
             } finally {
                 closeDevice();
diff --git a/tests/tests/hardware/src/android/hardware/camera2/cts/helpers/CameraMetadataGetter.java b/tests/tests/hardware/src/android/hardware/camera2/cts/helpers/CameraMetadataGetter.java
new file mode 100755
index 0000000..db75cdd
--- /dev/null
+++ b/tests/tests/hardware/src/android/hardware/camera2/cts/helpers/CameraMetadataGetter.java
@@ -0,0 +1,690 @@
+/*
+ * Copyright (C) 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.helpers;
+
+import static com.android.ex.camera2.blocking.BlockingStateCallback.*;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.CameraMetadata;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.TotalCaptureResult;
+import android.hardware.camera2.params.BlackLevelPattern;
+import android.hardware.camera2.params.ColorSpaceTransform;
+import android.hardware.camera2.params.Face;
+import android.hardware.camera2.params.LensShadingMap;
+import android.hardware.camera2.params.MeteringRectangle;
+import android.hardware.camera2.params.RggbChannelVector;
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.hardware.camera2.params.TonemapCurve;
+import android.location.Location;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Log;
+import android.util.Pair;
+import android.util.Rational;
+import android.util.Size;
+import android.util.SizeF;
+import android.util.Range;
+
+import com.android.ex.camera2.blocking.BlockingCameraManager;
+import com.android.ex.camera2.blocking.BlockingCameraManager.BlockingOpenException;
+import com.android.ex.camera2.blocking.BlockingStateCallback;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.Field;
+import java.lang.reflect.GenericArrayType;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+
+/**
+ * Utility class to dump the camera metadata.
+ */
+public final class CameraMetadataGetter implements AutoCloseable {
+    private static final String TAG = CameraMetadataGetter.class.getSimpleName();
+    private static final int CAMERA_CLOSE_TIMEOUT_MS = 5000;
+    public static final int[] TEMPLATE_IDS = {
+        CameraDevice.TEMPLATE_PREVIEW,
+        CameraDevice.TEMPLATE_STILL_CAPTURE,
+        CameraDevice.TEMPLATE_RECORD,
+        CameraDevice.TEMPLATE_VIDEO_SNAPSHOT,
+        CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG,
+        CameraDevice.TEMPLATE_MANUAL,
+    };
+    private CameraManager mCameraManager;
+    private BlockingStateCallback mCameraListener;
+    private HandlerThread mHandlerThread;
+    private Handler mHandler;
+
+    private static class MetadataEntry {
+        public MetadataEntry(String k, Object v) {
+            key = k;
+            value = v;
+        }
+
+        public String key;
+        public Object value;
+    }
+
+    public CameraMetadataGetter(CameraManager cameraManager) {
+        if (cameraManager == null) {
+            throw new IllegalArgumentException("can not create an CameraMetadataGetter object"
+                    + " with null CameraManager");
+        }
+
+        mCameraManager = cameraManager;
+
+        mCameraListener = new BlockingStateCallback();
+        mHandlerThread = new HandlerThread(TAG);
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+    }
+
+    public String getCameraInfo() {
+        StringBuffer cameraInfo = new StringBuffer("{\"CameraStaticMetadata\":{");
+        CameraCharacteristics staticMetadata;
+        String[] cameraIds;
+        try {
+            cameraIds = mCameraManager.getCameraIdList();
+        } catch (CameraAccessException e) {
+            Log.e(TAG, "Unable to get camera ids, skip this info, error: " + e.getMessage());
+            return "";
+        }
+        for (String id : cameraIds) {
+            String value = null;
+            try {
+                staticMetadata = mCameraManager.getCameraCharacteristics(id);
+                value = serialize(staticMetadata).toString();
+            } catch (CameraAccessException e) {
+                Log.e(TAG,
+                        "Unable to get camera camera static info, skip this camera, error: "
+                                + e.getMessage());
+            }
+            cameraInfo.append("\"camera" + id + "\":"); // Key
+            cameraInfo.append(value); // Value
+            // If not last, print "," // Separator
+            if (!id.equals(cameraIds[cameraIds.length - 1])) {
+                cameraInfo.append(",");
+            }
+        }
+        cameraInfo.append("}}");
+
+        return cameraInfo.toString();
+    }
+
+    public JSONObject getCameraInfo(String cameraId) {
+        JSONObject staticMetadata = null;
+        try {
+            staticMetadata = serialize(mCameraManager.getCameraCharacteristics(cameraId));
+        } catch (CameraAccessException e) {
+            Log.e(TAG,
+                    "Unable to get camera camera static info, skip this camera, error: "
+                            + e.getMessage());
+        }
+        return staticMetadata;
+    }
+
+    public JSONObject[] getCaptureRequestTemplates(String cameraId) {
+        JSONObject[] templates = new JSONObject[TEMPLATE_IDS.length];
+        CameraDevice camera = null;
+        try {
+            camera = (new BlockingCameraManager(mCameraManager)).openCamera(cameraId,
+                            mCameraListener, mHandler);
+            for (int i = 0; i < TEMPLATE_IDS.length; i++) {
+                CaptureRequest.Builder request;
+                try {
+                    request = camera.createCaptureRequest(TEMPLATE_IDS[i]);
+                    templates[i] = serialize(request.build());
+                } catch (Exception e) {
+                    Log.e(TAG, "Unable to create template " + TEMPLATE_IDS[i]
+                                    + " because of error " + e.getMessage());
+                    templates[i] = null;
+                }
+            }
+            return templates;
+        } catch (CameraAccessException | BlockingOpenException e) {
+            Log.e(TAG, "Unable to open camera " + cameraId + " because of error "
+                            + e.getMessage());
+            return new JSONObject[0];
+        } finally {
+            if (camera != null) {
+                camera.close();
+            }
+        }
+    }
+
+    public String getCaptureRequestTemplates() {
+        StringBuffer templates = new StringBuffer("{\"CameraRequestTemplates\":{");
+        String[] cameraIds;
+        try {
+            cameraIds = mCameraManager.getCameraIdList();
+        } catch (CameraAccessException e) {
+            Log.e(TAG, "Unable to get camera ids, skip this info, error: " + e.getMessage());
+            return "";
+        }
+        CameraDevice camera = null;
+        for (String id : cameraIds) {
+            try {
+                try {
+                    camera = (new BlockingCameraManager(mCameraManager)).openCamera(id,
+                                    mCameraListener, mHandler);
+                } catch (CameraAccessException | BlockingOpenException e) {
+                    Log.e(TAG, "Unable to open camera " + id + " because of error "
+                                    + e.getMessage());
+                    continue;
+                }
+
+                for (int i = 0; i < TEMPLATE_IDS.length; i++) {
+                    String value = null;
+                    CaptureRequest.Builder request;
+                    try {
+                        request = camera.createCaptureRequest(TEMPLATE_IDS[i]);
+                        value = serialize(request.build()).toString();
+                    } catch (Exception e) {
+                        Log.e(TAG, "Unable to create template " + TEMPLATE_IDS[i]
+                                        + " because of error " + e.getMessage());
+                    }
+                    templates.append("\"Camera" + id + "CaptureTemplate" +
+                                    TEMPLATE_IDS[i] + "\":");
+                    templates.append(value);
+                    if (!id.equals(cameraIds[cameraIds.length - 1]) ||
+                                    i < (TEMPLATE_IDS.length - 1)) {
+                        templates.append(",");
+                    }
+                }
+            } finally {
+                if (camera != null) {
+                    camera.close();
+                    mCameraListener.waitForState(STATE_CLOSED, CAMERA_CLOSE_TIMEOUT_MS);
+                }
+            }
+        }
+
+        templates.append("}}");
+        return templates.toString();
+    }
+
+    /*
+     * Cleanup the resources.
+     */
+    @Override
+    public void close() throws Exception {
+        mHandlerThread.quitSafely();
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            close();
+        } finally {
+            super.finalize();
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private static Object serializeRational(Rational rat) throws org.json.JSONException {
+        JSONObject ratObj = new JSONObject();
+        ratObj.put("numerator", rat.getNumerator());
+        ratObj.put("denominator", rat.getDenominator());
+        return ratObj;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static Object serializeSize(Size size) throws org.json.JSONException {
+        JSONObject sizeObj = new JSONObject();
+        sizeObj.put("width", size.getWidth());
+        sizeObj.put("height", size.getHeight());
+        return sizeObj;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static Object serializeSizeF(SizeF size) throws org.json.JSONException {
+        JSONObject sizeObj = new JSONObject();
+        sizeObj.put("width", size.getWidth());
+        sizeObj.put("height", size.getHeight());
+        return sizeObj;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static Object serializeRect(Rect rect) throws org.json.JSONException {
+        JSONObject rectObj = new JSONObject();
+        rectObj.put("left", rect.left);
+        rectObj.put("right", rect.right);
+        rectObj.put("top", rect.top);
+        rectObj.put("bottom", rect.bottom);
+        return rectObj;
+    }
+
+    private static Object serializePoint(Point point) throws org.json.JSONException {
+        JSONObject pointObj = new JSONObject();
+        pointObj.put("x", point.x);
+        pointObj.put("y", point.y);
+        return pointObj;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static Object serializeFace(Face face)
+                    throws org.json.JSONException {
+        JSONObject faceObj = new JSONObject();
+        faceObj.put("bounds", serializeRect(face.getBounds()));
+        faceObj.put("score", face.getScore());
+        faceObj.put("id", face.getId());
+        faceObj.put("leftEye", serializePoint(face.getLeftEyePosition()));
+        faceObj.put("rightEye", serializePoint(face.getRightEyePosition()));
+        faceObj.put("mouth", serializePoint(face.getMouthPosition()));
+        return faceObj;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static Object serializeStreamConfigurationMap(
+                    StreamConfigurationMap map)
+                    throws org.json.JSONException {
+        // TODO: Serialize the rest of the StreamConfigurationMap fields.
+        JSONObject mapObj = new JSONObject();
+        JSONArray cfgArray = new JSONArray();
+        int fmts[] = map.getOutputFormats();
+        if (fmts != null) {
+            for (int fi = 0; fi < Array.getLength(fmts); fi++) {
+                Size sizes[] = map.getOutputSizes(fmts[fi]);
+                if (sizes != null) {
+                    for (int si = 0; si < Array.getLength(sizes); si++) {
+                        JSONObject obj = new JSONObject();
+                        obj.put("format", fmts[fi]);
+                        obj.put("width", sizes[si].getWidth());
+                        obj.put("height", sizes[si].getHeight());
+                        obj.put("input", false);
+                        obj.put("minFrameDuration",
+                                        map.getOutputMinFrameDuration(fmts[fi], sizes[si]));
+                        cfgArray.put(obj);
+                    }
+                }
+            }
+        }
+        mapObj.put("availableStreamConfigurations", cfgArray);
+        return mapObj;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static Object serializeMeteringRectangle(MeteringRectangle rect)
+                    throws org.json.JSONException {
+        JSONObject rectObj = new JSONObject();
+        rectObj.put("x", rect.getX());
+        rectObj.put("y", rect.getY());
+        rectObj.put("width", rect.getWidth());
+        rectObj.put("height", rect.getHeight());
+        rectObj.put("weight", rect.getMeteringWeight());
+        return rectObj;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static Object serializePair(Pair pair)
+                    throws org.json.JSONException {
+        JSONArray pairObj = new JSONArray();
+        pairObj.put(pair.first);
+        pairObj.put(pair.second);
+        return pairObj;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static Object serializeRange(Range range)
+                    throws org.json.JSONException {
+        JSONArray rangeObj = new JSONArray();
+        rangeObj.put(range.getLower());
+        rangeObj.put(range.getUpper());
+        return rangeObj;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static Object serializeColorSpaceTransform(ColorSpaceTransform xform)
+                    throws org.json.JSONException {
+        JSONArray xformObj = new JSONArray();
+        for (int row = 0; row < 3; row++) {
+            for (int col = 0; col < 3; col++) {
+                xformObj.put(serializeRational(xform.getElement(col, row)));
+            }
+        }
+        return xformObj;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static Object serializeTonemapCurve(TonemapCurve curve)
+                    throws org.json.JSONException {
+        JSONObject curveObj = new JSONObject();
+        String names[] = {
+                        "red", "green", "blue" };
+        for (int ch = 0; ch < 3; ch++) {
+            JSONArray curveArr = new JSONArray();
+            int len = curve.getPointCount(ch);
+            for (int i = 0; i < len; i++) {
+                curveArr.put(curve.getPoint(ch, i).x);
+                curveArr.put(curve.getPoint(ch, i).y);
+            }
+            curveObj.put(names[ch], curveArr);
+        }
+        return curveObj;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static Object serializeRggbChannelVector(RggbChannelVector vec)
+                    throws org.json.JSONException {
+        JSONArray vecObj = new JSONArray();
+        vecObj.put(vec.getRed());
+        vecObj.put(vec.getGreenEven());
+        vecObj.put(vec.getGreenOdd());
+        vecObj.put(vec.getBlue());
+        return vecObj;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static Object serializeBlackLevelPattern(BlackLevelPattern pat)
+                    throws org.json.JSONException {
+        int patVals[] = new int[4];
+        pat.copyTo(patVals, 0);
+        JSONArray patObj = new JSONArray();
+        patObj.put(patVals[0]);
+        patObj.put(patVals[1]);
+        patObj.put(patVals[2]);
+        patObj.put(patVals[3]);
+        return patObj;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static Object serializeLocation(Location loc)
+                    throws org.json.JSONException {
+        return loc.toString();
+    }
+
+    @SuppressWarnings("unchecked")
+    private static Object serializeLensShadingMap(LensShadingMap map)
+            throws org.json.JSONException {
+        JSONArray mapObj = new JSONArray();
+        for (int row = 0; row < map.getRowCount(); row++) {
+            for (int col = 0; col < map.getColumnCount(); col++) {
+                for (int ch = 0; ch < 4; ch++) {
+                    mapObj.put(map.getGainFactor(ch, col, row));
+                }
+            }
+        }
+        return mapObj;
+    }
+
+    private static String getKeyName(Object keyObj) {
+        if (keyObj.getClass() == CaptureResult.Key.class
+                || keyObj.getClass() == TotalCaptureResult.class) {
+            return ((CaptureResult.Key) keyObj).getName();
+        } else if (keyObj.getClass() == CaptureRequest.Key.class) {
+            return ((CaptureRequest.Key) keyObj).getName();
+        } else if (keyObj.getClass() == CameraCharacteristics.Key.class) {
+            return ((CameraCharacteristics.Key) keyObj).getName();
+        }
+
+        throw new IllegalArgumentException("Invalid key object");
+    }
+
+    private static Object getKeyValue(CameraMetadata md, Object keyObj) {
+        if (md.getClass() == CaptureResult.class || md.getClass() == TotalCaptureResult.class) {
+            return ((CaptureResult) md).get((CaptureResult.Key) keyObj);
+        } else if (md.getClass() == CaptureRequest.class) {
+            return ((CaptureRequest) md).get((CaptureRequest.Key) keyObj);
+        } else if (md.getClass() == CameraCharacteristics.class) {
+            return ((CameraCharacteristics) md).get((CameraCharacteristics.Key) keyObj);
+        }
+
+        throw new IllegalArgumentException("Invalid key object");
+    }
+
+    @SuppressWarnings("unchecked")
+    private static MetadataEntry serializeEntry(Type keyType, Object keyObj, CameraMetadata md) {
+        String keyName = getKeyName(keyObj);
+
+        try {
+            Object keyValue = getKeyValue(md, keyObj);
+            if (keyValue == null) {
+                return new MetadataEntry(keyName, JSONObject.NULL);
+            } else if (keyType == Float.class) {
+                // The JSON serializer doesn't handle floating point NaN or Inf.
+                if (((Float) keyValue).isInfinite() || ((Float) keyValue).isNaN()) {
+                    Log.w(TAG, "Inf/NaN floating point value serialized: " + keyName);
+                    return null;
+                }
+                return new MetadataEntry(keyName, keyValue);
+            } else if (keyType == Integer.class || keyType == Long.class || keyType == Byte.class ||
+                    keyType == Boolean.class || keyType == String.class) {
+                return new MetadataEntry(keyName, keyValue);
+            } else if (keyType == Rational.class) {
+                return new MetadataEntry(keyName, serializeRational((Rational) keyValue));
+            } else if (keyType == Size.class) {
+                return new MetadataEntry(keyName, serializeSize((Size) keyValue));
+            } else if (keyType == SizeF.class) {
+                return new MetadataEntry(keyName, serializeSizeF((SizeF) keyValue));
+            } else if (keyType == Rect.class) {
+                return new MetadataEntry(keyName, serializeRect((Rect) keyValue));
+            } else if (keyType == Face.class) {
+                return new MetadataEntry(keyName, serializeFace((Face) keyValue));
+            } else if (keyType == StreamConfigurationMap.class) {
+                return new MetadataEntry(keyName,
+                        serializeStreamConfigurationMap((StreamConfigurationMap) keyValue));
+            } else if (keyType instanceof ParameterizedType &&
+                    ((ParameterizedType) keyType).getRawType() == Range.class) {
+                return new MetadataEntry(keyName, serializeRange((Range) keyValue));
+            } else if (keyType == ColorSpaceTransform.class) {
+                return new MetadataEntry(keyName,
+                        serializeColorSpaceTransform((ColorSpaceTransform) keyValue));
+            } else if (keyType == MeteringRectangle.class) {
+                return new MetadataEntry(keyName,
+                        serializeMeteringRectangle((MeteringRectangle) keyValue));
+            } else if (keyType == Location.class) {
+                return new MetadataEntry(keyName,
+                        serializeLocation((Location) keyValue));
+            } else if (keyType == RggbChannelVector.class) {
+                return new MetadataEntry(keyName,
+                        serializeRggbChannelVector((RggbChannelVector) keyValue));
+            } else if (keyType == BlackLevelPattern.class) {
+                return new MetadataEntry(keyName,
+                        serializeBlackLevelPattern((BlackLevelPattern) keyValue));
+            } else if (keyType == TonemapCurve.class) {
+                return new MetadataEntry(keyName,
+                        serializeTonemapCurve((TonemapCurve) keyValue));
+            } else if (keyType == Point.class) {
+                return new MetadataEntry(keyName,
+                        serializePoint((Point) keyValue));
+            } else if (keyType == LensShadingMap.class) {
+                return new MetadataEntry(keyName,
+                        serializeLensShadingMap((LensShadingMap) keyValue));
+            } else {
+                Log.w(TAG, String.format("Serializing unsupported key type: " + keyType));
+                return null;
+            }
+        } catch (org.json.JSONException e) {
+            throw new IllegalStateException("JSON error for key: " + keyName + ": ", e);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private static MetadataEntry serializeArrayEntry(Type keyType, Object keyObj,
+            CameraMetadata md) {
+        String keyName = getKeyName(keyObj);
+        try {
+            Object keyValue = getKeyValue(md, keyObj);
+            if (keyValue == null) {
+                return new MetadataEntry(keyName, JSONObject.NULL);
+            }
+            int arrayLen = Array.getLength(keyValue);
+            Type elmtType = ((GenericArrayType) keyType).getGenericComponentType();
+            if (elmtType == int.class || elmtType == float.class || elmtType == byte.class ||
+                    elmtType == long.class || elmtType == double.class
+                    || elmtType == boolean.class) {
+                return new MetadataEntry(keyName, new JSONArray(keyValue));
+            } else if (elmtType == Rational.class) {
+                JSONArray jsonArray = new JSONArray();
+                for (int i = 0; i < arrayLen; i++) {
+                    jsonArray.put(serializeRational((Rational) Array.get(keyValue, i)));
+                }
+                return new MetadataEntry(keyName, jsonArray);
+            } else if (elmtType == Size.class) {
+                JSONArray jsonArray = new JSONArray();
+                for (int i = 0; i < arrayLen; i++) {
+                    jsonArray.put(serializeSize((Size) Array.get(keyValue, i)));
+                }
+                return new MetadataEntry(keyName, jsonArray);
+            } else if (elmtType == Rect.class) {
+                JSONArray jsonArray = new JSONArray();
+                for (int i = 0; i < arrayLen; i++) {
+                    jsonArray.put(serializeRect((Rect) Array.get(keyValue, i)));
+                }
+                return new MetadataEntry(keyName, jsonArray);
+            } else if (elmtType == Face.class) {
+                JSONArray jsonArray = new JSONArray();
+                for (int i = 0; i < arrayLen; i++) {
+                    jsonArray.put(serializeFace((Face) Array.get(keyValue, i)));
+                }
+                return new MetadataEntry(keyName, jsonArray);
+            } else if (elmtType == StreamConfigurationMap.class) {
+                JSONArray jsonArray = new JSONArray();
+                for (int i = 0; i < arrayLen; i++) {
+                    jsonArray.put(serializeStreamConfigurationMap(
+                            (StreamConfigurationMap) Array.get(keyValue, i)));
+                }
+                return new MetadataEntry(keyName, jsonArray);
+            } else if (elmtType instanceof ParameterizedType &&
+                    ((ParameterizedType) elmtType).getRawType() == Range.class) {
+                JSONArray jsonArray = new JSONArray();
+                for (int i = 0; i < arrayLen; i++) {
+                    jsonArray.put(serializeRange((Range) Array.get(keyValue, i)));
+                }
+                return new MetadataEntry(keyName, jsonArray);
+            } else if (elmtType instanceof ParameterizedType &&
+                    ((ParameterizedType) elmtType).getRawType() == Pair.class) {
+                JSONArray jsonArray = new JSONArray();
+                for (int i = 0; i < arrayLen; i++) {
+                    jsonArray.put(serializePair((Pair) Array.get(keyValue, i)));
+                }
+                return new MetadataEntry(keyName, jsonArray);
+            } else if (elmtType == MeteringRectangle.class) {
+                JSONArray jsonArray = new JSONArray();
+                for (int i = 0; i < arrayLen; i++) {
+                    jsonArray.put(serializeMeteringRectangle(
+                            (MeteringRectangle) Array.get(keyValue, i)));
+                }
+                return new MetadataEntry(keyName, jsonArray);
+            } else if (elmtType == Location.class) {
+                JSONArray jsonArray = new JSONArray();
+                for (int i = 0; i < arrayLen; i++) {
+                    jsonArray.put(serializeLocation((Location) Array.get(keyValue, i)));
+                }
+                return new MetadataEntry(keyName, jsonArray);
+            } else if (elmtType == RggbChannelVector.class) {
+                JSONArray jsonArray = new JSONArray();
+                for (int i = 0; i < arrayLen; i++) {
+                    jsonArray.put(serializeRggbChannelVector(
+                            (RggbChannelVector) Array.get(keyValue, i)));
+                }
+                return new MetadataEntry(keyName, jsonArray);
+            } else if (elmtType == BlackLevelPattern.class) {
+                JSONArray jsonArray = new JSONArray();
+                for (int i = 0; i < arrayLen; i++) {
+                    jsonArray.put(serializeBlackLevelPattern(
+                            (BlackLevelPattern) Array.get(keyValue, i)));
+                }
+                return new MetadataEntry(keyName, jsonArray);
+            } else if (elmtType == Point.class) {
+                JSONArray jsonArray = new JSONArray();
+                for (int i = 0; i < arrayLen; i++) {
+                    jsonArray.put(serializePoint((Point) Array.get(keyValue, i)));
+                }
+                return new MetadataEntry(keyName, jsonArray);
+            } else {
+                Log.w(TAG, String.format("Serializing unsupported array type: " + elmtType));
+                return null;
+            }
+        } catch (org.json.JSONException e) {
+            throw new IllegalStateException("JSON error for key: " + keyName + ": ", e);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private static JSONObject serialize(CameraMetadata md) {
+        JSONObject jsonObj = new JSONObject();
+        Field[] allFields = md.getClass().getDeclaredFields();
+        if (md.getClass() == TotalCaptureResult.class) {
+            allFields = CaptureResult.class.getDeclaredFields();
+        }
+        for (Field field : allFields) {
+            if (Modifier.isPublic(field.getModifiers()) &&
+                    Modifier.isStatic(field.getModifiers()) &&
+                            (field.getType() == CaptureRequest.Key.class
+                            || field.getType() == CaptureResult.Key.class
+                            || field.getType() == TotalCaptureResult.Key.class
+                            || field.getType() == CameraCharacteristics.Key.class)
+                    &&
+                    field.getGenericType() instanceof ParameterizedType) {
+                ParameterizedType paramType = (ParameterizedType) field.getGenericType();
+                Type[] argTypes = paramType.getActualTypeArguments();
+                if (argTypes.length > 0) {
+                    try {
+                        Type keyType = argTypes[0];
+                        Object keyObj = field.get(md);
+                        MetadataEntry entry;
+                        if (keyType instanceof GenericArrayType) {
+                            entry = serializeArrayEntry(keyType, keyObj, md);
+                        } else {
+                            entry = serializeEntry(keyType, keyObj, md);
+                        }
+
+                        // TODO: Figure this weird case out.
+                        // There is a weird case where the entry is non-null but
+                        // the toString
+                        // of the entry is null, and if this happens, the
+                        // null-ness spreads like
+                        // a virus and makes the whole JSON object null from the
+                        // top level down.
+                        // Not sure if it's a bug in the library or I'm just not
+                        // using it right.
+                        // Workaround by checking for this case explicitly and
+                        // not adding the
+                        // value to the jsonObj when it is detected.
+                        if (entry != null && entry.key != null && entry.value != null
+                                && entry.value.toString() == null) {
+                            Log.w(TAG, "Error encountered serializing value for key: "
+                                    + entry.key);
+                        } else if (entry != null) {
+                            jsonObj.put(entry.key, entry.value);
+                        } else {
+                            // Ignore.
+                        }
+                    } catch (IllegalAccessException e) {
+                        throw new IllegalStateException(
+                                "Access error for field: " + field + ": ", e);
+                    } catch (org.json.JSONException e) {
+                        throw new IllegalStateException(
+                                "JSON error for field: " + field + ": ", e);
+                    }
+                }
+            }
+        }
+        return jsonObj;
+    }
+}
diff --git a/tests/tests/media/src/android/media/cts/ClearKeySystemTest.java b/tests/tests/media/src/android/media/cts/ClearKeySystemTest.java
index 673c1d7..ff05246 100644
--- a/tests/tests/media/src/android/media/cts/ClearKeySystemTest.java
+++ b/tests/tests/media/src/android/media/cts/ClearKeySystemTest.java
@@ -403,11 +403,6 @@
         }
     }
 
-    private boolean hasAudioOutput() {
-        return getInstrumentation().getTargetContext().getPackageManager()
-            .hasSystemFeature(PackageManager.FEATURE_AUDIO_OUTPUT);
-    }
-
     /**
      * Tests clear key system playback.
      */
diff --git a/tests/tests/media/src/android/media/cts/DecoderTest.java b/tests/tests/media/src/android/media/cts/DecoderTest.java
index 8917144..d71d38a 100644
--- a/tests/tests/media/src/android/media/cts/DecoderTest.java
+++ b/tests/tests/media/src/android/media/cts/DecoderTest.java
@@ -79,42 +79,6 @@
         masterFd.close();
     }
 
-    private boolean hasCodecForMimeType(String mimeType) {
-        MediaCodecList list = new MediaCodecList(MediaCodecList.ALL_CODECS);
-        for (MediaCodecInfo info : list.getCodecInfos()) {
-            for (String type : info.getSupportedTypes()) {
-                if (type.equalsIgnoreCase(mimeType)) {
-                    return true;
-                }
-            }
-        }
-        return false;
-    }
-
-    private boolean hasH264() {
-        return hasCodecForMimeType("video/avc");
-    }
-
-    private boolean hasHEVC() {
-        return hasCodecForMimeType("video/hevc");
-    }
-
-    private boolean hasH263() {
-        return hasCodecForMimeType("video/3gpp");
-    }
-
-    private boolean hasMpeg4() {
-        return hasCodecForMimeType("video/mp4v-es");
-    }
-
-    private boolean hasVP8() {
-        return hasCodecForMimeType("video/x-vnd.on2.vp8");
-    }
-
-    private boolean hasVP9() {
-        return hasCodecForMimeType("video/x-vnd.on2.vp9");
-    }
-
     // TODO: add similar tests for other audio and video formats
     public void testBug11696552() throws Exception {
         MediaCodec mMediaCodec = MediaCodec.createDecoderByType("audio/mp4a-latm");
@@ -212,8 +176,8 @@
         MediaFormat format = ex.getTrackFormat(0);
         String mime = format.getString(MediaFormat.KEY_MIME);
         assertTrue("not a video track. Wrong test file?", mime.startsWith("video/"));
-        if (!hasCodecForMimeType(mime)) {
-            Log.i(TAG, "Could not find a codec for mimeType: " + mime);
+        if (!hasCodecForMimeType(mime, false)) {
+            Log.i(TAG, "SKIPPING testBFrames(): Could not find a codec for mimeType: " + mime);
             return;
         }
         MediaCodec dec = MediaCodec.createDecoderByType(mime);
@@ -881,7 +845,8 @@
     }
 
     public void testCodecBasicH264() throws Exception {
-        if (!hasH264()) {
+        if (!hasH264(false)) {
+            Log.i(TAG, "SKIPPING testCodecBasicH264(): No codec found.");
             return;
         }
         Surface s = getActivity().getSurfaceHolder().getSurface();
@@ -897,7 +862,8 @@
     }
 
     public void testCodecBasicHEVC() throws Exception {
-        if (!hasHEVC()) {
+        if (!hasHEVC(false)) {
+            Log.i(TAG, "SKIPPING testCodecBasicHEVC(): No codec found.");
             return;
         }
         Surface s = getActivity().getSurfaceHolder().getSurface();
@@ -913,7 +879,8 @@
     }
 
     public void testCodecBasicH263() throws Exception {
-        if (!hasH263()) {
+        if (!hasH263(false)) {
+            Log.i(TAG, "SKIPPING testCodecBasicH263(): No codec found.");
             return;
         }
         Surface s = getActivity().getSurfaceHolder().getSurface();
@@ -929,7 +896,8 @@
     }
 
     public void testCodecBasicMpeg4() throws Exception {
-        if (!hasMpeg4()) {
+        if (!hasMpeg4(false)) {
+            Log.i(TAG, "SKIPPING testCodecBasicMpeg4(): No codec found.");
             return;
         }
         Surface s = getActivity().getSurfaceHolder().getSurface();
@@ -945,7 +913,8 @@
     }
 
     public void testCodecBasicVP8() throws Exception {
-        if (!hasVP8()) {
+        if (!hasVP8(false)) {
+            Log.i(TAG, "SKIPPING testCodecBasicVP8(): No codec found.");
             return;
         }
         Surface s = getActivity().getSurfaceHolder().getSurface();
@@ -961,7 +930,8 @@
     }
 
     public void testCodecBasicVP9() throws Exception {
-        if (!hasVP9()) {
+        if (!hasVP9(false)) {
+            Log.i(TAG, "SKIPPING testCodecBasicVP9(): No codec found.");
             return;
         }
         Surface s = getActivity().getSurfaceHolder().getSurface();
@@ -977,7 +947,8 @@
     }
 
     public void testCodecEarlyEOSH263() throws Exception {
-        if (!hasH263()) {
+        if (!hasH263(false)) {
+            Log.i(TAG, "SKIPPING testCodecEarlyEOSH263(): No codec found.");
             return;
         }
         Surface s = getActivity().getSurfaceHolder().getSurface();
@@ -988,7 +959,8 @@
     }
 
     public void testCodecEarlyEOSH264() throws Exception {
-        if (!hasH264()) {
+        if (!hasH264(false)) {
+            Log.i(TAG, "SKIPPING testCodecEarlyEOSH264(): No codec found.");
             return;
         }
         Surface s = getActivity().getSurfaceHolder().getSurface();
@@ -999,7 +971,8 @@
     }
 
     public void testCodecEarlyEOSHEVC() throws Exception {
-        if (!hasHEVC()) {
+        if (!hasHEVC(false)) {
+            Log.i(TAG, "SKIPPING testCodecEarlyEOSHEVC(): No codec found.");
             return;
         }
         Surface s = getActivity().getSurfaceHolder().getSurface();
@@ -1010,7 +983,8 @@
     }
 
     public void testCodecEarlyEOSMpeg4() throws Exception {
-        if (!hasMpeg4()) {
+        if (!hasMpeg4(false)) {
+            Log.i(TAG, "SKIPPING testCodecEarlyEOSMpeg4(): No codec found.");
             return;
         }
         Surface s = getActivity().getSurfaceHolder().getSurface();
@@ -1021,7 +995,8 @@
     }
 
     public void testCodecEarlyEOSVP8() throws Exception {
-        if (!hasVP8()) {
+        if (!hasVP8(false)) {
+            Log.i(TAG, "SKIPPING testCodecEarlyEOSVP8(): No codec found.");
             return;
         }
         Surface s = getActivity().getSurfaceHolder().getSurface();
@@ -1032,7 +1007,8 @@
     }
 
     public void testCodecEarlyEOSVP9() throws Exception {
-        if (!hasVP9()) {
+        if (!hasVP9(false)) {
+            Log.i(TAG, "SKIPPING testCodecEarlyEOSVP9(): No codec found.");
             return;
         }
         Surface s = getActivity().getSurfaceHolder().getSurface();
@@ -1043,7 +1019,8 @@
     }
 
     public void testCodecResetsH264WithoutSurface() throws Exception {
-        if (!hasH264()) {
+        if (!hasH264(false)) {
+            Log.i(TAG, "SKIPPING testCodecResetsH264WithoutSurface(): No codec found.");
             return;
         }
         testCodecResets(
@@ -1051,7 +1028,8 @@
     }
 
     public void testCodecResetsH264WithSurface() throws Exception {
-        if (!hasH264()) {
+        if (!hasH264(false)) {
+            Log.i(TAG, "SKIPPING testCodecResetsH264WithSurface(): No codec found.");
             return;
         }
         Surface s = getActivity().getSurfaceHolder().getSurface();
@@ -1060,7 +1038,8 @@
     }
 
     public void testCodecResetsHEVCWithoutSurface() throws Exception {
-        if (!hasHEVC()) {
+        if (!hasHEVC(false)) {
+            Log.i(TAG, "SKIPPING testCodecResetsHEVCWithoutSurface(): No codec found.");
             return;
         }
         testCodecResets(
@@ -1068,7 +1047,8 @@
     }
 
     public void testCodecResetsHEVCWithSurface() throws Exception {
-        if (!hasHEVC()) {
+        if (!hasHEVC(false)) {
+            Log.i(TAG, "SKIPPING testCodecResetsHEVCWithSurface(): No codec found.");
             return;
         }
         Surface s = getActivity().getSurfaceHolder().getSurface();
@@ -1077,7 +1057,8 @@
     }
 
     public void testCodecResetsH263WithoutSurface() throws Exception {
-        if (!hasH263()) {
+        if (!hasH263(false)) {
+            Log.i(TAG, "SKIPPING testCodecResetsH263WithoutSurface(): No codec found.");
             return;
         }
         testCodecResets(
@@ -1085,7 +1066,8 @@
     }
 
     public void testCodecResetsH263WithSurface() throws Exception {
-        if (!hasH263()) {
+        if (!hasH263(false)) {
+            Log.i(TAG, "SKIPPING testCodecResetsH263WithSurface(): No codec found.");
             return;
         }
         Surface s = getActivity().getSurfaceHolder().getSurface();
@@ -1094,7 +1076,8 @@
     }
 
     public void testCodecResetsMpeg4WithoutSurface() throws Exception {
-        if (!hasMpeg4()) {
+        if (!hasMpeg4(false)) {
+            Log.i(TAG, "SKIPPING testCodecResetsMpeg4WithoutSurface(): No codec found.");
             return;
         }
         testCodecResets(
@@ -1102,7 +1085,8 @@
     }
 
     public void testCodecResetsMpeg4WithSurface() throws Exception {
-        if (!hasMpeg4()) {
+        if (!hasMpeg4(false)) {
+            Log.i(TAG, "SKIPPING testCodecResetsMpeg4WithSurface(): No codec found.");
             return;
         }
         Surface s = getActivity().getSurfaceHolder().getSurface();
@@ -1111,7 +1095,8 @@
     }
 
     public void testCodecResetsVP8WithoutSurface() throws Exception {
-        if (!hasVP8()) {
+        if (!hasVP8(false)) {
+            Log.i(TAG, "SKIPPING testCodecResetsVP8WithoutSurface(): No codec found.");
             return;
         }
         testCodecResets(
@@ -1119,7 +1104,8 @@
     }
 
     public void testCodecResetsVP8WithSurface() throws Exception {
-        if (!hasVP8()) {
+        if (!hasVP8(false)) {
+            Log.i(TAG, "SKIPPING testCodecResetsVP8WithSurface(): No codec found.");
             return;
         }
         Surface s = getActivity().getSurfaceHolder().getSurface();
@@ -1128,7 +1114,8 @@
     }
 
     public void testCodecResetsVP9WithoutSurface() throws Exception {
-        if (!hasVP9()) {
+        if (!hasVP9(false)) {
+            Log.i(TAG, "SKIPPING testCodecResetsVP9WithoutSurface(): No codec found.");
             return;
         }
         testCodecResets(
@@ -1136,7 +1123,8 @@
     }
 
     public void testCodecResetsVP9WithSurface() throws Exception {
-        if (!hasVP9()) {
+        if (!hasVP9(false)) {
+            Log.i(TAG, "SKIPPING testCodecResetsVP9WithSurface(): No codec found.");
             return;
         }
         Surface s = getActivity().getSurfaceHolder().getSurface();
@@ -1245,6 +1233,13 @@
         extractor.setDataSource(testFd.getFileDescriptor(), testFd.getStartOffset(),
                 testFd.getLength());
         extractor.selectTrack(0); // consider variable looping on track
+        MediaFormat format = extractor.getTrackFormat(0);
+        String mimeType = format.getString(MediaFormat.KEY_MIME);
+        if (!hasCodecForMimeType(mimeType, false)) {
+            Log.i(TAG, "SKIPPING testEOSBehavior() for resid=" + movie + " No codec found for "
+                    + "mimeType = " + mimeType);
+            return;
+        }
         List<Long> outputChecksums = new ArrayList<Long>();
         List<Long> outputTimestamps = new ArrayList<Long>();
         Arrays.sort(stopAtSample);
diff --git a/tests/tests/media/src/android/media/cts/EnvReverbTest.java b/tests/tests/media/src/android/media/cts/EnvReverbTest.java
index e2e9b6d..4cfb744 100644
--- a/tests/tests/media/src/android/media/cts/EnvReverbTest.java
+++ b/tests/tests/media/src/android/media/cts/EnvReverbTest.java
@@ -304,6 +304,9 @@
 
     //Test case 2.1: test setEnabled() throws exception after release
     public void test2_1SetEnabledAfterRelease() throws Exception {
+        if (!isEnvReverbAvailable()) {
+            return;
+        }
         getReverb(0);
         mReverb.release();
         try {
diff --git a/tests/tests/media/src/android/media/cts/MediaPlayerTest.java b/tests/tests/media/src/android/media/cts/MediaPlayerTest.java
index 78ba149..108aa8b 100644
--- a/tests/tests/media/src/android/media/cts/MediaPlayerTest.java
+++ b/tests/tests/media/src/android/media/cts/MediaPlayerTest.java
@@ -21,6 +21,11 @@
 import android.content.pm.PackageManager;
 import android.content.res.AssetFileDescriptor;
 import android.media.AudioManager;
+import android.media.MediaCodec;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecList;
+import android.media.MediaExtractor;
+import android.media.MediaFormat;
 import android.media.MediaPlayer;
 import android.media.MediaPlayer.OnErrorListener;
 import android.media.MediaRecorder;
@@ -304,6 +309,11 @@
     }
 
     public void testPlayAudioTwice() throws Exception {
+        if (!hasAudioOutput()) {
+            Log.i(LOG_TAG, "SKIPPING testPlayAudioTwice(). No audio output.");
+            return;
+        }
+
         final int resid = R.raw.camera_click;
 
         MediaPlayer mp = MediaPlayer.create(mContext, resid);
@@ -550,6 +560,10 @@
     }
 
     private void testGapless(int resid1, int resid2) throws Exception {
+        if (!hasAudioOutput()) {
+            Log.i(LOG_TAG, "SKIPPING testPlayAudioTwice(). No audio output.");
+            return;
+        }
 
         MediaPlayer mp1 = new MediaPlayer();
         mp1.setAudioStreamType(AudioManager.STREAM_MUSIC);
@@ -660,7 +674,12 @@
             }
         });
 
-        loadResource(R.raw.testvideo);
+        try {
+            loadResource(R.raw.testvideo);
+        } catch (UnsupportedCodecException e) {
+            Log.i(LOG_TAG, "SKIPPING testVideoSurfaceResetting(). Could not find codec.");
+            return;
+        }
         playLoadedVideo(352, 288, -1);
 
         Thread.sleep(SLEEP_TIME);
@@ -1011,7 +1030,12 @@
     }
 
     public void testDeselectTrack() throws Throwable {
-        loadResource(R.raw.testvideo_with_2_subtitles);
+        try {
+            loadResource(R.raw.testvideo_with_2_subtitles);
+        } catch (UnsupportedCodecException e) {
+            Log.i(LOG_TAG, "SKIPPING testDeselectTrack(). Could not find codec.");
+            return;
+        }
         runTestOnUiThread(new Runnable() {
             public void run() {
                 try {
@@ -1082,7 +1106,12 @@
     }
 
     public void testChangeSubtitleTrack() throws Throwable {
-        loadResource(R.raw.testvideo_with_2_subtitles);
+        try {
+            loadResource(R.raw.testvideo_with_2_subtitles);
+        } catch (UnsupportedCodecException e) {
+            Log.i(LOG_TAG, "SKIPPING testChangeSubtitleTrack(). Could not find codec.");
+            return;
+        }
 
         mMediaPlayer.setDisplay(getActivity().getSurfaceHolder());
         mMediaPlayer.setScreenOnWhilePlaying(true);
@@ -1170,7 +1199,12 @@
     }
 
     public void testGetTrackInfo() throws Throwable {
-        loadResource(R.raw.testvideo_with_2_subtitles);
+        try {
+            loadResource(R.raw.testvideo_with_2_subtitles);
+        } catch (UnsupportedCodecException e) {
+            Log.i(LOG_TAG, "SKIPPING testGetTrackInfo(). Could not find codec.");
+            return;
+        }
         runTestOnUiThread(new Runnable() {
             public void run() {
                 try {
@@ -1245,7 +1279,13 @@
     public void testCallback() throws Throwable {
         final int mp4Duration = 8484;
 
-        loadResource(R.raw.testvideo);
+        try {
+            loadResource(R.raw.testvideo);
+        } catch (UnsupportedCodecException e) {
+            Log.i(LOG_TAG, "SKIPPING testCallback(). Could not find codec.");
+            return;
+        }
+
         mMediaPlayer.setDisplay(getActivity().getSurfaceHolder());
         mMediaPlayer.setScreenOnWhilePlaying(true);
 
@@ -1317,6 +1357,11 @@
 
     public void testRecordAndPlay() throws Exception {
         if (!hasMicrophone()) {
+            Log.i(LOG_TAG, "SKIPPING testRecordAndPlay(). No microphone.");
+            return;
+        }
+        if (!hasH263(false)) {
+            Log.i(LOG_TAG, "SKIPPING testRecordAndPlay(). Cound not find codec.");
             return;
         }
         File outputFile = new File(Environment.getExternalStorageDirectory(),
diff --git a/tests/tests/media/src/android/media/cts/MediaPlayerTestBase.java b/tests/tests/media/src/android/media/cts/MediaPlayerTestBase.java
index 61d8792..9225203 100644
--- a/tests/tests/media/src/android/media/cts/MediaPlayerTestBase.java
+++ b/tests/tests/media/src/android/media/cts/MediaPlayerTestBase.java
@@ -16,8 +16,15 @@
 package android.media.cts;
 
 import android.content.Context;
+
+import android.content.pm.PackageManager;
 import android.content.res.AssetFileDescriptor;
 import android.content.res.Resources;
+import android.media.MediaCodec;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecList;
+import android.media.MediaExtractor;
+import android.media.MediaFormat;
 import android.media.MediaPlayer;
 import android.test.ActivityInstrumentationTestCase2;
 
@@ -143,6 +150,10 @@
     }
 
     protected void loadResource(int resid) throws Exception {
+        if (!supportsPlayback(resid)) {
+            throw new UnsupportedCodecException();
+        }
+
         AssetFileDescriptor afd = mResources.openRawResourceFd(resid);
         try {
             mMediaPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(),
@@ -197,6 +208,12 @@
     }
 
     protected void playVideoTest(int resid, int width, int height) throws Exception {
+        if (!supportsPlayback(resid)) {
+            LOG.info("SKIPPING playVideoTest() for resid=" + resid 
+                    + " Could not find a codec for playback.");
+            return;
+        }
+
         loadResource(resid);
         playLoadedVideo(width, height, 0);
     }
@@ -278,4 +295,69 @@
     }
 
     private static class PrepareFailedException extends Exception {}
+    public static class UnsupportedCodecException extends Exception {}
+
+    public boolean supportsPlayback(int resid) throws IOException {
+        // First determine if the device supports playback of the requested resource.
+        AssetFileDescriptor fd = mResources.openRawResourceFd(resid);
+        MediaExtractor ex = new MediaExtractor();
+        ex.setDataSource(fd.getFileDescriptor(), fd.getStartOffset(), fd.getLength());
+        MediaFormat format = ex.getTrackFormat(0);
+        String mimeType = format.getString(MediaFormat.KEY_MIME);
+        return hasCodecForMimeType(mimeType, false);
+    }
+
+    public boolean supportsPlayback(String path) throws IOException {
+        MediaExtractor ex = new MediaExtractor();
+        ex.setDataSource(path);
+        MediaFormat format = ex.getTrackFormat(0);
+        String mimeType = format.getString(MediaFormat.KEY_MIME);
+        return hasCodecForMimeType(mimeType, false);
+    }
+
+    public static boolean hasCodecForMimeType(String mimeType, boolean encoder) {
+        MediaCodecList list = new MediaCodecList(MediaCodecList.ALL_CODECS);
+        for (MediaCodecInfo info : list.getCodecInfos()) {
+            if (encoder != info.isEncoder()) {
+                continue;
+            }
+
+            for (String type : info.getSupportedTypes()) {
+                if (type.equalsIgnoreCase(mimeType)) {
+                    LOG.info("Found codec for mimeType=" + mimeType + " codec=" + info.getName());
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    public static boolean hasH264(boolean encoder) {
+        return hasCodecForMimeType("video/avc", encoder);
+    }
+
+    public static boolean hasHEVC(boolean encoder) {
+        return hasCodecForMimeType("video/hevc", encoder);
+    }
+
+    public static boolean hasH263(boolean encoder) {
+        return hasCodecForMimeType("video/3gpp", encoder);
+    }
+
+    public static boolean hasMpeg4(boolean encoder) {
+        return hasCodecForMimeType("video/mp4v-es", encoder);
+    }
+
+    public static boolean hasVP8(boolean encoder) {
+        return hasCodecForMimeType("video/x-vnd.on2.vp8", encoder);
+    }
+
+    public static boolean hasVP9(boolean encoder) {
+        return hasCodecForMimeType("video/x-vnd.on2.vp9", encoder);
+    }
+
+    public boolean hasAudioOutput() {
+        return getInstrumentation().getTargetContext().getPackageManager()
+            .hasSystemFeature(PackageManager.FEATURE_AUDIO_OUTPUT);
+    }
 }
diff --git a/tests/tests/media/src/android/media/cts/NativeDecoderTest.java b/tests/tests/media/src/android/media/cts/NativeDecoderTest.java
index fc27dfa..76620c1 100644
--- a/tests/tests/media/src/android/media/cts/NativeDecoderTest.java
+++ b/tests/tests/media/src/android/media/cts/NativeDecoderTest.java
@@ -195,10 +195,14 @@
         testDecoder(R.raw.video_1280x720_webm_vp9_309kbps_25fps_vorbis_stereo_128kbps_44100hz);
         testDecoder(R.raw.video_176x144_3gp_h263_300kbps_12fps_aac_mono_24kbps_11025hz);
         testDecoder(R.raw.video_480x360_mp4_mpeg4_860kbps_25fps_aac_stereo_128kbps_44100hz);
-
     }
 
     private void testDecoder(int res) throws Exception {
+        if (!supportsPlayback(res)) {
+            Log.i(TAG, "SKIPPING testDecoder() resid=" + res + " Unsupported decorder.");
+            return;
+        }
+
         AssetFileDescriptor fd = mResources.openRawResourceFd(res);
 
         int[] jdata = getDecodedData(
@@ -382,6 +386,11 @@
     }
 
     private void testVideoPlayback(int res) throws Exception {
+        if (!supportsPlayback(res)) {
+            Log.i(TAG, "SKIPPING testVideoPlayback() resid=" + res + " Unsupported decorder.");
+            return;
+        }
+
         AssetFileDescriptor fd = mResources.openRawResourceFd(res);
 
         boolean ret = testPlaybackNative(mActivity.getSurfaceHolder().getSurface(),
diff --git a/tests/tests/media/src/android/media/cts/StreamingMediaPlayerTest.java b/tests/tests/media/src/android/media/cts/StreamingMediaPlayerTest.java
index 2b93064..6198d5f 100644
--- a/tests/tests/media/src/android/media/cts/StreamingMediaPlayerTest.java
+++ b/tests/tests/media/src/android/media/cts/StreamingMediaPlayerTest.java
@@ -28,6 +28,8 @@
  * Tests of MediaPlayer streaming capabilities.
  */
 public class StreamingMediaPlayerTest extends MediaPlayerTestBase {
+    private static final String TAG = "StreamingMediaPlayerTest";
+
     private CtsTestServer mServer;
 
 /* RTSP tests are more flaky and vulnerable to network condition.
@@ -62,6 +64,11 @@
 */
     // Streaming HTTP video from YouTube
     public void testHTTP_H263_AMR_Video1() throws Exception {
+        if (!hasH263(false)) {
+            Log.i(TAG, "Skipping testHTTP_H263_AMR_Video1(): No codec found.");
+            return;
+        }
+
         playVideoTest("http://redirector.c.youtube.com/videoplayback?id=271de9756065677e"
                 + "&itag=13&source=youtube&ip=0.0.0.0&ipbits=0&expire=19000000000"
                 + "&sparams=ip,ipbits,expire,id,itag,source"
@@ -70,6 +77,11 @@
                 + "&key=test_key1&user=android-device-test", 176, 144);
     }
     public void testHTTP_H263_AMR_Video2() throws Exception {
+        if (!hasH263(false)) {
+            Log.i(TAG, "Skipping testHTTP_H263_AMR_Video2(): No codec found.");
+            return;
+        }
+
         playVideoTest("http://redirector.c.youtube.com/videoplayback?id=c80658495af60617"
                 + "&itag=13&source=youtube&ip=0.0.0.0&ipbits=0&expire=19000000000"
                 + "&sparams=ip,ipbits,expire,id,itag,source"
@@ -79,6 +91,11 @@
     }
 
     public void testHTTP_MPEG4SP_AAC_Video1() throws Exception {
+        if (!hasH264(false)) {
+            Log.i(TAG, "Skipping testHTTP_MPEG4SP_AAC_Video1(): No codec found.");
+            return;
+        }
+
         playVideoTest("http://redirector.c.youtube.com/videoplayback?id=271de9756065677e"
                 + "&itag=17&source=youtube&ip=0.0.0.0&ipbits=0&expire=19000000000"
                 + "&sparams=ip,ipbits,expire,id,itag,source"
@@ -87,6 +104,11 @@
                 + "&key=test_key1&user=android-device-test", 176, 144);
     }
     public void testHTTP_MPEG4SP_AAC_Video2() throws Exception {
+        if (!hasH264(false)) {
+            Log.i(TAG, "Skipping testHTTP_MPEG4SP_AAC_Video2(): No codec found.");
+            return;
+        }
+
         playVideoTest("http://redirector.c.youtube.com/videoplayback?id=c80658495af60617"
                 + "&itag=17&source=youtube&ip=0.0.0.0&ipbits=0&expire=19000000000"
                 + "&sparams=ip,ipbits,expire,id,itag,source"
@@ -96,6 +118,11 @@
     }
 
     public void testHTTP_H264Base_AAC_Video1() throws Exception {
+        if (!hasH264(false)) {
+            Log.i(TAG, "Skipping testHTTP_H264Base_AAC_Video1(): No codec found.");
+            return;
+        }
+
         playVideoTest("http://redirector.c.youtube.com/videoplayback?id=271de9756065677e"
                 + "&itag=18&source=youtube&ip=0.0.0.0&ipbits=0&expire=19000000000"
                 + "&sparams=ip,ipbits,expire,id,itag,source"
@@ -104,6 +131,11 @@
                 + "&key=test_key1&user=android-device-test", 640, 360);
     }
     public void testHTTP_H264Base_AAC_Video2() throws Exception {
+        if (!hasH264(false)) {
+            Log.i(TAG, "Skipping testHTTP_H264Base_AAC_Video2(): No codec found.");
+            return;
+        }
+
         playVideoTest("http://redirector.c.youtube.com/videoplayback?id=c80658495af60617"
                 + "&itag=18&source=youtube&ip=0.0.0.0&ipbits=0&expire=19000000000"
                 + "&sparams=ip,ipbits,expire,id,itag,source"
@@ -114,6 +146,11 @@
 
     // Streaming HLS video from YouTube
     public void testHLS() throws Exception {
+        if (!hasH264(false)) {
+            Log.i(TAG, "Skipping testHLS(): No codec found.");
+            return;
+        }
+
         // Play stream for 60 seconds
         playLiveVideoTest("http://www.youtube.com/api/manifest/hls_variant/id/"
                 + "0168724d02bd9945/itag/5/source/youtube/playlist_type/DVR/ip/"
@@ -165,6 +202,11 @@
                 stream_url = stream_url + "?" + CtsTestServer.NOLENGTH_POSTFIX;
             }
 
+            if (!supportsPlayback(stream_url)) {
+                Log.i(TAG, "Failed to find codec for: '" + stream_url + "'. Skipping test.");
+                return;
+            }
+
             mMediaPlayer.setDataSource(stream_url);
 
             mMediaPlayer.setDisplay(getActivity().getSurfaceHolder());
@@ -252,14 +294,26 @@
     }
 
     public void testPlayHlsStream() throws Throwable {
+        if (!hasH264(false)) {
+            Log.i(TAG, "Skipping testPlayHlsStream(): No codec found.");
+            return;
+        }
         localHlsTest("hls.m3u8", false, false);
     }
 
     public void testPlayHlsStreamWithQueryString() throws Throwable {
+        if (!hasH264(false)) {
+            Log.i(TAG, "Skipping testPlayHlsStreamWithQueryString(): No codec found.");
+            return;
+        }
         localHlsTest("hls.m3u8", true, false);
     }
 
     public void testPlayHlsStreamWithRedirect() throws Throwable {
+        if (!hasH264(false)) {
+            Log.i(TAG, "Skipping testPlayHlsStreamWithRedirect(): No codec found.");
+            return;
+        }
         localHlsTest("hls.m3u8", false, true);
     }
 
diff --git a/tests/tests/speech/src/android/speech/tts/cts/TextToSpeechTest.java b/tests/tests/speech/src/android/speech/tts/cts/TextToSpeechTest.java
index 799fd8d..69acdd0 100644
--- a/tests/tests/speech/src/android/speech/tts/cts/TextToSpeechTest.java
+++ b/tests/tests/speech/src/android/speech/tts/cts/TextToSpeechTest.java
@@ -15,6 +15,7 @@
  */
 package android.speech.tts.cts;
 
+import android.content.pm.PackageManager;
 import android.os.Environment;
 import android.speech.tts.TextToSpeech;
 import android.test.AndroidTestCase;
@@ -39,6 +40,15 @@
     protected void setUp() throws Exception {
         super.setUp();
         mTts = TextToSpeechWrapper.createTextToSpeechWrapper(getContext());
+        if (mTts == null) {
+            PackageManager pm = getContext().getPackageManager();
+            if (!pm.hasSystemFeature(PackageManager.FEATURE_AUDIO_OUTPUT)) {
+                // It is OK to have no TTS, when audio-out is not supported.
+                return;
+            } else {
+                fail("FEATURE_AUDIO_OUTPUT is set, but there is no TTS engine");
+            }
+        }
         assertNotNull(mTts);
         assertTrue(checkAndSetLanguageAvailable());
     }
@@ -46,7 +56,9 @@
     @Override
     protected void tearDown() throws Exception {
         super.tearDown();
-        mTts.shutdown();
+        if (mTts != null) {
+            mTts.shutdown();
+        }
     }
 
     private TextToSpeech getTts() {
@@ -83,6 +95,9 @@
     }
 
     public void testSynthesizeToFile() throws Exception {
+        if (mTts == null) {
+            return;
+        }
         File sampleFile = new File(Environment.getExternalStorageDirectory(), SAMPLE_FILE_NAME);
         try {
             assertFalse(sampleFile.exists());
@@ -101,18 +116,27 @@
     }
 
     public void testSpeak() throws Exception {
+        if (mTts == null) {
+            return;
+        }
         int result = getTts().speak(SAMPLE_TEXT, TextToSpeech.QUEUE_FLUSH, createParams());
         assertEquals("speak() failed", TextToSpeech.SUCCESS, result);
         assertTrue("speak() completion timeout", waitForUtterance());
     }
 
     public void testGetEnginesIncludesDefault() throws Exception {
+        if (mTts == null) {
+            return;
+        }
         List<TextToSpeech.EngineInfo> engines = getTts().getEngines();
         assertNotNull("getEngines() returned null", engines);
         assertContainsEngine(getTts().getDefaultEngine(), engines);
     }
 
     public void testGetEnginesIncludesMock() throws Exception {
+        if (mTts == null) {
+            return;
+        }
         List<TextToSpeech.EngineInfo> engines = getTts().getEngines();
         assertNotNull("getEngines() returned null", engines);
         assertContainsEngine(TextToSpeechWrapper.MOCK_TTS_ENGINE, engines);