/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.drm;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.media.DeniedByServerException;
import android.media.MediaCrypto;
import android.media.MediaDrm;
import android.media.MediaDrm.KeyRequest;
import android.media.MediaDrm.OnEventListener;
import android.media.MediaDrm.ProvisionRequest;
import android.media.NotProvisionedException;
import android.media.UnsupportedSchemeException;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import java.util.Map;
import java.util.UUID;
/**
* A base class for {@link DrmSessionManager} implementations that support streaming playbacks
* using {@link MediaDrm}.
*/
@TargetApi(18)
public class StreamingDrmSessionManager implements DrmSessionManager {
/**
* Interface definition for a callback to be notified of {@link StreamingDrmSessionManager}
* events.
*/
public interface EventListener {
/**
* Invoked when a drm error occurs.
*
* @param e The corresponding exception.
*/
void onDrmSessionManagerError(Exception e);
}
private static final int MSG_PROVISION = 0;
private static final int MSG_KEYS = 1;
private final Handler eventHandler;
private final EventListener eventListener;
private final MediaDrm mediaDrm;
/* package */ final MediaDrmHandler mediaDrmHandler;
/* package */ final MediaDrmCallback callback;
/* package */ final PostResponseHandler postResponseHandler;
/* package */ final UUID uuid;
private HandlerThread requestHandlerThread;
private Handler postRequestHandler;
private int openCount;
private int state;
private MediaCrypto mediaCrypto;
private Exception lastException;
private String mimeType;
private byte[] schemePsshData;
private byte[] sessionId;
/**
* @param uuid The UUID of the drm scheme.
* @param playbackLooper The looper associated with the media playback thread. Should usually be
* obtained using {@link com.google.android.exoplayer.ExoPlayer#getPlaybackLooper()}.
* @param callback Performs key and provisioning requests.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @throws UnsupportedSchemeException If the specified DRM scheme is not supported.
*/
public StreamingDrmSessionManager(UUID uuid, Looper playbackLooper, MediaDrmCallback callback,
Handler eventHandler, EventListener eventListener) throws UnsupportedSchemeException {
this.uuid = uuid;
this.callback = callback;
this.eventHandler = eventHandler;
this.eventListener = eventListener;
mediaDrm = new MediaDrm(uuid);
mediaDrm.setOnEventListener(new MediaDrmEventListener());
mediaDrmHandler = new MediaDrmHandler(playbackLooper);
postResponseHandler = new PostResponseHandler(playbackLooper);
state = STATE_CLOSED;
}
@Override
public int getState() {
return state;
}
@Override
public MediaCrypto getMediaCrypto() {
if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
throw new IllegalStateException();
}
return mediaCrypto;
}
@Override
public boolean requiresSecureDecoderComponent(String mimeType) {
if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
throw new IllegalStateException();
}
return mediaCrypto.requiresSecureDecoderComponent(mimeType);
}
@Override
public Exception getError() {
return state == STATE_ERROR ? lastException : null;
}
/**
* Provides access to {@link MediaDrm#getPropertyString(String)}.
* <p>
* This method may be called when the manager is in any state.
*
* @param key The key to request.
* @return The retrieved property.
*/
public final String getPropertyString(String key) {
return mediaDrm.getPropertyString(key);
}
/**
* Provides access to {@link MediaDrm#getPropertyByteArray(String)}.
* <p>
* This method may be called when the manager is in any state.
*
* @param key The key to request.
* @return The retrieved property.
*/
public final byte[] getPropertyByteArray(String key) {
return mediaDrm.getPropertyByteArray(key);
}
@Override
public void open(Map<UUID, byte[]> psshData, String mimeType) {
if (++openCount != 1) {
return;
}
if (postRequestHandler == null) {
requestHandlerThread = new HandlerThread("DrmRequestHandler");
requestHandlerThread.start();
postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper());
}
if (this.schemePsshData == null) {
this.mimeType = mimeType;
schemePsshData = psshData.get(uuid);
if (schemePsshData == null) {
onError(new IllegalStateException("Media does not support uuid: " + uuid));
return;
}
}
state = STATE_OPENING;
openInternal(true);
}
@Override
public void close() {
if (--openCount != 0) {
return;
}
state = STATE_CLOSED;
mediaDrmHandler.removeCallbacksAndMessages(null);
postResponseHandler.removeCallbacksAndMessages(null);
postRequestHandler.removeCallbacksAndMessages(null);
postRequestHandler = null;
requestHandlerThread.quit();
requestHandlerThread = null;
schemePsshData = null;
mediaCrypto = null;
lastException = null;
if (sessionId != null) {
mediaDrm.closeSession(sessionId);
sessionId = null;
}
}
private void openInternal(boolean allowProvisioning) {
try {
sessionId = mediaDrm.openSession();
mediaCrypto = new MediaCrypto(uuid, sessionId);
state = STATE_OPENED;
postKeyRequest();
} catch (NotProvisionedException e) {
if (allowProvisioning) {
postProvisionRequest();
} else {
onError(e);
}
} catch (Exception e) {
onError(e);
}
}
private void postProvisionRequest() {
ProvisionRequest request = mediaDrm.getProvisionRequest();
postRequestHandler.obtainMessage(MSG_PROVISION, request).sendToTarget();
}
private void onProvisionResponse(Object response) {
if (state != STATE_OPENING && state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
// This event is stale.
return;
}
if (response instanceof Exception) {
onError((Exception) response);
return;
}
try {
mediaDrm.provideProvisionResponse((byte[]) response);
if (state == STATE_OPENING) {
openInternal(false);
} else {
postKeyRequest();
}
} catch (DeniedByServerException e) {
onError(e);
}
}
private void postKeyRequest() {
KeyRequest keyRequest;
try {
keyRequest = mediaDrm.getKeyRequest(sessionId, schemePsshData, mimeType,
MediaDrm.KEY_TYPE_STREAMING, null);
postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget();
} catch (NotProvisionedException e) {
onKeysError(e);
}
}
private void onKeyResponse(Object response) {
if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
// This event is stale.
return;
}
if (response instanceof Exception) {
onKeysError((Exception) response);
return;
}
try {
mediaDrm.provideKeyResponse(sessionId, (byte[]) response);
state = STATE_OPENED_WITH_KEYS;
} catch (Exception e) {
onKeysError(e);
}
}
private void onKeysError(Exception e) {
if (e instanceof NotProvisionedException) {
postProvisionRequest();
} else {
onError(e);
}
}
private void onError(Exception e) {
lastException = e;
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onDrmSessionManagerError(lastException);
}
});
}
if (state != STATE_OPENED_WITH_KEYS) {
state = STATE_ERROR;
}
}
@SuppressLint("HandlerLeak")
private class MediaDrmHandler extends Handler {
public MediaDrmHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
if (openCount == 0 || (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS)) {
return;
}
switch (msg.what) {
case MediaDrm.EVENT_KEY_REQUIRED:
postKeyRequest();
return;
case MediaDrm.EVENT_KEY_EXPIRED:
state = STATE_OPENED;
postKeyRequest();
return;
case MediaDrm.EVENT_PROVISION_REQUIRED:
state = STATE_OPENED;
postProvisionRequest();
return;
}
}
}
private class MediaDrmEventListener implements OnEventListener {
@Override
public void onEvent(MediaDrm md, byte[] sessionId, int event, int extra, byte[] data) {
mediaDrmHandler.sendEmptyMessage(event);
}
}
@SuppressLint("HandlerLeak")
private class PostResponseHandler extends Handler {
public PostResponseHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_PROVISION:
onProvisionResponse(msg.obj);
return;
case MSG_KEYS:
onKeyResponse(msg.obj);
return;
}
}
}
@SuppressLint("HandlerLeak")
private class PostRequestHandler extends Handler {
public PostRequestHandler(Looper backgroundLooper) {
super(backgroundLooper);
}
@Override
public void handleMessage(Message msg) {
Object response;
try {
switch (msg.what) {
case MSG_PROVISION:
response = callback.executeProvisionRequest(uuid, (ProvisionRequest) msg.obj);
break;
case MSG_KEYS:
response = callback.executeKeyRequest(uuid, (KeyRequest) msg.obj);
break;
default:
throw new RuntimeException();
}
} catch (Exception e) {
response = e;
}
postResponseHandler.obtainMessage(msg.what, response).sendToTarget();
}
}
}