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.cts;
18 
19 import android.media.MediaCas;
20 import android.media.MediaCas.PluginDescriptor;
21 import android.media.MediaCas.Session;
22 import android.media.MediaCasException;
23 import android.media.MediaCasException.UnsupportedCasException;
24 import android.media.MediaCasStateException;
25 import android.media.MediaCodec;
26 import android.media.MediaDescrambler;
27 import android.media.cts.R;
28 import android.os.Build;
29 import android.os.Handler;
30 import android.os.HandlerThread;
31 import android.platform.test.annotations.AppModeFull;
32 import android.platform.test.annotations.Presubmit;
33 import android.platform.test.annotations.RequiresDevice;
34 import android.test.AndroidTestCase;
35 import android.util.Log;
36 
37 import androidx.test.filters.SmallTest;
38 import androidx.test.InstrumentationRegistry;
39 
40 import com.android.compatibility.common.util.ApiLevelUtil;
41 import com.android.compatibility.common.util.MediaUtils;
42 import com.android.compatibility.common.util.PropertyUtil;
43 
44 import java.lang.ArrayIndexOutOfBoundsException;
45 import java.nio.ByteBuffer;
46 import java.util.Arrays;
47 import java.util.concurrent.CountDownLatch;
48 import java.util.concurrent.TimeUnit;
49 import java.util.regex.Matcher;
50 import java.util.regex.Pattern;
51 
52 @Presubmit
53 @SmallTest
54 @RequiresDevice
55 @AppModeFull(reason = "TODO: evaluate and port to instant")
56 public class MediaCasTest extends AndroidTestCase {
57     private static final String TAG = "MediaCasTest";
58 
59     // CA System Ids used for testing
60     private static final int sInvalidSystemId = 0;
61     private static final int sClearKeySystemId = 0xF6D8;
62     private static final int API_LEVEL_BEFORE_CAS_SESSION = 28;
63     private boolean mIsAtLeastR = ApiLevelUtil.isAtLeast(Build.VERSION_CODES.R);
64     private boolean mIsAtLeastS = ApiLevelUtil.isAtLeast(Build.VERSION_CODES.S);
65 
66     // ClearKey CAS/Descrambler test vectors
67     private static final String sProvisionStr =
68             "{                                                   " +
69             "  \"id\": 21140844,                                 " +
70             "  \"name\": \"Test Title\",                         " +
71             "  \"lowercase_organization_name\": \"Android\",     " +
72             "  \"asset_key\": {                                  " +
73             "  \"encryption_key\": \"nezAr3CHFrmBR9R8Tedotw==\"  " +
74             "  },                                                " +
75             "  \"cas_type\": 1,                                  " +
76             "  \"track_types\": [ ]                              " +
77             "}                                                   " ;
78 
79     private static final String sEcmBufferStr =
80             "00 00 01 f0 00 50 00 01  00 00 00 01 00 46 00 00" +
81             "00 02 00 00 00 00 00 01  00 00 27 10 02 00 01 77" +
82             "01 42 95 6c 0e e3 91 bc  fd 05 b1 60 4f 17 82 a4" +
83             "86 9b 23 56 00 01 00 00  00 01 00 00 27 10 02 00" +
84             "01 77 01 42 95 6c d7 43  62 f8 1c 62 19 05 c7 3a" +
85             "42 cd fd d9 13 48                               " ;
86 
87     private static final String sInputBufferStr =
88             "00 00 00 01 09 f0 00 00  00 01 67 42 c0 1e db 01" +
89             "40 16 ec 04 40 00 00 03  00 40 00 00 0f 03 c5 8b" +
90             "b8 00 00 00 01 68 ca 8c  b2 00 00 01 06 05 ff ff" +
91             "70 dc 45 e9 bd e6 d9 48  b7 96 2c d8 20 d9 23 ee" +
92             "ef 78 32 36 34 20 2d 20  63 6f 72 65 20 31 34 32" +
93             "20 2d 20 48 2e 32 36 34  2f 4d 50 45 47 2d 34 20" +
94             "41 56 43 20 63 6f 64 65  63 20 2d 20 43 6f 70 79" +
95             "6c 65 66 74 20 32 30 30  33 2d 32 30 31 34 20 2d" +
96             "20 68 74 74 70 3a 2f 2f  77 77 77 2e 76 69 64 65" +
97             "6f 6c 61 6e 2e 6f 72 67  2f 78 32 36 34 2e 68 74" +
98             "6d 6c 6e 45 21 82 38 f0  9d 7d 96 e6 94 ae e2 87" +
99             "8f 04 49 e5 f6 8c 8b 9a  10 18 ba 94 e9 22 31 04" +
100             "7e 60 5b c4 24 00 90 62  0d dc 85 74 75 78 d0 14" +
101             "08 cb 02 1d 7d 9d 34 e8  81 b9 f7 09 28 79 29 8d" +
102             "e3 14 ed 5f ca af f4 1c  49 15 e1 80 29 61 76 80" +
103             "43 f8 58 53 40 d7 31 6d  61 81 41 e9 77 9f 9c e1" +
104             "6d f2 ee d9 c8 67 d2 5f  48 73 e3 5c cd a7 45 58" +
105             "bb dd 28 1d 68 fc b4 c6  f6 92 f6 30 03 aa e4 32" +
106             "f6 34 51 4b 0f 8c f9 ac  98 22 fb 49 c8 bf ca 8c" +
107             "80 86 5d d7 a4 52 b1 d9  a6 04 4e b3 2d 1f b8 35" +
108             "cc 45 6d 9c 20 a7 a4 34  59 72 e3 ae ba 49 de d1" +
109             "aa ee 3d 77 fc 5d c6 1f  9d ac c2 15 66 b8 e1 54" +
110             "4e 74 93 db 9a 24 15 6e  20 a3 67 3e 5a 24 41 5e" +
111             "b0 e6 35 87 1b c8 7a f9  77 65 e0 01 f2 4c e4 2b" +
112             "a9 64 96 96 0b 46 ca ea  79 0e 78 a3 5f 43 fc 47" +
113             "6a 12 fa c4 33 0e 88 1c  19 3a 00 c3 4e b5 d8 fa" +
114             "8e f1 bc 3d b2 7e 50 8d  67 c3 6b ed e2 ea a6 1f" +
115             "25 24 7c 94 74 50 49 e3  c6 58 2e fd 28 b4 c6 73" +
116             "b1 53 74 27 94 5c df 69  b7 a1 d7 f5 d3 8a 2c 2d" +
117             "b4 5e 8a 16 14 54 64 6e  00 6b 11 59 8a 63 38 80" +
118             "76 c3 d5 59 f7 3f d2 fa  a5 ca 82 ff 4a 62 f0 e3" +
119             "42 f9 3b 38 27 8a 89 aa  50 55 4b 29 f1 46 7c 75" +
120             "ef 65 af 9b 0d 6d da 25  94 14 c1 1b f0 c5 4c 24" +
121             "0e 65                                           " ;
122 
123     private static final String sExpectedOutputBufferStr =
124             "00 00 00 01 09 f0 00 00  00 01 67 42 c0 1e db 01" +
125             "40 16 ec 04 40 00 00 03  00 40 00 00 0f 03 c5 8b" +
126             "b8 00 00 00 01 68 ca 8c  b2 00 00 01 06 05 ff ff" +
127             "70 dc 45 e9 bd e6 d9 48  b7 96 2c d8 20 d9 23 ee" +
128             "ef 78 32 36 34 20 2d 20  63 6f 72 65 20 31 34 32" +
129             "20 2d 20 48 2e 32 36 34  2f 4d 50 45 47 2d 34 20" +
130             "41 56 43 20 63 6f 64 65  63 20 2d 20 43 6f 70 79" +
131             "6c 65 66 74 20 32 30 30  33 2d 32 30 31 34 20 2d" +
132             "20 68 74 74 70 3a 2f 2f  77 77 77 2e 76 69 64 65" +
133             "6f 6c 61 6e 2e 6f 72 67  2f 78 32 36 34 2e 68 74" +
134             "6d 6c 20 2d 20 6f 70 74  69 6f 6e 73 3a 20 63 61" +
135             "62 61 63 3d 30 20 72 65  66 3d 32 20 64 65 62 6c" +
136             "6f 63 6b 3d 31 3a 30 3a  30 20 61 6e 61 6c 79 73" +
137             "65 3d 30 78 31 3a 30 78  31 31 31 20 6d 65 3d 68" +
138             "65 78 20 73 75 62 6d 65  3d 37 20 70 73 79 3d 31" +
139             "20 70 73 79 5f 72 64 3d  31 2e 30 30 3a 30 2e 30" +
140             "30 20 6d 69 78 65 64 5f  72 65 66 3d 31 20 6d 65" +
141             "5f 72 61 6e 67 65 3d 31  36 20 63 68 72 6f 6d 61" +
142             "5f 6d 65 3d 31 20 74 72  65 6c 6c 69 73 3d 31 20" +
143             "38 78 38 64 63 74 3d 30  20 63 71 6d 3d 30 20 64" +
144             "65 61 64 7a 6f 6e 65 3d  32 31 2c 31 31 20 66 61" +
145             "73 74 5f 70 73 6b 69 70  3d 31 20 63 68 72 6f 6d" +
146             "61 5f 71 70 5f 6f 66 66  73 65 74 3d 2d 32 20 74" +
147             "68 72 65 61 64 73 3d 36  30 20 6c 6f 6f 6b 61 68" +
148             "65 61 64 5f 74 68 72 65  61 64 73 3d 35 20 73 6c" +
149             "69 63 65 64 5f 74 68 72  65 61 64 73 3d 30 20 6e" +
150             "72 3d 30 20 64 65 63 69  6d 61 74 65 3d 31 20 69" +
151             "6e 74 65 72 6c 61 63 65  64 3d 30 20 62 6c 75 72" +
152             "61 79 5f 63 6f 6d 70 61  74 3d 30 20 63 6f 6e 73" +
153             "74 72 61 69 6e 65 64 5f  69 6e 74 72 61 3d 30 20" +
154             "62 66 72 61 6d 65 73 3d  30 20 77 65 69 67 68 74" +
155             "70 3d 30 20 6b 65 79 69  6e 74 3d 32 35 30 20 6b" +
156             "65 79 69 6e 74 5f 6d 69  6e 3d 32 35 20 73 63 65" +
157             "6e 65                                           " ;
158 
159     @Override
setUp()160     public void setUp() throws Exception {
161         super.setUp();
162         // Need MANAGE_USERS or CREATE_USERS permission to access ActivityManager#getCurrentUser in
163         // MediaCas. It is used by all tests, then adopt it from shell in setup
164         InstrumentationRegistry
165             .getInstrumentation().getUiAutomation().adoptShellPermissionIdentity();
166     }
167 
168     @Override
tearDown()169     protected void tearDown() throws Exception {
170         InstrumentationRegistry
171             .getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
172         super.tearDown();
173     }
174     /**
175      * Test that all enumerated CA systems can be instantiated.
176      *
177      * Due to the vendor-proprietary nature of CAS, we cannot verify all operations
178      * of an arbitrary plugin. We can only verify that isSystemIdSupported() is
179      * consistent with the enumeration results, and all enumerated CA system ids can
180      * be instantiated.
181      */
testEnumeratePlugins()182     public void testEnumeratePlugins() throws Exception {
183         PluginDescriptor[] descriptors = MediaCas.enumeratePlugins();
184         for (int i = 0; i < descriptors.length; i++) {
185             Log.d(TAG, "desciptor[" + i + "]: id=" + descriptors[i].getSystemId()
186                     + ", name=" + descriptors[i].getName());
187             MediaCas mediaCas = null;
188             MediaDescrambler descrambler = null;
189             byte[] sessionId = null, streamSessionId = null;
190             try {
191                 final int CA_system_id = descriptors[i].getSystemId();
192                 if (!MediaCas.isSystemIdSupported(CA_system_id)) {
193                     fail("Enumerated " + descriptors[i] + " but is not supported.");
194                 }
195                 try {
196                     mediaCas = new MediaCas(CA_system_id);
197                 } catch (UnsupportedCasException e) {
198                     Log.d(TAG, "Enumerated " + descriptors[i]
199                         + " but cannot instantiate MediaCas.");
200                     throw new UnsupportedCasException(
201                         descriptors[i] + " is enumerated, but cannot instantiate" );
202                 }
203                 try {
204                     descrambler = new MediaDescrambler(CA_system_id);
205                 } catch (UnsupportedCasException e) {
206                     // The descrambler can be supported through Tuner since R.
207                     if (mIsAtLeastR) {
208                         Log.d(TAG, "Enumerated "
209                             + descriptors[i] + ", it doesn't support MediaDescrambler.");
210                     } else {
211                         fail("Enumerated " + descriptors[i]
212                             + " but cannot instantiate MediaDescrambler.");
213                     }
214                 }
215 
216                 // Should always accept a listener (even if the plugin doesn't use it)
217                 mediaCas.setEventListener(new MediaCas.EventListener() {
218                     @Override
219                     public void onEvent(MediaCas MediaCas, int event, int arg, byte[] data) {
220                         Log.d(TAG, "Received MediaCas event: "
221                                 + "event=" + event + ", arg=" + arg
222                                 + ", data=" + Arrays.toString(data));
223                     }
224                     @Override
225                     public void onSessionEvent(MediaCas MediaCas, MediaCas.Session session,
226                             int event, int arg, byte[] data) {
227                         Log.d(TAG, "Received MediaCas Session event: "
228                                 + "event=" + event + ", arg=" + arg
229                                 + ", data=" + Arrays.toString(data));
230                     }
231                 }, null);
232             } finally {
233                 if (mediaCas != null) {
234                     mediaCas.close();
235                 }
236                 if (descrambler != null) {
237                     descrambler.close();
238                 }
239             }
240         }
241     }
242 
testInvalidSystemIdFails()243     public void testInvalidSystemIdFails() throws Exception {
244         assertFalse("Invalid id " + sInvalidSystemId + " should not be supported",
245                 MediaCas.isSystemIdSupported(sInvalidSystemId));
246 
247         MediaCas unsupportedCAS = null;
248         MediaDescrambler unsupportedDescrambler = null;
249 
250         try {
251             try {
252                 unsupportedCAS = new MediaCas(sInvalidSystemId);
253                 fail("Shouldn't be able to create MediaCas with invalid id " + sInvalidSystemId);
254             } catch (UnsupportedCasException e) {
255                 // expected
256             }
257 
258             try {
259                 unsupportedDescrambler = new MediaDescrambler(sInvalidSystemId);
260                 fail("Shouldn't be able to create MediaDescrambler with invalid id " + sInvalidSystemId);
261             } catch (UnsupportedCasException e) {
262                 // expected
263             }
264         } finally {
265             if (unsupportedCAS != null) {
266                 unsupportedCAS.close();
267             }
268             if (unsupportedDescrambler != null) {
269                 unsupportedDescrambler.close();
270             }
271         }
272     }
273 
testClearKeyPluginInstalled()274     public void testClearKeyPluginInstalled() throws Exception {
275         PluginDescriptor[] descriptors = MediaCas.enumeratePlugins();
276         for (int i = 0; i < descriptors.length; i++) {
277             if (descriptors[i].getSystemId() == sClearKeySystemId) {
278                 return;
279             }
280         }
281         fail("ClearKey plugin " + String.format("0x%d", sClearKeySystemId) + " is not found");
282     }
283 
284     /**
285      * Test that valid call sequences succeed.
286      */
testClearKeyApis()287     public void testClearKeyApis() throws Exception {
288         MediaCas mediaCas = null;
289         MediaDescrambler descrambler = null;
290 
291         try {
292             if (mIsAtLeastR) {
293                 mediaCas = new MediaCas(getContext(), sClearKeySystemId, null,
294                     android.media.tv.TvInputService.PRIORITY_HINT_USE_CASE_TYPE_LIVE);
295             } else {
296                 mediaCas = new MediaCas(sClearKeySystemId);
297             }
298             descrambler = new MediaDescrambler(sClearKeySystemId);
299 
300             mediaCas.provision(sProvisionStr);
301 
302             byte[] pvtData = new byte[256];
303             mediaCas.setPrivateData(pvtData);
304 
305             Session session = mediaCas.openSession();
306             if (session == null) {
307                 fail("Can't open session for program");
308             }
309 
310             if (mIsAtLeastR) {
311                 Log.d(TAG, "Session Id = " + Arrays.toString(session.getSessionId()));
312             }
313 
314             session.setPrivateData(pvtData);
315 
316             Session streamSession = mediaCas.openSession();
317             if (streamSession == null) {
318                 fail("Can't open session for stream");
319             }
320             streamSession.setPrivateData(pvtData);
321 
322             descrambler.setMediaCasSession(session);
323 
324             descrambler.setMediaCasSession(streamSession);
325 
326             mediaCas.refreshEntitlements(3, null);
327 
328             byte[] refreshBytes = new byte[4];
329             refreshBytes[0] = 0;
330             refreshBytes[1] = 1;
331             refreshBytes[2] = 2;
332             refreshBytes[3] = 3;
333 
334             mediaCas.refreshEntitlements(10, refreshBytes);
335 
336             final HandlerThread thread = new HandlerThread("EventListenerHandlerThread");
337             thread.start();
338             Handler handler = new Handler(thread.getLooper());
339             testEventEcho(mediaCas, 1, 2, null /* data */, handler);
340             testSessionEventEcho(mediaCas, session, 1, 2, null /* data */, handler);
341             if (mIsAtLeastR) {
342                 testOpenSessionEcho(mediaCas, 0, 2, handler);
343             }
344             thread.interrupt();
345 
346             String eventDataString = "event data string";
347             byte[] eventData = eventDataString.getBytes();
348             testEventEcho(mediaCas, 3, 4, eventData, null /* handler */);
349             testSessionEventEcho(mediaCas, session, 3, 4, eventData, null /* handler */);
350 
351             String emm = "clear key emm";
352             byte[] emmData = emm.getBytes();
353             mediaCas.processEmm(emmData);
354 
355             byte[] ecmData = loadByteArrayFromString(sEcmBufferStr);
356             session.processEcm(ecmData);
357             streamSession.processEcm(ecmData);
358 
359             ByteBuffer outputBuf = descrambleTestInputBuffer(descrambler);
360             ByteBuffer expectedOutputBuf = ByteBuffer.wrap(
361                     loadByteArrayFromString(sExpectedOutputBufferStr));
362             assertTrue("Incorrect decryption result",
363                     expectedOutputBuf.compareTo(outputBuf) == 0);
364 
365             session.close();
366             streamSession.close();
367         } finally {
368             if (mediaCas != null) {
369                 mediaCas.close();
370             }
371             if (descrambler != null) {
372                 descrambler.close();
373             }
374         }
375     }
376 
377     /**
378      * Test that all sessions are closed after a MediaCas object is released.
379      */
testClearKeySessionClosedAfterRelease()380     public void testClearKeySessionClosedAfterRelease() throws Exception {
381         MediaCas mediaCas = null;
382         MediaDescrambler descrambler = null;
383 
384         try {
385             mediaCas = new MediaCas(sClearKeySystemId);
386             descrambler = new MediaDescrambler(sClearKeySystemId);
387             mediaCas.provision(sProvisionStr);
388 
389             Session session = mediaCas.openSession();
390             if (session == null) {
391                 fail("Can't open session for program");
392             }
393 
394             Session streamSession = mediaCas.openSession();
395             if (streamSession == null) {
396                 fail("Can't open session for stream");
397             }
398 
399             mediaCas.close();
400             mediaCas = null;
401 
402             try {
403                 descrambler.setMediaCasSession(session);
404                 fail("Program session not closed after MediaCas is released");
405             } catch (MediaCasStateException e) {
406                 Log.d(TAG, "setMediaCasSession throws "
407                         + e.getDiagnosticInfo() + " (as expected)");
408             }
409             try {
410                 descrambler.setMediaCasSession(streamSession);
411                 fail("Stream session not closed after MediaCas is released");
412             } catch (MediaCasStateException e) {
413                 Log.d(TAG, "setMediaCasSession throws "
414                         + e.getDiagnosticInfo() + " (as expected)");
415             }
416         } finally {
417             if (mediaCas != null) {
418                 mediaCas.close();
419             }
420             if (descrambler != null) {
421                 descrambler.close();
422             }
423         }
424     }
425 
426     /**
427      * Test that invalid call sequences fail with expected exceptions.
428      */
testClearKeyExceptions()429     public void testClearKeyExceptions() throws Exception {
430         MediaCas mediaCas = null;
431         MediaDescrambler descrambler = null;
432 
433         try {
434             mediaCas = new MediaCas(sClearKeySystemId);
435             descrambler = new MediaDescrambler(sClearKeySystemId);
436 
437             /*
438              * Test MediaCas exceptions
439              */
440 
441             // provision should fail with an invalid asset string
442             try {
443                 mediaCas.provision("invalid asset string");
444                 fail("provision shouldn't succeed with invalid asset");
445             } catch (MediaCasStateException e) {
446                 Log.d(TAG, "provision throws " + e.getDiagnosticInfo() + " (as expected)");
447             }
448 
449             // processEmm should reject invalid offset and length
450             String emm = "clear key emm";
451             byte[] emmData = emm.getBytes();
452             try {
453                 mediaCas.processEmm(emmData, 8, 40);
454             } catch (ArrayIndexOutOfBoundsException e) {
455                 Log.d(TAG, "processEmm throws ArrayIndexOutOfBoundsException (as expected)");
456             }
457 
458             // open a session, then close it so that it should become invalid
459             Session invalidSession = mediaCas.openSession();
460             if (invalidSession == null) {
461                 fail("Can't open session for program");
462             }
463             invalidSession.close();
464 
465             byte[] ecmData = loadByteArrayFromString(sEcmBufferStr);
466 
467             // processEcm should fail with an invalid session id
468             try {
469                 invalidSession.processEcm(ecmData);
470                 fail("processEcm shouldn't succeed with invalid session id");
471             } catch (MediaCasStateException e) {
472                 Log.d(TAG, "processEcm throws " + e.getDiagnosticInfo() + " (as expected)");
473             }
474 
475             Session session = mediaCas.openSession();
476             if (session == null) {
477                 fail("Can't open session for program");
478             }
479 
480             // processEcm should fail without provisioning
481             try {
482                 session.processEcm(ecmData);
483                 fail("processEcm shouldn't succeed without provisioning");
484             } catch (MediaCasException.NotProvisionedException e) {
485                 Log.d(TAG, "processEcm throws NotProvisionedException (as expected)");
486             }
487 
488             // Now provision it, and expect failures other than NotProvisionedException
489             mediaCas.provision(sProvisionStr);
490 
491             // processEcm should fail with ecm buffer that's too short
492             try {
493                 session.processEcm(ecmData, 0, 8);
494                 fail("processEcm shouldn't succeed with truncated ecm");
495             } catch (IllegalArgumentException e) {
496                 Log.d(TAG, "processEcm throws " + e.toString() + " (as expected)");
497             }
498 
499             // processEcm should fail with ecm with bad descriptor count
500             try {
501                 ecmData[17] = 3; // change the descriptor count field to 3 (invalid)
502                 session.processEcm(ecmData);
503                 fail("processEcm shouldn't succeed with altered descriptor count");
504             } catch (MediaCasStateException e) {
505                 Log.d(TAG, "processEcm throws " + e.getDiagnosticInfo() + " (as expected)");
506             }
507 
508             /*
509              * Test MediaDescrambler exceptions
510              */
511 
512             // setMediaCasSession should fail with an invalid session id
513             try {
514                 descrambler.setMediaCasSession(invalidSession);
515                 fail("setMediaCasSession shouldn't succeed with invalid session id");
516             } catch (MediaCasStateException e) {
517                 Log.d(TAG, "setMediaCasSession throws "
518                         + e.getDiagnosticInfo() + " (as expected)");
519             }
520 
521             // descramble should fail without a valid session
522             try {
523                 ByteBuffer outputBuf = descrambleTestInputBuffer(descrambler);
524                 fail("descramble should fail without a valid session");
525             } catch (MediaCasStateException e) {
526                 Log.d(TAG, "descramble throws " + e.getDiagnosticInfo() + " (as expected)");
527             }
528 
529             // Now set a valid session, should still fail because no valid ecm is processed
530             descrambler.setMediaCasSession(session);
531             try {
532                 ByteBuffer outputBuf = descrambleTestInputBuffer(descrambler);
533                 fail("descramble should fail without valid ecm");
534             } catch (MediaCasStateException e) {
535                 Log.d(TAG, "descramble throws " + e.getDiagnosticInfo() + " (as expected)");
536             }
537         } finally {
538             if (mediaCas != null) {
539                 mediaCas.close();
540             }
541             if (descrambler != null) {
542                 descrambler.close();
543             }
544         }
545     }
546 
547     /**
548      * Test Resource Lost Event.
549      */
testResourceLostEvent()550     public void testResourceLostEvent() throws Exception {
551         MediaCas mediaCas = null;
552         if (!MediaUtils.check(mIsAtLeastR, "test needs Android 11")) return;
553 
554         try {
555             mediaCas = new MediaCas(getContext(), sClearKeySystemId, null,
556                 android.media.tv.TvInputService.PRIORITY_HINT_USE_CASE_TYPE_LIVE);
557 
558             mediaCas.provision(sProvisionStr);
559 
560             byte[] pvtData = new byte[256];
561             mediaCas.setPrivateData(pvtData);
562 
563             Session session = mediaCas.openSession();
564             if (session == null) {
565                 fail("Can't open session for program");
566             }
567 
568             Session streamSession = mediaCas.openSession();
569             if (streamSession == null) {
570                 fail("Can't open session for stream");
571             }
572 
573             final HandlerThread thread = new HandlerThread("EventListenerHandlerThread");
574             thread.start();
575             Handler handler = new Handler(thread.getLooper());
576             testForceResourceLost(mediaCas, handler);
577             thread.interrupt();
578 
579         } finally {
580             if (mediaCas != null) {
581                 mediaCas.close();
582             }
583         }
584     }
585 
586     /**
587      * Test Set Event Listener in MediaCas Constructor.
588      */
testConstructWithEventListener()589     public void testConstructWithEventListener() throws Exception {
590         MediaCas mediaCas = null;
591         if (!MediaUtils.check(mIsAtLeastS, "test needs Android 12")) return;
592 
593         try {
594             TestEventListener listener = new TestEventListener();
595             HandlerThread thread = new HandlerThread("EventListenerHandlerThread");
596             thread.start();
597             Handler handler = new Handler(thread.getLooper());
598 
599             mediaCas = new MediaCas(getContext(), sClearKeySystemId, null,
600                 android.media.tv.TvInputService.PRIORITY_HINT_USE_CASE_TYPE_LIVE, handler,
601                 listener);
602 
603             thread.interrupt();
604 
605         } finally {
606             if (mediaCas != null) {
607                 mediaCas.close();
608             }
609         }
610     }
611 
612     private class TestEventListener implements MediaCas.EventListener {
613         private final CountDownLatch mLatch = new CountDownLatch(1);
614         private final MediaCas mMediaCas;
615         private final MediaCas.Session mSession;
616         private final int mEvent;
617         private final int mArg;
618         private final byte[] mData;
619         private boolean mIsIdential;
620 
TestEventListener()621         TestEventListener() {
622             mMediaCas = null;
623             mEvent = 0;
624             mArg = 0;
625             mData = null;
626             mSession = null;
627         }
628 
TestEventListener(MediaCas mediaCas, int event, int arg, byte[] data)629         TestEventListener(MediaCas mediaCas, int event, int arg, byte[] data) {
630             mMediaCas = mediaCas;
631             mEvent = event;
632             mArg = arg;
633             mData = data;
634             mSession = null;
635         }
636 
TestEventListener(MediaCas mediaCas, MediaCas.Session session, int event, int arg, byte[] data)637         TestEventListener(MediaCas mediaCas, MediaCas.Session session, int event,
638                 int arg, byte[] data) {
639             mMediaCas = mediaCas;
640             mSession = session;
641             mEvent = event;
642             mArg = arg;
643             mData = data;
644         }
645 
TestEventListener(MediaCas mediaCas, int intent, int scramblingMode)646         TestEventListener(MediaCas mediaCas, int intent, int scramblingMode) {
647             mMediaCas = mediaCas;
648             mEvent = intent;
649             mArg = scramblingMode;
650             mData = null;
651             mSession = null;
652         }
653 
TestEventListener(MediaCas mediaCas)654         TestEventListener(MediaCas mediaCas) {
655             mMediaCas = mediaCas;
656             mEvent = 0;
657             mArg = 0;
658             mData = null;
659             mSession = null;
660         }
661 
waitForResult()662         boolean waitForResult() {
663             try {
664                 if (!mLatch.await(1, TimeUnit.SECONDS)) {
665                     return false;
666                 }
667                 return mIsIdential;
668             } catch (InterruptedException e) {}
669             return false;
670         }
671 
672         @Override
onEvent(MediaCas mediaCas, int event, int arg, byte[] data)673         public void onEvent(MediaCas mediaCas, int event, int arg, byte[] data) {
674             Log.d(TAG, "Received MediaCas event: event=" + event
675                     + ", arg=" + arg + ", data=" + Arrays.toString(data));
676             if (mediaCas == mMediaCas && event == mEvent
677                     && arg == mArg && (Arrays.equals(data, mData) ||
678                             data == null && mData.length == 0 ||
679                             mData == null && data.length == 0)) {
680                 mIsIdential = true;
681             }
682             mLatch.countDown();
683         }
684 
685         @Override
onSessionEvent(MediaCas mediaCas, MediaCas.Session session, int event, int arg, byte[] data)686         public void onSessionEvent(MediaCas mediaCas, MediaCas.Session session,
687             int event, int arg, byte[] data) {
688             Log.d(TAG, "Received MediaCas session event: event=" + event
689                     + ", arg=" + arg + ", data=" + Arrays.toString(data));
690             if (mediaCas == mMediaCas && mSession.equals(session) && event == mEvent
691                     && arg == mArg && (Arrays.equals(data, mData) ||
692                             data == null && mData.length == 0 ||
693                             mData == null && data.length == 0)) {
694                 mIsIdential = true;
695             }
696             mLatch.countDown();
697         }
698 
699         @Override
onPluginStatusUpdate(MediaCas mediaCas, int statusUpdated, int arg)700         public void onPluginStatusUpdate(MediaCas mediaCas, int statusUpdated, int arg) {
701             Log.d(TAG, "Received MediaCas Status Update event");
702             if (mediaCas == mMediaCas && statusUpdated == mEvent && arg == mArg ) {
703                 mIsIdential = true;
704             }
705             mLatch.countDown();
706         }
707 
708         @Override
onResourceLost(MediaCas mediaCas)709         public void onResourceLost(MediaCas mediaCas) {
710             Log.d(TAG, "Received MediaCas Resource Lost event");
711             if (mediaCas == mMediaCas) {
712                 mIsIdential = true;
713             }
714             mLatch.countDown();
715         }
716     }
717 
718     // helper to send an event and wait for echo
testEventEcho(MediaCas mediaCas, int event, int arg, byte[] data, Handler handler)719     private void testEventEcho(MediaCas mediaCas, int event,
720             int arg, byte[] data, Handler handler) throws Exception {
721         TestEventListener listener = new TestEventListener(mediaCas, event, arg, data);
722         mediaCas.setEventListener(listener, handler);
723         mediaCas.sendEvent(event, arg, data);
724         assertTrue("Didn't receive event callback for " + event, listener.waitForResult());
725     }
726 
727     // helper to send an event and wait for echo
testSessionEventEcho(MediaCas mediaCas, MediaCas.Session session, int event, int arg, byte[] data, Handler handler)728     private void testSessionEventEcho(MediaCas mediaCas, MediaCas.Session session, int event,
729             int arg, byte[] data, Handler handler) throws Exception {
730         TestEventListener listener = new TestEventListener(mediaCas, session, event, arg, data);
731         mediaCas.setEventListener(listener, handler);
732         try {
733             session.sendSessionEvent(event, arg, data);
734         } catch (UnsupportedCasException e) {
735             if (!PropertyUtil.isVendorApiLevelNewerThan(API_LEVEL_BEFORE_CAS_SESSION)){
736                 Log.d(TAG, "Send Session Event isn't supported, Skipped this test case");
737                 return;
738             }
739             throw e;
740         }
741         assertTrue("Didn't receive session event callback for " + event, listener.waitForResult());
742     }
743 
744     // helper to open Session with scrambling mode and wait for echo for status change event
testOpenSessionEcho(MediaCas mediaCas, int intent, int scramblingMode, Handler handler)745     private void testOpenSessionEcho(MediaCas mediaCas, int intent, int scramblingMode,
746         Handler handler) throws Exception {
747         TestEventListener listener = new TestEventListener(mediaCas, intent, scramblingMode);
748         mediaCas.setEventListener(listener, handler);
749         try {
750             mediaCas.openSession(intent, scramblingMode);
751         } catch (UnsupportedCasException e) {
752             if (!PropertyUtil.isVendorApiLevelNewerThan(API_LEVEL_BEFORE_CAS_SESSION + 1)) {
753                 Log.d(TAG,
754                     "Opens Session with scramblingMode isn't supported, Skipped this test case");
755                 return;
756             }
757             throw e;
758         }
759         assertTrue("Didn't receive Echo from openSession with scrambling mode: " + scramblingMode,
760             listener.waitForResult());
761     }
762 
763     // helper to force to lose resource and wait for Resource Lost event
testForceResourceLost(MediaCas mediaCas, Handler handler)764     private void testForceResourceLost(MediaCas mediaCas, Handler handler) throws Exception {
765         TestEventListener listener = new TestEventListener(mediaCas);
766         mediaCas.setEventListener(listener, handler);
767         mediaCas.forceResourceLost();
768         assertTrue("Didn't receive Resource Lost event ", listener.waitForResult());
769     }
770 
771     // helper to descramble from the sample input (sInputBufferStr) and get output buffer
descrambleTestInputBuffer( MediaDescrambler descrambler)772     private ByteBuffer descrambleTestInputBuffer(
773             MediaDescrambler descrambler) throws Exception {
774         MediaCodec.CryptoInfo cryptoInfo = new MediaCodec.CryptoInfo();
775         int[] numBytesOfClearData     = new int[] { 162,   0,   0 };
776         int[] numBytesOfEncryptedData = new int[] {   0, 184, 184 };
777         byte[] key = new byte[16];
778         key[0] = 2; // scrambling mode = even key
779         byte[] iv = new byte[16]; // not used
780         cryptoInfo.set(3, numBytesOfClearData, numBytesOfEncryptedData,
781                 key, iv, MediaCodec.CRYPTO_MODE_AES_CBC);
782         ByteBuffer inputBuf = ByteBuffer.wrap(
783                 loadByteArrayFromString(sInputBufferStr));
784         ByteBuffer outputBuf = ByteBuffer.allocate(inputBuf.capacity());
785         descrambler.descramble(inputBuf, outputBuf, cryptoInfo);
786 
787         return outputBuf;
788     }
789 
790     // helper to load byte[] from a String
loadByteArrayFromString(final String str)791     private byte[] loadByteArrayFromString(final String str) {
792         Pattern pattern = Pattern.compile("[0-9a-fA-F]{2}");
793         Matcher matcher = pattern.matcher(str);
794         // allocate a large enough byte array first
795         byte[] tempArray = new byte[str.length() / 2];
796         int i = 0;
797         while (matcher.find()) {
798           tempArray[i++] = (byte)Integer.parseInt(matcher.group(), 16);
799         }
800         return Arrays.copyOfRange(tempArray, 0, i);
801     }
802 }
803