| /* |
| * 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.IntDef; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.TestApi; |
| import android.content.Context; |
| 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_2.ICasListener; |
| import android.hardware.cas.V1_2.Status; |
| import android.media.MediaCasException.*; |
| import android.media.tv.TvInputService.PriorityHintUseCaseType; |
| import android.media.tv.tunerresourcemanager.CasSessionRequest; |
| import android.media.tv.tunerresourcemanager.ResourceClientProfile; |
| import android.media.tv.tunerresourcemanager.TunerResourceManager; |
| 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 com.android.internal.util.FrameworkStatsLog; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| |
| /** |
| * 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 android.hardware.cas.V1_2.ICas mICasV12; |
| private EventListener mListener; |
| private HandlerThread mHandlerThread; |
| private EventHandler mEventHandler; |
| private @PriorityHintUseCaseType int mPriorityHint; |
| private String mTvInputServiceSessionId; |
| private int mClientId; |
| private int mCasSystemId; |
| private int mUserId; |
| private TunerResourceManager mTunerResourceManager = null; |
| private final Map<Session, Integer> mSessionMap = new HashMap<>(); |
| |
| /** |
| * Scrambling modes used to open cas sessions. |
| * |
| * @hide |
| */ |
| @IntDef(prefix = "SCRAMBLING_MODE_", |
| value = {SCRAMBLING_MODE_RESERVED, SCRAMBLING_MODE_DVB_CSA1, SCRAMBLING_MODE_DVB_CSA2, |
| SCRAMBLING_MODE_DVB_CSA3_STANDARD, |
| SCRAMBLING_MODE_DVB_CSA3_MINIMAL, SCRAMBLING_MODE_DVB_CSA3_ENHANCE, |
| SCRAMBLING_MODE_DVB_CISSA_V1, SCRAMBLING_MODE_DVB_IDSA, |
| SCRAMBLING_MODE_MULTI2, SCRAMBLING_MODE_AES128, SCRAMBLING_MODE_AES_ECB, |
| SCRAMBLING_MODE_AES_SCTE52, SCRAMBLING_MODE_TDES_ECB, SCRAMBLING_MODE_TDES_SCTE52}) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface ScramblingMode {} |
| |
| /** |
| * DVB (Digital Video Broadcasting) reserved mode. |
| */ |
| public static final int SCRAMBLING_MODE_RESERVED = |
| android.hardware.cas.V1_2.ScramblingMode.RESERVED; |
| /** |
| * DVB (Digital Video Broadcasting) Common Scrambling Algorithm (CSA) 1. |
| */ |
| public static final int SCRAMBLING_MODE_DVB_CSA1 = |
| android.hardware.cas.V1_2.ScramblingMode.DVB_CSA1; |
| /** |
| * DVB CSA 2. |
| */ |
| public static final int SCRAMBLING_MODE_DVB_CSA2 = |
| android.hardware.cas.V1_2.ScramblingMode.DVB_CSA2; |
| /** |
| * DVB CSA 3 in standard mode. |
| */ |
| public static final int SCRAMBLING_MODE_DVB_CSA3_STANDARD = |
| android.hardware.cas.V1_2.ScramblingMode.DVB_CSA3_STANDARD; |
| /** |
| * DVB CSA 3 in minimally enhanced mode. |
| */ |
| public static final int SCRAMBLING_MODE_DVB_CSA3_MINIMAL = |
| android.hardware.cas.V1_2.ScramblingMode.DVB_CSA3_MINIMAL; |
| /** |
| * DVB CSA 3 in fully enhanced mode. |
| */ |
| public static final int SCRAMBLING_MODE_DVB_CSA3_ENHANCE = |
| android.hardware.cas.V1_2.ScramblingMode.DVB_CSA3_ENHANCE; |
| /** |
| * DVB Common IPTV Software-oriented Scrambling Algorithm (CISSA) Version 1. |
| */ |
| public static final int SCRAMBLING_MODE_DVB_CISSA_V1 = |
| android.hardware.cas.V1_2.ScramblingMode.DVB_CISSA_V1; |
| /** |
| * ATIS-0800006 IIF Default Scrambling Algorithm (IDSA). |
| */ |
| public static final int SCRAMBLING_MODE_DVB_IDSA = |
| android.hardware.cas.V1_2.ScramblingMode.DVB_IDSA; |
| /** |
| * A symmetric key algorithm. |
| */ |
| public static final int SCRAMBLING_MODE_MULTI2 = |
| android.hardware.cas.V1_2.ScramblingMode.MULTI2; |
| /** |
| * Advanced Encryption System (AES) 128-bit Encryption mode. |
| */ |
| public static final int SCRAMBLING_MODE_AES128 = |
| android.hardware.cas.V1_2.ScramblingMode.AES128; |
| /** |
| * Advanced Encryption System (AES) Electronic Code Book (ECB) mode. |
| */ |
| public static final int SCRAMBLING_MODE_AES_ECB = |
| android.hardware.cas.V1_2.ScramblingMode.AES_ECB; |
| /** |
| * Advanced Encryption System (AES) Society of Cable Telecommunications Engineers (SCTE) 52 |
| * mode. |
| */ |
| public static final int SCRAMBLING_MODE_AES_SCTE52 = |
| android.hardware.cas.V1_2.ScramblingMode.AES_SCTE52; |
| /** |
| * Triple Data Encryption Algorithm (TDES) Electronic Code Book (ECB) mode. |
| */ |
| public static final int SCRAMBLING_MODE_TDES_ECB = |
| android.hardware.cas.V1_2.ScramblingMode.TDES_ECB; |
| /** |
| * Triple Data Encryption Algorithm (TDES) Society of Cable Telecommunications Engineers (SCTE) |
| * 52 mode. |
| */ |
| public static final int SCRAMBLING_MODE_TDES_SCTE52 = |
| android.hardware.cas.V1_2.ScramblingMode.TDES_SCTE52; |
| |
| /** |
| * Usages used to open cas sessions. |
| * |
| * @hide |
| */ |
| @IntDef(prefix = "SESSION_USAGE_", |
| value = {SESSION_USAGE_LIVE, SESSION_USAGE_PLAYBACK, SESSION_USAGE_RECORD, |
| SESSION_USAGE_TIMESHIFT}) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface SessionUsage {} |
| /** |
| * Cas session is used to descramble live streams. |
| */ |
| public static final int SESSION_USAGE_LIVE = android.hardware.cas.V1_2.SessionIntent.LIVE; |
| /** |
| * Cas session is used to descramble recoreded streams. |
| */ |
| public static final int SESSION_USAGE_PLAYBACK = |
| android.hardware.cas.V1_2.SessionIntent.PLAYBACK; |
| /** |
| * Cas session is used to descramble live streams and encrypt local recorded content |
| */ |
| public static final int SESSION_USAGE_RECORD = android.hardware.cas.V1_2.SessionIntent.RECORD; |
| /** |
| * Cas session is used to descramble live streams , encrypt local recorded content and playback |
| * local encrypted content. |
| */ |
| public static final int SESSION_USAGE_TIMESHIFT = |
| android.hardware.cas.V1_2.SessionIntent.TIMESHIFT; |
| |
| /** |
| * Plugin status events sent from cas system. |
| * |
| * @hide |
| */ |
| @IntDef(prefix = "PLUGIN_STATUS_", |
| value = {PLUGIN_STATUS_PHYSICAL_MODULE_CHANGED, PLUGIN_STATUS_SESSION_NUMBER_CHANGED}) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface PluginStatus {} |
| |
| /** |
| * The event to indicate that the status of CAS system is changed by the removal or insertion of |
| * physical CAS modules. |
| */ |
| public static final int PLUGIN_STATUS_PHYSICAL_MODULE_CHANGED = |
| android.hardware.cas.V1_2.StatusEvent.PLUGIN_PHYSICAL_MODULE_CHANGED; |
| /** |
| * The event to indicate that the number of CAS system's session is changed. |
| */ |
| public static final int PLUGIN_STATUS_SESSION_NUMBER_CHANGED = |
| android.hardware.cas.V1_2.StatusEvent.PLUGIN_SESSION_NUMBER_CHANGED; |
| |
| private static final Singleton<IMediaCasService> sService = new Singleton<IMediaCasService>() { |
| @Override |
| protected IMediaCasService create() { |
| try { |
| Log.d(TAG, "Trying to get cas@1.2 service"); |
| android.hardware.cas.V1_2.IMediaCasService serviceV12 = |
| android.hardware.cas.V1_2.IMediaCasService.getService(true /*wait*/); |
| if (serviceV12 != null) { |
| return serviceV12; |
| } |
| } catch (Exception eV1_2) { |
| Log.d(TAG, "Failed to get cas@1.2 service"); |
| } |
| |
| try { |
| Log.d(TAG, "Trying 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) { |
| Log.d(TAG, "Failed to get cas@1.1 service"); |
| } |
| |
| try { |
| Log.d(TAG, "Trying 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; |
| mICasV12 = 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 int MSG_CAS_STATUS_EVENT = 2; |
| private static final int MSG_CAS_RESOURCE_LOST = 3; |
| 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)); |
| } else if (msg.what == MSG_CAS_STATUS_EVENT) { |
| if ((msg.arg1 == PLUGIN_STATUS_SESSION_NUMBER_CHANGED) |
| && (mTunerResourceManager != null)) { |
| mTunerResourceManager.updateCasInfo(mCasSystemId, msg.arg2); |
| } |
| mListener.onPluginStatusUpdate(MediaCas.this, msg.arg1, msg.arg2); |
| } else if (msg.what == MSG_CAS_RESOURCE_LOST) { |
| mListener.onResourceLost(MediaCas.this); |
| } |
| } |
| } |
| |
| private final ICasListener.Stub mBinder = new ICasListener.Stub() { |
| @Override |
| public void onEvent(int event, int arg, @Nullable ArrayList<Byte> data) |
| throws RemoteException { |
| if (mEventHandler != null) { |
| 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 { |
| if (mEventHandler != null) { |
| 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); |
| } |
| } |
| @Override |
| public void onStatusUpdate(byte status, int arg) |
| throws RemoteException { |
| if (mEventHandler != null) { |
| mEventHandler.sendMessage(mEventHandler.obtainMessage( |
| EventHandler.MSG_CAS_STATUS_EVENT, status, arg)); |
| } |
| } |
| }; |
| |
| private final TunerResourceManager.ResourcesReclaimListener mResourceListener = |
| new TunerResourceManager.ResourcesReclaimListener() { |
| @Override |
| public void onReclaimResources() { |
| synchronized (mSessionMap) { |
| List<Session> sessionList = new ArrayList<>(mSessionMap.keySet()); |
| for (Session casSession: sessionList) { |
| casSession.close(); |
| } |
| } |
| mEventHandler.sendMessage(mEventHandler.obtainMessage( |
| EventHandler.MSG_CAS_RESOURCE_LOST)); |
| } |
| }; |
| |
| /** |
| * 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; |
| boolean mIsClosed = false; |
| |
| Session(@NonNull ArrayList<Byte> sessionId) { |
| mSessionId = new ArrayList<Byte>(sessionId); |
| } |
| |
| private void validateSessionInternalStates() { |
| if (mICas == null) { |
| throw new IllegalStateException(); |
| } |
| if (mIsClosed) { |
| MediaCasStateException.throwExceptionIfNeeded(Status.ERROR_CAS_SESSION_NOT_OPENED); |
| } |
| } |
| |
| /** |
| * 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 { |
| validateSessionInternalStates(); |
| |
| 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 { |
| validateSessionInternalStates(); |
| |
| 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 { |
| validateSessionInternalStates(); |
| |
| 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(); |
| } |
| } |
| |
| /** |
| * Get Session Id. |
| * |
| * @return session Id of the session. |
| * |
| * @throws IllegalStateException if the MediaCas instance is not valid. |
| */ |
| @NonNull |
| public byte[] getSessionId() { |
| validateSessionInternalStates(); |
| return toBytes(mSessionId); |
| } |
| |
| /** |
| * Close the session. |
| * |
| * @throws IllegalStateException if the MediaCas instance is not valid. |
| * @throws MediaCasStateException for CAS-specific state exceptions. |
| */ |
| @Override |
| public void close() { |
| validateSessionInternalStates(); |
| try { |
| MediaCasStateException.throwExceptionIfNeeded( |
| mICas.closeSession(mSessionId)); |
| mIsClosed = true; |
| removeSessionFromResourceMap(this); |
| } 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; |
| } |
| |
| private void createPlugin(int casSystemId) throws UnsupportedCasException { |
| try { |
| mCasSystemId = casSystemId; |
| mUserId = Process.myUid(); |
| IMediaCasService service = getService(); |
| android.hardware.cas.V1_2.IMediaCasService serviceV12 = |
| android.hardware.cas.V1_2.IMediaCasService.castFrom(service); |
| if (serviceV12 == null) { |
| 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(casSystemId, mBinder); |
| } else { |
| Log.d(TAG, "Used cas@1.1 interface to create plugin"); |
| mICas = mICasV11 = serviceV11.createPluginExt(casSystemId, mBinder); |
| } |
| } else { |
| Log.d(TAG, "Used cas@1.2 interface to create plugin"); |
| mICas = mICasV11 = mICasV12 = |
| android.hardware.cas.V1_2.ICas |
| .castFrom(serviceV12.createPluginExt(casSystemId, mBinder)); |
| } |
| } catch(Exception e) { |
| Log.e(TAG, "Failed to create plugin: " + e); |
| mICas = null; |
| } finally { |
| if (mICas == null) { |
| throw new UnsupportedCasException( |
| "Unsupported casSystemId " + casSystemId); |
| } |
| } |
| } |
| |
| private void registerClient(@NonNull Context context, |
| @Nullable String tvInputServiceSessionId, @PriorityHintUseCaseType int priorityHint) { |
| |
| mTunerResourceManager = (TunerResourceManager) |
| context.getSystemService(Context.TV_TUNER_RESOURCE_MGR_SERVICE); |
| if (mTunerResourceManager != null) { |
| int[] clientId = new int[1]; |
| ResourceClientProfile profile = |
| new ResourceClientProfile(tvInputServiceSessionId, priorityHint); |
| mTunerResourceManager.registerClientProfile( |
| profile, context.getMainExecutor(), mResourceListener, clientId); |
| mClientId = clientId[0]; |
| } |
| } |
| /** |
| * Instantiate a CA system of the specified system id. |
| * |
| * @param casSystemId The system id of the CA system. |
| * |
| * @throws UnsupportedCasException if the device does not support the |
| * specified CA system. |
| */ |
| public MediaCas(int casSystemId) throws UnsupportedCasException { |
| createPlugin(casSystemId); |
| } |
| |
| /** |
| * Instantiate a CA system of the specified system id. |
| * |
| * @param context the context of the caller. |
| * @param casSystemId The system id of the CA system. |
| * @param tvInputServiceSessionId The Id of the session opened in TV Input Service (TIS) |
| * {@link android.media.tv.TvInputService#onCreateSession(String, String)} |
| * @param priorityHint priority hint from the use case type for new created CAS system. |
| * |
| * @throws UnsupportedCasException if the device does not support the |
| * specified CA system. |
| */ |
| public MediaCas(@NonNull Context context, int casSystemId, |
| @Nullable String tvInputServiceSessionId, |
| @PriorityHintUseCaseType int priorityHint) throws UnsupportedCasException { |
| Objects.requireNonNull(context, "context must not be null"); |
| createPlugin(casSystemId); |
| registerClient(context, tvInputServiceSessionId, priorityHint); |
| } |
| /** |
| * Instantiate a CA system of the specified system id with EvenListener. |
| * |
| * @param context the context of the caller. |
| * @param casSystemId The system id of the CA system. |
| * @param tvInputServiceSessionId The Id of the session opened in TV Input Service (TIS) |
| * {@link android.media.tv.TvInputService#onCreateSession(String, String)} |
| * @param priorityHint priority hint from the use case type for new created CAS system. |
| * @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. |
| * |
| * @throws UnsupportedCasException if the device does not support the |
| * specified CA system. |
| */ |
| public MediaCas(@NonNull Context context, int casSystemId, |
| @Nullable String tvInputServiceSessionId, |
| @PriorityHintUseCaseType int priorityHint, |
| @Nullable Handler handler, @Nullable EventListener listener) |
| throws UnsupportedCasException { |
| Objects.requireNonNull(context, "context must not be null"); |
| setEventListener(listener, handler); |
| createPlugin(casSystemId); |
| registerClient(context, tvInputServiceSessionId, priorityHint); |
| } |
| |
| 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"); |
| } |
| |
| /** |
| * Notify the listener that the cas plugin status is updated. |
| * |
| * @param mediaCas the MediaCas object to receive this event. |
| * @param status the plugin status which is updated. |
| * @param arg an integer whose meaning is specific to the status to be updated. |
| */ |
| default void onPluginStatusUpdate(@NonNull MediaCas mediaCas, @PluginStatus int status, |
| int arg) { |
| Log.d(TAG, "Received MediaCas Plugin Status event"); |
| } |
| |
| /** |
| * Notify the listener that the session resources was lost. |
| * |
| * @param mediaCas the MediaCas object to receive this event. |
| */ |
| default void onResourceLost(@NonNull MediaCas mediaCas) { |
| Log.d(TAG, "Received MediaCas Resource Reclaim 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); |
| } |
| } |
| |
| private class OpenSession_1_2_Callback implements |
| android.hardware.cas.V1_2.ICas.openSession_1_2Callback { |
| |
| public Session mSession; |
| public int mStatus; |
| |
| @Override |
| public void onValues(int status, ArrayList<Byte> sessionId) { |
| mStatus = status; |
| mSession = createFromSessionId(sessionId); |
| } |
| } |
| |
| private int getSessionResourceHandle() throws MediaCasException { |
| validateInternalStates(); |
| |
| int[] sessionResourceHandle = new int[1]; |
| sessionResourceHandle[0] = -1; |
| if (mTunerResourceManager != null) { |
| CasSessionRequest casSessionRequest = new CasSessionRequest(mClientId, mCasSystemId); |
| if (!mTunerResourceManager |
| .requestCasSession(casSessionRequest, sessionResourceHandle)) { |
| throw new MediaCasException.InsufficientResourceException( |
| "insufficient resource to Open Session"); |
| } |
| } |
| return sessionResourceHandle[0]; |
| } |
| |
| private void addSessionToResourceMap(Session session, int sessionResourceHandle) { |
| |
| if (sessionResourceHandle != TunerResourceManager.INVALID_RESOURCE_HANDLE) { |
| synchronized (mSessionMap) { |
| mSessionMap.put(session, sessionResourceHandle); |
| } |
| } |
| } |
| |
| private void removeSessionFromResourceMap(Session session) { |
| |
| synchronized (mSessionMap) { |
| if (mSessionMap.get(session) != null) { |
| mTunerResourceManager.releaseCasSession(mSessionMap.get(session), mClientId); |
| mSessionMap.remove(session); |
| } |
| } |
| } |
| |
| /** |
| * Open a session to descramble one or more streams scrambled by the |
| * conditional access system. |
| * |
| * <p>Tuner resource manager (TRM) uses the client priority value to decide whether it is able |
| * to get cas session resource if cas session resources is limited. If the client can't get the |
| * resource, this call returns {@link MediaCasException.InsufficientResourceException }. |
| * |
| * @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 { |
| int sessionResourceHandle = getSessionResourceHandle(); |
| |
| try { |
| OpenSessionCallback cb = new OpenSessionCallback(); |
| mICas.openSession(cb); |
| MediaCasException.throwExceptionIfNeeded(cb.mStatus); |
| addSessionToResourceMap(cb.mSession, sessionResourceHandle); |
| Log.d(TAG, "Write Stats Log for succeed to Open Session."); |
| FrameworkStatsLog |
| .write(FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS, mUserId, mCasSystemId, |
| FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS__STATE__SUCCEEDED); |
| return cb.mSession; |
| } catch (RemoteException e) { |
| cleanupAndRethrowIllegalState(); |
| } |
| Log.d(TAG, "Write Stats Log for fail to Open Session."); |
| FrameworkStatsLog |
| .write(FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS, mUserId, mCasSystemId, |
| FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS__STATE__FAILED); |
| return null; |
| } |
| |
| /** |
| * Open a session with usage and scrambling information, so that descrambler can be configured |
| * to descramble one or more streams scrambled by the conditional access system. |
| * |
| * <p>Tuner resource manager (TRM) uses the client priority value to decide whether it is able |
| * to get cas session resource if cas session resources is limited. If the client can't get the |
| * resource, this call returns {@link MediaCasException.InsufficientResourceException}. |
| * |
| * @param sessionUsage used for the created session. |
| * @param scramblingMode used for the created session. |
| * |
| * @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. |
| */ |
| @Nullable |
| public Session openSession(@SessionUsage int sessionUsage, @ScramblingMode int scramblingMode) |
| throws MediaCasException { |
| int sessionResourceHandle = getSessionResourceHandle(); |
| |
| if (mICasV12 == null) { |
| Log.d(TAG, "Open Session with scrambling mode is only supported by cas@1.2+ interface"); |
| throw new UnsupportedCasException("Open Session with scrambling mode is not supported"); |
| } |
| |
| try { |
| OpenSession_1_2_Callback cb = new OpenSession_1_2_Callback(); |
| mICasV12.openSession_1_2(sessionUsage, scramblingMode, cb); |
| MediaCasException.throwExceptionIfNeeded(cb.mStatus); |
| addSessionToResourceMap(cb.mSession, sessionResourceHandle); |
| Log.d(TAG, "Write Stats Log for succeed to Open Session."); |
| FrameworkStatsLog |
| .write(FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS, mUserId, mCasSystemId, |
| FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS__STATE__SUCCEEDED); |
| return cb.mSession; |
| } catch (RemoteException e) { |
| cleanupAndRethrowIllegalState(); |
| } |
| Log.d(TAG, "Write Stats Log for fail to Open Session."); |
| FrameworkStatsLog |
| .write(FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS, mUserId, mCasSystemId, |
| FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS__STATE__FAILED); |
| 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(); |
| } |
| } |
| |
| /** |
| * Release Cas session. This is primarily used as a test API for CTS. |
| * @hide |
| */ |
| @TestApi |
| public void forceResourceLost() { |
| if (mResourceListener != null) { |
| mResourceListener.onReclaimResources(); |
| } |
| } |
| |
| @Override |
| public void close() { |
| if (mICas != null) { |
| try { |
| mICas.release(); |
| } catch (RemoteException e) { |
| } finally { |
| mICas = null; |
| } |
| } |
| |
| if (mTunerResourceManager != null) { |
| mTunerResourceManager.unregisterClientProfile(mClientId); |
| mTunerResourceManager = null; |
| } |
| |
| if (mHandlerThread != null) { |
| mHandlerThread.quit(); |
| mHandlerThread = null; |
| } |
| } |
| |
| @Override |
| protected void finalize() { |
| close(); |
| } |
| } |