Home | History | Annotate | Download | only in media
      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 
    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,
    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 
    164         private PluginDescriptor() {
    165             mCASystemId = 0xffff;
    166             mName = null;
    167         }
    168 
    169         PluginDescriptor(@NonNull HidlCasPluginDescriptor descriptor) {
    170             mCASystemId = descriptor.caSystemId;
    171             mName = descriptor.name;
    172         }
    173 
    174         public int getSystemId() {
    175             return mCASystemId;
    176         }
    177 
    178         @NonNull
    179         public String getName() {
    180             return mName;
    181         }
    182 
    183         @Override
    184         public String toString() {
    185             return "PluginDescriptor {" + mCASystemId + ", " + mName + "}";
    186         }
    187     }
    188 
    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 
    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 
    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 
    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          */
    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          */
    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          */
    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
    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 
    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      */
    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      */
    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      */
    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 
    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          */
    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      */
    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      */
    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
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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
    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
    603     protected void finalize() {
    604         close();
    605     }
    606 }