/*
 * 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.media;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.StringDef;
import android.annotation.SystemApi;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Parcel;
import android.util.Log;

/**
 * MediaDrm can be used to obtain keys for decrypting protected media streams, in
 * conjunction with {@link android.media.MediaCrypto}.  The MediaDrm APIs
 * are designed to support the ISO/IEC 23001-7: Common Encryption standard, but
 * may also be used to implement other encryption schemes.
 * <p>
 * Encrypted content is prepared using an encryption server and stored in a content
 * library. The encrypted content is streamed or downloaded from the content library to
 * client devices via content servers.  Licenses to view the content are obtained from
 * a License Server.
 * <p>
 * <p><img src="../../../images/mediadrm_overview.png"
 *      alt="MediaDrm Overview diagram"
 *      border="0" /></p>
 * <p>
 * Keys are requested from the license server using a key request. The key
 * response is delivered to the client app, which provides the response to the
 * MediaDrm API.
 * <p>
 * A Provisioning server may be required to distribute device-unique credentials to
 * the devices.
 * <p>
 * Enforcing requirements related to the number of devices that may play content
 * simultaneously can be performed either through key renewal or using the secure
 * stop methods.
 * <p>
 * The following sequence diagram shows the interactions between the objects
 * involved while playing back encrypted content:
 * <p>
 * <p><img src="../../../images/mediadrm_decryption_sequence.png"
 *         alt="MediaDrm Overview diagram"
 *         border="0" /></p>
 * <p>
 * The app first constructs {@link android.media.MediaExtractor} and
 * {@link android.media.MediaCodec} objects. It accesses the DRM-scheme-identifying UUID,
 * typically from metadata in the content, and uses this UUID to construct an instance
 * of a MediaDrm object that is able to support the DRM scheme required by the content.
 * Crypto schemes are assigned 16 byte UUIDs.  The method {@link #isCryptoSchemeSupported}
 * can be used to query if a given scheme is supported on the device.
 * <p>
 * The app calls {@link #openSession} to generate a sessionId that will uniquely identify
 * the session in subsequent interactions. The app next uses the MediaDrm object to
 * obtain a key request message and send it to the license server, then provide
 * the server's response to the MediaDrm object.
 * <p>
 * Once the app has a sessionId, it can construct a MediaCrypto object from the UUID and
 * sessionId.  The MediaCrypto object is registered with the MediaCodec in the
 * {@link MediaCodec.#configure} method to enable the codec to decrypt content.
 * <p>
 * When the app has constructed {@link android.media.MediaExtractor},
 * {@link android.media.MediaCodec} and {@link android.media.MediaCrypto} objects,
 * it proceeds to pull samples from the extractor and queue them into the decoder.  For
 * encrypted content, the samples returned from the extractor remain encrypted, they
 * are only decrypted when the samples are delivered to the decoder.
 * <p>
 * MediaDrm methods throw {@link android.media.MediaDrm.MediaDrmStateException}
 * when a method is called on a MediaDrm object that has had an unrecoverable failure 
 * in the DRM plugin or security hardware. 
 * {@link android.media.MediaDrm.MediaDrmStateException} extends 
 * {@link java.lang.IllegalStateException} with the addition of a developer-readable 
 * diagnostic information string associated with the exception.
 * <p>
 * In the event of a mediaserver process crash or restart while a MediaDrm object
 * is active, MediaDrm methods may throw {@link android.media.MediaDrmResetException}.
 * To recover, the app must release the MediaDrm object, then create and initialize
 * a new one.
 * <p>
 * As {@link android.media.MediaDrmResetException} and 
 * {@link android.media.MediaDrm.MediaDrmStateException} both extend 
 * {@link java.lang.IllegalStateException}, they should be in an earlier catch() 
 * block than {@link java.lang.IllegalStateException} if handled separately.
 * <p>
 * <a name="Callbacks"></a>
 * <h3>Callbacks</h3>
 * <p>Applications should register for informational events in order
 * to be informed of key state updates during playback or streaming.
 * Registration for these events is done via a call to
 * {@link #setOnEventListener}. In order to receive the respective
 * callback associated with this listener, applications are required to create
 * MediaDrm objects on a thread with its own Looper running (main UI
 * thread by default has a Looper running).
 */
public final class MediaDrm {

    private static final String TAG = "MediaDrm";

    private static final String PERMISSION = android.Manifest.permission.ACCESS_DRM_CERTIFICATES;

    private EventHandler mEventHandler;
    private EventHandler mOnKeyStatusChangeEventHandler;
    private EventHandler mOnExpirationUpdateEventHandler;
    private OnEventListener mOnEventListener;
    private OnKeyStatusChangeListener mOnKeyStatusChangeListener;
    private OnExpirationUpdateListener mOnExpirationUpdateListener;

    private long mNativeContext;

    /**
     * Specify no certificate type
     *
     * @hide - not part of the public API at this time
     */
    public static final int CERTIFICATE_TYPE_NONE = 0;

    /**
     * Specify X.509 certificate type
     *
     * @hide - not part of the public API at this time
     */
    public static final int CERTIFICATE_TYPE_X509 = 1;

