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