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