    /** @hide */
    @IntDef({
        CERTIFICATE_TYPE_NONE,
        CERTIFICATE_TYPE_X509,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface CertificateType {}

    /**
     * Query if the given scheme identified by its UUID is supported on
     * this device.
     * @param uuid The UUID of the crypto scheme.
     */
    public static final boolean isCryptoSchemeSupported(@NonNull UUID uuid) {
        return isCryptoSchemeSupportedNative(getByteArrayFromUUID(uuid), null);
    }

    /**
     * Query if the given scheme identified by its UUID is supported on
     * this device, and whether the drm plugin is able to handle the
     * media container format specified by mimeType.
     * @param uuid The UUID of the crypto scheme.
     * @param mimeType The MIME type of the media container, e.g. "video/mp4"
     *   or "video/webm"
     */
    public static final boolean isCryptoSchemeSupported(
            @NonNull UUID uuid, @NonNull String mimeType) {
        return isCryptoSchemeSupportedNative(getByteArrayFromUUID(uuid), mimeType);
    }

    private static final byte[] getByteArrayFromUUID(@NonNull UUID uuid) {
        long msb = uuid.getMostSignificantBits();
        long lsb = uuid.getLeastSignificantBits();

        byte[] uuidBytes = new byte[16];
        for (int i = 0; i < 8; ++i) {
            uuidBytes[i] = (byte)(msb >>> (8 * (7 - i)));
            uuidBytes[8 + i] = (byte)(lsb >>> (8 * (7 - i)));
        }

        return uuidBytes;
    }

    private static final native boolean isCryptoSchemeSupportedNative(
            @NonNull byte[] uuid, @Nullable String mimeType);

    /**
     * Instantiate a MediaDrm object
     *
     * @param uuid The UUID of the crypto scheme.
     *
     * @throws UnsupportedSchemeException if the device does not support the
     * specified scheme UUID
     */
    public MediaDrm(@NonNull UUID uuid) throws UnsupportedSchemeException {
        Looper looper;
        if ((looper = Looper.myLooper()) != null) {
            mEventHandler = new EventHandler(this, looper);
        } else if ((looper = Looper.getMainLooper()) != null) {
            mEventHandler = new EventHandler(this, looper);
        } else {
            mEventHandler = null;
        }

        /* Native setup requires a weak reference to our object.
         * It's easier to create it here than in C++.
         */
        native_setup(new WeakReference<MediaDrm>(this),
                getByteArrayFromUUID(uuid));
    }

    /**
     * Thrown when an unrecoverable failure occurs during a MediaDrm operation.
     * Extends java.lang.IllegalStateException with the addition of an error
     * code that may be useful in diagnosing the failure.
     */
    public static final class MediaDrmStateException extends java.lang.IllegalStateException {
        private final int mErrorCode;
        private final String mDiagnosticInfo;

        /**
         * @hide
         */
        public MediaDrmStateException(int errorCode, @Nullable String detailMessage) {
            super(detailMessage);
            mErrorCode = errorCode;

            // TODO get this from DRM session
            final String sign = errorCode < 0 ? "neg_" : "";
            mDiagnosticInfo =
                "android.media.MediaDrm.error_" + sign + Math.abs(errorCode);

        }

        /**
         * Retrieve the associated error code
         *
         * @hide
         */
        public int getErrorCode() {
            return mErrorCode;
        }

        /**
         * Retrieve a developer-readable diagnostic information string
         * associated with the exception. Do not show this to end-users,
         * since this string will not be localized or generally comprehensible
         * to end-users.
         */
        @NonNull
        public String getDiagnosticInfo() {
            return mDiagnosticInfo;
        }
    }

    /**
     * Register a callback to be invoked when a session expiration update
     * occurs.  The app's OnExpirationUpdateListener will be notified
     * when the expiration time of the keys in the session have changed.
     * @param listener the callback that will be run, or {@code null} to unregister the
     *     previously registered callback.
     * @param handler the handler on which the listener should be invoked, or
     *     {@code null} if the listener should be invoked on the calling thread's looper.
     */
    public void setOnExpirationUpdateListener(
            @Nullable OnExpirationUpdateListener listener, @Nullable Handler handler) {
        if (listener != null) {
            Looper looper = handler != null ? handler.getLooper() : Looper.myLooper();
            if (looper != null) {
                if (mEventHandler == null || mEventHandler.getLooper() != looper) {
                    mEventHandler = new EventHandler(this, looper);
                }
            }
        }
        mOnExpirationUpdateListener = listener;
    }

    /**
     * Interface definition for a callback to be invoked when a drm session
     * expiration update occurs
     */
    public interface OnExpirationUpdateListener
    {
        /**
         * Called when a session expiration update occurs, to inform the app
         * about the change in expiration time
         *
         * @param md the MediaDrm object on which the event occurred
         * @param sessionId the DRM session ID on which the event occurred
         * @param expirationTime the new expiration time for the keys in the session.
         *     The time is in milliseconds, relative to the Unix epoch.  A time of
         *     0 indicates that the keys never expire.
         */
        void onExpirationUpdate(
                @NonNull MediaDrm md, @NonNull byte[] sessionId, long expirationTime);
    }

    /**
     * Register a callback to be invoked when the state of keys in a session
     * change, e.g. when a license update occurs or when a license expires.
     *
     * @param listener the callback that will be run when key status changes, or
     *     {@code null} to unregister the previously registered callback.
     * @param handler the handler on which the listener should be invoked, or
     *     null if the listener should be invoked on the calling thread's looper.
     */
    public void setOnKeyStatusChangeListener(
            @Nullable OnKeyStatusChangeListener listener, @Nullable Handler handler) {
        if (listener != null) {
            Looper looper = handler != null ? handler.getLooper() : Looper.myLooper();
            if (looper != null) {
                if (mEventHandler == null || mEventHandler.getLooper() != looper) {
                    mEventHandler = new EventHandler(this, looper);
                }
            }
        }
        mOnKeyStatusChangeListener = listener;
    }

    /**
     * Interface definition for a callback to be invoked when the keys in a drm
     * session change states.
     */
    public interface OnKeyStatusChangeListener
    {
        /**
         * Called when the keys in a session change status, such as when the license
         * is renewed or expires.
         *
         * @param md the MediaDrm object on which the event occurred
         * @param sessionId the DRM session ID on which the event occurred
         * @param keyInformation a list of {@link MediaDrm.KeyStatus}
         *     instances indicating the status for each key in the session
         * @param hasNewUsableKey indicates if a key has been added that is usable,
         *     which may trigger an attempt to resume playback on the media stream
         *     if it is currently blocked waiting for a key.
         */
        void onKeyStatusChange(
                @NonNull MediaDrm md, @NonNull byte[] sessionId,
                @NonNull List<KeyStatus> keyInformation,
                boolean hasNewUsableKey);
    }

    /**
     * Defines the status of a key.
     * A KeyStatus for each key in a session is provided to the
     * {@link OnKeyStatusChangeListener#onKeyStatusChange}
     * listener.
     */
    public static final class KeyStatus {
        private final byte[] mKeyId;
        private final int mStatusCode;

        /**
         * The key is currently usable to decrypt media data
         */
        public static final int STATUS_USABLE = 0;

        /**
         * The key is no longer usable to decrypt media data because its
         * expiration time has passed.
         */
        public static final int STATUS_EXPIRED = 1;

        /**
         * The key is not currently usable to decrypt media data because its
         * output requirements cannot currently be met.
         */
        public static final int STATUS_OUTPUT_NOT_ALLOWED = 2;

        /**
         * The status of the key is not yet known and is being determined.
         * The status will be updated with the actual status when it has
         * been determined.
         */
        public static final int STATUS_PENDING = 3;

        /**
         * The key is not currently usable to decrypt media data because of an
         * internal error in processing unrelated to input parameters.  This error
         * is not actionable by an app.
         */
        public static final int STATUS_INTERNAL_ERROR = 4;

        /** @hide */
        @IntDef({
            STATUS_USABLE,
            STATUS_EXPIRED,
            STATUS_OUTPUT_NOT_ALLOWED,
            STATUS_PENDING,
            STATUS_INTERNAL_ERROR,
        })
        @Retention(RetentionPolicy.SOURCE)
        public @interface KeyStatusCode {}

        KeyStatus(@NonNull byte[] keyId, @KeyStatusCode int statusCode) {
            mKeyId = keyId;
            mStatusCode = statusCode;
        }

        /**
         * Returns the status code for the key
         * @return one of {@link #STATUS_USABLE}, {@link #STATUS_EXPIRED},
         * {@link #STATUS_OUTPUT_NOT_ALLOWED}, {@link #STATUS_PENDING}
         * or {@link #STATUS_INTERNAL_ERROR}.
         */
        @KeyStatusCode
        public int getStatusCode() { return mStatusCode; }

        /**
         * Returns the id for the key
         */
        @NonNull
        public byte[] getKeyId() { return mKeyId; }
    }

    /**
     * Register a callback to be invoked when an event occurs
     *
     * @param listener the callback that will be run.  Use {@code null} to
     *        stop receiving event callbacks.
     */
    public void setOnEventListener(@Nullable OnEventListener listener)
    {
        mOnEventListener = listener;
    }

    /**
     * Interface definition for a callback to be invoked when a drm event
     * occurs
     */
    public interface OnEventListener
    {
        /**
         * Called when an event occurs that requires the app to be notified
         *
         * @param md the MediaDrm object on which the event occurred
         * @param sessionId the DRM session ID on which the event occurred,
         *        or {@code null} if there is no session ID associated with the event.
         * @param event indicates the event type
         * @param extra an secondary error code
         * @param data optional byte array of data that may be associated with the event
         */
        void onEvent(
                @NonNull MediaDrm md, @Nullable byte[] sessionId,
                @DrmEvent int event, int extra,
                @Nullable byte[] data);
    }

    /**
     * This event type indicates that the app needs to request a certificate from
     * the provisioning server.  The request message data is obtained using
     * {@link #getProvisionRequest}
     *
     * @deprecated Handle provisioning via {@link android.media.NotProvisionedException}
     * instead.
     */
    public static final int EVENT_PROVISION_REQUIRED = 1;

    /**
     * This event type indicates that the app needs to request keys from a license
     * server.  The request message data is obtained using {@link #getKeyRequest}.
     */
    public static final int EVENT_KEY_REQUIRED = 2;

    /**
     * This event type indicates that the licensed usage duration for keys in a session
     * has expired.  The keys are no longer valid.
     */
    public static final int EVENT_KEY_EXPIRED = 3;

    /**
     * This event may indicate some specific vendor-defined condition, see your
     * DRM provider documentation for details
     */
    public static final int EVENT_VENDOR_DEFINED = 4;

    /**
     * This event indicates that a session opened by the app has been reclaimed by the resource
     * manager.
     */
    public static final int EVENT_SESSION_RECLAIMED = 5;

    /** @hide */
    @IntDef({
        EVENT_PROVISION_REQUIRED,
        EVENT_KEY_REQUIRED,
        EVENT_KEY_EXPIRED,
        EVENT_VENDOR_DEFINED,
        EVENT_SESSION_RECLAIMED,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface DrmEvent {}

    private static final int DRM_EVENT = 200;
    private static final int EXPIRATION_UPDATE = 201;
    private static final int KEY_STATUS_CHANGE = 202;

    private class EventHandler extends Handler
    {
        private MediaDrm mMediaDrm;

        public EventHandler(@NonNull MediaDrm md, @NonNull Looper looper) {
            super(looper);
            mMediaDrm = md;
        }

        @Override
        public void handleMessage(@NonNull Message msg) {
            if (mMediaDrm.mNativeContext == 0) {
                Log.w(TAG, "MediaDrm went away with unhandled events");
                return;
            }
            switch(msg.what) {

            case DRM_EVENT:
                if (mOnEventListener != null) {
                    if (msg.obj != null && msg.obj instanceof Parcel) {
                        Parcel parcel = (Parcel)msg.obj;
                        byte[] sessionId = parcel.createByteArray();
                        if (sessionId.length == 0) {
                            sessionId = null;
                        }
                        byte[] data = parcel.createByteArray();
                        if (data.length == 0) {
                            data = null;
                        }

                        Log.i(TAG, "Drm event (" + msg.arg1 + "," + msg.arg2 + ")");
                        mOnEventListener.onEvent(mMediaDrm, sessionId, msg.arg1, msg.arg2, data);
                    }
                }
                return;

            case KEY_STATUS_CHANGE:
                if (mOnKeyStatusChangeListener != null) {
                    if (msg.obj != null && msg.obj instanceof Parcel) {
                        Parcel parcel = (Parcel)msg.obj;
                        byte[] sessionId = parcel.createByteArray();
                        if (sessionId.length > 0) {
                            List<KeyStatus> keyStatusList = keyStatusListFromParcel(parcel);
                            boolean hasNewUsableKey = (parcel.readInt() != 0);

                            Log.i(TAG, "Drm key status changed");
                            mOnKeyStatusChangeListener.onKeyStatusChange(mMediaDrm, sessionId,
                                    keyStatusList, hasNewUsableKey);
                        }
                    }
                }
                return;

            case EXPIRATION_UPDATE:
                if (mOnExpirationUpdateListener != null) {
                    if (msg.obj != null && msg.obj instanceof Parcel) {
                        Parcel parcel = (Parcel)msg.obj;
                        byte[] sessionId = parcel.createByteArray();
                        if (sessionId.length > 0) {
                            long expirationTime = parcel.readLong();

                            Log.i(TAG, "Drm key expiration update: " + expirationTime);
                            mOnExpirationUpdateListener.onExpirationUpdate(mMediaDrm, sessionId,
                                    expirationTime);
                        }
                    }
                }
                return;

            default:
                Log.e(TAG, "Unknown message type " + msg.what);
                return;
            }
        }
    }

    /**
     * Parse a list of KeyStatus objects from an event parcel
     */
    @NonNull
    private List<KeyStatus> keyStatusListFromParcel(@NonNull Parcel parcel) {
        int nelems = parcel.readInt();
        List<KeyStatus> keyStatusList = new ArrayList(nelems);
        while (nelems-- > 0) {
            byte[] keyId = parcel.createByteArray();
            int keyStatusCode = parcel.readInt();
            keyStatusList.add(new KeyStatus(keyId, keyStatusCode));
        }
        return keyStatusList;
    }

    /**
     * This method is called from native code when an event occurs.  This method
     * just uses the EventHandler system to post the event back to the main app thread.
     * We use a weak reference to the original MediaPlayer object so that the native
     * code is safe from the object disappearing from underneath it.  (This is
     * the cookie passed to native_setup().)
     */
    private static void postEventFromNative(@NonNull Object mediadrm_ref,
            int what, int eventType, int extra, @Nullable Object obj)
    {
        MediaDrm md = (MediaDrm)((WeakReference<MediaDrm>)mediadrm_ref).get();
        if (md == null) {
            return;
        }
        if (md.mEventHandler != null) {
            Message m = md.mEventHandler.obtainMessage(what, eventType, extra, obj);
            md.mEventHandler.sendMessage(m);
        }
    }

    /**
     * Open a new session with the MediaDrm object.  A session ID is returned.
     *
     * @throws NotProvisionedException if provisioning is needed
     * @throws ResourceBusyException if required resources are in use
     */
    @NonNull
    public native byte[] openSession() throws NotProvisionedException,
            ResourceBusyException;

    /**
     * Close a session on the MediaDrm object that was previously opened
     * with {@link #openSession}.
     */
    public native void closeSession(@NonNull byte[] sessionId);

    /**
     * This key request type species that the keys will be for online use, they will
     * not be saved to the device for subsequent use when the device is not connected
     * to a network.
     */
    public static final int KEY_TYPE_STREAMING = 1;

    /**
     * This key request type specifies that the keys will be for offline use, they
     * will be saved to the device for use when the device is not connected to a network.
     */
    public static final int KEY_TYPE_OFFLINE = 2;

    /**
     * This key request type specifies that previously saved offline keys should be released.
     */
    public static final int KEY_TYPE_RELEASE = 3;

    /** @hide */
    @IntDef({
        KEY_TYPE_STREAMING,
        KEY_TYPE_OFFLINE,
        KEY_TYPE_RELEASE,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface KeyType {}

    /**
     * Contains the opaque data an app uses to request keys from a license server
     */
    public static final class KeyRequest {
        private byte[] mData;
        private String mDefaultUrl;
        private int mRequestType;

        /**
         * Key request type is initial license request
         */
        public static final int REQUEST_TYPE_INITIAL = 0;

        /**
         * Key request type is license renewal
         */
        public static final int REQUEST_TYPE_RENEWAL = 1;

        /**
         * Key request type is license release
         */
        public static final int REQUEST_TYPE_RELEASE = 2;

        /** @hide */
        @IntDef({
            REQUEST_TYPE_INITIAL,
            REQUEST_TYPE_RENEWAL,
            REQUEST_TYPE_RELEASE,
        })
        @Retention(RetentionPolicy.SOURCE)
        public @interface RequestType {}

        KeyRequest() {}

        /**
         * Get the opaque message data
         */
        @NonNull
        public byte[] getData() {
            if (mData == null) {
                // this should never happen as mData is initialized in
                // JNI after construction of the KeyRequest object. The check
                // is needed here to guarantee @NonNull annotation.
                throw new RuntimeException("KeyRequest is not initialized");
            }
            return mData;
        }

        /**
         * Get the default URL to use when sending the key request message to a
         * server, if known.  The app may prefer to use a different license
         * server URL from other sources.
         * This method returns an empty string if the default URL is not known.
         */
        @NonNull
        public String getDefaultUrl() {
            if (mDefaultUrl == null) {
                // this should never happen as mDefaultUrl is initialized in
                // JNI after construction of the KeyRequest object. The check
                // is needed here to guarantee @NonNull annotation.
                throw new RuntimeException("KeyRequest is not initialized");
            }
            return mDefaultUrl;
        }

        /**
         * Get the type of the request
         * @return one of {@link #REQUEST_TYPE_INITIAL},
         * {@link #REQUEST_TYPE_RENEWAL} or {@link #REQUEST_TYPE_RELEASE}
         */
        @RequestType
        public int getRequestType() { return mRequestType; }
    };

    /**
     * A key request/response exchange occurs between the app and a license server
     * to obtain or release keys used to decrypt encrypted content.
     * <p>
     * getKeyRequest() is used to obtain an opaque key request byte array that is
     * delivered to the license server.  The opaque key request byte array is returned
     * in KeyRequest.data.  The recommended URL to deliver the key request to is
     * returned in KeyRequest.defaultUrl.
     * <p>
     * After the app has received the key request response from the server,
     * it should deliver to the response to the DRM engine plugin using the method
     * {@link #provideKeyResponse}.
     *
     * @param scope may be a sessionId or a keySetId, depending on the specified keyType.
     * When the keyType is KEY_TYPE_STREAMING or KEY_TYPE_OFFLINE,
     * scope should be set to the sessionId the keys will be provided to.  When the keyType
     * is KEY_TYPE_RELEASE, scope should be set to the keySetId of the keys
     * being released. Releasing keys from a device invalidates them for all sessions.
     * @param init container-specific data, its meaning is interpreted based on the
     * mime type provided in the mimeType parameter.  It could contain, for example,
     * the content ID, key ID or other data obtained from the content metadata that is
     * required in generating the key request. init may be null when keyType is
     * KEY_TYPE_RELEASE.
     * @param mimeType identifies the mime type of the content
     * @param keyType specifes the type of the request. The request may be to acquire
     * keys for streaming or offline content, or to release previously acquired
     * keys, which are identified by a keySetId.
     * @param optionalParameters are included in the key request message to
     * allow a client application to provide additional message parameters to the server.
     * This may be {@code null} if no additional parameters are to be sent.
     * @throws NotProvisionedException if reprovisioning is needed, due to a
     * problem with the certifcate
     */
    @NonNull
    public native KeyRequest getKeyRequest(
            @NonNull byte[] scope, @Nullable byte[] init,
            @Nullable String mimeType, @KeyType int keyType,
            @Nullable HashMap<String, String> optionalParameters)
            throws NotProvisionedException;


    /**
     * A key response is received from the license server by the app, then it is
     * provided to the DRM engine plugin using provideKeyResponse.  When the
     * response is for an offline key request, a keySetId is returned that can be
     * used to later restore the keys to a new session with the method
     * {@link #restoreKeys}.
     * When the response is for a streaming or release request, null is returned.
     *
     * @param scope may be a sessionId or keySetId depending on the type of the
     * response.  Scope should be set to the sessionId when the response is for either
     * streaming or offline key requests.  Scope should be set to the keySetId when
     * the response is for a release request.
     * @param response the byte array response from the server
     *
     * @throws NotProvisionedException if the response indicates that
     * reprovisioning is required
     * @throws DeniedByServerException if the response indicates that the
     * server rejected the request
     */
    @Nullable
    public native byte[] provideKeyResponse(
            @NonNull byte[] scope, @NonNull byte[] response)
            throws NotProvisionedException, DeniedByServerException;


    /**
     * Restore persisted offline keys into a new session.  keySetId identifies the
     * keys to load, obtained from a prior call to {@link #provideKeyResponse}.
     *
     * @param sessionId the session ID for the DRM session
     * @param keySetId identifies the saved key set to restore
     */
    public native void restoreKeys(@NonNull byte[] sessionId, @NonNull byte[] keySetId);

    /**
     * Remove the current keys from a session.
     *
     * @param sessionId the session ID for the DRM session
     */
    public native void removeKeys(@NonNull byte[] sessionId);

    /**
     * Request an informative description of the key status for the session.  The status is
     * in the form of {name, value} pairs.  Since DRM license policies vary by vendor,
     * the specific status field names are determined by each DRM vendor.  Refer to your
     * DRM provider documentation for definitions of the field names for a particular
     * DRM engine plugin.
     *
     * @param sessionId the session ID for the DRM session
     */
    @NonNull
    public native HashMap<String, String> queryKeyStatus(@NonNull byte[] sessionId);

    /**
     * Contains the opaque data an app uses to request a certificate from a provisioning
     * server
     */
    public static final class ProvisionRequest {
        ProvisionRequest() {}

        /**
         * Get the opaque message data
         */
        @NonNull
        public byte[] getData() {
            if (mData == null) {
                // this should never happen as mData is initialized in
                // JNI after construction of the KeyRequest object. The check
                // is needed here to guarantee @NonNull annotation.
                throw new RuntimeException("ProvisionRequest is not initialized");
            }
            return mData;
        }

        /**
         * Get the default URL to use when sending the provision request
         * message to a server, if known. The app may prefer to use a different
         * provisioning server URL obtained from other sources.
         * This method returns an empty string if the default URL is not known.
         */
        @NonNull
        public String getDefaultUrl() {
            if (mDefaultUrl == null) {
                // this should never happen as mDefaultUrl is initialized in
                // JNI after construction of the ProvisionRequest object. The check
                // is needed here to guarantee @NonNull annotation.
                throw new RuntimeException("ProvisionRequest is not initialized");
            }
            return mDefaultUrl;
        }

        private byte[] mData;
        private String mDefaultUrl;
    }

    /**
     * A provision request/response exchange occurs between the app and a provisioning
     * server to retrieve a device certificate.  If provisionining is required, the
     * EVENT_PROVISION_REQUIRED event will be sent to the event handler.
     * getProvisionRequest is used to obtain the opaque provision request byte array that
     * should be delivered to the provisioning server. The provision request byte array
     * is returned in ProvisionRequest.data. The recommended URL to deliver the provision
     * request to is returned in ProvisionRequest.defaultUrl.
     */
    @NonNull
    public ProvisionRequest getProvisionRequest() {
        return getProvisionRequestNative(CERTIFICATE_TYPE_NONE, "");
    }

    @NonNull
    private native ProvisionRequest getProvisionRequestNative(int certType,
           @NonNull String certAuthority);

    /**
     * After a provision response is received by the app, it is provided to the DRM
     * engine plugin using this method.
     *
     * @param response the opaque provisioning response byte array to provide to the
     * DRM engine plugin.
     *
     * @throws DeniedByServerException if the response indicates that the
     * server rejected the request
     */
    public void provideProvisionResponse(@NonNull byte[] response)
            throws DeniedByServerException {
        provideProvisionResponseNative(response);
    }

    @NonNull
    /* could there be a valid response with 0-sized certificate or key? */
    private native Certificate provideProvisionResponseNative(@NonNull byte[] response)
            throws DeniedByServerException;

    /**
     * Remove provisioning from a device.  Only system apps may unprovision a
     * device.  Note that removing provisioning will invalidate any keys saved
     * for offline use (KEY_TYPE_OFFLINE), which may render downloaded content
     * unplayable until new licenses are acquired.  Since provisioning is global
     * to the device, license invalidation will apply to all content downloaded
     * by any app, so appropriate warnings should be given to the user.
     * @hide
     */
    @SystemApi
    public native void unprovisionDevice();

    /**
     * A means of enforcing limits on the number of concurrent streams per subscriber
     * across devices is provided via SecureStop. This is achieved by securely
     * monitoring the lifetime of sessions.
     * <p>
     * Information from the server related to the current playback session is written
     * to persistent storage on the device when each MediaCrypto object is created.
     * <p>
     * In the normal case, playback will be completed, the session destroyed and the
     * Secure Stops will be queried. The app queries secure stops and forwards the
     * secure stop message to the server which verifies the signature and notifies the
     * server side database that the session destruction has been confirmed. The persisted
     * record on the client is only removed after positive confirmation that the server
     * received the message using releaseSecureStops().
     */
    @NonNull
    public native List<byte[]> getSecureStops();

    /**
     * Access secure stop by secure stop ID.
     *
     * @param ssid - The secure stop ID provided by the license server.
     */
    @NonNull
    public native byte[] getSecureStop(@NonNull byte[] ssid);

    /**
     * Process the SecureStop server response message ssRelease.  After authenticating
     * the message, remove the SecureStops identified in the response.
     *
     * @param ssRelease the server response indicating which secure stops to release
     */
    public native void releaseSecureStops(@NonNull byte[] ssRelease);

    /**
     * Remove all secure stops without requiring interaction with the server.
     */
     public native void releaseAllSecureStops();

    /**
     * String property name: identifies the maker of the DRM engine plugin
     */
    public static final String PROPERTY_VENDOR = "vendor";

    /**
     * String property name: identifies the version of the DRM engine plugin
     */
    public static final String PROPERTY_VERSION = "version";

    /**
     * String property name: describes the DRM engine plugin
     */
    public static final String PROPERTY_DESCRIPTION = "description";

    /**
     * String property name: a comma-separated list of cipher and mac algorithms
     * supported by CryptoSession.  The list may be empty if the DRM engine
     * plugin does not support CryptoSession operations.
     */
    public static final String PROPERTY_ALGORITHMS = "algorithms";

    /** @hide */
    @StringDef({
        PROPERTY_VENDOR,
        PROPERTY_VERSION,
        PROPERTY_DESCRIPTION,
        PROPERTY_ALGORITHMS,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface StringProperty {}

    /**
     * Read a DRM engine plugin String property value, given the property name string.
     * <p>
     * Standard fields names are:
     * {@link #PROPERTY_VENDOR}, {@link #PROPERTY_VERSION},
     * {@link #PROPERTY_DESCRIPTION}, {@link #PROPERTY_ALGORITHMS}
     */
    /* FIXME this throws IllegalStateException for invalid property names */
    @NonNull
    public native String getPropertyString(@NonNull @StringProperty String propertyName);

    /**
     * Byte array property name: the device unique identifier is established during
     * device provisioning and provides a means of uniquely identifying each device.
     */
    /* FIXME this throws IllegalStateException for invalid property names */
    public static final String PROPERTY_DEVICE_UNIQUE_ID = "deviceUniqueId";

    /** @hide */
    @StringDef({
        PROPERTY_DEVICE_UNIQUE_ID,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface ArrayProperty {}

    /**
     * Read a DRM engine plugin byte array property value, given the property name string.
     * <p>
     * Standard fields names are {@link #PROPERTY_DEVICE_UNIQUE_ID}
     */
    @NonNull
    public native byte[] getPropertyByteArray(@ArrayProperty String propertyName);

    /**
     * Set a DRM engine plugin String property value.
     */
    public native void setPropertyString(
            @StringProperty String propertyName, @NonNull String value);

    /**
     * Set a DRM engine plugin byte array property value.
     */
    public native void setPropertyByteArray(
            @ArrayProperty String propertyName, @NonNull byte[] value);

    private static final native void setCipherAlgorithmNative(
            @NonNull MediaDrm drm, @NonNull byte[] sessionId, @NonNull String algorithm);

    private static final native void setMacAlgorithmNative(
            @NonNull MediaDrm drm, @NonNull byte[] sessionId, @NonNull String algorithm);

    @NonNull
    private static final native byte[] encryptNative(
            @NonNull MediaDrm drm, @NonNull byte[] sessionId,
            @NonNull byte[] keyId, @NonNull byte[] input, @NonNull byte[] iv);

    @NonNull
    private static final native byte[] decryptNative(
            @NonNull MediaDrm drm, @NonNull byte[] sessionId,
            @NonNull byte[] keyId, @NonNull byte[] input, @NonNull byte[] iv);

    @NonNull
    private static final native byte[] signNative(
            @NonNull MediaDrm drm, @NonNull byte[] sessionId,
            @NonNull byte[] keyId, @NonNull byte[] message);

    private static final native boolean verifyNative(
            @NonNull MediaDrm drm, @NonNull byte[] sessionId,
            @NonNull byte[] keyId, @NonNull byte[] message, @NonNull byte[] signature);

    /**
     * In addition to supporting decryption of DASH Common Encrypted Media, the
     * MediaDrm APIs provide the ability to securely deliver session keys from
     * an operator's session key server to a client device, based on the factory-installed
     * root of trust, and then perform encrypt, decrypt, sign and verify operations
     * with the session key on arbitrary user data.
     * <p>
     * The CryptoSession class implements generic encrypt/decrypt/sign/verify methods
     * based on the established session keys.  These keys are exchanged using the
     * getKeyRequest/provideKeyResponse methods.
     * <p>
     * Applications of this capability could include securing various types of
     * purchased or private content, such as applications, books and other media,
     * photos or media delivery protocols.
     * <p>
     * Operators can create session key servers that are functionally similar to a
     * license key server, except that instead of receiving license key requests and
     * providing encrypted content keys which are used specifically to decrypt A/V media
     * content, the session key server receives session key requests and provides
     * encrypted session keys which can be used for general purpose crypto operations.
     * <p>
     * A CryptoSession is obtained using {@link #getCryptoSession}
     */
    public final class CryptoSession {
        private MediaDrm mDrm;
        private byte[] mSessionId;

        CryptoSession(@NonNull MediaDrm drm, @NonNull byte[] sessionId,
                @NonNull String cipherAlgorithm, @NonNull String macAlgorithm)
        {
            mSessionId = sessionId;
            mDrm = drm;
            setCipherAlgorithmNative(drm, sessionId, cipherAlgorithm);
            setMacAlgorithmNative(drm, sessionId, macAlgorithm);
        }

        /**
         * Encrypt data using the CryptoSession's cipher algorithm
         *
         * @param keyid specifies which key to use
         * @param input the data to encrypt
         * @param iv the initialization vector to use for the cipher
         */
        @NonNull
        public byte[] encrypt(
                @NonNull byte[] keyid, @NonNull byte[] input, @NonNull byte[] iv) {
            return encryptNative(mDrm, mSessionId, keyid, input, iv);
        }

        /**
         * Decrypt data using the CryptoSessions's cipher algorithm
         *
         * @param keyid specifies which key to use
         * @param input the data to encrypt
         * @param iv the initialization vector to use for the cipher
         */
        @NonNull
        public byte[] decrypt(
                @NonNull byte[] keyid, @NonNull byte[] input, @NonNull byte[] iv) {
            return decryptNative(mDrm, mSessionId, keyid, input, iv);
        }

        /**
         * Sign data using the CryptoSessions's mac algorithm.
         *
         * @param keyid specifies which key to use
         * @param message the data for which a signature is to be computed
         */
        @NonNull
        public byte[] sign(@NonNull byte[] keyid, @NonNull byte[] message) {
            return signNative(mDrm, mSessionId, keyid, message);
        }

        /**
         * Verify a signature using the CryptoSessions's mac algorithm. Return true
         * if the signatures match, false if they do no.
         *
         * @param keyid specifies which key to use
         * @param message the data to verify
         * @param signature the reference signature which will be compared with the
         *        computed signature
         */
        public boolean verify(
                @NonNull byte[] keyid, @NonNull byte[] message, @NonNull byte[] signature) {
            return verifyNative(mDrm, mSessionId, keyid, message, signature);
        }
    };

    /**
     * Obtain a CryptoSession object which can be used to encrypt, decrypt,
     * sign and verify messages or data using the session keys established
     * for the session using methods {@link #getKeyRequest} and
     * {@link #provideKeyResponse} using a session key server.
     *
     * @param sessionId the session ID for the session containing keys
     * to be used for encrypt, decrypt, sign and/or verify
     * @param cipherAlgorithm the algorithm to use for encryption and
     * decryption ciphers. The algorithm string conforms to JCA Standard
     * Names for Cipher Transforms and is case insensitive.  For example
     * "AES/CBC/NoPadding".
     * @param macAlgorithm the algorithm to use for sign and verify
     * The algorithm string conforms to JCA Standard Names for Mac
     * Algorithms and is case insensitive.  For example "HmacSHA256".
     * <p>
     * The list of supported algorithms for a DRM engine plugin can be obtained
     * using the method {@link #getPropertyString} with the property name
     * "algorithms".
     */
    public CryptoSession getCryptoSession(
            @NonNull byte[] sessionId,
            @NonNull String cipherAlgorithm, @NonNull String macAlgorithm)
    {
        return new CryptoSession(this, sessionId, cipherAlgorithm, macAlgorithm);
    }

    /**
     * Contains the opaque data an app uses to request a certificate from a provisioning
     * server
     *
     * @hide - not part of the public API at this time
     */
    public static final class CertificateRequest {
        private byte[] mData;
        private String mDefaultUrl;

        CertificateRequest(@NonNull byte[] data, @NonNull String defaultUrl) {
            mData = data;
            mDefaultUrl = defaultUrl;
        }

        /**
         * Get the opaque message data
         */
        @NonNull
        public byte[] getData() { return mData; }

        /**
         * Get the default URL to use when sending the certificate request
         * message to a server, if known. The app may prefer to use a different
         * certificate server URL obtained from other sources.
         */
        @NonNull
        public String getDefaultUrl() { return mDefaultUrl; }
    }

    /**
     * Generate a certificate request, specifying the certificate type
     * and authority. The response received should be passed to
     * provideCertificateResponse.
     *
     * @param certType Specifies the certificate type.
     *
     * @param certAuthority is passed to the certificate server to specify
     * the chain of authority.
     *
     * @hide - not part of the public API at this time
     */
    @NonNull
    public CertificateRequest getCertificateRequest(
            @CertificateType int certType, @NonNull String certAuthority)
    {
        ProvisionRequest provisionRequest = getProvisionRequestNative(certType, certAuthority);
        return new CertificateRequest(provisionRequest.getData(),
                provisionRequest.getDefaultUrl());
    }

    /**
     * Contains the wrapped private key and public certificate data associated
     * with a certificate.
     *
     * @hide - not part of the public API at this time
     */
    public static final class Certificate {
        Certificate() {}

        /**
         * Get the wrapped private key data
         */
        @NonNull
        public byte[] getWrappedPrivateKey() {
            if (mWrappedKey == null) {
                // this should never happen as mWrappedKey is initialized in
                // JNI after construction of the KeyRequest object. The check
                // is needed here to guarantee @NonNull annotation.
                throw new RuntimeException("Cerfificate is not initialized");
            }
            return mWrappedKey;
        }

        /**
         * Get the PEM-encoded certificate chain
         */
        @NonNull
        public byte[] getContent() {
            if (mCertificateData == null) {
                // this should never happen as mCertificateData is initialized in
                // JNI after construction of the KeyRequest object. The check
                // is needed here to guarantee @NonNull annotation.
                throw new RuntimeException("Cerfificate is not initialized");
            }
            return mCertificateData;
        }

        private byte[] mWrappedKey;
        private byte[] mCertificateData;
    }


    /**
     * Process a response from the certificate server.  The response
     * is obtained from an HTTP Post to the url provided by getCertificateRequest.
     * <p>
     * The public X509 certificate chain and wrapped private key are returned
     * in the returned Certificate objec.  The certificate chain is in PEM format.
     * The wrapped private key should be stored in application private
     * storage, and used when invoking the signRSA method.
     *
     * @param response the opaque certificate response byte array to provide to the
     * DRM engine plugin.
     *
     * @throws DeniedByServerException if the response indicates that the
     * server rejected the request
     *
     * @hide - not part of the public API at this time
     */
    @NonNull
    public Certificate provideCertificateResponse(@NonNull byte[] response)
            throws DeniedByServerException {
        return provideProvisionResponseNative(response);
    }

    @NonNull
    private static final native byte[] signRSANative(
            @NonNull MediaDrm drm, @NonNull byte[] sessionId,
            @NonNull String algorithm, @NonNull byte[] wrappedKey, @NonNull byte[] message);

    /**
     * Sign data using an RSA key
     *
     * @param sessionId a sessionId obtained from openSession on the MediaDrm object
     * @param algorithm the signing algorithm to use, e.g. "PKCS1-BlockType1"
     * @param wrappedKey - the wrapped (encrypted) RSA private key obtained
     * from provideCertificateResponse
     * @param message the data for which a signature is to be computed
     *
     * @hide - not part of the public API at this time
     */
    @NonNull
    public byte[] signRSA(
            @NonNull byte[] sessionId, @NonNull String algorithm,
            @NonNull byte[] wrappedKey, @NonNull byte[] message) {
        return signRSANative(this, sessionId, algorithm, wrappedKey, message);
    }

    @Override
    protected void finalize() {
        native_finalize();
    }

    public native final void release();
    private static native final void native_init();

    private native final void native_setup(Object mediadrm_this, byte[] uuid);

    private native final void native_finalize();

    static {
        System.loadLibrary("media_jni");
        native_init();
    }
}
