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; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.hardware.cas.V1_0.*; 22 import android.media.MediaCasException.*; 23 import android.os.Handler; 24 import android.os.HandlerThread; 25 import android.os.IHwBinder; 26 import android.os.Looper; 27 import android.os.Message; 28 import android.os.Process; 29 import android.os.RemoteException; 30 import android.util.Log; 31 import android.util.Singleton; 32 33 import java.util.ArrayList; 34 35 /** 36 * MediaCas can be used to obtain keys for descrambling protected media streams, in 37 * conjunction with {@link android.media.MediaDescrambler}. The MediaCas APIs are 38 * designed to support conditional access such as those in the ISO/IEC13818-1. 39 * The CA system is identified by a 16-bit integer CA_system_id. The scrambling 40 * algorithms are usually proprietary and implemented by vendor-specific CA plugins 41 * installed on the device. 42 * <p> 43 * The app is responsible for constructing a MediaCas object for the CA system it 44 * intends to use. The app can query if a certain CA system is supported using static 45 * method {@link #isSystemIdSupported}. It can also obtain the entire list of supported 46 * CA systems using static method {@link #enumeratePlugins}. 47 * <p> 48 * Once the MediaCas object is constructed, the app should properly provision it by 49 * using method {@link #provision} and/or {@link #processEmm}. The EMMs (Entitlement 50 * management messages) can be distributed out-of-band, or in-band with the stream. 51 * <p> 52 * To descramble elementary streams, the app first calls {@link #openSession} to 53 * generate a {@link Session} object that will uniquely identify a session. A session 54 * provides a context for subsequent key updates and descrambling activities. The ECMs 55 * (Entitlement control messages) are sent to the session via method 56 * {@link Session#processEcm}. 57 * <p> 58 * The app next constructs a MediaDescrambler object, and initializes it with the 59 * session using {@link MediaDescrambler#setMediaCasSession}. This ties the 60 * descrambler to the session, and the descrambler can then be used to descramble 61 * content secured with the session's key, either during extraction, or during decoding 62 * with {@link android.media.MediaCodec}. 63 * <p> 64 * If the app handles sample extraction using its own extractor, it can use 65 * MediaDescrambler to descramble samples into clear buffers (if the session's license 66 * doesn't require secure decoders), or descramble a small amount of data to retrieve 67 * information necessary for the downstream pipeline to process the sample (if the 68 * session's license requires secure decoders). 69 * <p> 70 * If the session requires a secure decoder, a MediaDescrambler needs to be provided to 71 * MediaCodec to descramble samples queued by {@link MediaCodec#queueSecureInputBuffer} 72 * into protected buffers. The app should use {@link MediaCodec#configure(MediaFormat, 73 * android.view.Surface, int, MediaDescrambler)} instead of the normal {@link 74 * MediaCodec#configure(MediaFormat, android.view.Surface, MediaCrypto, int)} method 75 * to configure MediaCodec. 76 * <p> 77 * <h3>Using Android's MediaExtractor</h3> 78 * <p> 79 * If the app uses {@link MediaExtractor}, it can delegate the CAS session 80 * management to MediaExtractor by calling {@link MediaExtractor#setMediaCas}. 81 * MediaExtractor will take over and call {@link #openSession}, {@link #processEmm} 82 * and/or {@link Session#processEcm}, etc.. if necessary. 83 * <p> 84 * When using {@link MediaExtractor}, the app would still need a MediaDescrambler 85 * to use with {@link MediaCodec} if the licensing requires a secure decoder. The 86 * session associated with the descrambler of a track can be retrieved by calling 87 * {@link MediaExtractor#getCasInfo}, and used to initialize a MediaDescrambler 88 * object for MediaCodec. 89 * <p> 90 * <h3>Listeners</h3> 91 * <p>The app may register a listener to receive events from the CA system using 92 * method {@link #setEventListener}. The exact format of the event is scheme-specific 93 * and is not specified by this API. 94 */ 95 public final class MediaCas implements AutoCloseable { 96 private static final String TAG = "MediaCas"; 97 private ICas mICas; 98 private EventListener mListener; 99 private HandlerThread mHandlerThread; 100 private EventHandler mEventHandler; 101 102 private static final Singleton<IMediaCasService> gDefault = 103 new Singleton<IMediaCasService>() { 104 @Override 105 protected IMediaCasService create() { 106 try { 107 return IMediaCasService.getService(); 108 } catch (RemoteException e) {} 109 return null; 110 } 111 }; 112 getService()113 static IMediaCasService getService() { 114 return gDefault.get(); 115 } 116 validateInternalStates()117 private void validateInternalStates() { 118 if (mICas == null) { 119 throw new IllegalStateException(); 120 } 121 } 122 cleanupAndRethrowIllegalState()123 private void cleanupAndRethrowIllegalState() { 124 mICas = null; 125 throw new IllegalStateException(); 126 } 127 128 private class EventHandler extends Handler 129 { 130 private static final int MSG_CAS_EVENT = 0; 131 EventHandler(Looper looper)132 public EventHandler(Looper looper) { 133 super(looper); 134 } 135 136 @Override handleMessage(Message msg)137 public void handleMessage(Message msg) { 138 if (msg.what == MSG_CAS_EVENT) { 139 mListener.onEvent(MediaCas.this, msg.arg1, msg.arg2, 140 toBytes((ArrayList<Byte>) msg.obj)); 141 } 142 } 143 } 144 145 private final ICasListener.Stub mBinder = new ICasListener.Stub() { 146 @Override 147 public void onEvent(int event, int arg, @Nullable ArrayList<Byte> data) 148 throws RemoteException { 149 mEventHandler.sendMessage(mEventHandler.obtainMessage( 150 EventHandler.MSG_CAS_EVENT, event, arg, data)); 151 } 152 }; 153 154 /** 155 * Describe a CAS plugin with its CA_system_ID and string name. 156 * 157 * Returned as results of {@link #enumeratePlugins}. 158 * 159 */ 160 public static class PluginDescriptor { 161 private final int mCASystemId; 162 private final String mName; 163 PluginDescriptor()164 private PluginDescriptor() { 165 mCASystemId = 0xffff; 166 mName = null; 167 } 168 PluginDescriptor(@onNull HidlCasPluginDescriptor descriptor)169 PluginDescriptor(@NonNull HidlCasPluginDescriptor descriptor) { 170 mCASystemId = descriptor.caSystemId; 171 mName = descriptor.name; 172 } 173 getSystemId()174 public int getSystemId() { 175 return mCASystemId; 176 } 177 178 @NonNull getName()179 public String getName() { 180 return mName; 181 } 182 183 @Override toString()184 public String toString() { 185 return "PluginDescriptor {" + mCASystemId + ", " + mName + "}"; 186 } 187 } 188 toByteArray(@onNull byte[] data, int offset, int length)189 private ArrayList<Byte> toByteArray(@NonNull byte[] data, int offset, int length) { 190 ArrayList<Byte> byteArray = new ArrayList<Byte>(length); 191 for (int i = 0; i < length; i++) { 192 byteArray.add(Byte.valueOf(data[offset + i])); 193 } 194 return byteArray; 195 } 196 toByteArray(@ullable byte[] data)197 private ArrayList<Byte> toByteArray(@Nullable byte[] data) { 198 if (data == null) { 199 return new ArrayList<Byte>(); 200 } 201 return toByteArray(data, 0, data.length); 202 } 203 toBytes(@onNull ArrayList<Byte> byteArray)204 private byte[] toBytes(@NonNull ArrayList<Byte> byteArray) { 205 byte[] data = null; 206 if (byteArray != null) { 207 data = new byte[byteArray.size()]; 208 for (int i = 0; i < data.length; i++) { 209 data[i] = byteArray.get(i); 210 } 211 } 212 return data; 213 } 214 /** 215 * Class for an open session with the CA system. 216 */ 217 public final class Session implements AutoCloseable { 218 final ArrayList<Byte> mSessionId; 219 Session(@onNull ArrayList<Byte> sessionId)220 Session(@NonNull ArrayList<Byte> sessionId) { 221 mSessionId = sessionId; 222 } 223 224 /** 225 * Set the private data for a session. 226 * 227 * @param data byte array of the private data. 228 * 229 * @throws IllegalStateException if the MediaCas instance is not valid. 230 * @throws MediaCasException for CAS-specific errors. 231 * @throws MediaCasStateException for CAS-specific state exceptions. 232 */ setPrivateData(@onNull byte[] data)233 public void setPrivateData(@NonNull byte[] data) 234 throws MediaCasException { 235 validateInternalStates(); 236 237 try { 238 MediaCasException.throwExceptionIfNeeded( 239 mICas.setSessionPrivateData(mSessionId, toByteArray(data, 0, data.length))); 240 } catch (RemoteException e) { 241 cleanupAndRethrowIllegalState(); 242 } 243 } 244 245 246 /** 247 * Send a received ECM packet to the specified session of the CA system. 248 * 249 * @param data byte array of the ECM data. 250 * @param offset position within data where the ECM data begins. 251 * @param length length of the data (starting from offset). 252 * 253 * @throws IllegalStateException if the MediaCas instance is not valid. 254 * @throws MediaCasException for CAS-specific errors. 255 * @throws MediaCasStateException for CAS-specific state exceptions. 256 */ processEcm(@onNull byte[] data, int offset, int length)257 public void processEcm(@NonNull byte[] data, int offset, int length) 258 throws MediaCasException { 259 validateInternalStates(); 260 261 try { 262 MediaCasException.throwExceptionIfNeeded( 263 mICas.processEcm(mSessionId, toByteArray(data, offset, length))); 264 } catch (RemoteException e) { 265 cleanupAndRethrowIllegalState(); 266 } 267 } 268 269 /** 270 * Send a received ECM packet to the specified session of the CA system. 271 * This is similar to {@link Session#processEcm(byte[], int, int)} 272 * except that the entire byte array is sent. 273 * 274 * @param data byte array of the ECM data. 275 * 276 * @throws IllegalStateException if the MediaCas instance is not valid. 277 * @throws MediaCasException for CAS-specific errors. 278 * @throws MediaCasStateException for CAS-specific state exceptions. 279 */ processEcm(@onNull byte[] data)280 public void processEcm(@NonNull byte[] data) throws MediaCasException { 281 processEcm(data, 0, data.length); 282 } 283 284 /** 285 * Close the session. 286 * 287 * @throws IllegalStateException if the MediaCas instance is not valid. 288 * @throws MediaCasStateException for CAS-specific state exceptions. 289 */ 290 @Override close()291 public void close() { 292 validateInternalStates(); 293 294 try { 295 MediaCasStateException.throwExceptionIfNeeded( 296 mICas.closeSession(mSessionId)); 297 } catch (RemoteException e) { 298 cleanupAndRethrowIllegalState(); 299 } 300 } 301 } 302 createFromSessionId(@onNull ArrayList<Byte> sessionId)303 Session createFromSessionId(@NonNull ArrayList<Byte> sessionId) { 304 if (sessionId == null || sessionId.size() == 0) { 305 return null; 306 } 307 return new Session(sessionId); 308 } 309 310 /** 311 * Query if a certain CA system is supported on this device. 312 * 313 * @param CA_system_id the id of the CA system. 314 * 315 * @return Whether the specified CA system is supported on this device. 316 */ isSystemIdSupported(int CA_system_id)317 public static boolean isSystemIdSupported(int CA_system_id) { 318 IMediaCasService service = getService(); 319 320 if (service != null) { 321 try { 322 return service.isSystemIdSupported(CA_system_id); 323 } catch (RemoteException e) { 324 } 325 } 326 return false; 327 } 328 329 /** 330 * List all available CA plugins on the device. 331 * 332 * @return an array of descriptors for the available CA plugins. 333 */ enumeratePlugins()334 public static PluginDescriptor[] enumeratePlugins() { 335 IMediaCasService service = getService(); 336 337 if (service != null) { 338 try { 339 ArrayList<HidlCasPluginDescriptor> descriptors = 340 service.enumeratePlugins(); 341 if (descriptors.size() == 0) { 342 return null; 343 } 344 PluginDescriptor[] results = new PluginDescriptor[descriptors.size()]; 345 for (int i = 0; i < results.length; i++) { 346 results[i] = new PluginDescriptor(descriptors.get(i)); 347 } 348 return results; 349 } catch (RemoteException e) { 350 } 351 } 352 return null; 353 } 354 355 /** 356 * Instantiate a CA system of the specified system id. 357 * 358 * @param CA_system_id The system id of the CA system. 359 * 360 * @throws UnsupportedCasException if the device does not support the 361 * specified CA system. 362 */ MediaCas(int CA_system_id)363 public MediaCas(int CA_system_id) throws UnsupportedCasException { 364 try { 365 mICas = getService().createPlugin(CA_system_id, mBinder); 366 } catch(Exception e) { 367 Log.e(TAG, "Failed to create plugin: " + e); 368 mICas = null; 369 } finally { 370 if (mICas == null) { 371 throw new UnsupportedCasException( 372 "Unsupported CA_system_id " + CA_system_id); 373 } 374 } 375 } 376 getBinder()377 IHwBinder getBinder() { 378 validateInternalStates(); 379 380 return mICas.asBinder(); 381 } 382 383 /** 384 * An interface registered by the caller to {@link #setEventListener} 385 * to receives scheme-specific notifications from a MediaCas instance. 386 */ 387 public interface EventListener { 388 /** 389 * Notify the listener of a scheme-specific event from the CA system. 390 * 391 * @param MediaCas the MediaCas object to receive this event. 392 * @param event an integer whose meaning is scheme-specific. 393 * @param arg an integer whose meaning is scheme-specific. 394 * @param data a byte array of data whose format and meaning are 395 * scheme-specific. 396 */ onEvent(MediaCas MediaCas, int event, int arg, @Nullable byte[] data)397 void onEvent(MediaCas MediaCas, int event, int arg, @Nullable byte[] data); 398 } 399 400 /** 401 * Set an event listener to receive notifications from the MediaCas instance. 402 * 403 * @param listener the event listener to be set. 404 * @param handler the handler whose looper the event listener will be called on. 405 * If handler is null, we'll try to use current thread's looper, or the main 406 * looper. If neither are available, an internal thread will be created instead. 407 */ setEventListener( @ullable EventListener listener, @Nullable Handler handler)408 public void setEventListener( 409 @Nullable EventListener listener, @Nullable Handler handler) { 410 mListener = listener; 411 412 if (mListener == null) { 413 mEventHandler = null; 414 return; 415 } 416 417 Looper looper = (handler != null) ? handler.getLooper() : null; 418 if (looper == null 419 && (looper = Looper.myLooper()) == null 420 && (looper = Looper.getMainLooper()) == null) { 421 if (mHandlerThread == null || !mHandlerThread.isAlive()) { 422 mHandlerThread = new HandlerThread("MediaCasEventThread", 423 Process.THREAD_PRIORITY_FOREGROUND); 424 mHandlerThread.start(); 425 } 426 looper = mHandlerThread.getLooper(); 427 } 428 mEventHandler = new EventHandler(looper); 429 } 430 431 /** 432 * Send the private data for the CA system. 433 * 434 * @param data byte array of the private data. 435 * 436 * @throws IllegalStateException if the MediaCas instance is not valid. 437 * @throws MediaCasException for CAS-specific errors. 438 * @throws MediaCasStateException for CAS-specific state exceptions. 439 */ setPrivateData(@onNull byte[] data)440 public void setPrivateData(@NonNull byte[] data) throws MediaCasException { 441 validateInternalStates(); 442 443 try { 444 MediaCasException.throwExceptionIfNeeded( 445 mICas.setPrivateData(toByteArray(data, 0, data.length))); 446 } catch (RemoteException e) { 447 cleanupAndRethrowIllegalState(); 448 } 449 } 450 451 private class OpenSessionCallback implements ICas.openSessionCallback { 452 public Session mSession; 453 public int mStatus; 454 @Override onValues(int status, ArrayList<Byte> sessionId)455 public void onValues(int status, ArrayList<Byte> sessionId) { 456 mStatus = status; 457 mSession = createFromSessionId(sessionId); 458 } 459 } 460 /** 461 * Open a session to descramble one or more streams scrambled by the 462 * conditional access system. 463 * 464 * @return session the newly opened session. 465 * 466 * @throws IllegalStateException if the MediaCas instance is not valid. 467 * @throws MediaCasException for CAS-specific errors. 468 * @throws MediaCasStateException for CAS-specific state exceptions. 469 */ openSession()470 public Session openSession() throws MediaCasException { 471 validateInternalStates(); 472 473 try { 474 OpenSessionCallback cb = new OpenSessionCallback(); 475 mICas.openSession(cb); 476 MediaCasException.throwExceptionIfNeeded(cb.mStatus); 477 return cb.mSession; 478 } catch (RemoteException e) { 479 cleanupAndRethrowIllegalState(); 480 } 481 return null; 482 } 483 484 /** 485 * Send a received EMM packet to the CA system. 486 * 487 * @param data byte array of the EMM data. 488 * @param offset position within data where the EMM data begins. 489 * @param length length of the data (starting from offset). 490 * 491 * @throws IllegalStateException if the MediaCas instance is not valid. 492 * @throws MediaCasException for CAS-specific errors. 493 * @throws MediaCasStateException for CAS-specific state exceptions. 494 */ processEmm(@onNull byte[] data, int offset, int length)495 public void processEmm(@NonNull byte[] data, int offset, int length) 496 throws MediaCasException { 497 validateInternalStates(); 498 499 try { 500 MediaCasException.throwExceptionIfNeeded( 501 mICas.processEmm(toByteArray(data, offset, length))); 502 } catch (RemoteException e) { 503 cleanupAndRethrowIllegalState(); 504 } 505 } 506 507 /** 508 * Send a received EMM packet to the CA system. This is similar to 509 * {@link #processEmm(byte[], int, int)} except that the entire byte 510 * array is sent. 511 * 512 * @param data byte array of the EMM data. 513 * 514 * @throws IllegalStateException if the MediaCas instance is not valid. 515 * @throws MediaCasException for CAS-specific errors. 516 * @throws MediaCasStateException for CAS-specific state exceptions. 517 */ processEmm(@onNull byte[] data)518 public void processEmm(@NonNull byte[] data) throws MediaCasException { 519 processEmm(data, 0, data.length); 520 } 521 522 /** 523 * Send an event to a CA system. The format of the event is scheme-specific 524 * and is opaque to the framework. 525 * 526 * @param event an integer denoting a scheme-specific event to be sent. 527 * @param arg a scheme-specific integer argument for the event. 528 * @param data a byte array containing scheme-specific data for the event. 529 * 530 * @throws IllegalStateException if the MediaCas instance is not valid. 531 * @throws MediaCasException for CAS-specific errors. 532 * @throws MediaCasStateException for CAS-specific state exceptions. 533 */ sendEvent(int event, int arg, @Nullable byte[] data)534 public void sendEvent(int event, int arg, @Nullable byte[] data) 535 throws MediaCasException { 536 validateInternalStates(); 537 538 try { 539 MediaCasException.throwExceptionIfNeeded( 540 mICas.sendEvent(event, arg, toByteArray(data))); 541 } catch (RemoteException e) { 542 cleanupAndRethrowIllegalState(); 543 } 544 } 545 546 /** 547 * Initiate a provisioning operation for a CA system. 548 * 549 * @param provisionString string containing information needed for the 550 * provisioning operation, the format of which is scheme and implementation 551 * specific. 552 * 553 * @throws IllegalStateException if the MediaCas instance is not valid. 554 * @throws MediaCasException for CAS-specific errors. 555 * @throws MediaCasStateException for CAS-specific state exceptions. 556 */ provision(@onNull String provisionString)557 public void provision(@NonNull String provisionString) throws MediaCasException { 558 validateInternalStates(); 559 560 try { 561 MediaCasException.throwExceptionIfNeeded( 562 mICas.provision(provisionString)); 563 } catch (RemoteException e) { 564 cleanupAndRethrowIllegalState(); 565 } 566 } 567 568 /** 569 * Notify the CA system to refresh entitlement keys. 570 * 571 * @param refreshType the type of the refreshment. 572 * @param refreshData private data associated with the refreshment. 573 * 574 * @throws IllegalStateException if the MediaCas instance is not valid. 575 * @throws MediaCasException for CAS-specific errors. 576 * @throws MediaCasStateException for CAS-specific state exceptions. 577 */ refreshEntitlements(int refreshType, @Nullable byte[] refreshData)578 public void refreshEntitlements(int refreshType, @Nullable byte[] refreshData) 579 throws MediaCasException { 580 validateInternalStates(); 581 582 try { 583 MediaCasException.throwExceptionIfNeeded( 584 mICas.refreshEntitlements(refreshType, toByteArray(refreshData))); 585 } catch (RemoteException e) { 586 cleanupAndRethrowIllegalState(); 587 } 588 } 589 590 @Override close()591 public void close() { 592 if (mICas != null) { 593 try { 594 mICas.release(); 595 } catch (RemoteException e) { 596 } finally { 597 mICas = null; 598 } 599 } 600 } 601 602 @Override finalize()603 protected void finalize() { 604 close(); 605 } 606 }