content: /*
   * Copyright (C) 2017 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.media;
  
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.hardware.cas.V1_0.HidlCasPluginDescriptor;
  import android.hardware.cas.V1_0.ICas;
  import android.hardware.cas.V1_0.IMediaCasService;
  import android.hardware.cas.V1_1.ICasListener;
  import android.media.MediaCasException.*;
  import android.os.Bundle;
  import android.os.Handler;
  import android.os.HandlerThread;
  import android.os.IHwBinder;
  import android.os.Looper;
  import android.os.Message;
  import android.os.Process;
  import android.os.RemoteException;
  import android.util.Log;
  import android.util.Singleton;
  
  import java.util.ArrayList;
  
  /**
   * MediaCas can be used to obtain keys for descrambling protected media streams, in
   * conjunction with {@link android.media.MediaDescrambler}. The MediaCas APIs are
   * designed to support conditional access such as those in the ISO/IEC13818-1.
   * The CA system is identified by a 16-bit integer CA_system_id. The scrambling
   * algorithms are usually proprietary and implemented by vendor-specific CA plugins
   * installed on the device.
   * <p>
   * The app is responsible for constructing a MediaCas object for the CA system it
   * intends to use. The app can query if a certain CA system is supported using static
   * method {@link #isSystemIdSupported}. It can also obtain the entire list of supported
   * CA systems using static method {@link #enumeratePlugins}.
   * <p>
   * Once the MediaCas object is constructed, the app should properly provision it by
   * using method {@link #provision} and/or {@link #processEmm}. The EMMs (Entitlement
   * management messages) can be distributed out-of-band, or in-band with the stream.
   * <p>
   * To descramble elementary streams, the app first calls {@link #openSession} to
   * generate a {@link Session} object that will uniquely identify a session. A session
   * provides a context for subsequent key updates and descrambling activities. The ECMs
   * (Entitlement control messages) are sent to the session via method
   * {@link Session#processEcm}.
   * <p>
   * The app next constructs a MediaDescrambler object, and initializes it with the
   * session using {@link MediaDescrambler#setMediaCasSession}. This ties the
   * descrambler to the session, and the descrambler can then be used to descramble
   * content secured with the session's key, either during extraction, or during decoding
   * with {@link android.media.MediaCodec}.
   * <p>
   * If the app handles sample extraction using its own extractor, it can use
   * MediaDescrambler to descramble samples into clear buffers (if the session's license
   * doesn't require secure decoders), or descramble a small amount of data to retrieve
   * information necessary for the downstream pipeline to process the sample (if the
   * session's license requires secure decoders).
   * <p>
   * If the session requires a secure decoder, a MediaDescrambler needs to be provided to
   * MediaCodec to descramble samples queued by {@link MediaCodec#queueSecureInputBuffer}
   * into protected buffers. The app should use {@link MediaCodec#configure(MediaFormat,
   * android.view.Surface, int, MediaDescrambler)} instead of the normal {@link
   * MediaCodec#configure(MediaFormat, android.view.Surface, MediaCrypto, int)} method
   * to configure MediaCodec.
   * <p>
   * <h3>Using Android's MediaExtractor</h3>
   * <p>
   * If the app uses {@link MediaExtractor}, it can delegate the CAS session
   * management to MediaExtractor by calling {@link MediaExtractor#setMediaCas}.
   * MediaExtractor will take over and call {@link #openSession}, {@link #processEmm}
   * and/or {@link Session#processEcm}, etc.. if necessary.
   * <p>
   * When using {@link MediaExtractor}, the app would still need a MediaDescrambler
   * to use with {@link MediaCodec} if the licensing requires a secure decoder. The
   * session associated with the descrambler of a track can be retrieved by calling
   * {@link MediaExtractor#getCasInfo}, and used to initialize a MediaDescrambler
   * object for MediaCodec.
   * <p>
   * <h3>Listeners</h3>
   * <p>The app may register a listener to receive events from the CA system using
   * method {@link #setEventListener}. The exact format of the event is scheme-specific
   * and is not specified by this API.
   */
  public final class MediaCas implements AutoCloseable {
      private static final String TAG = "MediaCas";
      private ICas mICas;
      private android.hardware.cas.V1_1.ICas mICasV11;
      private EventListener mListener;
      private HandlerThread mHandlerThread;
      private EventHandler mEventHandler;
  
      private static final Singleton<IMediaCasService> sService = new Singleton<IMediaCasService>() {
          @Override
          protected IMediaCasService create() {
              try {
                  Log.d(TAG, "Tried to get cas@1.1 service");
                  android.hardware.cas.V1_1.IMediaCasService serviceV11 =
                          android.hardware.cas.V1_1.IMediaCasService.getService(true /*wait*/);
                  if (serviceV11 != null) {
                      return serviceV11;
                  }
              } catch (Exception eV1_1) {
                  try {
                      Log.d(TAG, "Tried to get cas@1.0 service");
                      return IMediaCasService.getService(true /*wait*/);
                  } catch (Exception eV1_0) {
                      Log.d(TAG, "Failed to get cas@1.0 service");
                  }
              }
              return null;
          }
      };
  
      static IMediaCasService getService() {
          return sService.get();
      }
  
      private void validateInternalStates() {
          if (mICas == null) {
              throw new IllegalStateException();
          }
      }
  
      private void cleanupAndRethrowIllegalState() {
          mICas = null;
          mICasV11 = null;
          throw new IllegalStateException();
      }
  
      private class EventHandler extends Handler {
  
          private static final int MSG_CAS_EVENT = 0;
          private static final int MSG_CAS_SESSION_EVENT = 1;
          private static final String SESSION_KEY = "sessionId";
          private static final String DATA_KEY = "data";
  
          public EventHandler(Looper looper) {
              super(looper);
          }
  
          @Override
          public void handleMessage(Message msg) {
              if (msg.what == MSG_CAS_EVENT) {
                  mListener.onEvent(MediaCas.this, msg.arg1, msg.arg2,
                          toBytes((ArrayList<Byte>) msg.obj));
              } else if (msg.what == MSG_CAS_SESSION_EVENT) {
                  Bundle bundle = msg.getData();
                  ArrayList<Byte> sessionId = toByteArray(bundle.getByteArray(SESSION_KEY));
                  mListener.onSessionEvent(MediaCas.this,
                          createFromSessionId(sessionId), msg.arg1, msg.arg2,
                          bundle.getByteArray(DATA_KEY));
              }
          }
      }
  
      private final ICasListener.Stub mBinder = new ICasListener.Stub() {
          @Override
          public void onEvent(int event, int arg, @Nullable ArrayList<Byte> data)
                  throws RemoteException {
              mEventHandler.sendMessage(mEventHandler.obtainMessage(
                      EventHandler.MSG_CAS_EVENT, event, arg, data));
          }
          @Override
          public void onSessionEvent(@NonNull ArrayList<Byte> sessionId,
                  int event, int arg, @Nullable ArrayList<Byte> data)
                  throws RemoteException {
              Message msg = mEventHandler.obtainMessage();
              msg.what = EventHandler.MSG_CAS_SESSION_EVENT;
              msg.arg1 = event;
              msg.arg2 = arg;
              Bundle bundle = new Bundle();
              bundle.putByteArray(EventHandler.SESSION_KEY, toBytes(sessionId));
              bundle.putByteArray(EventHandler.DATA_KEY, toBytes(data));
              msg.setData(bundle);
              mEventHandler.sendMessage(msg);
          }
      };
      /**
       * Describe a CAS plugin with its CA_system_ID and string name.
       *
       * Returned as results of {@link #enumeratePlugins}.
       *
       */
      public static class PluginDescriptor {
          private final int mCASystemId;
          private final String mName;
  
          private PluginDescriptor() {
              mCASystemId = 0xffff;
              mName = null;
          }
  
          PluginDescriptor(@NonNull HidlCasPluginDescriptor descriptor) {
              mCASystemId = descriptor.caSystemId;
              mName = descriptor.name;
          }
  
          public int getSystemId() {
              return mCASystemId;
          }
  
          @NonNull
          public String getName() {
              return mName;
          }
  
          @Override
          public String toString() {
              return "PluginDescriptor {" + mCASystemId + ", " + mName + "}";
          }
      }
  
      private ArrayList<Byte> toByteArray(@NonNull byte[] data, int offset, int length) {
          ArrayList<Byte> byteArray = new ArrayList<Byte>(length);
          for (int i = 0; i < length; i++) {
              byteArray.add(Byte.valueOf(data[offset + i]));
          }
          return byteArray;
      }
  
      private ArrayList<Byte> toByteArray(@Nullable byte[] data) {
          if (data == null) {
              return new ArrayList<Byte>();
          }
          return toByteArray(data, 0, data.length);
      }
  
      private byte[] toBytes(@NonNull ArrayList<Byte> byteArray) {
          byte[] data = null;
          if (byteArray != null) {
              data = new byte[byteArray.size()];
              for (int i = 0; i < data.length; i++) {
                  data[i] = byteArray.get(i);
              }
          }
          return data;
      }
      /**
       * Class for an open session with the CA system.
       */
      public final class Session implements AutoCloseable {
          final ArrayList<Byte> mSessionId;
  
          Session(@NonNull ArrayList<Byte> sessionId) {
              mSessionId = sessionId;
          }
  
          /**
           * Query if an object equal current Session object.
           *
           * @param obj an object to compare to current Session object.
           *
           * @return Whether input object equal current Session object.
           */
          public boolean equals(Object obj) {
              if (obj instanceof Session) {
                  return mSessionId.equals(((Session) obj).mSessionId);
              }
              return false;
          }
  
          /**
           * Set the private data for a session.
           *
           * @param data byte array of the private data.
           *
           * @throws IllegalStateException if the MediaCas instance is not valid.
           * @throws MediaCasException for CAS-specific errors.
           * @throws MediaCasStateException for CAS-specific state exceptions.
           */
          public void setPrivateData(@NonNull byte[] data)
                  throws MediaCasException {
              validateInternalStates();
  
              try {
                  MediaCasException.throwExceptionIfNeeded(
                          mICas.setSessionPrivateData(mSessionId, toByteArray(data, 0, data.length)));
              } catch (RemoteException e) {
                  cleanupAndRethrowIllegalState();
              }
          }
  
  
          /**
           * Send a received ECM packet to the specified session of the CA system.
           *
           * @param data byte array of the ECM data.
           * @param offset position within data where the ECM data begins.
           * @param length length of the data (starting from offset).
           *
           * @throws IllegalStateException if the MediaCas instance is not valid.
           * @throws MediaCasException for CAS-specific errors.
           * @throws MediaCasStateException for CAS-specific state exceptions.
           */
          public void processEcm(@NonNull byte[] data, int offset, int length)
                  throws MediaCasException {
              validateInternalStates();
  
              try {
                  MediaCasException.throwExceptionIfNeeded(
                          mICas.processEcm(mSessionId, toByteArray(data, offset, length)));
              } catch (RemoteException e) {
                  cleanupAndRethrowIllegalState();
              }
          }
  
          /**
           * Send a received ECM packet to the specified session of the CA system.
           * This is similar to {@link Session#processEcm(byte[], int, int)}
           * except that the entire byte array is sent.
           *
           * @param data byte array of the ECM data.
           *
           * @throws IllegalStateException if the MediaCas instance is not valid.
           * @throws MediaCasException for CAS-specific errors.
           * @throws MediaCasStateException for CAS-specific state exceptions.
           */
          public void processEcm(@NonNull byte[] data) throws MediaCasException {
              processEcm(data, 0, data.length);
          }
  
          /**
           * Send a session event to a CA system. The format of the event is
           * scheme-specific and is opaque to the framework.
           *
           * @param event an integer denoting a scheme-specific event to be sent.
           * @param arg a scheme-specific integer argument for the event.
           * @param data a byte array containing scheme-specific data for the event.
           *
           * @throws IllegalStateException if the MediaCas instance is not valid.
           * @throws MediaCasException for CAS-specific errors.
           * @throws MediaCasStateException for CAS-specific state exceptions.
           */
          public void sendSessionEvent(int event, int arg, @Nullable byte[] data)
                  throws MediaCasException {
              validateInternalStates();
  
              if (mICasV11 == null) {
                  Log.d(TAG, "Send Session Event isn't supported by cas@1.0 interface");
                  throw new UnsupportedCasException("Send Session Event is not supported");
              }
  
              try {
                  MediaCasException.throwExceptionIfNeeded(
                          mICasV11.sendSessionEvent(mSessionId, event, arg, toByteArray(data)));
              } catch (RemoteException e) {
                  cleanupAndRethrowIllegalState();
              }
          }
  
          /**
           * Close the session.
           *
           * @throws IllegalStateException if the MediaCas instance is not valid.
           * @throws MediaCasStateException for CAS-specific state exceptions.
           */
          @Override
          public void close() {
              validateInternalStates();
  
              try {
                  MediaCasStateException.throwExceptionIfNeeded(
                          mICas.closeSession(mSessionId));
              } catch (RemoteException e) {
                  cleanupAndRethrowIllegalState();
              }
          }
      }
  
      Session createFromSessionId(@NonNull ArrayList<Byte> sessionId) {
          if (sessionId == null || sessionId.size() == 0) {
              return null;
          }
          return new Session(sessionId);
      }
  
      /**
       * Query if a certain CA system is supported on this device.
       *
       * @param CA_system_id the id of the CA system.
       *
       * @return Whether the specified CA system is supported on this device.
       */
      public static boolean isSystemIdSupported(int CA_system_id) {
          IMediaCasService service = getService();
  
          if (service != null) {
              try {
                  return service.isSystemIdSupported(CA_system_id);
              } catch (RemoteException e) {
              }
          }
          return false;
      }
  
      /**
       * List all available CA plugins on the device.
       *
       * @return an array of descriptors for the available CA plugins.
       */
      public static PluginDescriptor[] enumeratePlugins() {
          IMediaCasService service = getService();
  
          if (service != null) {
              try {
                  ArrayList<HidlCasPluginDescriptor> descriptors =
                          service.enumeratePlugins();
                  if (descriptors.size() == 0) {
                      return null;
                  }
                  PluginDescriptor[] results = new PluginDescriptor[descriptors.size()];
                  for (int i = 0; i < results.length; i++) {
                      results[i] = new PluginDescriptor(descriptors.get(i));
                  }
                  return results;
              } catch (RemoteException e) {
              }
          }
          return null;
      }
  
      /**
       * Instantiate a CA system of the specified system id.
       *
       * @param CA_system_id The system id of the CA system.
       *
       * @throws UnsupportedCasException if the device does not support the
       * specified CA system.
       */
      public MediaCas(int CA_system_id) throws UnsupportedCasException {
          try {
              IMediaCasService service = getService();
              android.hardware.cas.V1_1.IMediaCasService serviceV11 =
                      android.hardware.cas.V1_1.IMediaCasService.castFrom(service);
              if (serviceV11 == null) {
                  Log.d(TAG, "Used cas@1_0 interface to create plugin");
                  mICas = service.createPlugin(CA_system_id, mBinder);
              } else {
                  Log.d(TAG, "Used cas@1.1 interface to create plugin");
                  mICas = mICasV11 = serviceV11.createPluginExt(CA_system_id, mBinder);
              }
          } catch(Exception e) {
              Log.e(TAG, "Failed to create plugin: " + e);
              mICas = null;
          } finally {
              if (mICas == null) {
                  throw new UnsupportedCasException(
                          "Unsupported CA_system_id " + CA_system_id);
              }
          }
      }
  
      IHwBinder getBinder() {
          validateInternalStates();
  
          return mICas.asBinder();
      }
  
      /**
       * An interface registered by the caller to {@link #setEventListener}
       * to receives scheme-specific notifications from a MediaCas instance.
       */
      public interface EventListener {
          /**
           * Notify the listener of a scheme-specific event from the CA system.
           *
           * @param mediaCas the MediaCas object to receive this event.
           * @param event an integer whose meaning is scheme-specific.
           * @param arg an integer whose meaning is scheme-specific.
           * @param data a byte array of data whose format and meaning are
           * scheme-specific.
           */
          void onEvent(@NonNull MediaCas mediaCas, int event, int arg, @Nullable byte[] data);
  
          /**
           * Notify the listener of a scheme-specific session event from CA system.
           *
           * @param mediaCas the MediaCas object to receive this event.
           * @param session session object which the event is for.
           * @param event an integer whose meaning is scheme-specific.
           * @param arg an integer whose meaning is scheme-specific.
           * @param data a byte array of data whose format and meaning are
           * scheme-specific.
           */
          default void onSessionEvent(@NonNull MediaCas mediaCas, @NonNull Session session,
                  int event, int arg, @Nullable byte[] data) {
              Log.d(TAG, "Received MediaCas Session event");
          }
      }
  
      /**
       * Set an event listener to receive notifications from the MediaCas instance.
       *
       * @param listener the event listener to be set.
       * @param handler the handler whose looper the event listener will be called on.
       * If handler is null, we'll try to use current thread's looper, or the main
       * looper. If neither are available, an internal thread will be created instead.
       */
      public void setEventListener(
              @Nullable EventListener listener, @Nullable Handler handler) {
          mListener = listener;
  
          if (mListener == null) {
              mEventHandler = null;
              return;
          }
  
          Looper looper = (handler != null) ? handler.getLooper() : null;
          if (looper == null
                  && (looper = Looper.myLooper()) == null
                  && (looper = Looper.getMainLooper()) == null) {
              if (mHandlerThread == null || !mHandlerThread.isAlive()) {
                  mHandlerThread = new HandlerThread("MediaCasEventThread",
                          Process.THREAD_PRIORITY_FOREGROUND);
                  mHandlerThread.start();
              }
              looper = mHandlerThread.getLooper();
          }
          mEventHandler = new EventHandler(looper);
      }
  
      /**
       * Send the private data for the CA system.
       *
       * @param data byte array of the private data.
       *
       * @throws IllegalStateException if the MediaCas instance is not valid.
       * @throws MediaCasException for CAS-specific errors.
       * @throws MediaCasStateException for CAS-specific state exceptions.
       */
      public void setPrivateData(@NonNull byte[] data) throws MediaCasException {
          validateInternalStates();
  
          try {
              MediaCasException.throwExceptionIfNeeded(
                      mICas.setPrivateData(toByteArray(data, 0, data.length)));
          } catch (RemoteException e) {
              cleanupAndRethrowIllegalState();
          }
      }
  
      private class OpenSessionCallback implements android.hardware.cas.V1_1.ICas.openSessionCallback{
          public Session mSession;
          public int mStatus;
          @Override
          public void onValues(int status, ArrayList<Byte> sessionId) {
              mStatus = status;
              mSession = createFromSessionId(sessionId);
          }
      }
      /**
       * Open a session to descramble one or more streams scrambled by the
       * conditional access system.
       *
       * @return session the newly opened session.
       *
       * @throws IllegalStateException if the MediaCas instance is not valid.
       * @throws MediaCasException for CAS-specific errors.
       * @throws MediaCasStateException for CAS-specific state exceptions.
       */
      public Session openSession() throws MediaCasException {
          validateInternalStates();
  
          try {
              OpenSessionCallback cb = new OpenSessionCallback();
              mICas.openSession(cb);
              MediaCasException.throwExceptionIfNeeded(cb.mStatus);
              return cb.mSession;
          } catch (RemoteException e) {
              cleanupAndRethrowIllegalState();
          }
          return null;
      }
  
      /**
       * Send a received EMM packet to the CA system.
       *
       * @param data byte array of the EMM data.
       * @param offset position within data where the EMM data begins.
       * @param length length of the data (starting from offset).
       *
       * @throws IllegalStateException if the MediaCas instance is not valid.
       * @throws MediaCasException for CAS-specific errors.
       * @throws MediaCasStateException for CAS-specific state exceptions.
       */
      public void processEmm(@NonNull byte[] data, int offset, int length)
              throws MediaCasException {
          validateInternalStates();
  
          try {
              MediaCasException.throwExceptionIfNeeded(
                      mICas.processEmm(toByteArray(data, offset, length)));
          } catch (RemoteException e) {
              cleanupAndRethrowIllegalState();
          }
      }
  
      /**
       * Send a received EMM packet to the CA system. This is similar to
       * {@link #processEmm(byte[], int, int)} except that the entire byte
       * array is sent.
       *
       * @param data byte array of the EMM data.
       *
       * @throws IllegalStateException if the MediaCas instance is not valid.
       * @throws MediaCasException for CAS-specific errors.
       * @throws MediaCasStateException for CAS-specific state exceptions.
       */
      public void processEmm(@NonNull byte[] data) throws MediaCasException {
          processEmm(data, 0, data.length);
      }
  
      /**
       * Send an event to a CA system. The format of the event is scheme-specific
       * and is opaque to the framework.
       *
       * @param event an integer denoting a scheme-specific event to be sent.
       * @param arg a scheme-specific integer argument for the event.
       * @param data a byte array containing scheme-specific data for the event.
       *
       * @throws IllegalStateException if the MediaCas instance is not valid.
       * @throws MediaCasException for CAS-specific errors.
       * @throws MediaCasStateException for CAS-specific state exceptions.
       */
      public void sendEvent(int event, int arg, @Nullable byte[] data)
              throws MediaCasException {
          validateInternalStates();
  
          try {
              MediaCasException.throwExceptionIfNeeded(
                      mICas.sendEvent(event, arg, toByteArray(data)));
          } catch (RemoteException e) {
              cleanupAndRethrowIllegalState();
          }
      }
  
     /**
       * Initiate a provisioning operation for a CA system.
       *
       * @param provisionString string containing information needed for the
       * provisioning operation, the format of which is scheme and implementation
       * specific.
       *
       * @throws IllegalStateException if the MediaCas instance is not valid.
       * @throws MediaCasException for CAS-specific errors.
       * @throws MediaCasStateException for CAS-specific state exceptions.
       */
      public void provision(@NonNull String provisionString) throws MediaCasException {
          validateInternalStates();
  
          try {
              MediaCasException.throwExceptionIfNeeded(
                      mICas.provision(provisionString));
          } catch (RemoteException e) {
              cleanupAndRethrowIllegalState();
          }
      }
  
      /**
       * Notify the CA system to refresh entitlement keys.
       *
       * @param refreshType the type of the refreshment.
       * @param refreshData private data associated with the refreshment.
       *
       * @throws IllegalStateException if the MediaCas instance is not valid.
       * @throws MediaCasException for CAS-specific errors.
       * @throws MediaCasStateException for CAS-specific state exceptions.
       */
      public void refreshEntitlements(int refreshType, @Nullable byte[] refreshData)
              throws MediaCasException {
          validateInternalStates();
  
          try {
              MediaCasException.throwExceptionIfNeeded(
                      mICas.refreshEntitlements(refreshType, toByteArray(refreshData)));
          } catch (RemoteException e) {
              cleanupAndRethrowIllegalState();
          }
      }
  
      @Override
      public void close() {
          if (mICas != null) {
              try {
                  mICas.release();
              } catch (RemoteException e) {
              } finally {
                  mICas = null;
              }
          }
      }
  
      @Override
      protected void finalize() {
          close();
      }
  }
  