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.hardware.cas.V1_0.*;
22import android.media.MediaCasException.*;
23import android.os.Handler;
24import android.os.HandlerThread;
25import android.os.IHwBinder;
26import android.os.Looper;
27import android.os.Message;
28import android.os.Process;
29import android.os.RemoteException;
30import android.util.Log;
31import android.util.Singleton;
32
33import 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 */
95public 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
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,
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
164        private PluginDescriptor() {
165            mCASystemId = 0xffff;
166            mName = null;
167        }
168
169        PluginDescriptor(@NonNull HidlCasPluginDescriptor descriptor) {
170            mCASystemId = descriptor.caSystemId;
171            mName = descriptor.name;
172        }
173
174        public int getSystemId() {
175            return mCASystemId;
176        }
177
178        @NonNull
179        public String getName() {
180            return mName;
181        }
182
183        @Override
184        public String toString() {
185            return "PluginDescriptor {" + mCASystemId + ", " + mName + "}";
186        }
187    }
188
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
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
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
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         */
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         */
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         */
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
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
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     */
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     */
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     */
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
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         */
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     */
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     */
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
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     */
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     */
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     */
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     */
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     */
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     */
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
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
603    protected void finalize() {
604        close();
605    }
606}