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