- add sources.
[platform/framework/web/crosswalk.git] / src / media / base / android / java / src / org / chromium / media / MediaDrmBridge.java
1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 package org.chromium.media;
6
7 import android.media.MediaCrypto;
8 import android.media.MediaDrm;
9 import android.os.AsyncTask;
10 import android.os.Handler;
11 import android.util.Log;
12
13 import org.apache.http.HttpResponse;
14 import org.apache.http.client.methods.HttpPost;
15 import org.apache.http.client.HttpClient;
16 import org.apache.http.client.ClientProtocolException;
17 import org.apache.http.impl.client.DefaultHttpClient;
18 import org.apache.http.util.EntityUtils;
19 import org.chromium.base.CalledByNative;
20 import org.chromium.base.JNINamespace;
21
22 import java.io.IOException;
23 import java.util.HashMap;
24 import java.util.UUID;
25
26 /**
27  * A wrapper of the android MediaDrm class. Each MediaDrmBridge manages multiple
28  * sessions for a single MediaSourcePlayer.
29  */
30 @JNINamespace("media")
31 class MediaDrmBridge {
32
33     private static final String TAG = "MediaDrmBridge";
34     private static final String SECURITY_LEVEL = "securityLevel";
35     private static final String PRIVACY_MODE = "privacyMode";
36     private MediaDrm mMediaDrm;
37     private UUID mSchemeUUID;
38     private int mNativeMediaDrmBridge;
39     // TODO(qinmin): we currently only support one session per DRM bridge.
40     // Change this to a HashMap if we start to support multiple sessions.
41     private String mSessionId;
42     private MediaCrypto mMediaCrypto;
43     private String mMimeType;
44     private Handler mHandler;
45     private byte[] mPendingInitData;
46     private boolean mResetDeviceCredentialsPending;
47
48     private static UUID getUUIDFromBytes(byte[] data) {
49         if (data.length != 16) {
50             return null;
51         }
52         long mostSigBits = 0;
53         long leastSigBits = 0;
54         for (int i = 0; i < 8; i++) {
55             mostSigBits = (mostSigBits << 8) | (data[i] & 0xff);
56         }
57         for (int i = 8; i < 16; i++) {
58             leastSigBits = (leastSigBits << 8) | (data[i] & 0xff);
59         }
60         return new UUID(mostSigBits, leastSigBits);
61     }
62
63     private MediaDrmBridge(UUID schemeUUID, String securityLevel, int nativeMediaDrmBridge)
64             throws android.media.UnsupportedSchemeException {
65         mSchemeUUID = schemeUUID;
66         mMediaDrm = new MediaDrm(schemeUUID);
67         mHandler = new Handler();
68         mNativeMediaDrmBridge = nativeMediaDrmBridge;
69         mResetDeviceCredentialsPending = false;
70         mMediaDrm.setOnEventListener(new MediaDrmListener());
71         mMediaDrm.setPropertyString(PRIVACY_MODE, "enable");
72         String currentSecurityLevel = mMediaDrm.getPropertyString(SECURITY_LEVEL);
73         Log.e(TAG, "Security level: current " + currentSecurityLevel + ", new " + securityLevel);
74         if (!securityLevel.equals(currentSecurityLevel))
75             mMediaDrm.setPropertyString(SECURITY_LEVEL, securityLevel);
76     }
77
78     /**
79      * Create a MediaCrypto object.
80      *
81      * @return if a MediaCrypto object is successfully created.
82      */
83     private boolean createMediaCrypto() {
84         assert(mSessionId != null);
85         assert(mMediaCrypto == null);
86         try {
87             final byte[] session = mSessionId.getBytes("UTF-8");
88             if (MediaCrypto.isCryptoSchemeSupported(mSchemeUUID)) {
89                 mMediaCrypto = new MediaCrypto(mSchemeUUID, session);
90             }
91         } catch (android.media.MediaCryptoException e) {
92             Log.e(TAG, "Cannot create MediaCrypto " + e.toString());
93             return false;
94         } catch (java.io.UnsupportedEncodingException e) {
95             Log.e(TAG, "Cannot create MediaCrypto " + e.toString());
96             return false;
97         }
98
99         assert(mMediaCrypto != null);
100         nativeOnMediaCryptoReady(mNativeMediaDrmBridge);
101         return true;
102     }
103
104     /**
105      * Open a new session and return the sessionId.
106      *
107      * @return false if unexpected error happens. Return true if a new session
108      * is successfully opened, or if provisioning is required to open a session.
109      */
110     private boolean openSession() {
111         assert(mSessionId == null);
112
113         if (mMediaDrm == null) {
114             return false;
115         }
116
117         try {
118             final byte[] sessionId = mMediaDrm.openSession();
119             mSessionId = new String(sessionId, "UTF-8");
120         } catch (android.media.NotProvisionedException e) {
121             Log.e(TAG, "Cannot open a new session: " + e.toString());
122             return true;
123         } catch (Exception e) {
124             Log.e(TAG, "Cannot open a new session: " + e.toString());
125             return false;
126         }
127
128         assert(mSessionId != null);
129         return createMediaCrypto();
130     }
131
132     /**
133      * Check whether the crypto scheme is supported for the given container.
134      * If |containerMimeType| is an empty string, we just return whether
135      * the crypto scheme is supported.
136      * TODO(qinmin): Implement the checking for container.
137      *
138      * @return true if the container and the crypto scheme is supported, or
139      * false otherwise.
140      */
141     @CalledByNative
142     private static boolean isCryptoSchemeSupported(byte[] schemeUUID, String containerMimeType) {
143         UUID cryptoScheme = getUUIDFromBytes(schemeUUID);
144         return MediaDrm.isCryptoSchemeSupported(cryptoScheme);
145     }
146
147     /**
148      * Create a new MediaDrmBridge from the crypto scheme UUID.
149      *
150      * @param schemeUUID Crypto scheme UUID.
151      * @param securityLevel Security level to be used.
152      * @param nativeMediaDrmBridge Native object of this class.
153      */
154     @CalledByNative
155     private static MediaDrmBridge create(
156             byte[] schemeUUID, String securityLevel, int nativeMediaDrmBridge) {
157         UUID cryptoScheme = getUUIDFromBytes(schemeUUID);
158         if (cryptoScheme == null || !MediaDrm.isCryptoSchemeSupported(cryptoScheme)) {
159             return null;
160         }
161
162         MediaDrmBridge media_drm_bridge = null;
163         try {
164             media_drm_bridge = new MediaDrmBridge(
165                     cryptoScheme, securityLevel, nativeMediaDrmBridge);
166         } catch (android.media.UnsupportedSchemeException e) {
167             Log.e(TAG, "Unsupported DRM scheme: " + e.toString());
168         } catch (java.lang.IllegalArgumentException e) {
169             Log.e(TAG, "Failed to create MediaDrmBridge: " + e.toString());
170         } catch (java.lang.IllegalStateException e) {
171             Log.e(TAG, "Failed to create MediaDrmBridge: " + e.toString());
172         }
173
174         return media_drm_bridge;
175     }
176
177     /**
178      * Return the MediaCrypto object if available.
179      */
180     @CalledByNative
181     private MediaCrypto getMediaCrypto() {
182         return mMediaCrypto;
183     }
184
185     /**
186      * Reset the device DRM credentials.
187      */
188     @CalledByNative
189     private void resetDeviceCredentials() {
190         mResetDeviceCredentialsPending = true;
191         MediaDrm.ProvisionRequest request = mMediaDrm.getProvisionRequest();
192         PostRequestTask postTask = new PostRequestTask(request.getData());
193         postTask.execute(request.getDefaultUrl());
194     }
195
196     /**
197      * Release the MediaDrmBridge object.
198      */
199     @CalledByNative
200     private void release() {
201         if (mMediaCrypto != null) {
202             mMediaCrypto.release();
203             mMediaCrypto = null;
204         }
205         if (mSessionId != null) {
206             try {
207                 final byte[] session = mSessionId.getBytes("UTF-8");
208                 mMediaDrm.closeSession(session);
209             } catch (java.io.UnsupportedEncodingException e) {
210                 Log.e(TAG, "Failed to close session: " + e.toString());
211             }
212             mSessionId = null;
213         }
214         if (mMediaDrm != null) {
215             mMediaDrm.release();
216             mMediaDrm = null;
217         }
218     }
219
220     /**
221      * Generate a key request and post an asynchronous task to the native side
222      * with the response message.
223      *
224      * @param initData Data needed to generate the key request.
225      * @param mime Mime type.
226      */
227     @CalledByNative
228     private void generateKeyRequest(byte[] initData, String mime) {
229         Log.d(TAG, "generateKeyRequest().");
230
231         if (mMimeType == null) {
232             mMimeType = mime;
233         } else if (!mMimeType.equals(mime)) {
234             onKeyError();
235             return;
236         }
237
238         if (mSessionId == null) {
239             if (!openSession()) {
240                 onKeyError();
241                 return;
242             }
243
244             // NotProvisionedException happened during openSession().
245             if (mSessionId == null) {
246                 if (mPendingInitData != null) {
247                     Log.e(TAG, "generateKeyRequest called when another call is pending.");
248                     onKeyError();
249                     return;
250                 }
251
252                 // We assume MediaDrm.EVENT_PROVISION_REQUIRED is always fired if
253                 // NotProvisionedException is throwed in openSession().
254                 // generateKeyRequest() will be resumed after provisioning is finished.
255                 // TODO(xhwang): Double check if this assumption is true. Otherwise we need
256                 // to handle the exception in openSession more carefully.
257                 mPendingInitData = initData;
258                 return;
259             }
260         }
261
262         try {
263             final byte[] session = mSessionId.getBytes("UTF-8");
264             HashMap<String, String> optionalParameters = new HashMap<String, String>();
265             final MediaDrm.KeyRequest request = mMediaDrm.getKeyRequest(
266                     session, initData, mime, MediaDrm.KEY_TYPE_STREAMING, optionalParameters);
267             mHandler.post(new Runnable(){
268                 public void run() {
269                     nativeOnKeyMessage(mNativeMediaDrmBridge, mSessionId,
270                             request.getData(), request.getDefaultUrl());
271                 }
272             });
273             return;
274         } catch (android.media.NotProvisionedException e) {
275             // MediaDrm.EVENT_PROVISION_REQUIRED is also fired in this case.
276             // Provisioning is handled in the handler of that event.
277             Log.e(TAG, "Cannot get key request: " + e.toString());
278             return;
279         } catch (java.io.UnsupportedEncodingException e) {
280             Log.e(TAG, "Cannot get key request: " + e.toString());
281         }
282         onKeyError();
283     }
284
285     /**
286      * Cancel a key request for a session Id.
287      *
288      * @param sessionId Crypto session Id.
289      */
290     @CalledByNative
291     private void cancelKeyRequest(String sessionId) {
292         if (mSessionId == null || !mSessionId.equals(sessionId)) {
293             return;
294         }
295         try {
296             final byte[] session = sessionId.getBytes("UTF-8");
297             mMediaDrm.removeKeys(session);
298         } catch (java.io.UnsupportedEncodingException e) {
299             Log.e(TAG, "Cannot cancel key request: " + e.toString());
300         }
301     }
302
303     /**
304      * Add a key for a session Id.
305      *
306      * @param sessionId Crypto session Id.
307      * @param key Response data from the server.
308      */
309     @CalledByNative
310     private void addKey(String sessionId, byte[] key) {
311         if (mSessionId == null || !mSessionId.equals(sessionId)) {
312             return;
313         }
314         try {
315             final byte[] session = sessionId.getBytes("UTF-8");
316             try {
317                 mMediaDrm.provideKeyResponse(session, key);
318             } catch (java.lang.IllegalStateException e) {
319                 // This is not really an exception. Some error code are incorrectly
320                 // reported as an exception.
321                 // TODO(qinmin): remove this exception catch when b/10495563 is fixed.
322                 Log.e(TAG, "Exception intentionally caught when calling provideKeyResponse() "
323                         + e.toString());
324             }
325             mHandler.post(new Runnable() {
326                 public void run() {
327                     nativeOnKeyAdded(mNativeMediaDrmBridge, mSessionId);
328                 }
329             });
330             return;
331         } catch (android.media.NotProvisionedException e) {
332             Log.e(TAG, "failed to provide key response: " + e.toString());
333         } catch (android.media.DeniedByServerException e) {
334             Log.e(TAG, "failed to provide key response: " + e.toString());
335         } catch (java.io.UnsupportedEncodingException e) {
336             Log.e(TAG, "failed to provide key response: " + e.toString());
337         }
338         onKeyError();
339     }
340
341     /**
342      * Return the security level of this DRM object.
343      */
344     @CalledByNative
345     private String getSecurityLevel() {
346         return mMediaDrm.getPropertyString("securityLevel");
347     }
348
349     /**
350      * Called when the provision response is received.
351      *
352      * @param response Response data from the provision server.
353      */
354     private void onProvisionResponse(byte[] response) {
355         Log.d(TAG, "onProvisionResponse()");
356
357         // If |mMediaDrm| is released, there is no need to callback native.
358         if (mMediaDrm == null) {
359             return;
360         }
361
362         boolean success = provideProvisionResponse(response);
363         if (mResetDeviceCredentialsPending) {
364             nativeOnResetDeviceCredentialsCompleted(mNativeMediaDrmBridge, success);
365             mResetDeviceCredentialsPending = false;
366             return;
367         }
368
369         if (!success) {
370             onKeyError();
371         }
372     }
373
374     /**
375      * Provide the provisioning response to MediaDrm.
376      * @returns false if the response is invalid or on error, true otherwise.
377      */
378     boolean provideProvisionResponse(byte[] response) {
379         if (response == null || response.length == 0) {
380             Log.e(TAG, "Invalid provision response.");
381             return false;
382         }
383
384         try {
385             mMediaDrm.provideProvisionResponse(response);
386         } catch (android.media.DeniedByServerException e) {
387             Log.e(TAG, "failed to provide provision response: " + e.toString());
388             return false;
389         } catch (java.lang.IllegalStateException e) {
390             Log.e(TAG, "failed to provide provision response: " + e.toString());
391             return false;
392         }
393
394         if (mPendingInitData != null) {
395             assert(!mResetDeviceCredentialsPending);
396             byte[] initData = mPendingInitData;
397             mPendingInitData = null;
398             generateKeyRequest(initData, mMimeType);
399         }
400         return true;
401     }
402
403     private void onKeyError() {
404         // TODO(qinmin): pass the error code to native.
405         mHandler.post(new Runnable() {
406             public void run() {
407                 nativeOnKeyError(mNativeMediaDrmBridge, mSessionId);
408             }
409         });
410     }
411
412     private class MediaDrmListener implements MediaDrm.OnEventListener {
413         @Override
414         public void onEvent(MediaDrm mediaDrm, byte[] sessionId, int event, int extra,
415                 byte[] data) {
416             switch(event) {
417                 case MediaDrm.EVENT_PROVISION_REQUIRED:
418                     Log.d(TAG, "MediaDrm.EVENT_PROVISION_REQUIRED.");
419                     MediaDrm.ProvisionRequest request = mMediaDrm.getProvisionRequest();
420                     PostRequestTask postTask = new PostRequestTask(request.getData());
421                     postTask.execute(request.getDefaultUrl());
422                     break;
423                 case MediaDrm.EVENT_KEY_REQUIRED:
424                     Log.d(TAG, "MediaDrm.EVENT_KEY_REQUIRED.");
425                     generateKeyRequest(data, mMimeType);
426                     break;
427                 case MediaDrm.EVENT_KEY_EXPIRED:
428                     Log.d(TAG, "MediaDrm.EVENT_KEY_EXPIRED.");
429                     onKeyError();
430                     break;
431                 case MediaDrm.EVENT_VENDOR_DEFINED:
432                     Log.d(TAG, "MediaDrm.EVENT_VENDOR_DEFINED.");
433                     assert(false);
434                     break;
435                 default:
436                     Log.e(TAG, "Invalid DRM event " + (int)event);
437                     return;
438             }
439         }
440     }
441
442     private class PostRequestTask extends AsyncTask<String, Void, Void> {
443         private static final String TAG = "PostRequestTask";
444
445         private byte[] mDrmRequest;
446         private byte[] mResponseBody;
447
448         public PostRequestTask(byte[] drmRequest) {
449             mDrmRequest = drmRequest;
450         }
451
452         @Override
453         protected Void doInBackground(String... urls) {
454             mResponseBody = postRequest(urls[0], mDrmRequest);
455             if (mResponseBody != null) {
456                 Log.d(TAG, "response length=" + mResponseBody.length);
457             }
458             return null;
459         }
460
461         private byte[] postRequest(String url, byte[] drmRequest) {
462             HttpClient httpClient = new DefaultHttpClient();
463             HttpPost httpPost = new HttpPost(url + "&signedRequest=" + new String(drmRequest));
464
465             Log.d(TAG, "PostRequest:" + httpPost.getRequestLine());
466             try {
467                 // Add data
468                 httpPost.setHeader("Accept", "*/*");
469                 httpPost.setHeader("User-Agent", "Widevine CDM v1.0");
470                 httpPost.setHeader("Content-Type", "application/json");
471
472                 // Execute HTTP Post Request
473                 HttpResponse response = httpClient.execute(httpPost);
474
475                 byte[] responseBody;
476                 int responseCode = response.getStatusLine().getStatusCode();
477                 if (responseCode == 200) {
478                     responseBody = EntityUtils.toByteArray(response.getEntity());
479                 } else {
480                     Log.d(TAG, "Server returned HTTP error code " + responseCode);
481                     return null;
482                 }
483                 return responseBody;
484             } catch (ClientProtocolException e) {
485                 e.printStackTrace();
486             } catch (IOException e) {
487                 e.printStackTrace();
488             }
489             return null;
490         }
491
492         @Override
493         protected void onPostExecute(Void v) {
494             onProvisionResponse(mResponseBody);
495         }
496     }
497
498     private native void nativeOnMediaCryptoReady(int nativeMediaDrmBridge);
499
500     private native void nativeOnKeyMessage(int nativeMediaDrmBridge, String sessionId,
501                                            byte[] message, String destinationUrl);
502
503     private native void nativeOnKeyAdded(int nativeMediaDrmBridge, String sessionId);
504
505     private native void nativeOnKeyError(int nativeMediaDrmBridge, String sessionId);
506
507     private native void nativeOnResetDeviceCredentialsCompleted(
508             int nativeMediaDrmBridge, boolean success);
509 }