前言
Android TV 输入框架 (TIF) 简化了向 Android TV 传送直播内容的过程。Android TIF 为制造商提供了一个标准 API,供他们创建能够控制 Android TV 的输入模块,并让他们可以通过 TV Input 发布的元数据来搜索和推荐直播电视内容。
概念解释
什么是LiveTv
LiveTv是Android TV系统中的一个TV应用,它是一个系统应用,Aosp中提供了一个参考的LiveTv,那么什么是LiveTv,它和普通的TV应用有什么区别呢?简单一句就是:它向用户展示直播电视内容。但是LiveTv这个应用本身不提供这些直播的数据,它主要的功能是做展示。那么这些内容哪里来呢,答案是Tv Input.
什么是TvInput
TvInput就是我们上面LiveTv直播内容的数据来源,这些来源既可以是硬件来源(例如 HDMI 端口和内置调谐器)中的直播视频内容,也可以是软件来源(例如通过互联网在线播放的内容)中的直播视频内容,软件来源一般是个独立的apk,我们把它叫Input应用。有了这些输入来源,我们就可以在LiveTv中设置这些来源,然后将它们输入的内容展示到LiveTv中。
什么是Tv Input Framework(TIF)
有了LiveTv用于展示内容,有了TvInput提供内容,是不是就万事大吉了,事情并没有那么简单。因为LiveTv和TvInput是不能直接交互的,就好比两个说不同语言的人,彼此能看见对方,但是没法交流。这个时候Tv Input Framework出现了,到这里应该有人就纳闷了为什么LiveTv和TvInput不直接交互呢,需要TIF这个第三者。我的理解: TIF的作用是为了统一接口方便交流,因为TvInput很多情况下是由第三方实现的,而LiveTv是由厂商实现的,两者之间没法直接交互或者交互起来很麻烦,需要事先协商接口,于是Android 提供了一套TIF,说你们提供数据的和接受数据的都按这个标准来,于是这个问题就愉快的解决了。
下面贴一张Android官方的TIF原理图,非常的形象
图中的TVProvider和TV Input Manager就是TIF中的内容。其中TV主要的功能是将频道和节目信息从TVInput传入到LiveTv.而TV Input Manager它对LiveTv与 TV Input 之间的交互进行控制,并提供家长控制功能。TV Input Manager 必须与 TV Input 创建一对一的会话。
创建TvInput应用
前面 的概念解释中已经说过,TvInput应用是直播内容的输入来源,那该怎么创建这个应用呢?其实主要就是创建一个我们自定义的Service,而这个Service要继承系统的TvInputService,当然为了简化这个过程我们可以使用android官方提供的TIF 随播内容库:
compile 'com.google.android.libraries.tv:companionlibrary:0.2'
public class TvService extends BaseTvInputService {
@Nullable
@Override
public TvInputService.Session onCreateSession(@NonNull String inputId) {
TvInputSessionImpl session = new TvInputSessionImpl(this, inputId);
session.setOverlayViewEnabled(true);
return session;
}
}
这里的BaseTvInputService也是继承的TvInputService.然后我们需要复写onCreateSession方法,创建我们自己的Session,用于和TvInputManager交互,最后在清单文件中配置如下:
<service
android:name=".service.TvService"
android:permission="android.permission.BIND_TV_INPUT">
<intent-filter>
<action android:name="android.media.tv.TvInputService" />
</intent-filter>
<meta-data
android:name="android.media.tv.input"
android:resource="@xml/richtvinputservice" />
</service>
注意上面有3个地方的改动:
-
添加权限:
android:permission="android.permission.BIND_TV_INPUT"
-
添加过滤器action
<intent-filter> <action android:name="android.media.tv.TvInputService" /> </intent-filter>
-
添加meta-data
<meta-data android:name="android.media.tv.input" android:resource="@xml/richtvinputservice" />
<?xml version="1.0" encoding="utf-8"?>
<tv-input xmlns:android="http://schemas.android.com/apk/res/android"
android:settingsActivity="com.xray.tv.input.MainActivity"
android:setupActivity="com.xray.tv.input.MainActivity" />
在xml/richtvinputservice中配置了两个activty,这个是提供LiveTv去打开的,比如第一次启动这个源时,需要启动到setupActivity所指定的activity,设置时需要启动到
settingsActivity配置的activity. 更多的细节请查看官方的文档开发 TV 输入服务.这个不是本文的重点。
创建LiveTv
LiveTv其实是个系统应用,一般由设备厂商提供。在aosp中有一个参考的应用LiveTv.
LiveTv中的代码主要逻辑就是读取Tv Provider中内容,通过TvManager和TvInput进行交互。具体文档请看TvInput框架
TIF工作流程介绍
通过上面的介绍,大致了解了TIF的使用步骤,那它是怎么工作的,原理是怎么样的?我们在LiveTv和TvInput中用到的一个很重要的类就是TvInputManager.首先看看它的实现
TvInputManager
TvInputManager的获取
TvInputManager tvInputManager = (TvInputManager) getSystemService(Context.TV_INPUT_SERVICE);
TvInputManager只是我们当前进程的代理,它的真正实现其实是一个系统的Service,所以我们可以知道这个service其实在system_server进程中,在类TvInputManagerService中实现。由于这个地方是跨进程通信,其实它使用的是aidl的方式,所以我们可以找到TvInputManager在aidl中定义的接口
# frameworks/base/media/java/android/media/tv/ITvInputManager.aidl
interface ITvInputManager {
List<TvInputInfo> getTvInputList(int userId);
TvInputInfo getTvInputInfo(in String inputId, int userId);
void updateTvInputInfo(in TvInputInfo inputInfo, int userId);
int getTvInputState(in String inputId, int userId);
//省略若干...
...
}
它的实现是在TvInputManagerService的内部类BinderService中。
# frameworks/base/services/core/java/com/android/server/tv/TvInputManagerService.java
private final class BinderService extends ITvInputManager.Stub {
@Override
public List<TvInputInfo> getTvInputList(int userId) {
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
Binder.getCallingUid(), userId, "getTvInputList");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
UserState userState = getOrCreateUserStateLocked(resolvedUserId);
List<TvInputInfo> inputList = new ArrayList<>();
for (TvInputState state : userState.inputMap.values()) {
inputList.add(state.info);
}
return inputList;
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public TvInputInfo getTvInputInfo(String inputId, int userId) {
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
Binder.getCallingUid(), userId, "getTvInputInfo");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
UserState userState = getOrCreateUserStateLocked(resolvedUserId);
TvInputState state = userState.inputMap.get(inputId);
return state == null ? null : state.info;
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
...
}
那么BinderService实例化在什么地方呢?这就涉及到TvInputManagerService这个系统service的启动。
TvInputManagerService 启动
TvInputManagerService是在SystemServer中启动的,具体在SystemServer类的startOtherServices方法中
# frameworks/base/services/java/com/android/server/SystemServer.java
/**
* Starts a miscellaneous grab bag of stuff that has yet to be refactored and organized.
*/
private void startOtherServices() {
...
if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_LIVE_TV)
|| mPackageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
traceBeginAndSlog("StartTvInputManager");
mSystemServiceManager.startService(TvInputManagerService.class);
traceEnd();
}
...
}
通过反射实例化TvInputManagerService
# frameworks/base/services/core/java/com/android/server/SystemServiceManager.java
public <T extends SystemService> T startService(Class<T> serviceClass) {
try {
final String name = serviceClass.getName();
Slog.i(TAG, "Starting " + name);
Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "StartService " + name);
// Create the service.
if (!SystemService.class.isAssignableFrom(serviceClass)) {
throw new RuntimeException("Failed to create " + name
+ ": service must extend " + SystemService.class.getName());
}
final T service;
try {
Constructor<T> constructor = serviceClass.getConstructor(Context.class);
service = constructor.newInstance(mContext);
} catch (InstantiationException ex) {
throw new RuntimeException("Failed to create service " + name
+ ": service could not be instantiated", ex);
} catch (IllegalAccessException ex) {
throw new RuntimeException("Failed to create service " + name
+ ": service must have a public constructor with a Context argument", ex);
} catch (NoSuchMethodException ex) {
throw new RuntimeException("Failed to create service " + name
+ ": service must have a public constructor with a Context argument", ex);
} catch (InvocationTargetException ex) {
throw new RuntimeException("Failed to create service " + name
+ ": service constructor threw an exception", ex);
}
startService(service);
return service;
} finally {
Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
}
}
调用到SystemServiceManager的startService方法
# frameworks/base/services/core/java/com/android/server/SystemServiceManager.java
public void startService(@NonNull final SystemService service) {
// Register it.
mServices.add(service);
// Start it.
long time = SystemClock.elapsedRealtime();
try {
//这里调用了TvInputManagerService的onStart()
service.onStart();
} catch (RuntimeException ex) {
throw new RuntimeException("Failed to start service " + service.getClass().getName()
+ ": onStart threw an exception", ex);
}
warnIfTooLong(SystemClock.elapsedRealtime() - time, service, "onStart");
}
之后调用到TvInputManagerService的onStart()方法
# frameworks/base/services/core/java/com/android/server/tv/TvInputManagerService.java
@Override
public void onStart() {
publishBinderService(Context.TV_INPUT_SERVICE, new BinderService());
}
看见了没,BinderService实例化了。这里是我们TvInputManager的真正实现。
绑定TvInputService
在上面的章节,我们创建TvInput应用的时候创建了一个TvInputService,还创建了一个TvInputService.Session, 并且实现了Session里的方法,比如提供播放器等等(播放器是TvInput提供的,但是展示的页面在LiveTv中)。那么TIF是怎么和TvInputService取得交流的?,现在我们研究一下TvManagerService的实现。
监测安装包的状态
为什么要监测安装包的状态,因为TvInput经常是以第三方应用的方式实现的,当TvInput应用安装时,TvInputManagerService会检测安装包中是否包含TvInputService。
# frameworks/base/services/core/java/com/android/server/tv/TvInputManagerService.java
private void registerBroadcastReceivers() {
PackageMonitor monitor = new PackageMonitor() {
private void buildTvInputList(String[] packages) {
synchronized (mLock) {
if (mCurrentUserId == getChangingUserId()) {
buildTvInputListLocked(mCurrentUserId, packages);
buildTvContentRatingSystemListLocked(mCurrentUserId);
}
}
}
@Override
public void onPackageUpdateFinished(String packageName, int uid) {
if (DEBUG) Slog.d(TAG, "onPackageUpdateFinished(packageName=" + packageName + ")");
// This callback is invoked when the TV input is reinstalled.
// In this case, isReplacing() always returns true.
buildTvInputList(new String[] { packageName });
}
...
}
当有安装包安装时,监测其中是否有TvInputService,并且权限符合则绑定这个Service.
# frameworks/base/services/core/java/com/android/server/tv/TvInputManagerService.java
private void buildTvInputListLocked(int userId, String[] updatedPackages) {
UserState userState = getOrCreateUserStateLocked(userId);
userState.packageSet.clear();
if (DEBUG) Slog.d(TAG, "buildTvInputList");
PackageManager pm = mContext.getPackageManager();
List<ResolveInfo> services = pm.queryIntentServicesAsUser(
new Intent(TvInputService.SERVICE_INTERFACE),
PackageManager.GET_SERVICES | PackageManager.GET_META_DATA,
userId);
List<TvInputInfo> inputList = new ArrayList<>();
for (ResolveInfo ri : services) {
ServiceInfo si = ri.serviceInfo;
//检测是否有android.permission.BIND_TV_INPUT这个权限
if (!android.Manifest.permission.BIND_TV_INPUT.equals(si.permission)) {
Slog.w(TAG, "Skipping TV input " + si.name + ": it does not require the permission "
+ android.Manifest.permission.BIND_TV_INPUT);
continue;
}
ComponentName component = new ComponentName(si.packageName, si.name);
if (hasHardwarePermission(pm, component)) {
ServiceState serviceState = userState.serviceStateMap.get(component);
if (serviceState == null) {
// New hardware input found. Create a new ServiceState and connect to the
// service to populate the hardware list.
serviceState = new ServiceState(component, userId);
userState.serviceStateMap.put(component, serviceState);
updateServiceConnectionLocked(component, userId);
} else {
inputList.addAll(serviceState.hardwareInputMap.values());
}
} else {
try {
TvInputInfo info = new TvInputInfo.Builder(mContext, ri).build();
inputList.add(info);
} catch (Exception e) {
Slog.e(TAG, "failed to load TV input " + si.name, e);
continue;
}
}
userState.packageSet.add(si.packageName);
}
Map<String, TvInputState> inputMap = new HashMap<>();
for (TvInputInfo info : inputList) {
if (DEBUG) {
Slog.d(TAG, "add " + info.getId());
}
TvInputState inputState = userState.inputMap.get(info.getId());
if (inputState == null) {
inputState = new TvInputState();
}
inputState.info = info;
inputMap.put(info.getId(), inputState);
}
for (String inputId : inputMap.keySet()) {
if (!userState.inputMap.containsKey(inputId)) {
notifyInputAddedLocked(userState, inputId);
} else if (updatedPackages != null) {
// Notify the package updates
ComponentName component = inputMap.get(inputId).info.getComponent();
for (String updatedPackage : updatedPackages) {
if (component.getPackageName().equals(updatedPackage)) {
//绑定TvInputService
updateServiceConnectionLocked(component, userId);
notifyInputUpdatedLocked(userState, inputId);
break;
}
}
}
}
...
}
绑定第三方自定义的TvInputService
# frameworks/base/services/core/java/com/android/server/tv/TvInputManagerService.java
private void updateServiceConnectionLocked(ComponentName component, int userId) {
UserState userState = getOrCreateUserStateLocked(userId);
ServiceState serviceState = userState.serviceStateMap.get(component);
if (serviceState == null) {
return;
}
if (serviceState.reconnecting) {
if (!serviceState.sessionTokens.isEmpty()) {
// wait until all the sessions are removed.
return;
}
serviceState.reconnecting = false;
}
boolean shouldBind;
if (userId == mCurrentUserId) {
shouldBind = !serviceState.sessionTokens.isEmpty() || serviceState.isHardware;
} else {
// For a non-current user,
// if sessionTokens is not empty, it contains recording sessions only
// because other sessions must have been removed while switching user
// and non-recording sessions are not created by createSession().
shouldBind = !serviceState.sessionTokens.isEmpty();
}
if (serviceState.service == null && shouldBind) {
// This means that the service is not yet connected but its state indicates that we
// have pending requests. Then, connect the service.
if (serviceState.bound) {
// We have already bound to the service so we don't try to bind again until after we
// unbind later on.
return;
}
if (DEBUG) {
Slog.d(TAG, "bindServiceAsUser(service=" + component + ", userId=" + userId + ")");
}
//bind 第三方应用自定义的TvInputService
Intent i = new Intent(TvInputService.SERVICE_INTERFACE).setComponent(component);
serviceState.bound = mContext.bindServiceAsUser(
i, serviceState.connection,
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE,
new UserHandle(userId));
} else if (serviceState.service != null && !shouldBind) {
// This means that the service is already connected but its state indicates that we have
// nothing to do with it. Then, disconnect the service.
if (DEBUG) {
Slog.d(TAG, "unbindService(service=" + component + ")");
}
mContext.unbindService(serviceState.connection);
userState.serviceStateMap.remove(component);
}
}
到这里TvInputManagerService就绑定了第三方应用中自定义的TvInputService
InputServiceConnection
TvInputService现在绑定了,那么TvInputMangerService和TvInputService交互的逻辑就到了ServiceConnection中,它的实现在InputServiceConnection中,现在就看看InputServiceConnection里的逻辑。
onServiceConnected
在onServiceConnected成功后,就可以拿到从TvInputService中获取的Binder对象.
@Override
public void onServiceConnected(ComponentName component, IBinder service) {
if (DEBUG) {
Slog.d(TAG, "onServiceConnected(component=" + component + ")");
}
synchronized (mLock) {
UserState userState = mUserStates.get(mUserId);
if (userState == null) {
// The user was removed while connecting.
mContext.unbindService(this);
return;
}
ServiceState serviceState = userState.serviceStateMap.get(mComponent);
serviceState.service = ITvInputService.Stub.asInterface(service);
// Register a callback, if we need to.
if (serviceState.isHardware && serviceState.callback == null) {
serviceState.callback = new ServiceCallback(mComponent, mUserId);
try {
serviceState.service.registerCallback(serviceState.callback);
} catch (RemoteException e) {
Slog.e(TAG, "error in registerCallback", e);
}
}
List<IBinder> tokensToBeRemoved = new ArrayList<>();
// And create sessions, if any.
for (IBinder sessionToken : serviceState.sessionTokens) {
if (!createSessionInternalLocked(serviceState.service, sessionToken, mUserId)) {
tokensToBeRemoved.add(sessionToken);
}
}
for (IBinder sessionToken : tokensToBeRemoved) {
removeSessionStateLocked(sessionToken, mUserId);
}
for (TvInputState inputState : userState.inputMap.values()) {
if (inputState.info.getComponent().equals(component)
&& inputState.state != INPUT_STATE_CONNECTED) {
notifyInputStateChangedLocked(userState, inputState.info.getId(),
inputState.state, null);
}
}
if (serviceState.isHardware) {
serviceState.hardwareInputMap.clear();
for (TvInputHardwareInfo hardware : mTvInputHardwareManager.getHardwareList()) {
try {
serviceState.service.notifyHardwareAdded(hardware);
} catch (RemoteException e) {
Slog.e(TAG, "error in notifyHardwareAdded", e);
}
}
for (HdmiDeviceInfo device : mTvInputHardwareManager.getHdmiDeviceList()) {
try {
serviceState.service.notifyHdmiDeviceAdded(device);
} catch (RemoteException e) {
Slog.e(TAG, "error in notifyHdmiDeviceAdded", e);
}
}
}
}
}
和第三方的TvInputService联通之后,就需要进行交互了,它们之间交互需要创建一个Session,也就是TvInputService.Session,这个Session中的交互是通过ITvInputSessionCallback来实现的,现在看一下ITvInputSessionCallback.aidl这个文件
oneway interface ITvInputSessionCallback {
void onSessionCreated(ITvInputSession session, in IBinder hardwareSessionToken);
void onSessionEvent(in String name, in Bundle args);
void onChannelRetuned(in Uri channelUri);
void onTracksChanged(in List<TvTrackInfo> tracks);
void onTrackSelected(int type, in String trackId);
void onVideoAvailable();
void onVideoUnavailable(int reason);
void onContentAllowed();
void onContentBlocked(in String rating);
void onLayoutSurface(int left, int top, int right, int bottom);
void onTimeShiftStatusChanged(int status);
void onTimeShiftStartPositionChanged(long timeMs);
void onTimeShiftCurrentPositionChanged(long timeMs);
// For the recording session
void onTuned(in Uri channelUri);
void onRecordingStopped(in Uri recordedProgramUri);
void onError(int error);
}
这就是我们在自定义第三方TvInputService时,根据我们的需求,需要实现的方法。
讲到这里TvInputManager和第三方TvInputService的交互就完成了.
TvProvider
LiveTv和TvInput之间交互还有一种方式就是TvProvider, TvInput应用会将自己的频道和节目数据写入TvProvider对应的数据库中,数据库的地址在
/data/data/com.android.providers.tv/databases/tv.db
这样LiveTv就可以读取TvProvider中的数据了。当然这里的数据除了LiveTv和当前的TvInput应用,其他应用是没有权限读取这里的数据的。
参考Demo
总结
TIF是Android 电视特有的一个部分,但是他和Android Framework的其他模块逻辑是类似的,比如AMS、WMS、PMS. 在阅读源码的过程中,我们可以了解它为什么这么设计,比如TIF它本身就是为了第三方的TvInput能有一套统一的标准Api而设计的。还有TvInputService这块的设计可以给我们在跨进程通信设计Api时带来参考。
关于我
- 公众号: CodingDev