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