/* * 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 static android.media.tv.flags.Flags.FLAG_MEDIACAS_UPDATE_CLIENT_PROFILE_PRIORITY; import static android.media.tv.flags.Flags.FLAG_SET_RESOURCE_HOLDER_RETAIN; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.TestApi; import android.content.Context; import android.hardware.cas.AidlCasPluginDescriptor; import android.hardware.cas.ICas; import android.hardware.cas.ICasListener; import android.hardware.cas.IMediaCasService; import android.hardware.cas.Status; import android.hardware.cas.V1_0.HidlCasPluginDescriptor; 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.IBinder; import android.os.IHwBinder; import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; import android.os.ServiceSpecificException; import android.util.Log; import com.android.internal.util.FrameworkStatsLog; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; 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. *
* 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}. *
* 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. *
* 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}. *
* 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}. *
* 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). *
* 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. *
*
* 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. *
* 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. *
*
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 = null;
private android.hardware.cas.V1_0.ICas mICasHidl = null;
private android.hardware.cas.V1_1.ICas mICasHidl11 = null;
private android.hardware.cas.V1_2.ICas mICasHidl12 = null;
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 Tuner resource manager (TRM) uses the client priority value to decide whether it is able
* to reclaim insufficient resources from another client.
*
* The nice value represents how much the client intends to give up the resource when an
* insufficient resource situation happens.
*
* @see
* Priority value and nice value
* @param priority the new priority. Any negative value would cause no-op on priority setting
* and the API would only process nice value setting in that case.
* @param niceValue the nice value.
* @hide
*/
@FlaggedApi(FLAG_MEDIACAS_UPDATE_CLIENT_PROFILE_PRIORITY)
@SystemApi
@RequiresPermission(android.Manifest.permission.TUNER_RESOURCE_ACCESS)
public boolean updateResourcePriority(int priority, int niceValue) {
if (mTunerResourceManager != null) {
return mTunerResourceManager.updateClientPriority(mClientId, priority, niceValue);
}
return false;
}
/**
* Determines whether the resource holder retains ownership of the resource during a challenge
* scenario, when both resource holder and resource challenger have same processId and same
* priority.
*
*@param enabled Set to {@code true} to allow the resource holder to retain ownership,
* or false to allow the resource challenger to acquire the resource.
* If not explicitly set, enabled is set to {@code false}.
* @hide
*/
@FlaggedApi(FLAG_SET_RESOURCE_HOLDER_RETAIN)
@SystemApi
@RequiresPermission(android.Manifest.permission.TUNER_RESOURCE_ACCESS)
public void setResourceOwnershipRetention(boolean enabled) {
if (mTunerResourceManager != null) {
mTunerResourceManager.setResourceOwnershipRetention(mClientId, enabled);
}
}
IHwBinder getBinder() {
if (mICas != null) {
return null; // Return IHwBinder only for HIDL
}
validateInternalStates();
return mICasHidl.asBinder();
}
/**
* Check if the HAL is an AIDL implementation. For CTS testing purpose.
*
* @hide
*/
@TestApi
public boolean isAidlHal() {
return mICas != null;
}
/**
* 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 {
if (mICas != null) {
try {
mICas.setPrivateData(data);
} catch (ServiceSpecificException se) {
MediaCasException.throwExceptionIfNeeded(se.errorCode);
}
} else {
MediaCasException.throwExceptionIfNeeded(
mICasHidl.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 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 {
long sessionResourceHandle = getSessionResourceHandle();
try {
if (mICas != null) {
try {
byte[] sessionId = mICas.openSessionDefault();
Session session = createFromSessionId(sessionId);
addSessionToResourceMap(session, 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 session;
} catch (ServiceSpecificException se) {
MediaCasException.throwExceptionIfNeeded(se.errorCode);
}
} else if (mICasHidl != null) {
OpenSessionCallback cb = new OpenSessionCallback();
mICasHidl.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.
*
* 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 {
long sessionResourceHandle = getSessionResourceHandle();
if (mICas != null) {
try {
byte[] sessionId = mICas.openSession(sessionUsage, scramblingMode);
Session session = createFromSessionId(sessionId);
addSessionToResourceMap(session, 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 session;
} catch (ServiceSpecificException | RemoteException e) {
cleanupAndRethrowIllegalState();
}
}
if (mICasHidl12 == 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();
mICasHidl12.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 {
if (mICas != null) {
try {
mICas.processEmm(Arrays.copyOfRange(data, offset, length));
} catch (ServiceSpecificException se) {
MediaCasException.throwExceptionIfNeeded(se.errorCode);
}
} else {
MediaCasException.throwExceptionIfNeeded(
mICasHidl.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 {
if (mICas != null) {
try {
if (data == null) {
data = new byte[0];
}
mICas.sendEvent(event, arg, data);
} catch (ServiceSpecificException se) {
MediaCasException.throwExceptionIfNeeded(se.errorCode);
}
} else {
MediaCasException.throwExceptionIfNeeded(
mICasHidl.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 {
if (mICas != null) {
try {
mICas.provision(provisionString);
} catch (ServiceSpecificException se) {
MediaCasException.throwExceptionIfNeeded(se.errorCode);
}
} else {
MediaCasException.throwExceptionIfNeeded(mICasHidl.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 {
if (mICas != null) {
try {
if (refreshData == null) {
refreshData = new byte[0];
}
mICas.refreshEntitlements(refreshType, refreshData);
} catch (ServiceSpecificException se) {
MediaCasException.throwExceptionIfNeeded(se.errorCode);
}
} else {
MediaCasException.throwExceptionIfNeeded(
mICasHidl.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;
}
} else if (mICasHidl != null) {
try {
mICasHidl.release();
} catch (RemoteException e) {
} finally {
mICasHidl = mICasHidl11 = mICasHidl12 = null;
}
}
if (mTunerResourceManager != null) {
mTunerResourceManager.unregisterClientProfile(mClientId);
mTunerResourceManager = null;
}
if (mHandlerThread != null) {
mHandlerThread.quit();
mHandlerThread = null;
}
}
@Override
protected void finalize() {
close();
}
}