/*
 * Copyright (C) 2013 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.impl;

import android.annotation.NonNull;
import android.compat.annotation.UnsupportedAppUsage;
import android.graphics.ImageFormat;
import android.graphics.Point;
import android.graphics.Rect;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraMetadata;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.marshal.MarshalQueryable;
import android.hardware.camera2.marshal.MarshalRegistry;
import android.hardware.camera2.marshal.Marshaler;
import android.hardware.camera2.marshal.impl.MarshalQueryableArray;
import android.hardware.camera2.marshal.impl.MarshalQueryableBlackLevelPattern;
import android.hardware.camera2.marshal.impl.MarshalQueryableBoolean;
import android.hardware.camera2.marshal.impl.MarshalQueryableColorSpaceTransform;
import android.hardware.camera2.marshal.impl.MarshalQueryableEnum;
import android.hardware.camera2.marshal.impl.MarshalQueryableHighSpeedVideoConfiguration;
import android.hardware.camera2.marshal.impl.MarshalQueryableMeteringRectangle;
import android.hardware.camera2.marshal.impl.MarshalQueryableNativeByteToInteger;
import android.hardware.camera2.marshal.impl.MarshalQueryablePair;
import android.hardware.camera2.marshal.impl.MarshalQueryableParcelable;
import android.hardware.camera2.marshal.impl.MarshalQueryablePrimitive;
import android.hardware.camera2.marshal.impl.MarshalQueryableRange;
import android.hardware.camera2.marshal.impl.MarshalQueryableRecommendedStreamConfiguration;
import android.hardware.camera2.marshal.impl.MarshalQueryableRect;
import android.hardware.camera2.marshal.impl.MarshalQueryableReprocessFormatsMap;
import android.hardware.camera2.marshal.impl.MarshalQueryableRggbChannelVector;
import android.hardware.camera2.marshal.impl.MarshalQueryableSize;
import android.hardware.camera2.marshal.impl.MarshalQueryableSizeF;
import android.hardware.camera2.marshal.impl.MarshalQueryableStreamConfiguration;
import android.hardware.camera2.marshal.impl.MarshalQueryableStreamConfigurationDuration;
import android.hardware.camera2.marshal.impl.MarshalQueryableString;
import android.hardware.camera2.params.Capability;
import android.hardware.camera2.params.DeviceStateSensorOrientationMap;
import android.hardware.camera2.params.DynamicRangeProfiles;
import android.hardware.camera2.params.Face;
import android.hardware.camera2.params.HighSpeedVideoConfiguration;
import android.hardware.camera2.params.LensShadingMap;
import android.hardware.camera2.params.MandatoryStreamCombination;
import android.hardware.camera2.params.MultiResolutionStreamConfigurationMap;
import android.hardware.camera2.params.OisSample;
import android.hardware.camera2.params.RecommendedStreamConfiguration;
import android.hardware.camera2.params.RecommendedStreamConfigurationMap;
import android.hardware.camera2.params.ReprocessFormatsMap;
import android.hardware.camera2.params.StreamConfiguration;
import android.hardware.camera2.params.StreamConfigurationDuration;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.hardware.camera2.params.TonemapCurve;
import android.hardware.camera2.utils.ArrayUtils;
import android.hardware.camera2.utils.TypeReference;
import android.location.Location;
import android.location.LocationManager;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.ServiceSpecificException;
import android.util.Log;
import android.util.Range;
import android.util.Size;

import dalvik.annotation.optimization.FastNative;
import dalvik.system.VMRuntime;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.List;
import java.util.Objects;
import java.util.Set;

/**
 * Implementation of camera metadata marshal/unmarshal across Binder to
 * the camera service
 */
public class CameraMetadataNative implements Parcelable {

    public static class Key<T> {
        private boolean mHasTag;
        private int mTag;
        private long mVendorId = Long.MAX_VALUE;
        private final Class<T> mType;
        private final TypeReference<T> mTypeReference;
        private final String mName;
        private final String mFallbackName;
        private final int mHash;

        /**
         * @hide
         */
        public Key(String name, Class<T> type, long vendorId) {
            if (name == null) {
                throw new NullPointerException("Key needs a valid name");
            } else if (type == null) {
                throw new NullPointerException("Type needs to be non-null");
            }
            mName = name;
            mFallbackName = null;
            mType = type;
            mVendorId = vendorId;
            mTypeReference = TypeReference.createSpecializedTypeReference(type);
            mHash = mName.hashCode() ^ mTypeReference.hashCode();
        }

        /**
         * @hide
         */
        public Key(String name, String fallbackName, Class<T> type) {
            if (name == null) {
                throw new NullPointerException("Key needs a valid name");
            } else if (type == null) {
                throw new NullPointerException("Type needs to be non-null");
            }
            mName = name;
            mFallbackName = fallbackName;
            mType = type;
            mTypeReference = TypeReference.createSpecializedTypeReference(type);
            mHash = mName.hashCode() ^ mTypeReference.hashCode();
        }

        /**
         * Visible for testing only.
         *
         * <p>Use the CameraCharacteristics.Key, CaptureResult.Key, or CaptureRequest.Key
         * for application code or vendor-extended keys.</p>
         */
        public Key(String name, Class<T> type) {
            if (name == null) {
                throw new NullPointerException("Key needs a valid name");
            } else if (type == null) {
                throw new NullPointerException("Type needs to be non-null");
            }
            mName = name;
            mFallbackName = null;
            mType = type;
            mTypeReference = TypeReference.createSpecializedTypeReference(type);
            mHash = mName.hashCode() ^ mTypeReference.hashCode();
        }

        /**
         * Visible for testing only.
         *
         * <p>Use the CameraCharacteristics.Key, CaptureResult.Key, or CaptureRequest.Key
         * for application code or vendor-extended keys.</p>
         */
        @SuppressWarnings("unchecked")
        public Key(String name, TypeReference<T> typeReference) {
            if (name == null) {
                throw new NullPointerException("Key needs a valid name");
            } else if (typeReference == null) {
                throw new NullPointerException("TypeReference needs to be non-null");
            }
            mName = name;
            mFallbackName = null;
            mType = (Class<T>)typeReference.getRawType();
            mTypeReference = typeReference;
            mHash = mName.hashCode() ^ mTypeReference.hashCode();
        }

        /**
         * Return a camelCase, period separated name formatted like:
         * {@code "root.section[.subsections].name"}.
         *
         * <p>Built-in keys exposed by the Android SDK are always prefixed with {@code "android."};
         * keys that are device/platform-specific are prefixed with {@code "com."}.</p>
         *
         * <p>For example, {@code CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP} would
         * have a name of {@code "android.scaler.streamConfigurationMap"}; whereas a device
         * specific key might look like {@code "com.google.nexus.data.private"}.</p>
         *
         * @return String representation of the key name
         */
        public final String getName() {
            return mName;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public final int hashCode() {
            return mHash;
        }

        /**
         * Compare this key against other native keys, request keys, result keys, and
         * characteristics keys.
         *
         * <p>Two keys are considered equal if their name and type reference are equal.</p>
         *
         * <p>Note that the equality against non-native keys is one-way. A native key may be equal
         * to a result key; but that same result key will not be equal to a native key.</p>
         */
        @SuppressWarnings("rawtypes")
        @Override
        public final boolean equals(Object o) {
            if (this == o) {
                return true;
            }

            if (o == null || this.hashCode() != o.hashCode()) {
                return false;
            }

            Key<?> lhs;

            if (o instanceof CaptureResult.Key) {
                lhs = ((CaptureResult.Key)o).getNativeKey();
            } else if (o instanceof CaptureRequest.Key) {
                lhs = ((CaptureRequest.Key)o).getNativeKey();
            } else if (o instanceof CameraCharacteristics.Key) {
                lhs = ((CameraCharacteristics.Key)o).getNativeKey();
            } else if ((o instanceof Key)) {
                lhs = (Key<?>)o;
            } else {
                return false;
            }

            return mName.equals(lhs.mName) && mTypeReference.equals(lhs.mTypeReference);
        }

        /**
         * <p>
         * Get the tag corresponding to this key. This enables insertion into the
         * native metadata.
         * </p>
         *
         * <p>This value is looked up the first time, and cached subsequently.</p>
         *
         * <p>This function may be called without cacheTag() if this is not a vendor key.
         * If this is a vendor key, cacheTag() must be called first before getTag() can
         * be called. Otherwise, mVendorId could be default (Long.MAX_VALUE) and vendor
         * tag lookup could fail.</p>
         *
         * @return The tag numeric value corresponding to the string
         */
        @UnsupportedAppUsage
        public final int getTag() {
            if (!mHasTag) {
                mTag = CameraMetadataNative.getTag(mName, mVendorId);
                mHasTag = true;
            }
            return mTag;
        }

        /**
         * Whether this key's tag is cached.
         *
         * @hide
         */
        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
        public final boolean hasTag() {
            return mHasTag;
        }

        /**
         * Cache this key's tag.
         *
         * @hide
         */
        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
        public final void cacheTag(int tag) {
            mHasTag = true;
            mTag = tag;
        }

        /**
         * Get the raw class backing the type {@code T} for this key.
         *
         * <p>The distinction is only important if {@code T} is a generic, e.g.
         * {@code Range<Integer>} since the nested type will be erased.</p>
         */
        public final Class<T> getType() {
            // TODO: remove this; other places should use #getTypeReference() instead
            return mType;
        }

        /**
         * Get the vendor tag provider id.
         *
         * @hide
         */
        public final long getVendorId() {
            return mVendorId;
        }

        /**
         * Get the type reference backing the type {@code T} for this key.
         *
         * <p>The distinction is only important if {@code T} is a generic, e.g.
         * {@code Range<Integer>} since the nested type will be retained.</p>
         */
        public final TypeReference<T> getTypeReference() {
            return mTypeReference;
        }
    }

    private static final String TAG = "CameraMetadataJV";
    private static final boolean DEBUG = false;

    // this should be in sync with HAL_PIXEL_FORMAT_BLOB defined in graphics.h
    public static final int NATIVE_JPEG_FORMAT = 0x21;

    private static final String CELLID_PROCESS = "CELLID";
    private static final String GPS_PROCESS = "GPS";
    private static final int FACE_LANDMARK_SIZE = 6;

    private static final int MANDATORY_STREAM_CONFIGURATIONS_DEFAULT = 0;
    private static final int MANDATORY_STREAM_CONFIGURATIONS_MAX_RESOLUTION = 1;
    private static final int MANDATORY_STREAM_CONFIGURATIONS_CONCURRENT = 2;
    private static final int MANDATORY_STREAM_CONFIGURATIONS_10BIT = 3;
    private static final int MANDATORY_STREAM_CONFIGURATIONS_USE_CASE = 4;
    private static final int MANDATORY_STREAM_CONFIGURATIONS_PREVIEW_STABILIZATION = 5;

