1472 lines
59 KiB
Java
1472 lines
59 KiB
Java
/*
|
|
* 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.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.
|
|
* <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 = 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<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_CBC,
|
|
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.ScramblingMode.RESERVED;
|
|
|
|
/** DVB (Digital Video Broadcasting) Common Scrambling Algorithm (CSA) 1. */
|
|
public static final int SCRAMBLING_MODE_DVB_CSA1 = android.hardware.cas.ScramblingMode.DVB_CSA1;
|
|
|
|
/** DVB CSA 2. */
|
|
public static final int SCRAMBLING_MODE_DVB_CSA2 = android.hardware.cas.ScramblingMode.DVB_CSA2;
|
|
|
|
/** DVB CSA 3 in standard mode. */
|
|
public static final int SCRAMBLING_MODE_DVB_CSA3_STANDARD =
|
|
android.hardware.cas.ScramblingMode.DVB_CSA3_STANDARD;
|
|
|
|
/** DVB CSA 3 in minimally enhanced mode. */
|
|
public static final int SCRAMBLING_MODE_DVB_CSA3_MINIMAL =
|
|
android.hardware.cas.ScramblingMode.DVB_CSA3_MINIMAL;
|
|
|
|
/** DVB CSA 3 in fully enhanced mode. */
|
|
public static final int SCRAMBLING_MODE_DVB_CSA3_ENHANCE =
|
|
android.hardware.cas.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.ScramblingMode.DVB_CISSA_V1;
|
|
|
|
/** ATIS-0800006 IIF Default Scrambling Algorithm (IDSA). */
|
|
public static final int SCRAMBLING_MODE_DVB_IDSA = android.hardware.cas.ScramblingMode.DVB_IDSA;
|
|
|
|
/** A symmetric key algorithm. */
|
|
public static final int SCRAMBLING_MODE_MULTI2 = android.hardware.cas.ScramblingMode.MULTI2;
|
|
|
|
/** Advanced Encryption System (AES) 128-bit Encryption mode. */
|
|
public static final int SCRAMBLING_MODE_AES128 = android.hardware.cas.ScramblingMode.AES128;
|
|
|
|
/** Advanced Encryption System (AES) Cipher Block Chaining (CBC) mode. */
|
|
public static final int SCRAMBLING_MODE_AES_CBC = android.hardware.cas.ScramblingMode.AES_CBC;
|
|
|
|
/** Advanced Encryption System (AES) Electronic Code Book (ECB) mode. */
|
|
public static final int SCRAMBLING_MODE_AES_ECB = android.hardware.cas.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.ScramblingMode.AES_SCTE52;
|
|
|
|
/** Triple Data Encryption Algorithm (TDES) Electronic Code Book (ECB) mode. */
|
|
public static final int SCRAMBLING_MODE_TDES_ECB = android.hardware.cas.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.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.SessionIntent.LIVE;
|
|
|
|
/** Cas session is used to descramble recoreded streams. */
|
|
public static final int SESSION_USAGE_PLAYBACK = android.hardware.cas.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.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.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.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.StatusEvent.PLUGIN_SESSION_NUMBER_CHANGED;
|
|
|
|
private static IMediaCasService sService = null;
|
|
private static Object sAidlLock = new Object();
|
|
|
|
/** DeathListener for AIDL service */
|
|
private static IBinder.DeathRecipient sDeathListener =
|
|
new IBinder.DeathRecipient() {
|
|
@Override
|
|
public void binderDied() {
|
|
synchronized (sAidlLock) {
|
|
Log.d(TAG, "The service is dead");
|
|
sService.asBinder().unlinkToDeath(sDeathListener, 0);
|
|
sService = null;
|
|
}
|
|
}
|
|
};
|
|
|
|
static IMediaCasService getService() {
|
|
synchronized (sAidlLock) {
|
|
if (sService == null || !sService.asBinder().isBinderAlive()) {
|
|
try {
|
|
Log.d(TAG, "Trying to get AIDL service");
|
|
sService =
|
|
IMediaCasService.Stub.asInterface(
|
|
ServiceManager.waitForDeclaredService(
|
|
IMediaCasService.DESCRIPTOR + "/default"));
|
|
if (sService != null) {
|
|
sService.asBinder().linkToDeath(sDeathListener, 0);
|
|
}
|
|
} catch (Exception eAidl) {
|
|
Log.d(TAG, "Failed to get cas AIDL service");
|
|
}
|
|
}
|
|
return sService;
|
|
}
|
|
}
|
|
|
|
private static android.hardware.cas.V1_0.IMediaCasService sServiceHidl = null;
|
|
private static Object sHidlLock = new Object();
|
|
|
|
/** Used to indicate the right end-point to handle the serviceDied method */
|
|
private static final long MEDIA_CAS_HIDL_COOKIE = 394;
|
|
|
|
/** DeathListener for HIDL service */
|
|
private static IHwBinder.DeathRecipient sDeathListenerHidl =
|
|
new IHwBinder.DeathRecipient() {
|
|
@Override
|
|
public void serviceDied(long cookie) {
|
|
if (cookie == MEDIA_CAS_HIDL_COOKIE) {
|
|
synchronized (sHidlLock) {
|
|
sServiceHidl = null;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
static android.hardware.cas.V1_0.IMediaCasService getServiceHidl() {
|
|
synchronized (sHidlLock) {
|
|
if (sServiceHidl != null) {
|
|
return sServiceHidl;
|
|
} else {
|
|
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) {
|
|
sServiceHidl = serviceV12;
|
|
sServiceHidl.linkToDeath(sDeathListenerHidl, MEDIA_CAS_HIDL_COOKIE);
|
|
return sServiceHidl;
|
|
}
|
|
} 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) {
|
|
sServiceHidl = serviceV11;
|
|
sServiceHidl.linkToDeath(sDeathListenerHidl, MEDIA_CAS_HIDL_COOKIE);
|
|
return sServiceHidl;
|
|
}
|
|
} 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");
|
|
sServiceHidl =
|
|
android.hardware.cas.V1_0.IMediaCasService.getService(true /*wait*/);
|
|
if (sServiceHidl != null) {
|
|
sServiceHidl.linkToDeath(sDeathListenerHidl, MEDIA_CAS_HIDL_COOKIE);
|
|
}
|
|
return sServiceHidl;
|
|
} catch (Exception eV1_0) {
|
|
Log.d(TAG, "Failed to get cas@1.0 service");
|
|
}
|
|
}
|
|
}
|
|
// Couldn't find an HIDL service, returning null.
|
|
return null;
|
|
}
|
|
|
|
private void validateInternalStates() {
|
|
if (mICas == null && mICasHidl == null) {
|
|
throw new IllegalStateException();
|
|
}
|
|
}
|
|
|
|
private void cleanupAndRethrowIllegalState() {
|
|
mICas = null;
|
|
mICasHidl = null;
|
|
mICasHidl11 = null;
|
|
mICasHidl12 = 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) {
|
|
byte[] data = (msg.obj == null) ? new byte[0] : (byte[]) msg.obj;
|
|
mListener.onEvent(MediaCas.this, msg.arg1, msg.arg2, data);
|
|
} else if (msg.what == MSG_CAS_SESSION_EVENT) {
|
|
Bundle bundle = msg.getData();
|
|
byte[] sessionId = bundle.getByteArray(SESSION_KEY);
|
|
byte[] data = bundle.getByteArray(DATA_KEY);
|
|
mListener.onSessionEvent(
|
|
MediaCas.this, createFromSessionId(sessionId), msg.arg1, msg.arg2, data);
|
|
} 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, byte[] data) throws RemoteException {
|
|
if (mEventHandler != null) {
|
|
mEventHandler.sendMessage(
|
|
mEventHandler.obtainMessage(
|
|
EventHandler.MSG_CAS_EVENT, event, arg, data));
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onSessionEvent(byte[] sessionId, int event, int arg, 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, sessionId);
|
|
bundle.putByteArray(EventHandler.DATA_KEY, 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));
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public synchronized String getInterfaceHash() throws android.os.RemoteException {
|
|
return ICasListener.Stub.HASH;
|
|
}
|
|
|
|
@Override
|
|
public int getInterfaceVersion() throws android.os.RemoteException {
|
|
return ICasListener.Stub.VERSION;
|
|
}
|
|
};
|
|
|
|
private final android.hardware.cas.V1_2.ICasListener.Stub mBinderHidl =
|
|
new android.hardware.cas.V1_2.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, toBytes(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 AidlCasPluginDescriptor descriptor) {
|
|
mCASystemId = descriptor.caSystemId;
|
|
mName = descriptor.name;
|
|
}
|
|
|
|
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 byte[] mSessionId;
|
|
boolean mIsClosed = false;
|
|
|
|
Session(@NonNull byte[] sessionId) {
|
|
mSessionId = sessionId;
|
|
}
|
|
|
|
private void validateSessionInternalStates() {
|
|
if (mICas == null && mICasHidl == 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 Arrays.equals(mSessionId, ((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 {
|
|
if (mICas != null) {
|
|
try {
|
|
mICas.setSessionPrivateData(mSessionId, data);
|
|
} catch (ServiceSpecificException se) {
|
|
MediaCasException.throwExceptionIfNeeded(se.errorCode);
|
|
}
|
|
} else {
|
|
MediaCasException.throwExceptionIfNeeded(
|
|
mICasHidl.setSessionPrivateData(
|
|
toByteArray(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 {
|
|
if (mICas != null) {
|
|
try {
|
|
mICas.processEcm(
|
|
mSessionId, Arrays.copyOfRange(data, offset, length + offset));
|
|
} catch (ServiceSpecificException se) {
|
|
MediaCasException.throwExceptionIfNeeded(se.errorCode);
|
|
}
|
|
} else {
|
|
MediaCasException.throwExceptionIfNeeded(
|
|
mICasHidl.processEcm(
|
|
toByteArray(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 (mICas != null) {
|
|
try {
|
|
if (data == null) {
|
|
data = new byte[0];
|
|
}
|
|
mICas.sendSessionEvent(mSessionId, event, arg, data);
|
|
} catch (RemoteException e) {
|
|
cleanupAndRethrowIllegalState();
|
|
}
|
|
} else {
|
|
if (mICasHidl11 == 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(
|
|
mICasHidl11.sendSessionEvent(
|
|
toByteArray(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 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 {
|
|
if (mICas != null) {
|
|
mICas.closeSession(mSessionId);
|
|
} else {
|
|
MediaCasStateException.throwExceptionIfNeeded(
|
|
mICasHidl.closeSession(toByteArray(mSessionId)));
|
|
}
|
|
mIsClosed = true;
|
|
removeSessionFromResourceMap(this);
|
|
} catch (RemoteException e) {
|
|
cleanupAndRethrowIllegalState();
|
|
}
|
|
}
|
|
}
|
|
|
|
Session createFromSessionId(byte[] sessionId) {
|
|
if (sessionId == null || sessionId.length == 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;
|
|
}
|
|
}
|
|
|
|
android.hardware.cas.V1_0.IMediaCasService serviceHidl = getServiceHidl();
|
|
if (serviceHidl != null) {
|
|
try {
|
|
return serviceHidl.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 {
|
|
AidlCasPluginDescriptor[] descriptors = service.enumeratePlugins();
|
|
if (descriptors.length == 0) {
|
|
return null;
|
|
}
|
|
PluginDescriptor[] results = new PluginDescriptor[descriptors.length];
|
|
for (int i = 0; i < results.length; i++) {
|
|
results[i] = new PluginDescriptor(descriptors[i]);
|
|
}
|
|
return results;
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "Some exception while enumerating plugins");
|
|
}
|
|
}
|
|
|
|
android.hardware.cas.V1_0.IMediaCasService serviceHidl = getServiceHidl();
|
|
if (serviceHidl != null) {
|
|
try {
|
|
ArrayList<HidlCasPluginDescriptor> descriptors = serviceHidl.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();
|
|
if (service != null) {
|
|
Log.d(TAG, "Use CAS AIDL interface to create plugin");
|
|
mICas = service.createPlugin(casSystemId, mBinder);
|
|
} else {
|
|
android.hardware.cas.V1_0.IMediaCasService serviceV10 = getServiceHidl();
|
|
android.hardware.cas.V1_2.IMediaCasService serviceV12 =
|
|
android.hardware.cas.V1_2.IMediaCasService.castFrom(serviceV10);
|
|
if (serviceV12 == null) {
|
|
android.hardware.cas.V1_1.IMediaCasService serviceV11 =
|
|
android.hardware.cas.V1_1.IMediaCasService.castFrom(serviceV10);
|
|
if (serviceV11 == null) {
|
|
Log.d(TAG, "Used cas@1_0 interface to create plugin");
|
|
mICasHidl = serviceV10.createPlugin(casSystemId, mBinderHidl);
|
|
} else {
|
|
Log.d(TAG, "Used cas@1.1 interface to create plugin");
|
|
mICasHidl =
|
|
mICasHidl11 = serviceV11.createPluginExt(casSystemId, mBinderHidl);
|
|
}
|
|
} else {
|
|
Log.d(TAG, "Used cas@1.2 interface to create plugin");
|
|
mICasHidl =
|
|
mICasHidl11 =
|
|
mICasHidl12 =
|
|
android.hardware.cas.V1_2.ICas.castFrom(
|
|
serviceV12.createPluginExt(
|
|
casSystemId, mBinderHidl));
|
|
}
|
|
}
|
|
} catch(Exception e) {
|
|
Log.e(TAG, "Failed to create plugin: " + e);
|
|
mICas = null;
|
|
mICasHidl = null;
|
|
} finally {
|
|
if (mICas == null && mICasHidl == 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();
|
|
profile.tvInputSessionId = tvInputServiceSessionId;
|
|
profile.useCase = 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() {
|
|
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<Byte> sessionId) {
|
|
mStatus = status;
|
|
mSession = createFromSessionId(toBytes(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(toBytes(sessionId));
|
|
}
|
|
}
|
|
|
|
private int getSessionResourceHandle() throws MediaCasException {
|
|
validateInternalStates();
|
|
|
|
int[] sessionResourceHandle = new int[1];
|
|
sessionResourceHandle[0] = -1;
|
|
if (mTunerResourceManager != null) {
|
|
CasSessionRequest casSessionRequest = new CasSessionRequest();
|
|
casSessionRequest.clientId = mClientId;
|
|
casSessionRequest.casSystemId = 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 {
|
|
if (mICas != null) {
|
|
try {
|
|
byte[] sessionId = mICas.openSessionDefault();
|
|
Session session = createFromSessionId(sessionId);
|
|
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.
|
|
*
|
|
* <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 (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();
|
|
}
|
|
}
|