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