package com.kaltura.playersdk.drm;
import android.content.ContentValues;
import android.content.Context;
import android.drm.DrmErrorEvent;
import android.drm.DrmEvent;
import android.drm.DrmInfo;
import android.drm.DrmInfoEvent;
import android.drm.DrmInfoRequest;
import android.drm.DrmInfoStatus;
import android.drm.DrmManagerClient;
import android.drm.DrmStore;
import android.os.Build;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Iterator;
import static com.kaltura.playersdk.utils.LogUtils.LOGD;
import static com.kaltura.playersdk.utils.LogUtils.LOGE;
import static com.kaltura.playersdk.utils.LogUtils.LOGI;
// Based on Widevine for Android demo app
public class WidevineDrmClient {
public static final String TAG = "WidevineDrm";
private final static String DEVICE_IS_PROVISIONED = "0";
private final static String DEVICE_IS_NOT_PROVISIONED = "1";
private final static String DEVICE_IS_PROVISIONED_SD_ONLY = "2";
public static final String WV_DRM_SERVER_KEY = "WVDRMServerKey";
public static final String WV_ASSET_URI_KEY = "WVAssetURIKey";
public static final String WV_DEVICE_ID_KEY = "WVDeviceIDKey";
public static final String WV_PORTAL_KEY = "WVPortalKey";
public static final String WV_DRM_INFO_REQUEST_STATUS_KEY = "WVDrmInfoRequestStatusKey";
public static final String WV_DRM_INFO_REQUEST_VERSION_KEY = "WVDrmInfoRequestVersionKey";
private String mWVDrmInfoRequestStatusKey = DEVICE_IS_PROVISIONED;
public static String WIDEVINE_MIME_TYPE = "video/wvm";
public static String PORTAL_NAME = "kaltura";
private String mDeviceId;
private DrmManagerClient mDrmManager;
public void setEventListener(EventListener eventListener) {
mEventListener = eventListener;
}
private EventListener mEventListener;
public static class RightsInfo {
public enum Status {
VALID,
INVALID,
EXPIRED,
NOT_ACQUIRED,
}
public Status status;
public int startTime;
public int expiryTime;
public int availableTime;
public ContentValues rawConstraints;
private RightsInfo(int status, ContentValues values) {
this.rawConstraints = values;
switch (status) {
case DrmStore.RightsStatus.RIGHTS_VALID:
this.status = Status.VALID;
if (values != null) {
try {
this.startTime = values.getAsInteger(DrmStore.ConstraintsColumns.LICENSE_START_TIME);
this.expiryTime = values.getAsInteger(DrmStore.ConstraintsColumns.LICENSE_EXPIRY_TIME);
this.availableTime = values.getAsInteger(DrmStore.ConstraintsColumns.LICENSE_AVAILABLE_TIME);
} catch (NullPointerException e) {
LOGE(TAG, "Invalid constraints: " + values);
}
}
break;
case DrmStore.RightsStatus.RIGHTS_INVALID:
this.status = Status.INVALID;
break;
case DrmStore.RightsStatus.RIGHTS_EXPIRED:
this.status = Status.EXPIRED;
break;
case DrmStore.RightsStatus.RIGHTS_NOT_ACQUIRED:
this.status = Status.NOT_ACQUIRED;
break;
}
}
}
public interface EventListener {
void onError(DrmErrorEvent event);
void onEvent(DrmEvent event);
}
public static boolean isSupported(Context context) {
DrmManagerClient drmManagerClient = new DrmManagerClient(context);
boolean canHandle = false;
// adding try catch due some android devices have different canHandle method implementation regarding the arguments validation inside it
try {
canHandle = drmManagerClient.canHandle("", WIDEVINE_MIME_TYPE);
} catch (IllegalArgumentException ex) {
LOGE(TAG, "drmManagerClient.canHandle failed");
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
LOGI(TAG, "Assuming WV Classic is supported although canHandle has failed");
canHandle = true;
}
} finally {
drmManagerClient.release();
}
return canHandle;
}
public WidevineDrmClient(Context context) {
mDrmManager = new DrmManagerClient(context) {
@Override
protected void finalize() throws Throwable {
// Release on finalize. Doesn't matter when, just prevent Android's CloseGuard errors.
try {
release();
} finally {
super.finalize();
}
}
};
// Detect if this device can play widevine classic
if (! mDrmManager.canHandle("", WIDEVINE_MIME_TYPE)) {
throw new UnsupportedOperationException("Widevine Classic is not supported");
}
mDeviceId = new DeviceUuidFactory(context).getDeviceUuid().toString();
mDrmManager.setOnInfoListener(new DrmManagerClient.OnInfoListener() {
@Override
public void onInfo(DrmManagerClient client, DrmInfoEvent event) {
logEvent(event);
if (mEventListener != null) {
mEventListener.onEvent(event);
}
}
});
mDrmManager.setOnEventListener(new DrmManagerClient.OnEventListener() {
@Override
public void onEvent(DrmManagerClient client, DrmEvent event) {
logEvent(event);
if (mEventListener != null) {
mEventListener.onEvent(event);
}
}
});
mDrmManager.setOnErrorListener(new DrmManagerClient.OnErrorListener() {
@Override
public void onError(DrmManagerClient client, DrmErrorEvent event) {
logEvent(event);
if (mEventListener != null) {
mEventListener.onError(event);
}
}
});
registerPortal();
}
private void logEvent(DrmEvent event) {
// if (! BuildConfig.DEBUG) {
// // Basic log
// LOGD(TAG, "DrmEvent(" + event + ")");
// return;
// }
String eventTypeString = null;
String eventClass;
// pbpaste | perl -ne 'if (/.+public static final int (\w+).+/) {print qq(case DrmInfoEvent.$1: eventTypeString="$1"; break;\n);}'
int eventType = event.getType();
if (event instanceof DrmInfoEvent) {
eventClass = "info";
switch (eventType) {
case DrmInfoEvent.TYPE_ALREADY_REGISTERED_BY_ANOTHER_ACCOUNT: eventTypeString="TYPE_ALREADY_REGISTERED_BY_ANOTHER_ACCOUNT"; break;
case DrmInfoEvent.TYPE_REMOVE_RIGHTS: eventTypeString="TYPE_REMOVE_RIGHTS"; break;
case DrmInfoEvent.TYPE_RIGHTS_INSTALLED: eventTypeString="TYPE_RIGHTS_INSTALLED"; break;
case DrmInfoEvent.TYPE_WAIT_FOR_RIGHTS: eventTypeString="TYPE_WAIT_FOR_RIGHTS"; break;
case DrmInfoEvent.TYPE_ACCOUNT_ALREADY_REGISTERED: eventTypeString="TYPE_ACCOUNT_ALREADY_REGISTERED"; break;
case DrmInfoEvent.TYPE_RIGHTS_REMOVED: eventTypeString="TYPE_RIGHTS_REMOVED"; break;
}
} else if (event instanceof DrmErrorEvent) {
eventClass = "error";
switch (eventType) {
case DrmErrorEvent.TYPE_RIGHTS_NOT_INSTALLED: eventTypeString="TYPE_RIGHTS_NOT_INSTALLED"; break;
case DrmErrorEvent.TYPE_RIGHTS_RENEWAL_NOT_ALLOWED: eventTypeString="TYPE_RIGHTS_RENEWAL_NOT_ALLOWED"; break;
case DrmErrorEvent.TYPE_NOT_SUPPORTED: eventTypeString="TYPE_NOT_SUPPORTED"; break;
case DrmErrorEvent.TYPE_OUT_OF_MEMORY: eventTypeString="TYPE_OUT_OF_MEMORY"; break;
case DrmErrorEvent.TYPE_NO_INTERNET_CONNECTION: eventTypeString="TYPE_NO_INTERNET_CONNECTION"; break;
case DrmErrorEvent.TYPE_PROCESS_DRM_INFO_FAILED: eventTypeString="TYPE_PROCESS_DRM_INFO_FAILED"; break;
case DrmErrorEvent.TYPE_REMOVE_ALL_RIGHTS_FAILED: eventTypeString="TYPE_REMOVE_ALL_RIGHTS_FAILED"; break;
case DrmErrorEvent.TYPE_ACQUIRE_DRM_INFO_FAILED: eventTypeString="TYPE_ACQUIRE_DRM_INFO_FAILED"; break;
}
} else {
eventClass = "generic";
switch (eventType) {
case DrmEvent.TYPE_ALL_RIGHTS_REMOVED: eventTypeString="TYPE_ALL_RIGHTS_REMOVED"; break;
case DrmEvent.TYPE_DRM_INFO_PROCESSED: eventTypeString="TYPE_DRM_INFO_PROCESSED"; break;
}
}
StringBuilder logString = new StringBuilder(50);
logString.append("DrmEvent class=").append(eventClass).append(" type=")
.append(eventTypeString).append(" message={").append(event.getMessage()).append("}");
DrmInfoStatus drmStatus = (DrmInfoStatus) event.getAttribute(DrmEvent.DRM_INFO_STATUS_OBJECT);
if (drmStatus != null) {
logString.append(" status=").append(drmStatus.statusCode==DrmInfoStatus.STATUS_OK ? "OK" : "ERROR");
}
DrmInfo drmInfo = (DrmInfo) event.getAttribute(DrmEvent.DRM_INFO_OBJECT);
logString.append("info=").append(extractDrmInfo(drmInfo));
LOGD(TAG, logString.toString());
}
private String extractDrmInfo(DrmInfo drmInfo) {
StringBuilder sb = new StringBuilder();
if (drmInfo != null) {
sb.append("{");
for (Iterator<String> it = drmInfo.keyIterator(); it.hasNext();) {
String key = it.next();
Object value = drmInfo.get(key);
sb.append("{").append(key).append("=").append(value).append("}");
if (it.hasNext()) {
sb.append(" ");
}
}
sb.append("}");
}
return sb.toString();
}
private DrmInfoRequest createDrmInfoRequest(String assetUri, String licenseServerUri) {
DrmInfoRequest rightsAcquisitionInfo;
rightsAcquisitionInfo = new DrmInfoRequest(DrmInfoRequest.TYPE_RIGHTS_ACQUISITION_INFO,
WIDEVINE_MIME_TYPE);
if (licenseServerUri != null) {
rightsAcquisitionInfo.put(WV_DRM_SERVER_KEY, licenseServerUri);
}
rightsAcquisitionInfo.put(WV_ASSET_URI_KEY, assetUri);
rightsAcquisitionInfo.put(WV_DEVICE_ID_KEY, mDeviceId);
rightsAcquisitionInfo.put(WV_PORTAL_KEY, PORTAL_NAME);
return rightsAcquisitionInfo;
}
private DrmInfoRequest createDrmInfoRequest(String assetUri) {
return createDrmInfoRequest(assetUri, null);
}
public void registerPortal() {
String portal = PORTAL_NAME;
DrmInfoRequest request = new DrmInfoRequest(DrmInfoRequest.TYPE_REGISTRATION_INFO,
WIDEVINE_MIME_TYPE);
request.put(WV_PORTAL_KEY, portal);
DrmInfo response = mDrmManager.acquireDrmInfo(request);
LOGI(TAG, "Widevine Plugin Info: " + extractDrmInfo(response));
String drmInfoRequestStatusKey = (String)response.get(WV_DRM_INFO_REQUEST_STATUS_KEY);
LOGI(TAG, "Widevine provision status: " + drmInfoRequestStatusKey);
}
/**
* returns whether or not we should acquire rights for this url
*
* @param assetUri
* @return
*/
public boolean needToAcquireRights(String assetUri){
mDrmManager.acquireDrmInfo(createDrmInfoRequest(assetUri));
int rightsStatus = mDrmManager.checkRightsStatus(assetUri);
if(rightsStatus == DrmStore.RightsStatus.RIGHTS_INVALID){
mDrmManager.removeRights(assetUri); // clear current invalid rights and re-acquire new rights
}
return rightsStatus != DrmStore.RightsStatus.RIGHTS_VALID;
}
public int acquireRights(String assetUri, String licenseServerUri) {
if (assetUri.startsWith("/")) {
return acquireLocalAssetRights(assetUri, licenseServerUri);
}
DrmInfoRequest drmInfoRequest = createDrmInfoRequest(assetUri, licenseServerUri);
DrmInfo drmInfo = mDrmManager.acquireDrmInfo(drmInfoRequest);
if (drmInfo == null) {
return DrmManagerClient.ERROR_UNKNOWN;
}
int rights = mDrmManager.processDrmInfo(drmInfo);
logMessage("acquireRights = " + rights + "\n");
return rights;
}
public int acquireLocalAssetRights(String assetPath, String licenseServerUri) {
DrmInfoRequest drmInfoRequest = createDrmInfoRequest(assetPath, licenseServerUri);
FileInputStream fis = null;
int rights = 0;
DrmInfo drmInfo;
// A local file needs special treatment -- open and get FD
try {
fis = new FileInputStream(assetPath);
FileDescriptor fd = fis.getFD();
if (fd != null && fd.valid()) {
drmInfoRequest.put("FileDescriptorKey", fd.toString());
drmInfo = mDrmManager.acquireDrmInfo(drmInfoRequest);
if (drmInfo == null) {
throw new IOException("DrmManagerClient couldn't prepare request for asset " + assetPath);
}
rights = mDrmManager.processDrmInfo(drmInfo);
}
} catch (java.io.IOException e) {
LOGE(TAG, "Error opening local file:", e);
rights = -1;
} finally {
safeClose(fis);
}
logMessage("acquireRights = " + rights + "\n");
return rights;
}
private static void safeClose(FileInputStream fis) {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
LOGE(TAG, "Failed to close file", e);
}
}
}
public RightsInfo getRightsInfo(String assetUri) {
// Need to use acquireDrmInfo prior to calling checkRightsStatus
mDrmManager.acquireDrmInfo(createDrmInfoRequest(assetUri));
int status = mDrmManager.checkRightsStatus(assetUri);
logMessage("getRightsInfo = " + status + "\n");
ContentValues values = mDrmManager.getConstraints(assetUri, DrmStore.Action.PLAY);
return new RightsInfo(status, values);
}
public int removeRights(String assetUri) {
// Need to use acquireDrmInfo prior to calling removeRights
mDrmManager.acquireDrmInfo(createDrmInfoRequest(assetUri));
int removeStatus = mDrmManager.removeRights(assetUri);
logMessage("removeRights = " + removeStatus + "\n");
return removeStatus;
}
public int removeAllRights() {
int removeAllStatus = mDrmManager.removeAllRights();
logMessage("removeAllRights = " + removeAllStatus + "\n");
return removeAllStatus;
}
private void logMessage(String message) {
LOGD(TAG, message);
}
}