Camera2: add static metadata collection

Bug: 18103166
Change-Id: I95ecfa6dd605132dee57aa09d3bdc3cff97e5d3c
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/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;
+    }
+}