    private static String translateLocationProviderToProcess(final String provider) {
        if (provider == null) {
            return null;
        }
        switch(provider) {
            case LocationManager.GPS_PROVIDER:
                return GPS_PROCESS;
            case LocationManager.NETWORK_PROVIDER:
                return CELLID_PROCESS;
            default:
                return null;
        }
    }

    private static String translateProcessToLocationProvider(final String process) {
        if (process == null) {
            return null;
        }
        switch(process) {
            case GPS_PROCESS:
                return LocationManager.GPS_PROVIDER;
            case CELLID_PROCESS:
                return LocationManager.NETWORK_PROVIDER;
            default:
                return null;
        }
    }

    public CameraMetadataNative() {
        super();
        mMetadataPtr = nativeAllocate();
        if (mMetadataPtr == 0) {
            throw new OutOfMemoryError("Failed to allocate native CameraMetadata");
        }
        updateNativeAllocation();
    }

    /**
     * Copy constructor - clone metadata
     */
    public CameraMetadataNative(CameraMetadataNative other) {
        super();
        mMetadataPtr = nativeAllocateCopy(other.mMetadataPtr);
        if (mMetadataPtr == 0) {
            throw new OutOfMemoryError("Failed to allocate native CameraMetadata");
        }
        updateNativeAllocation();
    }

    /**
     * Move the contents from {@code other} into a new camera metadata instance.</p>
     *
     * <p>After this call, {@code other} will become empty.</p>
     *
     * @param other the previous metadata instance which will get pilfered
     * @return a new metadata instance with the values from {@code other} moved into it
     */
    public static CameraMetadataNative move(CameraMetadataNative other) {
        CameraMetadataNative newObject = new CameraMetadataNative();
        newObject.swap(other);
        return newObject;
    }

    /**
     * Set all metadata values in the destination argument by using the corresponding
     * values from the source. Metadata tags present in the destination and absent
     * from the source will remain unmodified.
     *
     * @param dst Destination metadata
     * @param src Source metadata
     * @hide
     */
    public static void update(CameraMetadataNative dst, CameraMetadataNative src) {
        nativeUpdate(dst.mMetadataPtr, src.mMetadataPtr);
    }

