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