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.IntDef;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.TestApi;
23 import android.content.Context;
24 import android.hardware.cas.V1_0.HidlCasPluginDescriptor;
25 import android.hardware.cas.V1_0.ICas;
26 import android.hardware.cas.V1_0.IMediaCasService;
27 import android.hardware.cas.V1_2.ICasListener;
28 import android.hardware.cas.V1_2.Status;
29 import android.media.MediaCasException.*;
30 import android.media.tv.TvInputService.PriorityHintUseCaseType;
31 import android.media.tv.tunerresourcemanager.CasSessionRequest;
32 import android.media.tv.tunerresourcemanager.ResourceClientProfile;
33 import android.media.tv.tunerresourcemanager.TunerResourceManager;
34 import android.os.Bundle;
35 import android.os.Handler;
36 import android.os.HandlerThread;
37 import android.os.IHwBinder;
38 import android.os.Looper;
39 import android.os.Message;
40 import android.os.Process;
41 import android.os.RemoteException;
42 import android.util.Log;
43 import android.util.Singleton;
44 
45 import com.android.internal.util.FrameworkStatsLog;
46 
47 import java.lang.annotation.Retention;
48 import java.lang.annotation.RetentionPolicy;
49 import java.util.ArrayList;
50 import java.util.HashMap;
51 import java.util.List;
52 import java.util.Map;
53 import java.util.Objects;
54 
55 /**
56  * MediaCas can be used to obtain keys for descrambling protected media streams, in
57  * conjunction with {@link android.media.MediaDescrambler}. The MediaCas APIs are
58  * designed to support conditional access such as those in the ISO/IEC13818-1.
59  * The CA system is identified by a 16-bit integer CA_system_id. The scrambling
60  * algorithms are usually proprietary and implemented by vendor-specific CA plugins
61  * installed on the device.
62  * <p>
63  * The app is responsible for constructing a MediaCas object for the CA system it
64  * intends to use. The app can query if a certain CA system is supported using static
65  * method {@link #isSystemIdSupported}. It can also obtain the entire list of supported
66  * CA systems using static method {@link #enumeratePlugins}.
67  * <p>
68  * Once the MediaCas object is constructed, the app should properly provision it by
69  * using method {@link #provision} and/or {@link #processEmm}. The EMMs (Entitlement
70  * management messages) can be distributed out-of-band, or in-band with the stream.
71  * <p>
72  * To descramble elementary streams, the app first calls {@link #openSession} to
73  * generate a {@link Session} object that will uniquely identify a session. A session
74  * provides a context for subsequent key updates and descrambling activities. The ECMs
75  * (Entitlement control messages) are sent to the session via method
76  * {@link Session#processEcm}.
77  * <p>
78  * The app next constructs a MediaDescrambler object, and initializes it with the
79  * session using {@link MediaDescrambler#setMediaCasSession}. This ties the
80  * descrambler to the session, and the descrambler can then be used to descramble
81  * content secured with the session's key, either during extraction, or during decoding
82  * with {@link android.media.MediaCodec}.
83  * <p>
84  * If the app handles sample extraction using its own extractor, it can use
85  * MediaDescrambler to descramble samples into clear buffers (if the session's license
86  * doesn't require secure decoders), or descramble a small amount of data to retrieve
87  * information necessary for the downstream pipeline to process the sample (if the
88  * session's license requires secure decoders).
89  * <p>
90  * If the session requires a secure decoder, a MediaDescrambler needs to be provided to
91  * MediaCodec to descramble samples queued by {@link MediaCodec#queueSecureInputBuffer}
92  * into protected buffers. The app should use {@link MediaCodec#configure(MediaFormat,
93  * android.view.Surface, int, MediaDescrambler)} instead of the normal {@link
94  * MediaCodec#configure(MediaFormat, android.view.Surface, MediaCrypto, int)} method
95  * to configure MediaCodec.
96  * <p>
97  * <h3>Using Android's MediaExtractor</h3>
98  * <p>
99  * If the app uses {@link MediaExtractor}, it can delegate the CAS session
100  * management to MediaExtractor by calling {@link MediaExtractor#setMediaCas}.
101  * MediaExtractor will take over and call {@link #openSession}, {@link #processEmm}
102  * and/or {@link Session#processEcm}, etc.. if necessary.
103  * <p>
104  * When using {@link MediaExtractor}, the app would still need a MediaDescrambler
105  * to use with {@link MediaCodec} if the licensing requires a secure decoder. The
106  * session associated with the descrambler of a track can be retrieved by calling
107  * {@link MediaExtractor#getCasInfo}, and used to initialize a MediaDescrambler
108  * object for MediaCodec.
109  * <p>
110  * <h3>Listeners</h3>
111  * <p>The app may register a listener to receive events from the CA system using
112  * method {@link #setEventListener}. The exact format of the event is scheme-specific
113  * and is not specified by this API.
114  */
115 public final class MediaCas implements AutoCloseable {
116     private static final String TAG = "MediaCas";
117     private ICas mICas;
118     private android.hardware.cas.V1_1.ICas mICasV11;
119     private android.hardware.cas.V1_2.ICas mICasV12;
120     private EventListener mListener;
121     private HandlerThread mHandlerThread;
122     private EventHandler mEventHandler;
123     private @PriorityHintUseCaseType int mPriorityHint;
124     private String mTvInputServiceSessionId;
125     private int mClientId;
126     private int mCasSystemId;
127     private int mUserId;
128     private TunerResourceManager mTunerResourceManager = null;
129     private final Map<Session, Integer> mSessionMap = new HashMap<>();
130 
131     /**
132      * Scrambling modes used to open cas sessions.
133      *
134      * @hide
135      */
136     @IntDef(prefix = "SCRAMBLING_MODE_",
137             value = {SCRAMBLING_MODE_RESERVED, SCRAMBLING_MODE_DVB_CSA1, SCRAMBLING_MODE_DVB_CSA2,
138             SCRAMBLING_MODE_DVB_CSA3_STANDARD,
139             SCRAMBLING_MODE_DVB_CSA3_MINIMAL, SCRAMBLING_MODE_DVB_CSA3_ENHANCE,
140             SCRAMBLING_MODE_DVB_CISSA_V1, SCRAMBLING_MODE_DVB_IDSA,
141             SCRAMBLING_MODE_MULTI2, SCRAMBLING_MODE_AES128, SCRAMBLING_MODE_AES_ECB,
142             SCRAMBLING_MODE_AES_SCTE52, SCRAMBLING_MODE_TDES_ECB, SCRAMBLING_MODE_TDES_SCTE52})
143     @Retention(RetentionPolicy.SOURCE)
144     public @interface ScramblingMode {}
145 
146     /**
147      * DVB (Digital Video Broadcasting) reserved mode.
148      */
149     public static final int SCRAMBLING_MODE_RESERVED =
150             android.hardware.cas.V1_2.ScramblingMode.RESERVED;
151     /**
152      * DVB (Digital Video Broadcasting) Common Scrambling Algorithm (CSA) 1.
153      */
154     public static final int SCRAMBLING_MODE_DVB_CSA1 =
155             android.hardware.cas.V1_2.ScramblingMode.DVB_CSA1;
156     /**
157      * DVB CSA 2.
158      */
159     public static final int SCRAMBLING_MODE_DVB_CSA2 =
160             android.hardware.cas.V1_2.ScramblingMode.DVB_CSA2;
161     /**
162      * DVB CSA 3 in standard mode.
163      */
164     public static final int SCRAMBLING_MODE_DVB_CSA3_STANDARD =
165             android.hardware.cas.V1_2.ScramblingMode.DVB_CSA3_STANDARD;
166     /**
167      * DVB CSA 3 in minimally enhanced mode.
168      */
169     public static final int SCRAMBLING_MODE_DVB_CSA3_MINIMAL =
170             android.hardware.cas.V1_2.ScramblingMode.DVB_CSA3_MINIMAL;
171     /**
172      * DVB CSA 3 in fully enhanced mode.
173      */
174     public static final int SCRAMBLING_MODE_DVB_CSA3_ENHANCE =
175             android.hardware.cas.V1_2.ScramblingMode.DVB_CSA3_ENHANCE;
176     /**
177      * DVB Common IPTV Software-oriented Scrambling Algorithm (CISSA) Version 1.
178      */
179     public static final int SCRAMBLING_MODE_DVB_CISSA_V1 =
180             android.hardware.cas.V1_2.ScramblingMode.DVB_CISSA_V1;
181     /**
182      * ATIS-0800006 IIF Default Scrambling Algorithm (IDSA).
183      */
184     public static final int SCRAMBLING_MODE_DVB_IDSA =
185             android.hardware.cas.V1_2.ScramblingMode.DVB_IDSA;
186     /**
187      * A symmetric key algorithm.
188      */
189     public static final int SCRAMBLING_MODE_MULTI2 =
190             android.hardware.cas.V1_2.ScramblingMode.MULTI2;
191     /**
192      * Advanced Encryption System (AES) 128-bit Encryption mode.
193      */
194     public static final int SCRAMBLING_MODE_AES128 =
195             android.hardware.cas.V1_2.ScramblingMode.AES128;
196     /**
197      * Advanced Encryption System (AES) Electronic Code Book (ECB) mode.
198      */
199     public static final int SCRAMBLING_MODE_AES_ECB =
200             android.hardware.cas.V1_2.ScramblingMode.AES_ECB;
201     /**
202      * Advanced Encryption System (AES) Society of Cable Telecommunications Engineers (SCTE) 52
203      * mode.
204      */
205     public static final int SCRAMBLING_MODE_AES_SCTE52 =
206             android.hardware.cas.V1_2.ScramblingMode.AES_SCTE52;
207     /**
208      * Triple Data Encryption Algorithm (TDES) Electronic Code Book (ECB) mode.
209      */
210     public static final int SCRAMBLING_MODE_TDES_ECB =
211             android.hardware.cas.V1_2.ScramblingMode.TDES_ECB;
212     /**
213      * Triple Data Encryption Algorithm (TDES) Society of Cable Telecommunications Engineers (SCTE)
214      * 52 mode.
215      */
216     public static final int SCRAMBLING_MODE_TDES_SCTE52 =
217             android.hardware.cas.V1_2.ScramblingMode.TDES_SCTE52;
218 
219     /**
220      * Usages used to open cas sessions.
221      *
222      * @hide
223      */
224     @IntDef(prefix = "SESSION_USAGE_",
225             value = {SESSION_USAGE_LIVE, SESSION_USAGE_PLAYBACK, SESSION_USAGE_RECORD,
226             SESSION_USAGE_TIMESHIFT})
227     @Retention(RetentionPolicy.SOURCE)
228     public @interface SessionUsage {}
229     /**
230      * Cas session is used to descramble live streams.
231      */
232     public static final int SESSION_USAGE_LIVE = android.hardware.cas.V1_2.SessionIntent.LIVE;
233     /**
234      * Cas session is used to descramble recoreded streams.
235      */
236     public static final int SESSION_USAGE_PLAYBACK =
237             android.hardware.cas.V1_2.SessionIntent.PLAYBACK;
238     /**
239      * Cas session is used to descramble live streams and encrypt local recorded content
240      */
241     public static final int SESSION_USAGE_RECORD = android.hardware.cas.V1_2.SessionIntent.RECORD;
242     /**
243      * Cas session is used to descramble live streams , encrypt local recorded content and playback
244      * local encrypted content.
245      */
246     public static final int SESSION_USAGE_TIMESHIFT =
247             android.hardware.cas.V1_2.SessionIntent.TIMESHIFT;
248 
249     /**
250      * Plugin status events sent from cas system.
251      *
252      * @hide
253      */
254     @IntDef(prefix = "PLUGIN_STATUS_",
255             value = {PLUGIN_STATUS_PHYSICAL_MODULE_CHANGED, PLUGIN_STATUS_SESSION_NUMBER_CHANGED})
256     @Retention(RetentionPolicy.SOURCE)
257     public @interface PluginStatus {}
258 
259     /**
260      * The event to indicate that the status of CAS system is changed by the removal or insertion of
261      * physical CAS modules.
262      */
263     public static final int PLUGIN_STATUS_PHYSICAL_MODULE_CHANGED =
264             android.hardware.cas.V1_2.StatusEvent.PLUGIN_PHYSICAL_MODULE_CHANGED;
265     /**
266      * The event to indicate that the number of CAS system's session is changed.
267      */
268     public static final int PLUGIN_STATUS_SESSION_NUMBER_CHANGED =
269             android.hardware.cas.V1_2.StatusEvent.PLUGIN_SESSION_NUMBER_CHANGED;
270 
271     private static final Singleton<IMediaCasService> sService = new Singleton<IMediaCasService>() {
272         @Override
273         protected IMediaCasService create() {
274             try {
275                 Log.d(TAG, "Trying to get cas@1.2 service");
276                 android.hardware.cas.V1_2.IMediaCasService serviceV12 =
277                         android.hardware.cas.V1_2.IMediaCasService.getService(true /*wait*/);
278                 if (serviceV12 != null) {
279                     return serviceV12;
280                 }
281             } catch (Exception eV1_2) {
282                 Log.d(TAG, "Failed to get cas@1.2 service");
283             }
284 
285             try {
286                     Log.d(TAG, "Trying to get cas@1.1 service");
287                     android.hardware.cas.V1_1.IMediaCasService serviceV11 =
288                             android.hardware.cas.V1_1.IMediaCasService.getService(true /*wait*/);
289                     if (serviceV11 != null) {
290                         return serviceV11;
291                     }
292             } catch (Exception eV1_1) {
293                 Log.d(TAG, "Failed to get cas@1.1 service");
294             }
295 
296             try {
297                 Log.d(TAG, "Trying to get cas@1.0 service");
298                 return IMediaCasService.getService(true /*wait*/);
299             } catch (Exception eV1_0) {
300                 Log.d(TAG, "Failed to get cas@1.0 service");
301             }
302 
303             return null;
304         }
305     };
306 
getService()307     static IMediaCasService getService() {
308         return sService.get();
309     }
310 
validateInternalStates()311     private void validateInternalStates() {
312         if (mICas == null) {
313             throw new IllegalStateException();
314         }
315     }
316 
cleanupAndRethrowIllegalState()317     private void cleanupAndRethrowIllegalState() {
318         mICas = null;
319         mICasV11 = null;
320         mICasV12 = null;
321         throw new IllegalStateException();
322     }
323 
324     private class EventHandler extends Handler {
325 
326         private static final int MSG_CAS_EVENT = 0;
327         private static final int MSG_CAS_SESSION_EVENT = 1;
328         private static final int MSG_CAS_STATUS_EVENT = 2;
329         private static final int MSG_CAS_RESOURCE_LOST = 3;
330         private static final String SESSION_KEY = "sessionId";
331         private static final String DATA_KEY = "data";
332 
EventHandler(Looper looper)333         public EventHandler(Looper looper) {
334             super(looper);
335         }
336 
337         @Override
handleMessage(Message msg)338         public void handleMessage(Message msg) {
339             if (msg.what == MSG_CAS_EVENT) {
340                 mListener.onEvent(MediaCas.this, msg.arg1, msg.arg2,
341                         toBytes((ArrayList<Byte>) msg.obj));
342             } else if (msg.what == MSG_CAS_SESSION_EVENT) {
343                 Bundle bundle = msg.getData();
344                 ArrayList<Byte> sessionId = toByteArray(bundle.getByteArray(SESSION_KEY));
345                 mListener.onSessionEvent(MediaCas.this,
346                         createFromSessionId(sessionId), msg.arg1, msg.arg2,
347                         bundle.getByteArray(DATA_KEY));
348             } else if (msg.what == MSG_CAS_STATUS_EVENT) {
349                 if ((msg.arg1 == PLUGIN_STATUS_SESSION_NUMBER_CHANGED)
350                         && (mTunerResourceManager != null)) {
351                     mTunerResourceManager.updateCasInfo(mCasSystemId, msg.arg2);
352                 }
353                 mListener.onPluginStatusUpdate(MediaCas.this, msg.arg1, msg.arg2);
354             } else if (msg.what == MSG_CAS_RESOURCE_LOST) {
355                 mListener.onResourceLost(MediaCas.this);
356             }
357         }
358     }
359 
360     private final ICasListener.Stub mBinder = new ICasListener.Stub() {
361         @Override
362         public void onEvent(int event, int arg, @Nullable ArrayList<Byte> data)
363                 throws RemoteException {
364             if (mEventHandler != null) {
365                 mEventHandler.sendMessage(mEventHandler.obtainMessage(
366                     EventHandler.MSG_CAS_EVENT, event, arg, data));
367             }
368         }
369         @Override
370         public void onSessionEvent(@NonNull ArrayList<Byte> sessionId,
371                 int event, int arg, @Nullable ArrayList<Byte> data)
372                 throws RemoteException {
373             if (mEventHandler != null) {
374                 Message msg = mEventHandler.obtainMessage();
375                 msg.what = EventHandler.MSG_CAS_SESSION_EVENT;
376                 msg.arg1 = event;
377                 msg.arg2 = arg;
378                 Bundle bundle = new Bundle();
379                 bundle.putByteArray(EventHandler.SESSION_KEY, toBytes(sessionId));
380                 bundle.putByteArray(EventHandler.DATA_KEY, toBytes(data));
381                 msg.setData(bundle);
382                 mEventHandler.sendMessage(msg);
383             }
384         }
385         @Override
386         public void onStatusUpdate(byte status, int arg)
387                 throws RemoteException {
388             if (mEventHandler != null) {
389                 mEventHandler.sendMessage(mEventHandler.obtainMessage(
390                     EventHandler.MSG_CAS_STATUS_EVENT, status, arg));
391             }
392         }
393     };
394 
395     private final TunerResourceManager.ResourcesReclaimListener mResourceListener =
396             new TunerResourceManager.ResourcesReclaimListener() {
397             @Override
398             public void onReclaimResources() {
399                 synchronized (mSessionMap) {
400                     List<Session> sessionList = new ArrayList<>(mSessionMap.keySet());
401                     for (Session casSession: sessionList) {
402                         casSession.close();
403                     }
404                 }
405                 mEventHandler.sendMessage(mEventHandler.obtainMessage(
406                         EventHandler.MSG_CAS_RESOURCE_LOST));
407             }
408         };
409 
410     /**
411      * Describe a CAS plugin with its CA_system_ID and string name.
412      *
413      * Returned as results of {@link #enumeratePlugins}.
414      *
415      */
416     public static class PluginDescriptor {
417         private final int mCASystemId;
418         private final String mName;
419 
PluginDescriptor()420         private PluginDescriptor() {
421             mCASystemId = 0xffff;
422             mName = null;
423         }
424 
PluginDescriptor(@onNull HidlCasPluginDescriptor descriptor)425         PluginDescriptor(@NonNull HidlCasPluginDescriptor descriptor) {
426             mCASystemId = descriptor.caSystemId;
427             mName = descriptor.name;
428         }
429 
getSystemId()430         public int getSystemId() {
431             return mCASystemId;
432         }
433 
434         @NonNull
getName()435         public String getName() {
436             return mName;
437         }
438 
439         @Override
toString()440         public String toString() {
441             return "PluginDescriptor {" + mCASystemId + ", " + mName + "}";
442         }
443     }
444 
toByteArray(@onNull byte[] data, int offset, int length)445     private ArrayList<Byte> toByteArray(@NonNull byte[] data, int offset, int length) {
446         ArrayList<Byte> byteArray = new ArrayList<Byte>(length);
447         for (int i = 0; i < length; i++) {
448             byteArray.add(Byte.valueOf(data[offset + i]));
449         }
450         return byteArray;
451     }
452 
toByteArray(@ullable byte[] data)453     private ArrayList<Byte> toByteArray(@Nullable byte[] data) {
454         if (data == null) {
455             return new ArrayList<Byte>();
456         }
457         return toByteArray(data, 0, data.length);
458     }
459 
toBytes(@onNull ArrayList<Byte> byteArray)460     private byte[] toBytes(@NonNull ArrayList<Byte> byteArray) {
461         byte[] data = null;
462         if (byteArray != null) {
463             data = new byte[byteArray.size()];
464             for (int i = 0; i < data.length; i++) {
465                 data[i] = byteArray.get(i);
466             }
467         }
468         return data;
469     }
470     /**
471      * Class for an open session with the CA system.
472      */
473     public final class Session implements AutoCloseable {
474         final ArrayList<Byte> mSessionId;
475         boolean mIsClosed = false;
476 
Session(@onNull ArrayList<Byte> sessionId)477         Session(@NonNull ArrayList<Byte> sessionId) {
478             mSessionId = new ArrayList<Byte>(sessionId);
479         }
480 
validateSessionInternalStates()481         private void validateSessionInternalStates() {
482             if (mICas == null) {
483                 throw new IllegalStateException();
484             }
485             if (mIsClosed) {
486                 MediaCasStateException.throwExceptionIfNeeded(Status.ERROR_CAS_SESSION_NOT_OPENED);
487             }
488         }
489 
490         /**
491          * Query if an object equal current Session object.
492          *
493          * @param obj an object to compare to current Session object.
494          *
495          * @return Whether input object equal current Session object.
496          */
equals(Object obj)497         public boolean equals(Object obj) {
498             if (obj instanceof Session) {
499                 return mSessionId.equals(((Session) obj).mSessionId);
500             }
501             return false;
502         }
503 
504         /**
505          * Set the private data for a session.
506          *
507          * @param data byte array of the private data.
508          *
509          * @throws IllegalStateException if the MediaCas instance is not valid.
510          * @throws MediaCasException for CAS-specific errors.
511          * @throws MediaCasStateException for CAS-specific state exceptions.
512          */
setPrivateData(@onNull byte[] data)513         public void setPrivateData(@NonNull byte[] data)
514                 throws MediaCasException {
515             validateSessionInternalStates();
516 
517             try {
518                 MediaCasException.throwExceptionIfNeeded(
519                         mICas.setSessionPrivateData(mSessionId, toByteArray(data, 0, data.length)));
520             } catch (RemoteException e) {
521                 cleanupAndRethrowIllegalState();
522             }
523         }
524 
525 
526         /**
527          * Send a received ECM packet to the specified session of the CA system.
528          *
529          * @param data byte array of the ECM data.
530          * @param offset position within data where the ECM data begins.
531          * @param length length of the data (starting from offset).
532          *
533          * @throws IllegalStateException if the MediaCas instance is not valid.
534          * @throws MediaCasException for CAS-specific errors.
535          * @throws MediaCasStateException for CAS-specific state exceptions.
536          */
processEcm(@onNull byte[] data, int offset, int length)537         public void processEcm(@NonNull byte[] data, int offset, int length)
538                 throws MediaCasException {
539             validateSessionInternalStates();
540 
541             try {
542                 MediaCasException.throwExceptionIfNeeded(
543                         mICas.processEcm(mSessionId, toByteArray(data, offset, length)));
544             } catch (RemoteException e) {
545                 cleanupAndRethrowIllegalState();
546             }
547         }
548 
549         /**
550          * Send a received ECM packet to the specified session of the CA system.
551          * This is similar to {@link Session#processEcm(byte[], int, int)}
552          * except that the entire byte array is sent.
553          *
554          * @param data byte array of the ECM data.
555          *
556          * @throws IllegalStateException if the MediaCas instance is not valid.
557          * @throws MediaCasException for CAS-specific errors.
558          * @throws MediaCasStateException for CAS-specific state exceptions.
559          */
processEcm(@onNull byte[] data)560         public void processEcm(@NonNull byte[] data) throws MediaCasException {
561             processEcm(data, 0, data.length);
562         }
563 
564         /**
565          * Send a session event to a CA system. The format of the event is
566          * scheme-specific and is opaque to the framework.
567          *
568          * @param event an integer denoting a scheme-specific event to be sent.
569          * @param arg a scheme-specific integer argument for the event.
570          * @param data a byte array containing scheme-specific data for the event.
571          *
572          * @throws IllegalStateException if the MediaCas instance is not valid.
573          * @throws MediaCasException for CAS-specific errors.
574          * @throws MediaCasStateException for CAS-specific state exceptions.
575          */
sendSessionEvent(int event, int arg, @Nullable byte[] data)576         public void sendSessionEvent(int event, int arg, @Nullable byte[] data)
577                 throws MediaCasException {
578             validateSessionInternalStates();
579 
580             if (mICasV11 == null) {
581                 Log.d(TAG, "Send Session Event isn't supported by cas@1.0 interface");
582                 throw new UnsupportedCasException("Send Session Event is not supported");
583             }
584 
585             try {
586                 MediaCasException.throwExceptionIfNeeded(
587                         mICasV11.sendSessionEvent(mSessionId, event, arg, toByteArray(data)));
588             } catch (RemoteException e) {
589                 cleanupAndRethrowIllegalState();
590             }
591         }
592 
593         /**
594          * Get Session Id.
595          *
596          * @return session Id of the session.
597          *
598          * @throws IllegalStateException if the MediaCas instance is not valid.
599          */
600         @NonNull
getSessionId()601         public byte[] getSessionId() {
602             validateSessionInternalStates();
603             return toBytes(mSessionId);
604         }
605 
606         /**
607          * Close the session.
608          *
609          * @throws IllegalStateException if the MediaCas instance is not valid.
610          * @throws MediaCasStateException for CAS-specific state exceptions.
611          */
612         @Override
close()613         public void close() {
614             validateSessionInternalStates();
615             try {
616                 MediaCasStateException.throwExceptionIfNeeded(
617                         mICas.closeSession(mSessionId));
618                 mIsClosed = true;
619                 removeSessionFromResourceMap(this);
620             } catch (RemoteException e) {
621                 cleanupAndRethrowIllegalState();
622             }
623         }
624     }
625 
createFromSessionId(@onNull ArrayList<Byte> sessionId)626     Session createFromSessionId(@NonNull ArrayList<Byte> sessionId) {
627         if (sessionId == null || sessionId.size() == 0) {
628             return null;
629         }
630         return new Session(sessionId);
631     }
632 
633     /**
634      * Query if a certain CA system is supported on this device.
635      *
636      * @param CA_system_id the id of the CA system.
637      *
638      * @return Whether the specified CA system is supported on this device.
639      */
isSystemIdSupported(int CA_system_id)640     public static boolean isSystemIdSupported(int CA_system_id) {
641         IMediaCasService service = getService();
642 
643         if (service != null) {
644             try {
645                 return service.isSystemIdSupported(CA_system_id);
646             } catch (RemoteException e) {
647             }
648         }
649         return false;
650     }
651 
652     /**
653      * List all available CA plugins on the device.
654      *
655      * @return an array of descriptors for the available CA plugins.
656      */
enumeratePlugins()657     public static PluginDescriptor[] enumeratePlugins() {
658         IMediaCasService service = getService();
659 
660         if (service != null) {
661             try {
662                 ArrayList<HidlCasPluginDescriptor> descriptors =
663                         service.enumeratePlugins();
664                 if (descriptors.size() == 0) {
665                     return null;
666                 }
667                 PluginDescriptor[] results = new PluginDescriptor[descriptors.size()];
668                 for (int i = 0; i < results.length; i++) {
669                     results[i] = new PluginDescriptor(descriptors.get(i));
670                 }
671                 return results;
672             } catch (RemoteException e) {
673             }
674         }
675         return null;
676     }
677 
createPlugin(int casSystemId)678     private void createPlugin(int casSystemId) throws UnsupportedCasException {
679         try {
680             mCasSystemId = casSystemId;
681             mUserId = Process.myUid();
682             IMediaCasService service = getService();
683             android.hardware.cas.V1_2.IMediaCasService serviceV12 =
684                     android.hardware.cas.V1_2.IMediaCasService.castFrom(service);
685             if (serviceV12 == null) {
686                 android.hardware.cas.V1_1.IMediaCasService serviceV11 =
687                     android.hardware.cas.V1_1.IMediaCasService.castFrom(service);
688                 if (serviceV11 == null) {
689                     Log.d(TAG, "Used cas@1_0 interface to create plugin");
690                     mICas = service.createPlugin(casSystemId, mBinder);
691                 } else {
692                     Log.d(TAG, "Used cas@1.1 interface to create plugin");
693                     mICas = mICasV11 = serviceV11.createPluginExt(casSystemId, mBinder);
694                 }
695             } else {
696                 Log.d(TAG, "Used cas@1.2 interface to create plugin");
697                 mICas = mICasV11 = mICasV12 =
698                     android.hardware.cas.V1_2.ICas
699                         .castFrom(serviceV12.createPluginExt(casSystemId, mBinder));
700             }
701         } catch(Exception e) {
702             Log.e(TAG, "Failed to create plugin: " + e);
703             mICas = null;
704         } finally {
705             if (mICas == null) {
706                 throw new UnsupportedCasException(
707                     "Unsupported casSystemId " + casSystemId);
708             }
709         }
710     }
711 
registerClient(@onNull Context context, @Nullable String tvInputServiceSessionId, @PriorityHintUseCaseType int priorityHint)712     private void registerClient(@NonNull Context context,
713             @Nullable String tvInputServiceSessionId,  @PriorityHintUseCaseType int priorityHint)  {
714 
715         mTunerResourceManager = (TunerResourceManager)
716             context.getSystemService(Context.TV_TUNER_RESOURCE_MGR_SERVICE);
717         if (mTunerResourceManager != null) {
718             int[] clientId = new int[1];
719             ResourceClientProfile profile = new ResourceClientProfile();
720             profile.tvInputSessionId = tvInputServiceSessionId;
721             profile.useCase = priorityHint;
722             mTunerResourceManager.registerClientProfile(
723                     profile, context.getMainExecutor(), mResourceListener, clientId);
724             mClientId = clientId[0];
725         }
726     }
727     /**
728      * Instantiate a CA system of the specified system id.
729      *
730      * @param casSystemId The system id of the CA system.
731      *
732      * @throws UnsupportedCasException if the device does not support the
733      * specified CA system.
734      */
MediaCas(int casSystemId)735     public MediaCas(int casSystemId) throws UnsupportedCasException {
736         createPlugin(casSystemId);
737     }
738 
739     /**
740      * Instantiate a CA system of the specified system id.
741      *
742      * @param context the context of the caller.
743      * @param casSystemId The system id of the CA system.
744      * @param tvInputServiceSessionId The Id of the session opened in TV Input Service (TIS)
745      *        {@link android.media.tv.TvInputService#onCreateSession(String, String)}
746      * @param priorityHint priority hint from the use case type for new created CAS system.
747      *
748      * @throws UnsupportedCasException if the device does not support the
749      * specified CA system.
750      */
MediaCas(@onNull Context context, int casSystemId, @Nullable String tvInputServiceSessionId, @PriorityHintUseCaseType int priorityHint)751     public MediaCas(@NonNull Context context, int casSystemId,
752             @Nullable String tvInputServiceSessionId,
753             @PriorityHintUseCaseType int priorityHint) throws UnsupportedCasException {
754         Objects.requireNonNull(context, "context must not be null");
755         createPlugin(casSystemId);
756         registerClient(context, tvInputServiceSessionId, priorityHint);
757     }
758     /**
759      * Instantiate a CA system of the specified system id with EvenListener.
760      *
761      * @param context the context of the caller.
762      * @param casSystemId The system id of the CA system.
763      * @param tvInputServiceSessionId The Id of the session opened in TV Input Service (TIS)
764      *        {@link android.media.tv.TvInputService#onCreateSession(String, String)}
765      * @param priorityHint priority hint from the use case type for new created CAS system.
766      * @param listener the event listener to be set.
767      * @param handler the handler whose looper the event listener will be called on.
768      * If handler is null, we'll try to use current thread's looper, or the main
769      * looper. If neither are available, an internal thread will be created instead.
770      *
771      * @throws UnsupportedCasException if the device does not support the
772      * specified CA system.
773      */
MediaCas(@onNull Context context, int casSystemId, @Nullable String tvInputServiceSessionId, @PriorityHintUseCaseType int priorityHint, @Nullable Handler handler, @Nullable EventListener listener)774     public MediaCas(@NonNull Context context, int casSystemId,
775             @Nullable String tvInputServiceSessionId,
776             @PriorityHintUseCaseType int priorityHint,
777             @Nullable Handler handler, @Nullable EventListener listener)
778             throws UnsupportedCasException {
779         Objects.requireNonNull(context, "context must not be null");
780         setEventListener(listener, handler);
781         createPlugin(casSystemId);
782         registerClient(context, tvInputServiceSessionId, priorityHint);
783     }
784 
getBinder()785     IHwBinder getBinder() {
786         validateInternalStates();
787 
788         return mICas.asBinder();
789     }
790 
791     /**
792      * An interface registered by the caller to {@link #setEventListener}
793      * to receives scheme-specific notifications from a MediaCas instance.
794      */
795     public interface EventListener {
796 
797         /**
798          * Notify the listener of a scheme-specific event from the CA system.
799          *
800          * @param mediaCas the MediaCas object to receive this event.
801          * @param event an integer whose meaning is scheme-specific.
802          * @param arg an integer whose meaning is scheme-specific.
803          * @param data a byte array of data whose format and meaning are
804          * scheme-specific.
805          */
onEvent(@onNull MediaCas mediaCas, int event, int arg, @Nullable byte[] data)806         void onEvent(@NonNull MediaCas mediaCas, int event, int arg, @Nullable byte[] data);
807 
808         /**
809          * Notify the listener of a scheme-specific session event from CA system.
810          *
811          * @param mediaCas the MediaCas object to receive this event.
812          * @param session session object which the event is for.
813          * @param event an integer whose meaning is scheme-specific.
814          * @param arg an integer whose meaning is scheme-specific.
815          * @param data a byte array of data whose format and meaning are
816          * scheme-specific.
817          */
onSessionEvent(@onNull MediaCas mediaCas, @NonNull Session session, int event, int arg, @Nullable byte[] data)818         default void onSessionEvent(@NonNull MediaCas mediaCas, @NonNull Session session,
819                 int event, int arg, @Nullable byte[] data) {
820             Log.d(TAG, "Received MediaCas Session event");
821         }
822 
823         /**
824          * Notify the listener that the cas plugin status is updated.
825          *
826          * @param mediaCas the MediaCas object to receive this event.
827          * @param status the plugin status which is updated.
828          * @param arg an integer whose meaning is specific to the status to be updated.
829          */
onPluginStatusUpdate(@onNull MediaCas mediaCas, @PluginStatus int status, int arg)830         default void onPluginStatusUpdate(@NonNull MediaCas mediaCas, @PluginStatus int status,
831                 int arg) {
832             Log.d(TAG, "Received MediaCas Plugin Status event");
833         }
834 
835         /**
836          * Notify the listener that the session resources was lost.
837          *
838          * @param mediaCas the MediaCas object to receive this event.
839          */
onResourceLost(@onNull MediaCas mediaCas)840         default void onResourceLost(@NonNull MediaCas mediaCas) {
841             Log.d(TAG, "Received MediaCas Resource Reclaim event");
842         }
843     }
844 
845     /**
846      * Set an event listener to receive notifications from the MediaCas instance.
847      *
848      * @param listener the event listener to be set.
849      * @param handler the handler whose looper the event listener will be called on.
850      * If handler is null, we'll try to use current thread's looper, or the main
851      * looper. If neither are available, an internal thread will be created instead.
852      */
setEventListener( @ullable EventListener listener, @Nullable Handler handler)853     public void setEventListener(
854             @Nullable EventListener listener, @Nullable Handler handler) {
855         mListener = listener;
856 
857         if (mListener == null) {
858             mEventHandler = null;
859             return;
860         }
861 
862         Looper looper = (handler != null) ? handler.getLooper() : null;
863         if (looper == null
864                 && (looper = Looper.myLooper()) == null
865                 && (looper = Looper.getMainLooper()) == null) {
866             if (mHandlerThread == null || !mHandlerThread.isAlive()) {
867                 mHandlerThread = new HandlerThread("MediaCasEventThread",
868                         Process.THREAD_PRIORITY_FOREGROUND);
869                 mHandlerThread.start();
870             }
871             looper = mHandlerThread.getLooper();
872         }
873         mEventHandler = new EventHandler(looper);
874     }
875 
876     /**
877      * Send the private data for the CA system.
878      *
879      * @param data byte array of the private data.
880      *
881      * @throws IllegalStateException if the MediaCas instance is not valid.
882      * @throws MediaCasException for CAS-specific errors.
883      * @throws MediaCasStateException for CAS-specific state exceptions.
884      */
setPrivateData(@onNull byte[] data)885     public void setPrivateData(@NonNull byte[] data) throws MediaCasException {
886         validateInternalStates();
887 
888         try {
889             MediaCasException.throwExceptionIfNeeded(
890                     mICas.setPrivateData(toByteArray(data, 0, data.length)));
891         } catch (RemoteException e) {
892             cleanupAndRethrowIllegalState();
893         }
894     }
895 
896     private class OpenSessionCallback implements android.hardware.cas.V1_1.ICas.openSessionCallback{
897         public Session mSession;
898         public int mStatus;
899         @Override
onValues(int status, ArrayList<Byte> sessionId)900         public void onValues(int status, ArrayList<Byte> sessionId) {
901             mStatus = status;
902             mSession = createFromSessionId(sessionId);
903         }
904     }
905 
906     private class OpenSession_1_2_Callback implements
907             android.hardware.cas.V1_2.ICas.openSession_1_2Callback {
908 
909         public Session mSession;
910         public int mStatus;
911 
912         @Override
onValues(int status, ArrayList<Byte> sessionId)913         public void onValues(int status, ArrayList<Byte> sessionId) {
914             mStatus = status;
915             mSession = createFromSessionId(sessionId);
916         }
917     }
918 
getSessionResourceHandle()919     private int getSessionResourceHandle() throws MediaCasException {
920         validateInternalStates();
921 
922         int[] sessionResourceHandle = new int[1];
923         sessionResourceHandle[0] = -1;
924         if (mTunerResourceManager != null) {
925             CasSessionRequest casSessionRequest = new CasSessionRequest();
926             casSessionRequest.clientId = mClientId;
927             casSessionRequest.casSystemId = mCasSystemId;
928             if (!mTunerResourceManager
929                     .requestCasSession(casSessionRequest, sessionResourceHandle)) {
930                 throw new MediaCasException.InsufficientResourceException(
931                     "insufficient resource to Open Session");
932             }
933         }
934         return sessionResourceHandle[0];
935     }
936 
addSessionToResourceMap(Session session, int sessionResourceHandle)937     private void addSessionToResourceMap(Session session, int sessionResourceHandle) {
938 
939         if (sessionResourceHandle != TunerResourceManager.INVALID_RESOURCE_HANDLE) {
940             synchronized (mSessionMap) {
941                 mSessionMap.put(session, sessionResourceHandle);
942             }
943         }
944     }
945 
removeSessionFromResourceMap(Session session)946     private void removeSessionFromResourceMap(Session session) {
947 
948         synchronized (mSessionMap) {
949             if (mSessionMap.get(session) != null) {
950                 mTunerResourceManager.releaseCasSession(mSessionMap.get(session), mClientId);
951                 mSessionMap.remove(session);
952             }
953         }
954     }
955 
956     /**
957      * Open a session to descramble one or more streams scrambled by the
958      * conditional access system.
959      *
960      * <p>Tuner resource manager (TRM) uses the client priority value to decide whether it is able
961      * to get cas session resource if cas session resources is limited. If the client can't get the
962      * resource, this call returns {@link MediaCasException.InsufficientResourceException }.
963      *
964      * @return session the newly opened session.
965      *
966      * @throws IllegalStateException if the MediaCas instance is not valid.
967      * @throws MediaCasException for CAS-specific errors.
968      * @throws MediaCasStateException for CAS-specific state exceptions.
969      */
openSession()970     public Session openSession() throws MediaCasException {
971         int sessionResourceHandle = getSessionResourceHandle();
972 
973         try {
974             OpenSessionCallback cb = new OpenSessionCallback();
975             mICas.openSession(cb);
976             MediaCasException.throwExceptionIfNeeded(cb.mStatus);
977             addSessionToResourceMap(cb.mSession, sessionResourceHandle);
978             Log.d(TAG, "Write Stats Log for succeed to Open Session.");
979             FrameworkStatsLog
980                     .write(FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS, mUserId, mCasSystemId,
981                         FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS__STATE__SUCCEEDED);
982             return cb.mSession;
983         } catch (RemoteException e) {
984             cleanupAndRethrowIllegalState();
985         }
986         Log.d(TAG, "Write Stats Log for fail to Open Session.");
987         FrameworkStatsLog
988                 .write(FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS, mUserId, mCasSystemId,
989                     FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS__STATE__FAILED);
990         return null;
991     }
992 
993     /**
994      * Open a session with usage and scrambling information, so that descrambler can be configured
995      * to descramble one or more streams scrambled by the conditional access system.
996      *
997      * <p>Tuner resource manager (TRM) uses the client priority value to decide whether it is able
998      * to get cas session resource if cas session resources is limited. If the client can't get the
999      * resource, this call returns {@link MediaCasException.InsufficientResourceException}.
1000      *
1001      * @param sessionUsage used for the created session.
1002      * @param scramblingMode used for the created session.
1003      *
1004      * @return session the newly opened session.
1005      *
1006      * @throws IllegalStateException if the MediaCas instance is not valid.
1007      * @throws MediaCasException for CAS-specific errors.
1008      * @throws MediaCasStateException for CAS-specific state exceptions.
1009      */
1010     @Nullable
openSession(@essionUsage int sessionUsage, @ScramblingMode int scramblingMode)1011     public Session openSession(@SessionUsage int sessionUsage, @ScramblingMode int scramblingMode)
1012             throws MediaCasException {
1013         int sessionResourceHandle = getSessionResourceHandle();
1014 
1015         if (mICasV12 == null) {
1016             Log.d(TAG, "Open Session with scrambling mode is only supported by cas@1.2+ interface");
1017             throw new UnsupportedCasException("Open Session with scrambling mode is not supported");
1018         }
1019 
1020         try {
1021             OpenSession_1_2_Callback cb = new OpenSession_1_2_Callback();
1022             mICasV12.openSession_1_2(sessionUsage, scramblingMode, cb);
1023             MediaCasException.throwExceptionIfNeeded(cb.mStatus);
1024             addSessionToResourceMap(cb.mSession, sessionResourceHandle);
1025             Log.d(TAG, "Write Stats Log for succeed to Open Session.");
1026             FrameworkStatsLog
1027                     .write(FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS, mUserId, mCasSystemId,
1028                         FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS__STATE__SUCCEEDED);
1029             return cb.mSession;
1030         } catch (RemoteException e) {
1031             cleanupAndRethrowIllegalState();
1032         }
1033         Log.d(TAG, "Write Stats Log for fail to Open Session.");
1034         FrameworkStatsLog
1035                 .write(FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS, mUserId, mCasSystemId,
1036                     FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS__STATE__FAILED);
1037         return null;
1038     }
1039 
1040     /**
1041      * Send a received EMM packet to the CA system.
1042      *
1043      * @param data byte array of the EMM data.
1044      * @param offset position within data where the EMM data begins.
1045      * @param length length of the data (starting from offset).
1046      *
1047      * @throws IllegalStateException if the MediaCas instance is not valid.
1048      * @throws MediaCasException for CAS-specific errors.
1049      * @throws MediaCasStateException for CAS-specific state exceptions.
1050      */
processEmm(@onNull byte[] data, int offset, int length)1051     public void processEmm(@NonNull byte[] data, int offset, int length)
1052             throws MediaCasException {
1053         validateInternalStates();
1054 
1055         try {
1056             MediaCasException.throwExceptionIfNeeded(
1057                     mICas.processEmm(toByteArray(data, offset, length)));
1058         } catch (RemoteException e) {
1059             cleanupAndRethrowIllegalState();
1060         }
1061     }
1062 
1063     /**
1064      * Send a received EMM packet to the CA system. This is similar to
1065      * {@link #processEmm(byte[], int, int)} except that the entire byte
1066      * array is sent.
1067      *
1068      * @param data byte array of the EMM data.
1069      *
1070      * @throws IllegalStateException if the MediaCas instance is not valid.
1071      * @throws MediaCasException for CAS-specific errors.
1072      * @throws MediaCasStateException for CAS-specific state exceptions.
1073      */
processEmm(@onNull byte[] data)1074     public void processEmm(@NonNull byte[] data) throws MediaCasException {
1075         processEmm(data, 0, data.length);
1076     }
1077 
1078     /**
1079      * Send an event to a CA system. The format of the event is scheme-specific
1080      * and is opaque to the framework.
1081      *
1082      * @param event an integer denoting a scheme-specific event to be sent.
1083      * @param arg a scheme-specific integer argument for the event.
1084      * @param data a byte array containing scheme-specific data for the event.
1085      *
1086      * @throws IllegalStateException if the MediaCas instance is not valid.
1087      * @throws MediaCasException for CAS-specific errors.
1088      * @throws MediaCasStateException for CAS-specific state exceptions.
1089      */
sendEvent(int event, int arg, @Nullable byte[] data)1090     public void sendEvent(int event, int arg, @Nullable byte[] data)
1091             throws MediaCasException {
1092         validateInternalStates();
1093 
1094         try {
1095             MediaCasException.throwExceptionIfNeeded(
1096                     mICas.sendEvent(event, arg, toByteArray(data)));
1097         } catch (RemoteException e) {
1098             cleanupAndRethrowIllegalState();
1099         }
1100     }
1101 
1102    /**
1103      * Initiate a provisioning operation for a CA system.
1104      *
1105      * @param provisionString string containing information needed for the
1106      * provisioning operation, the format of which is scheme and implementation
1107      * specific.
1108      *
1109      * @throws IllegalStateException if the MediaCas instance is not valid.
1110      * @throws MediaCasException for CAS-specific errors.
1111      * @throws MediaCasStateException for CAS-specific state exceptions.
1112      */
provision(@onNull String provisionString)1113     public void provision(@NonNull String provisionString) throws MediaCasException {
1114         validateInternalStates();
1115 
1116         try {
1117             MediaCasException.throwExceptionIfNeeded(
1118                     mICas.provision(provisionString));
1119         } catch (RemoteException e) {
1120             cleanupAndRethrowIllegalState();
1121         }
1122     }
1123 
1124     /**
1125      * Notify the CA system to refresh entitlement keys.
1126      *
1127      * @param refreshType the type of the refreshment.
1128      * @param refreshData private data associated with the refreshment.
1129      *
1130      * @throws IllegalStateException if the MediaCas instance is not valid.
1131      * @throws MediaCasException for CAS-specific errors.
1132      * @throws MediaCasStateException for CAS-specific state exceptions.
1133      */
refreshEntitlements(int refreshType, @Nullable byte[] refreshData)1134     public void refreshEntitlements(int refreshType, @Nullable byte[] refreshData)
1135             throws MediaCasException {
1136         validateInternalStates();
1137 
1138         try {
1139             MediaCasException.throwExceptionIfNeeded(
1140                     mICas.refreshEntitlements(refreshType, toByteArray(refreshData)));
1141         } catch (RemoteException e) {
1142             cleanupAndRethrowIllegalState();
1143         }
1144     }
1145 
1146     /**
1147      * Release Cas session. This is primarily used as a test API for CTS.
1148      * @hide
1149      */
1150     @TestApi
forceResourceLost()1151     public void forceResourceLost() {
1152         if (mResourceListener != null) {
1153             mResourceListener.onReclaimResources();
1154         }
1155     }
1156 
1157     @Override
close()1158     public void close() {
1159         if (mICas != null) {
1160             try {
1161                 mICas.release();
1162             } catch (RemoteException e) {
1163             } finally {
1164                 mICas = null;
1165             }
1166         }
1167 
1168         if (mTunerResourceManager != null) {
1169             mTunerResourceManager.unregisterClientProfile(mClientId);
1170             mTunerResourceManager = null;
1171         }
1172 
1173         if (mHandlerThread != null) {
1174             mHandlerThread.quit();
1175             mHandlerThread = null;
1176         }
1177     }
1178 
1179     @Override
finalize()1180     protected void finalize() {
1181         close();
1182     }
1183 }
1184