    public static final @android.annotation.NonNull Parcelable.Creator<CameraMetadataNative> CREATOR =
            new Parcelable.Creator<CameraMetadataNative>() {
        @Override
        public CameraMetadataNative createFromParcel(Parcel in) {
            CameraMetadataNative metadata = new CameraMetadataNative();
            metadata.readFromParcel(in);
            return metadata;
        }

        @Override
        public CameraMetadataNative[] newArray(int size) {
            return new CameraMetadataNative[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        nativeWriteToParcel(dest, mMetadataPtr);
    }

    /**
     * @hide
     */
    public <T> T get(CameraCharacteristics.Key<T> key) {
        return get(key.getNativeKey());
    }

    /**
     * @hide
     */
    public <T> T get(CaptureResult.Key<T> key) {
        return get(key.getNativeKey());
    }

    /**
     * @hide
     */
    public <T> T get(CaptureRequest.Key<T> key) {
        return get(key.getNativeKey());
    }

    /**
     * Look-up a metadata field value by its key.
     *
     * @param key a non-{@code null} key instance
     * @return the field corresponding to the {@code key}, or {@code null} if no value was set
     */
    public <T> T get(Key<T> key) {
        Objects.requireNonNull(key, "key must not be null");

        // Check if key has been overridden to use a wrapper class on the java side.
        GetCommand g = sGetCommandMap.get(key);
        if (g != null) {
            return g.getValue(this, key);
        }
        return getBase(key);
    }

    public void readFromParcel(Parcel in) {
        nativeReadFromParcel(in, mMetadataPtr);
        updateNativeAllocation();
    }

    /**
     * Set the global client-side vendor tag descriptor to allow use of vendor
     * tags in camera applications.
     *
     * @throws ServiceSpecificException
     * @hide
     */
    public static void setupGlobalVendorTagDescriptor() throws ServiceSpecificException {
        int err = nativeSetupGlobalVendorTagDescriptor();
        if (err != 0) {
            throw new ServiceSpecificException(err, "Failure to set up global vendor tags");
        }
    }

    /**
     * Set the global client-side vendor tag descriptor to allow use of vendor
     * tags in camera applications.
     *
     * @return int An error code corresponding to one of the
     * {@link ICameraService} error constants, or 0 on success.
     */
    private static native int nativeSetupGlobalVendorTagDescriptor();

    /**
     * Set a camera metadata field to a value. The field definitions can be
     * found in {@link CameraCharacteristics}, {@link CaptureResult}, and
     * {@link CaptureRequest}.
     *
     * @param key The metadata field to write.
     * @param value The value to set the field to, which must be of a matching
     * type to the key.
     */
    public <T> void set(Key<T> key, T value) {
        SetCommand s = sSetCommandMap.get(key);
        if (s != null) {
            s.setValue(this, value);
            return;
        }

        setBase(key, value);
    }

    public <T> void set(CaptureRequest.Key<T> key, T value) {
        set(key.getNativeKey(), value);
    }

    public <T> void set(CaptureResult.Key<T> key, T value) {
        set(key.getNativeKey(), value);
    }

    public <T> void set(CameraCharacteristics.Key<T> key, T value) {
        set(key.getNativeKey(), value);
    }

    // Keep up-to-date with camera_metadata.h
    /**
     * @hide
     */
    public static final int TYPE_BYTE = 0;
    /**
     * @hide
     */
    public static final int TYPE_INT32 = 1;
    /**
     * @hide
     */
    public static final int TYPE_FLOAT = 2;
    /**
     * @hide
     */
    public static final int TYPE_INT64 = 3;
    /**
     * @hide
     */
    public static final int TYPE_DOUBLE = 4;
    /**
     * @hide
     */
    public static final int TYPE_RATIONAL = 5;
    /**
     * @hide
     */
    public static final int NUM_TYPES = 6;

    private void close() {
        // Delete native pointer, but does not clear it
        nativeClose(mMetadataPtr);
        mMetadataPtr = 0;

        if (mBufferSize > 0) {
            VMRuntime.getRuntime().registerNativeFree(mBufferSize);
        }
        mBufferSize = 0;
    }

    private <T> T getBase(CameraCharacteristics.Key<T> key) {
        return getBase(key.getNativeKey());
    }

    private <T> T getBase(CaptureResult.Key<T> key) {
        return getBase(key.getNativeKey());
    }

    private <T> T getBase(CaptureRequest.Key<T> key) {
        return getBase(key.getNativeKey());
    }

    private <T> T getBase(Key<T> key) {
        int tag;
        if (key.hasTag()) {
            tag = key.getTag();
        } else {
            tag = nativeGetTagFromKeyLocal(mMetadataPtr, key.getName());
            key.cacheTag(tag);
        }
        byte[] values = readValues(tag);
        if (values == null) {
            // If the key returns null, use the fallback key if exists.
            // This is to support old key names for the newly published keys.
            if (key.mFallbackName == null) {
                return null;
            }
            tag = nativeGetTagFromKeyLocal(mMetadataPtr, key.mFallbackName);
            values = readValues(tag);
            if (values == null) {
                return null;
            }
        }

        int nativeType = nativeGetTypeFromTagLocal(mMetadataPtr, tag);
        Marshaler<T> marshaler = getMarshalerForKey(key, nativeType);
        ByteBuffer buffer = ByteBuffer.wrap(values).order(ByteOrder.nativeOrder());
        return marshaler.unmarshal(buffer);
    }

    // Use Command pattern here to avoid lots of expensive if/equals checks in get for overridden
    // metadata.
    private static final HashMap<Key<?>, GetCommand> sGetCommandMap =
            new HashMap<Key<?>, GetCommand>();
    static {
        sGetCommandMap.put(
                CameraCharacteristics.SCALER_AVAILABLE_FORMATS.getNativeKey(), new GetCommand() {
                    @Override
                    @SuppressWarnings("unchecked")
                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
                        return (T) metadata.getAvailableFormats();
                    }
                });
        sGetCommandMap.put(
                CaptureResult.STATISTICS_FACES.getNativeKey(), new GetCommand() {
                    @Override
                    @SuppressWarnings("unchecked")
                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
                        return (T) metadata.getFaces();
                    }
                });
        sGetCommandMap.put(
                CaptureResult.STATISTICS_FACE_RECTANGLES.getNativeKey(), new GetCommand() {
                    @Override
                    @SuppressWarnings("unchecked")
                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
                        return (T) metadata.getFaceRectangles();
                    }
                });
        sGetCommandMap.put(
                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP.getNativeKey(),
                        new GetCommand() {
                    @Override
                    @SuppressWarnings("unchecked")
                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
                        return (T) metadata.getStreamConfigurationMap();
                    }
                });
         sGetCommandMap.put(
                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP_MAXIMUM_RESOLUTION.getNativeKey(),
                        new GetCommand() {
                    @Override
                    @SuppressWarnings("unchecked")
                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
                        return (T) metadata.getStreamConfigurationMapMaximumResolution();
                    }
                });
        sGetCommandMap.put(
                CameraCharacteristics.SCALER_MANDATORY_STREAM_COMBINATIONS.getNativeKey(),
                        new GetCommand() {
                    @Override
                    @SuppressWarnings("unchecked")
                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
                        return (T) metadata.getMandatoryStreamCombinations();
                    }
                });
        sGetCommandMap.put(
                CameraCharacteristics.SCALER_MANDATORY_CONCURRENT_STREAM_COMBINATIONS.getNativeKey(),
                        new GetCommand() {
                    @Override
                    @SuppressWarnings("unchecked")
                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
                        return (T) metadata.getMandatoryConcurrentStreamCombinations();
                    }
                });

        sGetCommandMap.put(
                CameraCharacteristics.SCALER_MANDATORY_TEN_BIT_OUTPUT_STREAM_COMBINATIONS.getNativeKey(),
                new GetCommand() {
                    @Override
                    @SuppressWarnings("unchecked")
                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
                        return (T) metadata.getMandatory10BitStreamCombinations();
                    }
                });

        sGetCommandMap.put(
                CameraCharacteristics.SCALER_MANDATORY_MAXIMUM_RESOLUTION_STREAM_COMBINATIONS.getNativeKey(),
                        new GetCommand() {
                    @Override
                    @SuppressWarnings("unchecked")
                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
                        return (T) metadata.getMandatoryMaximumResolutionStreamCombinations();
                    }
                });

        sGetCommandMap.put(
                CameraCharacteristics.SCALER_MANDATORY_USE_CASE_STREAM_COMBINATIONS.getNativeKey(),
                        new GetCommand() {
                    @Override
                    @SuppressWarnings("unchecked")
                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
                        return (T) metadata.getMandatoryUseCaseStreamCombinations();
                    }
                });
        sGetCommandMap.put(
                CameraCharacteristics.SCALER_MANDATORY_PREVIEW_STABILIZATION_OUTPUT_STREAM_COMBINATIONS.getNativeKey(),
                new GetCommand() {
                    @Override
                    @SuppressWarnings("unchecked")
                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
                        return (T) metadata.getMandatoryPreviewStabilizationStreamCombinations();
                    }
                });

        sGetCommandMap.put(
                CameraCharacteristics.CONTROL_MAX_REGIONS_AE.getNativeKey(), new GetCommand() {
                    @Override
                    @SuppressWarnings("unchecked")
                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
                        return (T) metadata.getMaxRegions(key);
                    }
                });
        sGetCommandMap.put(
                CameraCharacteristics.CONTROL_MAX_REGIONS_AWB.getNativeKey(), new GetCommand() {
                    @Override
                    @SuppressWarnings("unchecked")
                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
                        return (T) metadata.getMaxRegions(key);
                    }
                });
        sGetCommandMap.put(
                CameraCharacteristics.CONTROL_MAX_REGIONS_AF.getNativeKey(), new GetCommand() {
                    @Override
                    @SuppressWarnings("unchecked")
                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
                        return (T) metadata.getMaxRegions(key);
                    }
                });
        sGetCommandMap.put(
                CameraCharacteristics.REQUEST_MAX_NUM_OUTPUT_RAW.getNativeKey(), new GetCommand() {
                    @Override
                    @SuppressWarnings("unchecked")
                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
                        return (T) metadata.getMaxNumOutputs(key);
                    }
                });
        sGetCommandMap.put(
                CameraCharacteristics.REQUEST_MAX_NUM_OUTPUT_PROC.getNativeKey(), new GetCommand() {
                    @Override
                    @SuppressWarnings("unchecked")
                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
                        return (T) metadata.getMaxNumOutputs(key);
                    }
                });
        sGetCommandMap.put(
                CameraCharacteristics.REQUEST_MAX_NUM_OUTPUT_PROC_STALLING.getNativeKey(),
                        new GetCommand() {
                    @Override
                    @SuppressWarnings("unchecked")
                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
                        return (T) metadata.getMaxNumOutputs(key);
                    }
                });
        sGetCommandMap.put(
                CaptureRequest.TONEMAP_CURVE.getNativeKey(), new GetCommand() {
                    @Override
                    @SuppressWarnings("unchecked")
                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
                        return (T) metadata.getTonemapCurve();
                    }
                });
        sGetCommandMap.put(
                CaptureResult.JPEG_GPS_LOCATION.getNativeKey(), new GetCommand() {
                    @Override
                    @SuppressWarnings("unchecked")
                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
                        return (T) metadata.getGpsLocation();
                    }
                });
        sGetCommandMap.put(
                CaptureResult.STATISTICS_LENS_SHADING_CORRECTION_MAP.getNativeKey(),
                new GetCommand() {
                    @Override
                    @SuppressWarnings("unchecked")
                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
                        return (T) metadata.getLensShadingMap();
                    }
                });
        sGetCommandMap.put(
                CameraCharacteristics.INFO_DEVICE_STATE_SENSOR_ORIENTATION_MAP.getNativeKey(),
                        new GetCommand() {
                    @Override
                    @SuppressWarnings("unchecked")
                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
                        return (T) metadata.getDeviceStateOrientationMap();
                    }
                });
        sGetCommandMap.put(
                CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES.getNativeKey(),
                        new GetCommand() {
                    @Override
                    @SuppressWarnings("unchecked")
                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
                        return (T) metadata.getDynamicRangeProfiles();
                    }
                });
        sGetCommandMap.put(
                CaptureResult.STATISTICS_OIS_SAMPLES.getNativeKey(),
                        new GetCommand() {
                    @Override
                    @SuppressWarnings("unchecked")
                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
                        return (T) metadata.getOisSamples();
                    }
                });
        sGetCommandMap.put(
                CameraCharacteristics.CONTROL_AVAILABLE_EXTENDED_SCENE_MODE_CAPABILITIES.getNativeKey(),
                        new GetCommand() {
                    @Override
                    @SuppressWarnings("unchecked")
                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
                        return (T) metadata.getExtendedSceneModeCapabilities();
                    }
                });
        sGetCommandMap.put(
                CameraCharacteristics.SCALER_MULTI_RESOLUTION_STREAM_CONFIGURATION_MAP.getNativeKey(),
                        new GetCommand() {
                    @Override
                    @SuppressWarnings("unchecked")
                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
                        return (T) metadata.getMultiResolutionStreamConfigurationMap();
                    }
                });
    }

    private int[] getAvailableFormats() {
        int[] availableFormats = getBase(CameraCharacteristics.SCALER_AVAILABLE_FORMATS);
        if (availableFormats != null) {
            for (int i = 0; i < availableFormats.length; i++) {
                // JPEG has different value between native and managed side, need override.
                if (availableFormats[i] == NATIVE_JPEG_FORMAT) {
                    availableFormats[i] = ImageFormat.JPEG;
                }
            }
        }

        return availableFormats;
    }

    private boolean setFaces(Face[] faces) {
        if (faces == null) {
            return false;
        }

        int numFaces = faces.length;

        // Detect if all faces are SIMPLE or not; count # of valid faces
        boolean fullMode = true;
        for (Face face : faces) {
            if (face == null) {
                numFaces--;
                Log.w(TAG, "setFaces - null face detected, skipping");
                continue;
            }

            if (face.getId() == Face.ID_UNSUPPORTED) {
                fullMode = false;
            }
        }

        Rect[] faceRectangles = new Rect[numFaces];
        byte[] faceScores = new byte[numFaces];
        int[] faceIds = null;
        int[] faceLandmarks = null;

        if (fullMode) {
            faceIds = new int[numFaces];
            faceLandmarks = new int[numFaces * FACE_LANDMARK_SIZE];
        }

        int i = 0;
        for (Face face : faces) {
            if (face == null) {
                continue;
            }

            faceRectangles[i] = face.getBounds();
            faceScores[i] = (byte)face.getScore();

            if (fullMode) {
                faceIds[i] = face.getId();

                int j = 0;

                faceLandmarks[i * FACE_LANDMARK_SIZE + j++] = face.getLeftEyePosition().x;
                faceLandmarks[i * FACE_LANDMARK_SIZE + j++] = face.getLeftEyePosition().y;
                faceLandmarks[i * FACE_LANDMARK_SIZE + j++] = face.getRightEyePosition().x;
                faceLandmarks[i * FACE_LANDMARK_SIZE + j++] = face.getRightEyePosition().y;
                faceLandmarks[i * FACE_LANDMARK_SIZE + j++] = face.getMouthPosition().x;
                faceLandmarks[i * FACE_LANDMARK_SIZE + j++] = face.getMouthPosition().y;
            }

            i++;
        }

        set(CaptureResult.STATISTICS_FACE_RECTANGLES, faceRectangles);
        set(CaptureResult.STATISTICS_FACE_IDS, faceIds);
        set(CaptureResult.STATISTICS_FACE_LANDMARKS, faceLandmarks);
        set(CaptureResult.STATISTICS_FACE_SCORES, faceScores);

        return true;
    }

    private Face[] getFaces() {
        Integer faceDetectMode = get(CaptureResult.STATISTICS_FACE_DETECT_MODE);
        byte[] faceScores = get(CaptureResult.STATISTICS_FACE_SCORES);
        Rect[] faceRectangles = get(CaptureResult.STATISTICS_FACE_RECTANGLES);
        int[] faceIds = get(CaptureResult.STATISTICS_FACE_IDS);
        int[] faceLandmarks = get(CaptureResult.STATISTICS_FACE_LANDMARKS);

        if (areValuesAllNull(faceDetectMode, faceScores, faceRectangles, faceIds, faceLandmarks)) {
            return null;
        }

        if (faceDetectMode == null) {
            Log.w(TAG, "Face detect mode metadata is null, assuming the mode is SIMPLE");
            faceDetectMode = CaptureResult.STATISTICS_FACE_DETECT_MODE_SIMPLE;
        } else if (faceDetectMode > CaptureResult.STATISTICS_FACE_DETECT_MODE_FULL) {
            // Face detect mode is larger than FULL, assuming the mode is FULL
            faceDetectMode = CaptureResult.STATISTICS_FACE_DETECT_MODE_FULL;
        } else {
            if (faceDetectMode == CaptureResult.STATISTICS_FACE_DETECT_MODE_OFF) {
                return new Face[0];
            }
            if (faceDetectMode != CaptureResult.STATISTICS_FACE_DETECT_MODE_SIMPLE &&
                    faceDetectMode != CaptureResult.STATISTICS_FACE_DETECT_MODE_FULL) {
                Log.w(TAG, "Unknown face detect mode: " + faceDetectMode);
                return new Face[0];
            }
        }

        // Face scores and rectangles are required by SIMPLE and FULL mode.
        if (faceScores == null || faceRectangles == null) {
            Log.w(TAG, "Expect face scores and rectangles to be non-null");
            return new Face[0];
        } else if (faceScores.length != faceRectangles.length) {
            Log.w(TAG, String.format("Face score size(%d) doesn match face rectangle size(%d)!",
                    faceScores.length, faceRectangles.length));
        }

        // To be safe, make number of faces is the minimal of all face info metadata length.
        int numFaces = Math.min(faceScores.length, faceRectangles.length);
        // Face id and landmarks are only required by FULL mode.
        if (faceDetectMode == CaptureResult.STATISTICS_FACE_DETECT_MODE_FULL) {
            if (faceIds == null || faceLandmarks == null) {
                Log.w(TAG, "Expect face ids and landmarks to be non-null for FULL mode," +
                        "fallback to SIMPLE mode");
                faceDetectMode = CaptureResult.STATISTICS_FACE_DETECT_MODE_SIMPLE;
            } else {
                if (faceIds.length != numFaces ||
                        faceLandmarks.length != numFaces * FACE_LANDMARK_SIZE) {
                    Log.w(TAG, String.format("Face id size(%d), or face landmark size(%d) don't" +
                            "match face number(%d)!",
                            faceIds.length, faceLandmarks.length * FACE_LANDMARK_SIZE, numFaces));
                }
                // To be safe, make number of faces is the minimal of all face info metadata length.
                numFaces = Math.min(numFaces, faceIds.length);
                numFaces = Math.min(numFaces, faceLandmarks.length / FACE_LANDMARK_SIZE);
            }
        }

        ArrayList<Face> faceList = new ArrayList<Face>();
        if (faceDetectMode == CaptureResult.STATISTICS_FACE_DETECT_MODE_SIMPLE) {
            for (int i = 0; i < numFaces; i++) {
                if (faceScores[i] <= Face.SCORE_MAX &&
                        faceScores[i] >= Face.SCORE_MIN) {
                    faceList.add(new Face(faceRectangles[i], faceScores[i]));
                }
            }
        } else {
            // CaptureResult.STATISTICS_FACE_DETECT_MODE_FULL
            for (int i = 0; i < numFaces; i++) {
                if (faceScores[i] <= Face.SCORE_MAX &&
                        faceScores[i] >= Face.SCORE_MIN &&
                        faceIds[i] >= 0) {
                    Point leftEye = new Point(faceLandmarks[i*FACE_LANDMARK_SIZE],
                            faceLandmarks[i*FACE_LANDMARK_SIZE+1]);
                    Point rightEye = new Point(faceLandmarks[i*FACE_LANDMARK_SIZE+2],
                            faceLandmarks[i*FACE_LANDMARK_SIZE+3]);
                    Point mouth = new Point(faceLandmarks[i*FACE_LANDMARK_SIZE+4],
                            faceLandmarks[i*FACE_LANDMARK_SIZE+5]);
                    Face face = new Face(faceRectangles[i], faceScores[i], faceIds[i],
                            leftEye, rightEye, mouth);
                    faceList.add(face);
                }
            }
        }
        Face[] faces = new Face[faceList.size()];
        faceList.toArray(faces);
        return faces;
    }

    // Face rectangles are defined as (left, top, right, bottom) instead of
    // (left, top, width, height) at the native level, so the normal Rect
    // conversion that does (l, t, w, h) -> (l, t, r, b) is unnecessary. Undo
    // that conversion here for just the faces.
    private Rect[] getFaceRectangles() {
        Rect[] faceRectangles = getBase(CaptureResult.STATISTICS_FACE_RECTANGLES);
        if (faceRectangles == null) return null;

        Rect[] fixedFaceRectangles = new Rect[faceRectangles.length];
        for (int i = 0; i < faceRectangles.length; i++) {
            fixedFaceRectangles[i] = new Rect(
                    faceRectangles[i].left,
                    faceRectangles[i].top,
                    faceRectangles[i].right - faceRectangles[i].left,
                    faceRectangles[i].bottom - faceRectangles[i].top);
        }
        return fixedFaceRectangles;
    }

    private LensShadingMap getLensShadingMap() {
        float[] lsmArray = getBase(CaptureResult.STATISTICS_LENS_SHADING_MAP);
        Size s = get(CameraCharacteristics.LENS_INFO_SHADING_MAP_SIZE);

        // Do not warn if lsmArray is null while s is not. This is valid.
        if (lsmArray == null) {
            return null;
        }

        if (s == null) {
            Log.w(TAG, "getLensShadingMap - Lens shading map size was null.");
            return null;
        }

        LensShadingMap map = new LensShadingMap(lsmArray, s.getHeight(), s.getWidth());
        return map;
    }

    private DeviceStateSensorOrientationMap getDeviceStateOrientationMap() {
        long[] mapArray = getBase(CameraCharacteristics.INFO_DEVICE_STATE_ORIENTATIONS);

        // Do not warn if map is null while s is not. This is valid.
        if (mapArray == null) {
            return null;
        }

        DeviceStateSensorOrientationMap map = new DeviceStateSensorOrientationMap(mapArray);
        return map;
    }

    private DynamicRangeProfiles getDynamicRangeProfiles() {
        long[] profileArray = getBase(
                CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES_MAP);

        if (profileArray == null) {
            return null;
        }

        return new DynamicRangeProfiles(profileArray);
    }

    private Location getGpsLocation() {
        String processingMethod = get(CaptureResult.JPEG_GPS_PROCESSING_METHOD);
        double[] coords = get(CaptureResult.JPEG_GPS_COORDINATES);
        Long timeStamp = get(CaptureResult.JPEG_GPS_TIMESTAMP);

        if (areValuesAllNull(processingMethod, coords, timeStamp)) {
            return null;
        }

        Location l = new Location(translateProcessToLocationProvider(processingMethod));
        if (timeStamp != null) {
            // Location expects timestamp in [ms.]
            l.setTime(timeStamp * 1000);
        } else {
            Log.w(TAG, "getGpsLocation - No timestamp for GPS location.");
        }

        if (coords != null) {
            l.setLatitude(coords[0]);
            l.setLongitude(coords[1]);
            l.setAltitude(coords[2]);
        } else {
            Log.w(TAG, "getGpsLocation - No coordinates for GPS location");
        }

        return l;
    }

    private boolean setGpsLocation(Location l) {
        if (l == null) {
            return false;
        }

        double[] coords = { l.getLatitude(), l.getLongitude(), l.getAltitude() };
        String processMethod = translateLocationProviderToProcess(l.getProvider());
        //JPEG_GPS_TIMESTAMP expects sec. instead of msec.
        long timestamp = l.getTime() / 1000;

        set(CaptureRequest.JPEG_GPS_TIMESTAMP, timestamp);
        set(CaptureRequest.JPEG_GPS_COORDINATES, coords);

        if (processMethod == null) {
            Log.w(TAG, "setGpsLocation - No process method, Location is not from a GPS or NETWORK" +
                    "provider");
        } else {
            setBase(CaptureRequest.JPEG_GPS_PROCESSING_METHOD, processMethod);
        }
        return true;
    }

    private void parseRecommendedConfigurations(RecommendedStreamConfiguration[] configurations,
            StreamConfigurationMap fullMap, boolean isDepth,
            ArrayList<ArrayList<StreamConfiguration>> /*out*/streamConfigList,
            ArrayList<ArrayList<StreamConfigurationDuration>> /*out*/streamDurationList,
            ArrayList<ArrayList<StreamConfigurationDuration>> /*out*/streamStallList,
            boolean[] /*out*/supportsPrivate) {

        streamConfigList.ensureCapacity(RecommendedStreamConfigurationMap.MAX_USECASE_COUNT);
        streamDurationList.ensureCapacity(RecommendedStreamConfigurationMap.MAX_USECASE_COUNT);
        streamStallList.ensureCapacity(RecommendedStreamConfigurationMap.MAX_USECASE_COUNT);
        for (int i = 0; i < RecommendedStreamConfigurationMap.MAX_USECASE_COUNT; i++) {
            streamConfigList.add(new ArrayList<StreamConfiguration> ());
            streamDurationList.add(new ArrayList<StreamConfigurationDuration> ());
            streamStallList.add(new ArrayList<StreamConfigurationDuration> ());
        }

        for (RecommendedStreamConfiguration c : configurations) {
            int width = c.getWidth();
            int height = c.getHeight();
            int internalFormat = c.getFormat();
            int publicFormat =
                (isDepth) ? StreamConfigurationMap.depthFormatToPublic(internalFormat) :
                StreamConfigurationMap.imageFormatToPublic(internalFormat);
            Size sz = new Size(width, height);
            int usecaseBitmap = c.getUsecaseBitmap();

            if (!c.isInput()) {
                StreamConfigurationDuration minDurationConfiguration = null;
                StreamConfigurationDuration stallDurationConfiguration = null;

                StreamConfiguration streamConfiguration = new StreamConfiguration(internalFormat,
                        width, height, /*input*/ false);

                long minFrameDuration = fullMap.getOutputMinFrameDuration(publicFormat, sz);
                if (minFrameDuration > 0) {
                    minDurationConfiguration = new StreamConfigurationDuration(internalFormat,
                            width, height, minFrameDuration);
                }

                long stallDuration = fullMap.getOutputStallDuration(publicFormat, sz);
                if (stallDuration > 0) {
                    stallDurationConfiguration = new StreamConfigurationDuration(internalFormat,
                            width, height, stallDuration);
                }

                for (int i = 0; i < RecommendedStreamConfigurationMap.MAX_USECASE_COUNT; i++) {
                    if ((usecaseBitmap & (1 << i)) != 0) {
                        ArrayList<StreamConfiguration> sc = streamConfigList.get(i);
                        sc.add(streamConfiguration);

                        if (minFrameDuration > 0) {
                            ArrayList<StreamConfigurationDuration> scd = streamDurationList.get(i);
                            scd.add(minDurationConfiguration);
                        }

                        if (stallDuration > 0) {
                            ArrayList<StreamConfigurationDuration> scs = streamStallList.get(i);
                            scs.add(stallDurationConfiguration);
                        }

                        if ((supportsPrivate != null) && !supportsPrivate[i] &&
                                (publicFormat == ImageFormat.PRIVATE)) {
                            supportsPrivate[i] = true;
                        }
                    }
                }
            } else {
                if (usecaseBitmap != (1 << RecommendedStreamConfigurationMap.USECASE_ZSL)) {
                    throw new IllegalArgumentException("Recommended input stream configurations " +
                            "should only be advertised in the ZSL use case!");
                }

                ArrayList<StreamConfiguration> sc = streamConfigList.get(
                        RecommendedStreamConfigurationMap.USECASE_ZSL);
                sc.add(new StreamConfiguration(internalFormat,
                        width, height, /*input*/ true));
            }
        }
    }

    private class StreamConfigurationData {
        StreamConfiguration [] streamConfigurationArray = null;
        StreamConfigurationDuration [] minDurationArray = null;
        StreamConfigurationDuration [] stallDurationArray = null;
    }

    public void initializeStreamConfigurationData(ArrayList<StreamConfiguration> sc,
            ArrayList<StreamConfigurationDuration> scd, ArrayList<StreamConfigurationDuration> scs,
            StreamConfigurationData /*out*/scData) {
        if ((scData == null) || (sc == null)) {
            return;
        }

        scData.streamConfigurationArray = new StreamConfiguration[sc.size()];
        scData.streamConfigurationArray = sc.toArray(scData.streamConfigurationArray);

        if ((scd != null) && !scd.isEmpty()) {
            scData.minDurationArray = new StreamConfigurationDuration[scd.size()];
            scData.minDurationArray = scd.toArray(scData.minDurationArray);
        } else {
            scData.minDurationArray = new StreamConfigurationDuration[0];
        }

        if ((scs != null) && !scs.isEmpty()) {
            scData.stallDurationArray = new StreamConfigurationDuration[scs.size()];
            scData.stallDurationArray = scs.toArray(scData.stallDurationArray);
        } else {
            scData.stallDurationArray = new StreamConfigurationDuration[0];
        }
    }

    /**
     * Retrieve the list of recommended stream configurations.
     *
     * @return A list of recommended stream configuration maps for each common use case or null
     *         in case the recommended stream configurations are invalid or incomplete.
     * @hide
     */
    public ArrayList<RecommendedStreamConfigurationMap> getRecommendedStreamConfigurations() {
        RecommendedStreamConfiguration[] configurations = getBase(
                CameraCharacteristics.SCALER_AVAILABLE_RECOMMENDED_STREAM_CONFIGURATIONS);
        RecommendedStreamConfiguration[] depthConfigurations = getBase(
                CameraCharacteristics.DEPTH_AVAILABLE_RECOMMENDED_DEPTH_STREAM_CONFIGURATIONS);
        if ((configurations == null) && (depthConfigurations == null)) {
            return null;
        }

        StreamConfigurationMap fullMap = getStreamConfigurationMap();
        ArrayList<RecommendedStreamConfigurationMap> recommendedConfigurations =
            new ArrayList<RecommendedStreamConfigurationMap> ();

        ArrayList<ArrayList<StreamConfiguration>> streamConfigList =
            new ArrayList<ArrayList<StreamConfiguration>>();
        ArrayList<ArrayList<StreamConfigurationDuration>> streamDurationList =
            new ArrayList<ArrayList<StreamConfigurationDuration>>();
        ArrayList<ArrayList<StreamConfigurationDuration>> streamStallList =
            new ArrayList<ArrayList<StreamConfigurationDuration>>();
        boolean[] supportsPrivate =
                new boolean[RecommendedStreamConfigurationMap.MAX_USECASE_COUNT];
        try {
            if (configurations != null) {
                parseRecommendedConfigurations(configurations, fullMap, /*isDepth*/ false,
                        streamConfigList, streamDurationList, streamStallList, supportsPrivate);
            }
        } catch (IllegalArgumentException e) {
            Log.e(TAG, "Failed parsing the recommended stream configurations!");
            return null;
        }

        ArrayList<ArrayList<StreamConfiguration>> depthStreamConfigList =
            new ArrayList<ArrayList<StreamConfiguration>>();
        ArrayList<ArrayList<StreamConfigurationDuration>> depthStreamDurationList =
            new ArrayList<ArrayList<StreamConfigurationDuration>>();
        ArrayList<ArrayList<StreamConfigurationDuration>> depthStreamStallList =
            new ArrayList<ArrayList<StreamConfigurationDuration>>();
        if (depthConfigurations != null) {
            try {
                parseRecommendedConfigurations(depthConfigurations, fullMap, /*isDepth*/ true,
                        depthStreamConfigList, depthStreamDurationList, depthStreamStallList,
                        /*supportsPrivate*/ null);
            } catch (IllegalArgumentException e) {
                Log.e(TAG, "Failed parsing the recommended depth stream configurations!");
                return null;
            }
        }

        ReprocessFormatsMap inputOutputFormatsMap = getBase(
                CameraCharacteristics.SCALER_AVAILABLE_RECOMMENDED_INPUT_OUTPUT_FORMATS_MAP);
        HighSpeedVideoConfiguration[] highSpeedVideoConfigurations = getBase(
                CameraCharacteristics.CONTROL_AVAILABLE_HIGH_SPEED_VIDEO_CONFIGURATIONS);
        boolean listHighResolution = isBurstSupported();
        recommendedConfigurations.ensureCapacity(
                RecommendedStreamConfigurationMap.MAX_USECASE_COUNT);
        for (int i = 0; i < RecommendedStreamConfigurationMap.MAX_USECASE_COUNT; i++) {
            StreamConfigurationData scData = new StreamConfigurationData();
            if (configurations != null) {
                initializeStreamConfigurationData(streamConfigList.get(i),
                        streamDurationList.get(i), streamStallList.get(i), scData);
            }

            StreamConfigurationData depthScData = new StreamConfigurationData();
            if (depthConfigurations != null) {
                initializeStreamConfigurationData(depthStreamConfigList.get(i),
                        depthStreamDurationList.get(i), depthStreamStallList.get(i), depthScData);
            }

            if ((scData.streamConfigurationArray == null ||
                    scData.streamConfigurationArray.length == 0) &&
                    (depthScData.streamConfigurationArray == null ||
                     depthScData.streamConfigurationArray.length == 0)) {
                recommendedConfigurations.add(null);
                continue;
            }

            // Dynamic depth streams involve alot of SW processing and currently cannot be
            // recommended.
            StreamConfigurationMap map = null;
            switch (i) {
                case RecommendedStreamConfigurationMap.USECASE_PREVIEW:
                case RecommendedStreamConfigurationMap.USECASE_RAW:
                case RecommendedStreamConfigurationMap.USECASE_LOW_LATENCY_SNAPSHOT:
                case RecommendedStreamConfigurationMap.USECASE_VIDEO_SNAPSHOT:
                    map = new StreamConfigurationMap(scData.streamConfigurationArray,
                            scData.minDurationArray, scData.stallDurationArray,
                            /*depthconfiguration*/ null, /*depthminduration*/ null,
                            /*depthstallduration*/ null,
                            /*dynamicDepthConfigurations*/ null,
                            /*dynamicDepthMinFrameDurations*/ null,
                            /*dynamicDepthStallDurations*/ null,
                            /*heicconfiguration*/ null,
                            /*heicminduration*/ null,
                            /*heicstallduration*/ null,
                            /*highspeedvideoconfigurations*/ null,
                            /*inputoutputformatsmap*/ null, listHighResolution, supportsPrivate[i]);
                    break;
                case RecommendedStreamConfigurationMap.USECASE_RECORD:
                    map = new StreamConfigurationMap(scData.streamConfigurationArray,
                            scData.minDurationArray, scData.stallDurationArray,
                            /*depthconfiguration*/ null, /*depthminduration*/ null,
                            /*depthstallduration*/ null,
                            /*dynamicDepthConfigurations*/ null,
                            /*dynamicDepthMinFrameDurations*/ null,
                            /*dynamicDepthStallDurations*/ null,
                            /*heicconfiguration*/ null,
                            /*heicminduration*/ null,
                            /*heicstallduration*/ null,
                            highSpeedVideoConfigurations,
                            /*inputoutputformatsmap*/ null, listHighResolution, supportsPrivate[i]);
                    break;
                case RecommendedStreamConfigurationMap.USECASE_ZSL:
                    map = new StreamConfigurationMap(scData.streamConfigurationArray,
                            scData.minDurationArray, scData.stallDurationArray,
                            depthScData.streamConfigurationArray, depthScData.minDurationArray,
                            depthScData.stallDurationArray,
                            /*dynamicDepthConfigurations*/ null,
                            /*dynamicDepthMinFrameDurations*/ null,
                            /*dynamicDepthStallDurations*/ null,
                            /*heicconfiguration*/ null,
                            /*heicminduration*/ null,
                            /*heicstallduration*/ null,
                            /*highSpeedVideoConfigurations*/ null,
                            inputOutputFormatsMap, listHighResolution, supportsPrivate[i]);
                    break;
                default:
                    map = new StreamConfigurationMap(scData.streamConfigurationArray,
                            scData.minDurationArray, scData.stallDurationArray,
                            depthScData.streamConfigurationArray, depthScData.minDurationArray,
                            depthScData.stallDurationArray,
                            /*dynamicDepthConfigurations*/ null,
                            /*dynamicDepthMinFrameDurations*/ null,
                            /*dynamicDepthStallDurations*/ null,
                            /*heicconfiguration*/ null,
                            /*heicminduration*/ null,
                            /*heicstallduration*/ null,
                            /*highSpeedVideoConfigurations*/ null,
                            /*inputOutputFormatsMap*/ null, listHighResolution, supportsPrivate[i]);
            }

            recommendedConfigurations.add(new RecommendedStreamConfigurationMap(map, /*usecase*/i,
                        supportsPrivate[i]));
        }

        return recommendedConfigurations;
    }

    private boolean isCapabilitySupported(int capabilityRequested) {
        boolean ret = false;

        int[] capabilities = getBase(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
        for (int capability : capabilities) {
            if (capabilityRequested == capability) {
                ret = true;
                break;
            }
        }

        return ret;
    }

    /**
     * @hide
     */
    public boolean isUltraHighResolutionSensor() {
        return isCapabilitySupported(
                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR);

    }
    private boolean isBurstSupported() {
        return isCapabilitySupported(
                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE);
    }

    private boolean isPreviewStabilizationSupported() {
        boolean ret = false;

        int[] videoStabilizationModes =
                getBase(CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES);
        if (videoStabilizationModes == null) {
            return false;
        }
        for (int mode : videoStabilizationModes) {
            if (mode == CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION) {
                ret = true;
                break;
            }
        }

        return ret;
    }

    private MandatoryStreamCombination[] getMandatoryStreamCombinationsHelper(
            int mandatoryStreamsType) {
        int[] capabilities = getBase(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
        ArrayList<Integer> caps = new ArrayList<Integer>();
        caps.ensureCapacity(capabilities.length);
        for (int c : capabilities) {
            caps.add(new Integer(c));
        }
        int hwLevel = getBase(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
        MandatoryStreamCombination.Builder build = new MandatoryStreamCombination.Builder(
                mCameraId, hwLevel, mDisplaySize, caps, getStreamConfigurationMap(),
                getStreamConfigurationMapMaximumResolution(), isPreviewStabilizationSupported());

        List<MandatoryStreamCombination> combs = null;
        switch (mandatoryStreamsType) {
            case MANDATORY_STREAM_CONFIGURATIONS_CONCURRENT:
                combs = build.getAvailableMandatoryConcurrentStreamCombinations();
                break;
            case MANDATORY_STREAM_CONFIGURATIONS_MAX_RESOLUTION:
                combs = build.getAvailableMandatoryMaximumResolutionStreamCombinations();
                break;
            case MANDATORY_STREAM_CONFIGURATIONS_10BIT:
                combs = build.getAvailableMandatory10BitStreamCombinations();
                break;
            case MANDATORY_STREAM_CONFIGURATIONS_USE_CASE:
                combs = build.getAvailableMandatoryStreamUseCaseCombinations();
                break;
            case MANDATORY_STREAM_CONFIGURATIONS_PREVIEW_STABILIZATION:
                combs = build.getAvailableMandatoryPreviewStabilizedStreamCombinations();
                break;
            default:
                combs = build.getAvailableMandatoryStreamCombinations();
        }
        if ((combs != null) && (!combs.isEmpty())) {
            MandatoryStreamCombination[] combArray = new MandatoryStreamCombination[combs.size()];
            combArray = combs.toArray(combArray);
            return combArray;
        }
        return null;
    }

    private MandatoryStreamCombination[] getMandatory10BitStreamCombinations() {
        return getMandatoryStreamCombinationsHelper(MANDATORY_STREAM_CONFIGURATIONS_10BIT);
    }

    private MandatoryStreamCombination[] getMandatoryConcurrentStreamCombinations() {
        if (!mHasMandatoryConcurrentStreams) {
            return null;
        }
        return getMandatoryStreamCombinationsHelper(MANDATORY_STREAM_CONFIGURATIONS_CONCURRENT);
    }

    private MandatoryStreamCombination[] getMandatoryMaximumResolutionStreamCombinations() {
        if (!isUltraHighResolutionSensor()) {
            return null;
        }
        return getMandatoryStreamCombinationsHelper(MANDATORY_STREAM_CONFIGURATIONS_MAX_RESOLUTION);
    }

    private MandatoryStreamCombination[] getMandatoryStreamCombinations() {
        return getMandatoryStreamCombinationsHelper(MANDATORY_STREAM_CONFIGURATIONS_DEFAULT);
    }

    private MandatoryStreamCombination[] getMandatoryUseCaseStreamCombinations() {
        return getMandatoryStreamCombinationsHelper(MANDATORY_STREAM_CONFIGURATIONS_USE_CASE);
    }

    private MandatoryStreamCombination[] getMandatoryPreviewStabilizationStreamCombinations() {
        return getMandatoryStreamCombinationsHelper(
                MANDATORY_STREAM_CONFIGURATIONS_PREVIEW_STABILIZATION);
    }

    private StreamConfigurationMap getStreamConfigurationMap() {
        StreamConfiguration[] configurations = getBase(
                CameraCharacteristics.SCALER_AVAILABLE_STREAM_CONFIGURATIONS);
        StreamConfigurationDuration[] minFrameDurations = getBase(
                CameraCharacteristics.SCALER_AVAILABLE_MIN_FRAME_DURATIONS);
        StreamConfigurationDuration[] stallDurations = getBase(
                CameraCharacteristics.SCALER_AVAILABLE_STALL_DURATIONS);
        StreamConfiguration[] depthConfigurations = getBase(
                CameraCharacteristics.DEPTH_AVAILABLE_DEPTH_STREAM_CONFIGURATIONS);
        StreamConfigurationDuration[] depthMinFrameDurations = getBase(
                CameraCharacteristics.DEPTH_AVAILABLE_DEPTH_MIN_FRAME_DURATIONS);
        StreamConfigurationDuration[] depthStallDurations = getBase(
                CameraCharacteristics.DEPTH_AVAILABLE_DEPTH_STALL_DURATIONS);
        StreamConfiguration[] dynamicDepthConfigurations = getBase(
                CameraCharacteristics.DEPTH_AVAILABLE_DYNAMIC_DEPTH_STREAM_CONFIGURATIONS);
        StreamConfigurationDuration[] dynamicDepthMinFrameDurations = getBase(
                CameraCharacteristics.DEPTH_AVAILABLE_DYNAMIC_DEPTH_MIN_FRAME_DURATIONS);
        StreamConfigurationDuration[] dynamicDepthStallDurations = getBase(
                CameraCharacteristics.DEPTH_AVAILABLE_DYNAMIC_DEPTH_STALL_DURATIONS);
        StreamConfiguration[] heicConfigurations = getBase(
                CameraCharacteristics.HEIC_AVAILABLE_HEIC_STREAM_CONFIGURATIONS);
        StreamConfigurationDuration[] heicMinFrameDurations = getBase(
                CameraCharacteristics.HEIC_AVAILABLE_HEIC_MIN_FRAME_DURATIONS);
        StreamConfigurationDuration[] heicStallDurations = getBase(
                CameraCharacteristics.HEIC_AVAILABLE_HEIC_STALL_DURATIONS);
        HighSpeedVideoConfiguration[] highSpeedVideoConfigurations = getBase(
                CameraCharacteristics.CONTROL_AVAILABLE_HIGH_SPEED_VIDEO_CONFIGURATIONS);
        ReprocessFormatsMap inputOutputFormatsMap = getBase(
                CameraCharacteristics.SCALER_AVAILABLE_INPUT_OUTPUT_FORMATS_MAP);
        boolean listHighResolution = isBurstSupported();
        return new StreamConfigurationMap(
                configurations, minFrameDurations, stallDurations,
                depthConfigurations, depthMinFrameDurations, depthStallDurations,
                dynamicDepthConfigurations, dynamicDepthMinFrameDurations,
                dynamicDepthStallDurations, heicConfigurations,
                heicMinFrameDurations, heicStallDurations,
                highSpeedVideoConfigurations, inputOutputFormatsMap,
                listHighResolution);
    }

    private StreamConfigurationMap getStreamConfigurationMapMaximumResolution() {
        if (!isUltraHighResolutionSensor()) {
            return null;
        }
        StreamConfiguration[] configurations = getBase(
                CameraCharacteristics.SCALER_AVAILABLE_STREAM_CONFIGURATIONS_MAXIMUM_RESOLUTION);
        StreamConfigurationDuration[] minFrameDurations = getBase(
                CameraCharacteristics.SCALER_AVAILABLE_MIN_FRAME_DURATIONS_MAXIMUM_RESOLUTION);
        StreamConfigurationDuration[] stallDurations = getBase(
                CameraCharacteristics.SCALER_AVAILABLE_STALL_DURATIONS_MAXIMUM_RESOLUTION);
        StreamConfiguration[] depthConfigurations = getBase(
                CameraCharacteristics.DEPTH_AVAILABLE_DEPTH_STREAM_CONFIGURATIONS_MAXIMUM_RESOLUTION);
        StreamConfigurationDuration[] depthMinFrameDurations = getBase(
                CameraCharacteristics.DEPTH_AVAILABLE_DEPTH_MIN_FRAME_DURATIONS_MAXIMUM_RESOLUTION);
        StreamConfigurationDuration[] depthStallDurations = getBase(
                CameraCharacteristics.DEPTH_AVAILABLE_DEPTH_STALL_DURATIONS_MAXIMUM_RESOLUTION);
        StreamConfiguration[] dynamicDepthConfigurations = getBase(
                CameraCharacteristics.DEPTH_AVAILABLE_DYNAMIC_DEPTH_STREAM_CONFIGURATIONS_MAXIMUM_RESOLUTION);
        StreamConfigurationDuration[] dynamicDepthMinFrameDurations = getBase(
                CameraCharacteristics.DEPTH_AVAILABLE_DYNAMIC_DEPTH_MIN_FRAME_DURATIONS_MAXIMUM_RESOLUTION);
        StreamConfigurationDuration[] dynamicDepthStallDurations = getBase(
                CameraCharacteristics.DEPTH_AVAILABLE_DYNAMIC_DEPTH_STALL_DURATIONS_MAXIMUM_RESOLUTION);
        StreamConfiguration[] heicConfigurations = getBase(
                CameraCharacteristics.HEIC_AVAILABLE_HEIC_STREAM_CONFIGURATIONS_MAXIMUM_RESOLUTION);
        StreamConfigurationDuration[] heicMinFrameDurations = getBase(
                CameraCharacteristics.HEIC_AVAILABLE_HEIC_MIN_FRAME_DURATIONS_MAXIMUM_RESOLUTION);
        StreamConfigurationDuration[] heicStallDurations = getBase(
                CameraCharacteristics.HEIC_AVAILABLE_HEIC_STALL_DURATIONS_MAXIMUM_RESOLUTION);
        HighSpeedVideoConfiguration[] highSpeedVideoConfigurations = getBase(
                CameraCharacteristics.CONTROL_AVAILABLE_HIGH_SPEED_VIDEO_CONFIGURATIONS_MAXIMUM_RESOLUTION);
        ReprocessFormatsMap inputOutputFormatsMap = getBase(
                CameraCharacteristics.SCALER_AVAILABLE_INPUT_OUTPUT_FORMATS_MAP_MAXIMUM_RESOLUTION);
        // TODO: Is this correct, burst capability shouldn't necessarily correspond to max res mode
        boolean listHighResolution = isBurstSupported();
        return new StreamConfigurationMap(
                configurations, minFrameDurations, stallDurations,
                depthConfigurations, depthMinFrameDurations, depthStallDurations,
                dynamicDepthConfigurations, dynamicDepthMinFrameDurations,
                dynamicDepthStallDurations, heicConfigurations,
                heicMinFrameDurations, heicStallDurations,
                highSpeedVideoConfigurations, inputOutputFormatsMap,
                listHighResolution, false);
    }

    private <T> Integer getMaxRegions(Key<T> key) {
        final int AE = 0;
        final int AWB = 1;
        final int AF = 2;

        // The order of the elements is: (AE, AWB, AF)
        int[] maxRegions = getBase(CameraCharacteristics.CONTROL_MAX_REGIONS);

        if (maxRegions == null) {
            return null;
        }

        if (key.equals(CameraCharacteristics.CONTROL_MAX_REGIONS_AE)) {
            return maxRegions[AE];
        } else if (key.equals(CameraCharacteristics.CONTROL_MAX_REGIONS_AWB)) {
            return maxRegions[AWB];
        } else if (key.equals(CameraCharacteristics.CONTROL_MAX_REGIONS_AF)) {
            return maxRegions[AF];
        } else {
            throw new AssertionError("Invalid key " + key);
        }
    }

    private <T> Integer getMaxNumOutputs(Key<T> key) {
        final int RAW = 0;
        final int PROC = 1;
        final int PROC_STALLING = 2;

        // The order of the elements is: (raw, proc+nonstalling, proc+stalling)
        int[] maxNumOutputs = getBase(CameraCharacteristics.REQUEST_MAX_NUM_OUTPUT_STREAMS);

        if (maxNumOutputs == null) {
            return null;
        }

        if (key.equals(CameraCharacteristics.REQUEST_MAX_NUM_OUTPUT_RAW)) {
            return maxNumOutputs[RAW];
        } else if (key.equals(CameraCharacteristics.REQUEST_MAX_NUM_OUTPUT_PROC)) {
            return maxNumOutputs[PROC];
        } else if (key.equals(CameraCharacteristics.REQUEST_MAX_NUM_OUTPUT_PROC_STALLING)) {
            return maxNumOutputs[PROC_STALLING];
        } else {
            throw new AssertionError("Invalid key " + key);
        }
    }

    private <T> TonemapCurve getTonemapCurve() {
        float[] red = getBase(CaptureRequest.TONEMAP_CURVE_RED);
        float[] green = getBase(CaptureRequest.TONEMAP_CURVE_GREEN);
        float[] blue = getBase(CaptureRequest.TONEMAP_CURVE_BLUE);

        if (areValuesAllNull(red, green, blue)) {
            return null;
        }

        if (red == null || green == null || blue == null) {
            Log.w(TAG, "getTonemapCurve - missing tone curve components");
            return null;
        }
        TonemapCurve tc = new TonemapCurve(red, green, blue);
        return tc;
    }

    private OisSample[] getOisSamples() {
        long[] timestamps = getBase(CaptureResult.STATISTICS_OIS_TIMESTAMPS);
        float[] xShifts = getBase(CaptureResult.STATISTICS_OIS_X_SHIFTS);
        float[] yShifts = getBase(CaptureResult.STATISTICS_OIS_Y_SHIFTS);

        if (timestamps == null) {
            if (xShifts != null) {
                throw new AssertionError("timestamps is null but xShifts is not");
            }

            if (yShifts != null) {
                throw new AssertionError("timestamps is null but yShifts is not");
            }

            return null;
        }

        if (xShifts == null) {
            throw new AssertionError("timestamps is not null but xShifts is");
        }

        if (yShifts == null) {
            throw new AssertionError("timestamps is not null but yShifts is");
        }

        if (xShifts.length != timestamps.length) {
            throw new AssertionError(String.format(
                    "timestamps has %d entries but xShifts has %d", timestamps.length,
                    xShifts.length));
        }

        if (yShifts.length != timestamps.length) {
            throw new AssertionError(String.format(
                    "timestamps has %d entries but yShifts has %d", timestamps.length,
                    yShifts.length));
        }

        OisSample[] samples = new OisSample[timestamps.length];
        for (int i = 0; i < timestamps.length; i++) {
            samples[i] = new OisSample(timestamps[i], xShifts[i], yShifts[i]);
        }
        return samples;
    }

    private Capability[] getExtendedSceneModeCapabilities() {
        int[] maxSizes =
                getBase(CameraCharacteristics.CONTROL_AVAILABLE_EXTENDED_SCENE_MODE_MAX_SIZES);
        float[] zoomRanges = getBase(
                CameraCharacteristics.CONTROL_AVAILABLE_EXTENDED_SCENE_MODE_ZOOM_RATIO_RANGES);
        Range<Float> zoomRange = getBase(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE);
        float maxDigitalZoom = getBase(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM);

        if (maxSizes == null) {
            return null;
        }
        if (maxSizes.length % 3 != 0) {
            throw new AssertionError("availableExtendedSceneModeMaxSizes must be tuples of "
                    + "[mode, width, height]");
        }
        int numExtendedSceneModes = maxSizes.length / 3;
        int numExtendedSceneModeZoomRanges = 0;
        if (zoomRanges != null) {
            if (zoomRanges.length % 2 != 0) {
                throw new AssertionError("availableExtendedSceneModeZoomRanges must be tuples of "
                        + "[minZoom, maxZoom]");
            }
            numExtendedSceneModeZoomRanges = zoomRanges.length / 2;
            if (numExtendedSceneModes - numExtendedSceneModeZoomRanges != 1) {
                throw new AssertionError("Number of extended scene mode zoom ranges must be 1 "
                        + "less than number of supported modes");
            }
        }

        float modeOffMinZoomRatio = 1.0f;
        float modeOffMaxZoomRatio = maxDigitalZoom;
        if (zoomRange != null) {
            modeOffMinZoomRatio = zoomRange.getLower();
            modeOffMaxZoomRatio = zoomRange.getUpper();
        }

        Capability[] capabilities = new Capability[numExtendedSceneModes];
        for (int i = 0, j = 0; i < numExtendedSceneModes; i++) {
            int mode = maxSizes[3 * i];
            int width = maxSizes[3 * i + 1];
            int height = maxSizes[3 * i + 2];
            if (mode != CameraMetadata.CONTROL_EXTENDED_SCENE_MODE_DISABLED
                    && j < numExtendedSceneModeZoomRanges) {
                capabilities[i] = new Capability(mode, width, height, zoomRanges[2 * j],
                        zoomRanges[2 * j + 1]);
                j++;
            } else {
                capabilities[i] = new Capability(mode, width, height, modeOffMinZoomRatio,
                        modeOffMaxZoomRatio);
            }
        }

        return capabilities;
    }

    private <T> void setBase(CameraCharacteristics.Key<T> key, T value) {
        setBase(key.getNativeKey(), value);
    }

    private <T> void setBase(CaptureResult.Key<T> key, T value) {
        setBase(key.getNativeKey(), value);
    }

    private <T> void setBase(CaptureRequest.Key<T> key, T value) {
        setBase(key.getNativeKey(), value);
    }

    private <T> void setBase(Key<T> key, T value) {
        int tag;
        if (key.hasTag()) {
            tag = key.getTag();
        } else {
            tag = nativeGetTagFromKeyLocal(mMetadataPtr, key.getName());
            key.cacheTag(tag);
        }
        if (value == null) {
            // Erase the entry
            writeValues(tag, /*src*/null);
            return;
        } // else update the entry to a new value

        int nativeType = nativeGetTypeFromTagLocal(mMetadataPtr, tag);
        Marshaler<T> marshaler = getMarshalerForKey(key, nativeType);
        int size = marshaler.calculateMarshalSize(value);

        // TODO: Optimization. Cache the byte[] and reuse if the size is big enough.
        byte[] values = new byte[size];

        ByteBuffer buffer = ByteBuffer.wrap(values).order(ByteOrder.nativeOrder());
        marshaler.marshal(value, buffer);

        writeValues(tag, values);
    }

    // Use Command pattern here to avoid lots of expensive if/equals checks in get for overridden
    // metadata.
    private static final HashMap<Key<?>, SetCommand> sSetCommandMap =
            new HashMap<Key<?>, SetCommand>();
    static {
        sSetCommandMap.put(CameraCharacteristics.SCALER_AVAILABLE_FORMATS.getNativeKey(),
                new SetCommand() {
            @Override
            public <T> void setValue(CameraMetadataNative metadata, T value) {
                metadata.setAvailableFormats((int[]) value);
            }
        });
        sSetCommandMap.put(CaptureResult.STATISTICS_FACE_RECTANGLES.getNativeKey(),
                new SetCommand() {
            @Override
            public <T> void setValue(CameraMetadataNative metadata, T value) {
                metadata.setFaceRectangles((Rect[]) value);
            }
        });
        sSetCommandMap.put(CaptureResult.STATISTICS_FACES.getNativeKey(),
                new SetCommand() {
            @Override
            public <T> void setValue(CameraMetadataNative metadata, T value) {
                metadata.setFaces((Face[])value);
            }
        });
        sSetCommandMap.put(CaptureRequest.TONEMAP_CURVE.getNativeKey(), new SetCommand() {
            @Override
            public <T> void setValue(CameraMetadataNative metadata, T value) {
                metadata.setTonemapCurve((TonemapCurve) value);
            }
        });
        sSetCommandMap.put(CaptureResult.JPEG_GPS_LOCATION.getNativeKey(), new SetCommand() {
            @Override
            public <T> void setValue(CameraMetadataNative metadata, T value) {
                metadata.setGpsLocation((Location) value);
            }
        });
        sSetCommandMap.put(CaptureRequest.SCALER_CROP_REGION.getNativeKey(),
                new SetCommand() {
            @Override
            public <T> void setValue(CameraMetadataNative metadata, T value) {
                metadata.setScalerCropRegion((Rect) value);
            }
        });
        sSetCommandMap.put(CaptureRequest.CONTROL_AWB_REGIONS.getNativeKey(),
                new SetCommand() {
            @Override
            public <T> void setValue(CameraMetadataNative metadata, T value) {
                metadata.setAWBRegions(value);
            }
        });
        sSetCommandMap.put(CaptureRequest.CONTROL_AF_REGIONS.getNativeKey(),
                new SetCommand() {
            @Override
            public <T> void setValue(CameraMetadataNative metadata, T value) {
                metadata.setAFRegions(value);
            }
        });
        sSetCommandMap.put(CaptureRequest.CONTROL_AE_REGIONS.getNativeKey(),
                new SetCommand() {
            @Override
            public <T> void setValue(CameraMetadataNative metadata, T value) {
                metadata.setAERegions(value);
            }
        });
    }

    private boolean setAvailableFormats(int[] value) {
        int[] availableFormat = value;
        if (value == null) {
            // Let setBase() to handle the null value case.
            return false;
        }

        int[] newValues = new int[availableFormat.length];
        for (int i = 0; i < availableFormat.length; i++) {
            newValues[i] = availableFormat[i];
            if (availableFormat[i] == ImageFormat.JPEG) {
                newValues[i] = NATIVE_JPEG_FORMAT;
            }
        }

        setBase(CameraCharacteristics.SCALER_AVAILABLE_FORMATS, newValues);
        return true;
    }

    /**
     * Convert Face Rectangles from managed side to native side as they have different definitions.
     * <p>
     * Managed side face rectangles are defined as: left, top, width, height.
     * Native side face rectangles are defined as: left, top, right, bottom.
     * The input face rectangle need to be converted to native side definition when set is called.
     * </p>
     *
     * @param faceRects Input face rectangles.
     * @return true if face rectangles can be set successfully. Otherwise, Let the caller
     *             (setBase) to handle it appropriately.
     */
    private boolean setFaceRectangles(Rect[] faceRects) {
        if (faceRects == null) {
            return false;
        }

        Rect[] newFaceRects = new Rect[faceRects.length];
        for (int i = 0; i < newFaceRects.length; i++) {
            newFaceRects[i] = new Rect(
                    faceRects[i].left,
                    faceRects[i].top,
                    faceRects[i].right + faceRects[i].left,
                    faceRects[i].bottom + faceRects[i].top);
        }

        setBase(CaptureResult.STATISTICS_FACE_RECTANGLES, newFaceRects);
        return true;
    }

    private <T> boolean setTonemapCurve(TonemapCurve tc) {
        if (tc == null) {
            return false;
        }

        float[][] curve = new float[3][];
        for (int i = TonemapCurve.CHANNEL_RED; i <= TonemapCurve.CHANNEL_BLUE; i++) {
            int pointCount = tc.getPointCount(i);
            curve[i] = new float[pointCount * TonemapCurve.POINT_SIZE];
            tc.copyColorCurve(i, curve[i], 0);
        }
        setBase(CaptureRequest.TONEMAP_CURVE_RED, curve[0]);
        setBase(CaptureRequest.TONEMAP_CURVE_GREEN, curve[1]);
        setBase(CaptureRequest.TONEMAP_CURVE_BLUE, curve[2]);

        return true;
    }

    private <T> boolean setScalerCropRegion(Rect cropRegion) {
        if (cropRegion == null) {
            return false;
        }
        setBase(CaptureRequest.SCALER_CROP_REGION_SET, true);
        setBase(CaptureRequest.SCALER_CROP_REGION, cropRegion);
        return true;
    }

    private <T> boolean setAFRegions(T afRegions) {
        if (afRegions == null) {
            return false;
        }
        setBase(CaptureRequest.CONTROL_AF_REGIONS_SET, true);
        // The cast to CaptureRequest.Key is needed since java does not support template
        // specialization and we need to route this method to
        // setBase(CaptureRequest.Key<T> key, T value)
        setBase((CaptureRequest.Key)CaptureRequest.CONTROL_AF_REGIONS, afRegions);
        return true;
    }

    private <T> boolean setAERegions(T aeRegions) {
        if (aeRegions == null) {
            return false;
        }
        setBase(CaptureRequest.CONTROL_AE_REGIONS_SET, true);
        setBase((CaptureRequest.Key)CaptureRequest.CONTROL_AE_REGIONS, aeRegions);
        return true;
    }

    private <T> boolean setAWBRegions(T awbRegions) {
        if (awbRegions == null) {
            return false;
        }
        setBase(CaptureRequest.CONTROL_AWB_REGIONS_SET, true);
        setBase((CaptureRequest.Key)CaptureRequest.CONTROL_AWB_REGIONS, awbRegions);
        return true;
    }

    private void updateNativeAllocation() {
        long currentBufferSize = nativeGetBufferSize(mMetadataPtr);

        if (currentBufferSize != mBufferSize) {
            if (mBufferSize > 0) {
                VMRuntime.getRuntime().registerNativeFree(mBufferSize);
            }

            mBufferSize = currentBufferSize;

            if (mBufferSize > 0) {
                VMRuntime.getRuntime().registerNativeAllocation(mBufferSize);
            }
        }
    }

    private int mCameraId = -1;
    private boolean mHasMandatoryConcurrentStreams = false;
    private Size mDisplaySize = new Size(0, 0);
    private long mBufferSize = 0;
    private MultiResolutionStreamConfigurationMap mMultiResolutionStreamConfigurationMap = null;

    /**
     * Set the current camera Id.
     *
     * @param cameraId Current camera id.
     *
     * @hide
     */
    public void setCameraId(int cameraId) {
        mCameraId = cameraId;
    }

    /**
     * Set the current camera Id.
     *
     * @param hasMandatoryConcurrentStreams whether the metadata advertises mandatory concurrent
     *        streams.
     *
     * @hide
     */
    public void setHasMandatoryConcurrentStreams(boolean hasMandatoryConcurrentStreams) {
        mHasMandatoryConcurrentStreams = hasMandatoryConcurrentStreams;
    }

    /**
     * Set the current display size.
     *
     * @param displaySize The current display size.
     *
     * @hide
     */
    public void setDisplaySize(Size displaySize) {
        mDisplaySize = displaySize;
    }

    /**
     * Set the multi-resolution stream configuration map.
     *
     * @param multiResolutionMap The multi-resolution stream configuration map.
     *
     * @hide
     */
    public void setMultiResolutionStreamConfigurationMap(
            @NonNull Map<String, StreamConfiguration[]> multiResolutionMap) {
        mMultiResolutionStreamConfigurationMap =
                new MultiResolutionStreamConfigurationMap(multiResolutionMap);
    }

    /**
     * Get the multi-resolution stream configuration map.
     *
     * @return The multi-resolution stream configuration map.
     *
     * @hide
     */
    public MultiResolutionStreamConfigurationMap getMultiResolutionStreamConfigurationMap() {
        return mMultiResolutionStreamConfigurationMap;
    }

    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    private long mMetadataPtr; // native std::shared_ptr<CameraMetadata>*

    @FastNative
    private static native long nativeAllocate();
    @FastNative
    private static native long nativeAllocateCopy(long ptr)
            throws NullPointerException;


    @FastNative
    private static native void nativeUpdate(long dst, long src);
    private static synchronized native void nativeWriteToParcel(Parcel dest, long ptr);
    private static synchronized native void nativeReadFromParcel(Parcel source, long ptr);
    private static synchronized native void nativeSwap(long ptr, long otherPtr)
            throws NullPointerException;
    @FastNative
    private static native void nativeSetVendorId(long ptr, long vendorId);
    private static synchronized native void nativeClose(long ptr);
    private static synchronized native boolean nativeIsEmpty(long ptr);
    private static synchronized native int nativeGetEntryCount(long ptr);
    private static synchronized native long nativeGetBufferSize(long ptr);

    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    private static synchronized native byte[] nativeReadValues(int tag, long ptr);
    private static synchronized native void nativeWriteValues(int tag, byte[] src, long ptr);
    private static synchronized native void nativeDump(long ptr) throws IOException; // dump to LOGD

    private static synchronized native ArrayList nativeGetAllVendorKeys(long ptr, Class keyClass);
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    private static synchronized native int nativeGetTagFromKeyLocal(long ptr, String keyName)
            throws IllegalArgumentException;
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    private static synchronized native int nativeGetTypeFromTagLocal(long ptr, int tag)
            throws IllegalArgumentException;
    @FastNative
    private static native int nativeGetTagFromKey(String keyName, long vendorId)
            throws IllegalArgumentException;
    @FastNative
    private static native int nativeGetTypeFromTag(int tag, long vendorId)
            throws IllegalArgumentException;

    /**
     * <p>Perform a 0-copy swap of the internal metadata with another object.</p>
     *
     * <p>Useful to convert a CameraMetadata into e.g. a CaptureRequest.</p>
     *
     * @param other Metadata to swap with
     * @throws NullPointerException if other was null
     * @hide
     */
    public void swap(CameraMetadataNative other) {
        nativeSwap(mMetadataPtr, other.mMetadataPtr);
        mCameraId = other.mCameraId;
        mHasMandatoryConcurrentStreams = other.mHasMandatoryConcurrentStreams;
        mDisplaySize = other.mDisplaySize;
        mMultiResolutionStreamConfigurationMap = other.mMultiResolutionStreamConfigurationMap;
        updateNativeAllocation();
        other.updateNativeAllocation();
    }

    /**
     * Set the native metadata vendor id.
     *
     * @hide
     */
    public void setVendorId(long vendorId) {
        nativeSetVendorId(mMetadataPtr, vendorId);
    }

    /**
     * @hide
     */
    public int getEntryCount() {
        return nativeGetEntryCount(mMetadataPtr);
    }

    /**
     * Does this metadata contain at least 1 entry?
     *
     * @hide
     */
    public boolean isEmpty() {
        return nativeIsEmpty(mMetadataPtr);
    }


    /**
     * Retrieves the pointer to the native shared_ptr<CameraMetadata> as a Java long.
     *
     * @hide
     */
    public long getMetadataPtr() {
        return mMetadataPtr;
    }

    /**
     * Return a list containing keys of the given key class for all defined vendor tags.
     *
     * @hide
     */
    public <K>  ArrayList<K> getAllVendorKeys(Class<K> keyClass) {
        if (keyClass == null) {
            throw new NullPointerException();
        }
        return (ArrayList<K>) nativeGetAllVendorKeys(mMetadataPtr, keyClass);
    }

    /**
     * Convert a key string into the equivalent native tag.
     *
     * @throws IllegalArgumentException if the key was not recognized
     * @throws NullPointerException if the key was null
     *
     * @hide
     */
    public static int getTag(String key) {
        return nativeGetTagFromKey(key, Long.MAX_VALUE);
    }

    /**
     * Convert a key string into the equivalent native tag.
     *
     * @throws IllegalArgumentException if the key was not recognized
     * @throws NullPointerException if the key was null
     *
     * @hide
     */
    public static int getTag(String key, long vendorId) {
        return nativeGetTagFromKey(key, vendorId);
    }

    /**
     * Get the underlying native type for a tag.
     *
     * @param tag An integer tag, see e.g. {@link #getTag}
     * @param vendorId A vendor tag provider id
     * @return An int enum for the metadata type, see e.g. {@link #TYPE_BYTE}
     *
     * @hide
     */
    public static int getNativeType(int tag, long vendorId) {
        return nativeGetTypeFromTag(tag, vendorId);
    }

    /**
     * <p>Updates the existing entry for tag with the new bytes pointed by src, erasing
     * the entry if src was null.</p>
     *
     * <p>An empty array can be passed in to update the entry to 0 elements.</p>
     *
     * @param tag An integer tag, see e.g. {@link #getTag}
     * @param src An array of bytes, or null to erase the entry
     *
     * @hide
     */
    public void writeValues(int tag, byte[] src) {
        nativeWriteValues(tag, src, mMetadataPtr);
    }

    /**
     * <p>Returns a byte[] of data corresponding to this tag. Use a wrapped bytebuffer to unserialize
     * the data properly.</p>
     *
     * <p>An empty array can be returned to denote an existing entry with 0 elements.</p>
     *
     * @param tag An integer tag, see e.g. {@link #getTag}
     *
     * @return {@code null} if there were 0 entries for this tag, a byte[] otherwise.
     * @hide
     */
    public byte[] readValues(int tag) {
        // TODO: Optimization. Native code returns a ByteBuffer instead.
        return nativeReadValues(tag, mMetadataPtr);
    }

    /**
     * Dumps the native metadata contents to logcat.
     *
     * <p>Visibility for testing/debugging only. The results will not
     * include any synthesized keys, as they are invisible to the native layer.</p>
     *
     * @hide
     */
    public void dumpToLog() {
        try {
            nativeDump(mMetadataPtr);
        } catch (IOException e) {
            Log.wtf(TAG, "Dump logging failed", e);
        }
    }

    @Override
    protected void finalize() throws Throwable {
        try {
            close();
        } finally {
            super.finalize();
        }
    }

    /**
     * Get the marshaler compatible with the {@code key} and type {@code T}.
     *
     * @throws UnsupportedOperationException
     *          if the native/managed type combination for {@code key} is not supported
     */
    private static <T> Marshaler<T> getMarshalerForKey(Key<T> key, int nativeType) {
        return MarshalRegistry.getMarshaler(key.getTypeReference(),
                nativeType);
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    private static void registerAllMarshalers() {
        if (DEBUG) {
            Log.v(TAG, "Shall register metadata marshalers");
        }

        MarshalQueryable[] queryList = new MarshalQueryable[] {
                // marshalers for standard types
                new MarshalQueryablePrimitive(),
                new MarshalQueryableEnum(),
                new MarshalQueryableArray(),

                // pseudo standard types, that expand/narrow the native type into a managed type
                new MarshalQueryableBoolean(),
                new MarshalQueryableNativeByteToInteger(),

                // marshalers for custom types
                new MarshalQueryableRect(),
                new MarshalQueryableSize(),
                new MarshalQueryableSizeF(),
                new MarshalQueryableString(),
                new MarshalQueryableReprocessFormatsMap(),
                new MarshalQueryableRange(),
                new MarshalQueryablePair(),
                new MarshalQueryableMeteringRectangle(),
                new MarshalQueryableColorSpaceTransform(),
                new MarshalQueryableStreamConfiguration(),
                new MarshalQueryableStreamConfigurationDuration(),
                new MarshalQueryableRggbChannelVector(),
                new MarshalQueryableBlackLevelPattern(),
                new MarshalQueryableHighSpeedVideoConfiguration(),
                new MarshalQueryableRecommendedStreamConfiguration(),

                // generic parcelable marshaler (MUST BE LAST since it has lowest priority)
                new MarshalQueryableParcelable(),
        };

        for (MarshalQueryable query : queryList) {
            MarshalRegistry.registerMarshalQueryable(query);
        }
        if (DEBUG) {
            Log.v(TAG, "Registered metadata marshalers");
        }
    }

    /** Check if input arguments are all {@code null}.
     *
     * @param objs Input arguments for null check
     * @return {@code true} if input arguments are all {@code null}, otherwise {@code false}
     */
    private static boolean areValuesAllNull(Object... objs) {
        for (Object o : objs) {
            if (o != null) return false;
        }
        return true;
    }

    /**
     * Return the set of physical camera ids that this logical {@link CameraDevice} is made
     * up of.
     *
     * If the camera device isn't a logical camera, return an empty set.
     *
     * @hide
     */
    public Set<String> getPhysicalCameraIds() {
        int[] availableCapabilities = get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
        if (availableCapabilities == null) {
            throw new AssertionError("android.request.availableCapabilities must be non-null "
                        + "in the characteristics");
        }

        if (!ArrayUtils.contains(availableCapabilities,
                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA)) {
            return Collections.emptySet();
        }
        byte[] physicalCamIds = get(CameraCharacteristics.LOGICAL_MULTI_CAMERA_PHYSICAL_IDS);

        String physicalCamIdString = null;
        try {
            physicalCamIdString = new String(physicalCamIds, "UTF-8");
        } catch (java.io.UnsupportedEncodingException e) {
            throw new AssertionError("android.logicalCam.physicalIds must be UTF-8 string");
        }
        String[] physicalCameraIdArray = physicalCamIdString.split("\0");

        return Collections.unmodifiableSet(
                new HashSet<String>(Arrays.asList(physicalCameraIdArray)));
    }

    static {
        registerAllMarshalers();
    }
}
