• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.media;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.hardware.cas.V1_0.*;
22 import android.media.MediaCasException.*;
23 import android.os.Handler;
24 import android.os.HandlerThread;
25 import android.os.IHwBinder;
26 import android.os.Looper;
27 import android.os.Message;
28 import android.os.Process;
29 import android.os.RemoteException;
30 import android.util.Log;
31 import android.util.Singleton;
32 
33 import java.util.ArrayList;
34 
35 /**
36  * MediaCas can be used to obtain keys for descrambling protected media streams, in
37  * conjunction with {@link android.media.MediaDescrambler}. The MediaCas APIs are
38  * designed to support conditional access such as those in the ISO/IEC13818-1.
39  * The CA system is identified by a 16-bit integer CA_system_id. The scrambling
40  * algorithms are usually proprietary and implemented by vendor-specific CA plugins
41  * installed on the device.
42  * <p>
43  * The app is responsible for constructing a MediaCas object for the CA system it
44  * intends to use. The app can query if a certain CA system is supported using static
45  * method {@link #isSystemIdSupported}. It can also obtain the entire list of supported
46  * CA systems using static method {@link #enumeratePlugins}.
47  * <p>
48  * Once the MediaCas object is constructed, the app should properly provision it by
49  * using method {@link #provision} and/or {@link #processEmm}. The EMMs (Entitlement
50  * management messages) can be distributed out-of-band, or in-band with the stream.
51  * <p>
52  * To descramble elementary streams, the app first calls {@link #openSession} to
53  * generate a {@link Session} object that will uniquely identify a session. A session
54  * provides a context for subsequent key updates and descrambling activities. The ECMs
55  * (Entitlement control messages) are sent to the session via method
56  * {@link Session#processEcm}.
57  * <p>
58  * The app next constructs a MediaDescrambler object, and initializes it with the
59  * session using {@link MediaDescrambler#setMediaCasSession}. This ties the
60  * descrambler to the session, and the descrambler can then be used to descramble
61  * content secured with the session's key, either during extraction, or during decoding
62  * with {@link android.media.MediaCodec}.
63  * <p>
64  * If the app handles sample extraction using its own extractor, it can use
65  * MediaDescrambler to descramble samples into clear buffers (if the session's license
66  * doesn't require secure decoders), or descramble a small amount of data to retrieve
67  * information necessary for the downstream pipeline to process the sample (if the
68  * session's license requires secure decoders).
69  * <p>
70  * If the session requires a secure decoder, a MediaDescrambler needs to be provided to
71  * MediaCodec to descramble samples queued by {@link MediaCodec#queueSecureInputBuffer}
72  * into protected buffers. The app should use {@link MediaCodec#configure(MediaFormat,
73  * android.view.Surface, int, MediaDescrambler)} instead of the normal {@link
74  * MediaCodec#configure(MediaFormat, android.view.Surface, MediaCrypto, int)} method
75  * to configure MediaCodec.
76  * <p>
77  * <h3>Using Android's MediaExtractor</h3>
78  * <p>
79  * If the app uses {@link MediaExtractor}, it can delegate the CAS session
80  * management to MediaExtractor by calling {@link MediaExtractor#setMediaCas}.
81  * MediaExtractor will take over and call {@link #openSession}, {@link #processEmm}
82  * and/or {@link Session#processEcm}, etc.. if necessary.
83  * <p>
84  * When using {@link MediaExtractor}, the app would still need a MediaDescrambler
85  * to use with {@link MediaCodec} if the licensing requires a secure decoder. The
86  * session associated with the descrambler of a track can be retrieved by calling
87  * {@link MediaExtractor#getCasInfo}, and used to initialize a MediaDescrambler
88  * object for MediaCodec.
89  * <p>
90  * <h3>Listeners</h3>
91  * <p>The app may register a listener to receive events from the CA system using
92  * method {@link #setEventListener}. The exact format of the event is scheme-specific
93  * and is not specified by this API.
94  */
95 public final class MediaCas implements AutoCloseable {
96     private static final String TAG = "MediaCas";
97     private ICas mICas;
98     private EventListener mListener;
99     private HandlerThread mHandlerThread;
100     private EventHandler mEventHandler;
101 
102     private static final Singleton<IMediaCasService> gDefault =
103             new Singleton<IMediaCasService>() {
104         @Override
105         protected IMediaCasService create() {
106             try {
107                 return IMediaCasService.getService();
108             } catch (RemoteException e) {}
109             return null;
110         }
111     };
112 
getService()113     static IMediaCasService getService() {
114         return gDefault.get();
115     }
116 
validateInternalStates()117     private void validateInternalStates() {
118         if (mICas == null) {
119             throw new IllegalStateException();
120         }
121     }
122 
cleanupAndRethrowIllegalState()123     private void cleanupAndRethrowIllegalState() {
124         mICas = null;
125         throw new IllegalStateException();
126     }
127 
128     private class EventHandler extends Handler
129     {
130         private static final int MSG_CAS_EVENT = 0;
131 
EventHandler(Looper looper)132         public EventHandler(Looper looper) {
133             super(looper);
134         }
135 
136         @Override
handleMessage(Message msg)137         public void handleMessage(Message msg) {
138             if (msg.what == MSG_CAS_EVENT) {
139                 mListener.onEvent(MediaCas.this, msg.arg1, msg.arg2,
140                         toBytes((ArrayList<Byte>) msg.obj));
141             }
142         }
143     }
144 
145     private final ICasListener.Stub mBinder = new ICasListener.Stub() {
146         @Override
147         public void onEvent(int event, int arg, @Nullable ArrayList<Byte> data)
148                 throws RemoteException {
149             mEventHandler.sendMessage(mEventHandler.obtainMessage(
150                     EventHandler.MSG_CAS_EVENT, event, arg, data));
151         }
152     };
153 
154     /**
155      * Describe a CAS plugin with its CA_system_ID and string name.
156      *
157      * Returned as results of {@link #enumeratePlugins}.
158      *
159      */
160     public static class PluginDescriptor {
161         private final int mCASystemId;
162         private final String mName;
163 
PluginDescriptor()164         private PluginDescriptor() {
165             mCASystemId = 0xffff;
166             mName = null;
167         }
168 
PluginDescriptor(@onNull HidlCasPluginDescriptor descriptor)169         PluginDescriptor(@NonNull HidlCasPluginDescriptor descriptor) {
170             mCASystemId = descriptor.caSystemId;
171             mName = descriptor.name;
172         }
173 
getSystemId()174         public int getSystemId() {
175             return mCASystemId;
176         }
177 
178         @NonNull
getName()179         public String getName() {
180             return mName;
181         }
182 
183         @Override
toString()184         public String toString() {
185             return "PluginDescriptor {" + mCASystemId + ", " + mName + "}";
186         }
187     }
188 
toByteArray(@onNull byte[] data, int offset, int length)189     private ArrayList<Byte> toByteArray(@NonNull byte[] data, int offset, int length) {
190         ArrayList<Byte> byteArray = new ArrayList<Byte>(length);
191         for (int i = 0; i < length; i++) {
192             byteArray.add(Byte.valueOf(data[offset + i]));
193         }
194         return byteArray;
195     }
196 
toByteArray(@ullable byte[] data)197     private ArrayList<Byte> toByteArray(@Nullable byte[] data) {
198         if (data == null) {
199             return new ArrayList<Byte>();
200         }
201         return toByteArray(data, 0, data.length);
202     }
203 
toBytes(@onNull ArrayList<Byte> byteArray)204     private byte[] toBytes(@NonNull ArrayList<Byte> byteArray) {
205         byte[] data = null;
206         if (byteArray != null) {
207             data = new byte[byteArray.size()];
208             for (int i = 0; i < data.length; i++) {
209                 data[i] = byteArray.get(i);
210             }
211         }
212         return data;
213     }
214     /**
215      * Class for an open session with the CA system.
216      */
217     public final class Session implements AutoCloseable {
218         final ArrayList<Byte> mSessionId;
219 
Session(@onNull ArrayList<Byte> sessionId)220         Session(@NonNull ArrayList<Byte> sessionId) {
221             mSessionId = sessionId;
222         }
223 
224         /**
225          * Set the private data for a session.
226          *
227          * @param data byte array of the private data.
228          *
229          * @throws IllegalStateException if the MediaCas instance is not valid.
230          * @throws MediaCasException for CAS-specific errors.
231          * @throws MediaCasStateException for CAS-specific state exceptions.
232          */
setPrivateData(@onNull byte[] data)233         public void setPrivateData(@NonNull byte[] data)
234                 throws MediaCasException {
235             validateInternalStates();
236 
237             try {
238                 MediaCasException.throwExceptionIfNeeded(
239                         mICas.setSessionPrivateData(mSessionId, toByteArray(data, 0, data.length)));
240             } catch (RemoteException e) {
241                 cleanupAndRethrowIllegalState();
242             }
243         }
244 
245 
246         /**
247          * Send a received ECM packet to the specified session of the CA system.
248          *
249          * @param data byte array of the ECM data.
250          * @param offset position within data where the ECM data begins.
251          * @param length length of the data (starting from offset).
252          *
253          * @throws IllegalStateException if the MediaCas instance is not valid.
254          * @throws MediaCasException for CAS-specific errors.
255          * @throws MediaCasStateException for CAS-specific state exceptions.
256          */
processEcm(@onNull byte[] data, int offset, int length)257         public void processEcm(@NonNull byte[] data, int offset, int length)
258                 throws MediaCasException {
259             validateInternalStates();
260 
261             try {
262                 MediaCasException.throwExceptionIfNeeded(
263                         mICas.processEcm(mSessionId, toByteArray(data, offset, length)));
264             } catch (RemoteException e) {
265                 cleanupAndRethrowIllegalState();
266             }
267         }
268 
269         /**
270          * Send a received ECM packet to the specified session of the CA system.
271          * This is similar to {@link Session#processEcm(byte[], int, int)}
272          * except that the entire byte array is sent.
273          *
274          * @param data byte array of the ECM data.
275          *
276          * @throws IllegalStateException if the MediaCas instance is not valid.
277          * @throws MediaCasException for CAS-specific errors.
278          * @throws MediaCasStateException for CAS-specific state exceptions.
279          */
processEcm(@onNull byte[] data)280         public void processEcm(@NonNull byte[] data) throws MediaCasException {
281             processEcm(data, 0, data.length);
282         }
283 
284         /**
285          * Close the session.
286          *
287          * @throws IllegalStateException if the MediaCas instance is not valid.
288          * @throws MediaCasStateException for CAS-specific state exceptions.
289          */
290         @Override
close()291         public void close() {
292             validateInternalStates();
293 
294             try {
295                 MediaCasStateException.throwExceptionIfNeeded(
296                         mICas.closeSession(mSessionId));
297             } catch (RemoteException e) {
298                 cleanupAndRethrowIllegalState();
299             }
300         }
301     }
302 
createFromSessionId(@onNull ArrayList<Byte> sessionId)303     Session createFromSessionId(@NonNull ArrayList<Byte> sessionId) {
304         if (sessionId == null || sessionId.size() == 0) {
305             return null;
306         }
307         return new Session(sessionId);
308     }
309 
310     /**
311      * Query if a certain CA system is supported on this device.
312      *
313      * @param CA_system_id the id of the CA system.
314      *
315      * @return Whether the specified CA system is supported on this device.
316      */
isSystemIdSupported(int CA_system_id)317     public static boolean isSystemIdSupported(int CA_system_id) {
318         IMediaCasService service = getService();
319 
320         if (service != null) {
321             try {
322                 return service.isSystemIdSupported(CA_system_id);
323             } catch (RemoteException e) {
324             }
325         }
326         return false;
327     }
328 
329     /**
330      * List all available CA plugins on the device.
331      *
332      * @return an array of descriptors for the available CA plugins.
333      */
enumeratePlugins()334     public static PluginDescriptor[] enumeratePlugins() {
335         IMediaCasService service = getService();
336 
337         if (service != null) {
338             try {
339                 ArrayList<HidlCasPluginDescriptor> descriptors =
340                         service.enumeratePlugins();
341                 if (descriptors.size() == 0) {
342                     return null;
343                 }
344                 PluginDescriptor[] results = new PluginDescriptor[descriptors.size()];
345                 for (int i = 0; i < results.length; i++) {
346                     results[i] = new PluginDescriptor(descriptors.get(i));
347                 }
348                 return results;
349             } catch (RemoteException e) {
350             }
351         }
352         return null;
353     }
354 
355     /**
356      * Instantiate a CA system of the specified system id.
357      *
358      * @param CA_system_id The system id of the CA system.
359      *
360      * @throws UnsupportedCasException if the device does not support the
361      * specified CA system.
362      */
MediaCas(int CA_system_id)363     public MediaCas(int CA_system_id) throws UnsupportedCasException {
364         try {
365             mICas = getService().createPlugin(CA_system_id, mBinder);
366         } catch(Exception e) {
367             Log.e(TAG, "Failed to create plugin: " + e);
368             mICas = null;
369         } finally {
370             if (mICas == null) {
371                 throw new UnsupportedCasException(
372                         "Unsupported CA_system_id " + CA_system_id);
373             }
374         }
375     }
376 
getBinder()377     IHwBinder getBinder() {
378         validateInternalStates();
379 
380         return mICas.asBinder();
381     }
382 
383     /**
384      * An interface registered by the caller to {@link #setEventListener}
385      * to receives scheme-specific notifications from a MediaCas instance.
386      */
387     public interface EventListener {
388         /**
389          * Notify the listener of a scheme-specific event from the CA system.
390          *
391          * @param MediaCas the MediaCas object to receive this event.
392          * @param event an integer whose meaning is scheme-specific.
393          * @param arg an integer whose meaning is scheme-specific.
394          * @param data a byte array of data whose format and meaning are
395          * scheme-specific.
396          */
onEvent(MediaCas MediaCas, int event, int arg, @Nullable byte[] data)397         void onEvent(MediaCas MediaCas, int event, int arg, @Nullable byte[] data);
398     }
399 
400     /**
401      * Set an event listener to receive notifications from the MediaCas instance.
402      *
403      * @param listener the event listener to be set.
404      * @param handler the handler whose looper the event listener will be called on.
405      * If handler is null, we'll try to use current thread's looper, or the main
406      * looper. If neither are available, an internal thread will be created instead.
407      */
setEventListener( @ullable EventListener listener, @Nullable Handler handler)408     public void setEventListener(
409             @Nullable EventListener listener, @Nullable Handler handler) {
410         mListener = listener;
411 
412         if (mListener == null) {
413             mEventHandler = null;
414             return;
415         }
416 
417         Looper looper = (handler != null) ? handler.getLooper() : null;
418         if (looper == null
419                 && (looper = Looper.myLooper()) == null
420                 && (looper = Looper.getMainLooper()) == null) {
421             if (mHandlerThread == null || !mHandlerThread.isAlive()) {
422                 mHandlerThread = new HandlerThread("MediaCasEventThread",
423                         Process.THREAD_PRIORITY_FOREGROUND);
424                 mHandlerThread.start();
425             }
426             looper = mHandlerThread.getLooper();
427         }
428         mEventHandler = new EventHandler(looper);
429     }
430 
431     /**
432      * Send the private data for the CA system.
433      *
434      * @param data byte array of the private data.
435      *
436      * @throws IllegalStateException if the MediaCas instance is not valid.
437      * @throws MediaCasException for CAS-specific errors.
438      * @throws MediaCasStateException for CAS-specific state exceptions.
439      */
setPrivateData(@onNull byte[] data)440     public void setPrivateData(@NonNull byte[] data) throws MediaCasException {
441         validateInternalStates();
442 
443         try {
444             MediaCasException.throwExceptionIfNeeded(
445                     mICas.setPrivateData(toByteArray(data, 0, data.length)));
446         } catch (RemoteException e) {
447             cleanupAndRethrowIllegalState();
448         }
449     }
450 
451     private class OpenSessionCallback implements ICas.openSessionCallback {
452         public Session mSession;
453         public int mStatus;
454         @Override
onValues(int status, ArrayList<Byte> sessionId)455         public void onValues(int status, ArrayList<Byte> sessionId) {
456             mStatus = status;
457             mSession = createFromSessionId(sessionId);
458         }
459     }
460     /**
461      * Open a session to descramble one or more streams scrambled by the
462      * conditional access system.
463      *
464      * @return session the newly opened session.
465      *
466      * @throws IllegalStateException if the MediaCas instance is not valid.
467      * @throws MediaCasException for CAS-specific errors.
468      * @throws MediaCasStateException for CAS-specific state exceptions.
469      */
openSession()470     public Session openSession() throws MediaCasException {
471         validateInternalStates();
472 
473         try {
474             OpenSessionCallback cb = new OpenSessionCallback();
475             mICas.openSession(cb);
476             MediaCasException.throwExceptionIfNeeded(cb.mStatus);
477             return cb.mSession;
478         } catch (RemoteException e) {
479             cleanupAndRethrowIllegalState();
480         }
481         return null;
482     }
483 
484     /**
485      * Send a received EMM packet to the CA system.
486      *
487      * @param data byte array of the EMM data.
488      * @param offset position within data where the EMM data begins.
489      * @param length length of the data (starting from offset).
490      *
491      * @throws IllegalStateException if the MediaCas instance is not valid.
492      * @throws MediaCasException for CAS-specific errors.
493      * @throws MediaCasStateException for CAS-specific state exceptions.
494      */
processEmm(@onNull byte[] data, int offset, int length)495     public void processEmm(@NonNull byte[] data, int offset, int length)
496             throws MediaCasException {
497         validateInternalStates();
498 
499         try {
500             MediaCasException.throwExceptionIfNeeded(
501                     mICas.processEmm(toByteArray(data, offset, length)));
502         } catch (RemoteException e) {
503             cleanupAndRethrowIllegalState();
504         }
505     }
506 
507     /**
508      * Send a received EMM packet to the CA system. This is similar to
509      * {@link #processEmm(byte[], int, int)} except that the entire byte
510      * array is sent.
511      *
512      * @param data byte array of the EMM data.
513      *
514      * @throws IllegalStateException if the MediaCas instance is not valid.
515      * @throws MediaCasException for CAS-specific errors.
516      * @throws MediaCasStateException for CAS-specific state exceptions.
517      */
processEmm(@onNull byte[] data)518     public void processEmm(@NonNull byte[] data) throws MediaCasException {
519         processEmm(data, 0, data.length);
520     }
521 
522     /**
523      * Send an event to a CA system. The format of the event is scheme-specific
524      * and is opaque to the framework.
525      *
526      * @param event an integer denoting a scheme-specific event to be sent.
527      * @param arg a scheme-specific integer argument for the event.
528      * @param data a byte array containing scheme-specific data for the event.
529      *
530      * @throws IllegalStateException if the MediaCas instance is not valid.
531      * @throws MediaCasException for CAS-specific errors.
532      * @throws MediaCasStateException for CAS-specific state exceptions.
533      */
sendEvent(int event, int arg, @Nullable byte[] data)534     public void sendEvent(int event, int arg, @Nullable byte[] data)
535             throws MediaCasException {
536         validateInternalStates();
537 
538         try {
539             MediaCasException.throwExceptionIfNeeded(
540                     mICas.sendEvent(event, arg, toByteArray(data)));
541         } catch (RemoteException e) {
542             cleanupAndRethrowIllegalState();
543         }
544     }
545 
546     /**
547      * Initiate a provisioning operation for a CA system.
548      *
549      * @param provisionString string containing information needed for the
550      * provisioning operation, the format of which is scheme and implementation
551      * specific.
552      *
553      * @throws IllegalStateException if the MediaCas instance is not valid.
554      * @throws MediaCasException for CAS-specific errors.
555      * @throws MediaCasStateException for CAS-specific state exceptions.
556      */
provision(@onNull String provisionString)557     public void provision(@NonNull String provisionString) throws MediaCasException {
558         validateInternalStates();
559 
560         try {
561             MediaCasException.throwExceptionIfNeeded(
562                     mICas.provision(provisionString));
563         } catch (RemoteException e) {
564             cleanupAndRethrowIllegalState();
565         }
566     }
567 
568     /**
569      * Notify the CA system to refresh entitlement keys.
570      *
571      * @param refreshType the type of the refreshment.
572      * @param refreshData private data associated with the refreshment.
573      *
574      * @throws IllegalStateException if the MediaCas instance is not valid.
575      * @throws MediaCasException for CAS-specific errors.
576      * @throws MediaCasStateException for CAS-specific state exceptions.
577      */
refreshEntitlements(int refreshType, @Nullable byte[] refreshData)578     public void refreshEntitlements(int refreshType, @Nullable byte[] refreshData)
579             throws MediaCasException {
580         validateInternalStates();
581 
582         try {
583             MediaCasException.throwExceptionIfNeeded(
584                     mICas.refreshEntitlements(refreshType, toByteArray(refreshData)));
585         } catch (RemoteException e) {
586             cleanupAndRethrowIllegalState();
587         }
588     }
589 
590     @Override
close()591     public void close() {
592         if (mICas != null) {
593             try {
594                 mICas.release();
595             } catch (RemoteException e) {
596             } finally {
597                 mICas = null;
598             }
599         }
600     }
601 
602     @Override
finalize()603     protected void finalize() {
604         close();
605     }
606 }