preface
The Android TV Input Framework (TIF) simplifies the process of delivering live content to Android TV. Android TIF provides a standard API for manufacturers to create Input modules that control Android TV, and allows them to search for and recommend live TV content using metadata published by TV Input.
Concept to explain
What is the LiveTv
LiveTv is a TV application in Android TV system, it is a system application, Aosp provides a reference of LiveTv, so what is LiveTv, what is the difference between it and ordinary TV application? Simply put: it shows users live TV content. But the LiveTv app itself doesn’t provide data about these live broadcasts. Its main function is to make presentations. So where does that come from? The answer is Tv Input.
What is a TvInput
TvInput is our data source for LiveTv live content above. These sources can be either live video content from hardware sources (such as HDMI ports and built-in tuners) or live video content from software sources (such as content streamed online over the Internet). The software source is usually a separate APK, which we call the Input application. With these input sources, we can set them up in LiveTv and display their input to LiveTv.
Tv Input Framework(TIF)
With LiveTv to show the content and TvInput to provide the content, is everything all right? It’s not that simple. Because LiveTv and TvInput cannot directly interact, just like two people who speak different languages can see each other, but cannot communicate. This is where the TvInput Framework comes in. At this point, some people are wondering why LiveTv and TvInput don’t interact directly. My understanding: The purpose of TIF is to unify the interface and facilitate communication. In many cases, TvInput is implemented by a third party, while LiveTv is implemented by a vendor. They cannot directly interact with each other or it is troublesome to interact with each other, so the interface needs to be negotiated in advance. Say that those who provide data and those who receive data will follow this standard, and the problem is happily solved.
Here is an official Android TIF schematic, very image
The TVProvider and TV Input Manager in the figure are contents in TIF. The main function of TV is to transfer channel and program information from TVInput to LiveTv. TV Input Manager controls the interaction between LiveTv and TV Input and provides parental control function. The TV Input Manager must create a one-on-one session with THE TV Input.
Create the TvInput application
As mentioned in the previous conceptual explanation, the TvInput app is the input source for live streaming. How do you create this app? To simplify this process, we can use the official TIF content library provided by Android. If you want to create a custom TIF content library, you can use the TvInputService.
The compile 'com. Google. Android. Libraries. TV: companionlibrary: 0.2'Copy the code
public class TvService extends BaseTvInputService {
@Nullable
@Override
public TvInputService.Session onCreateSession(@NonNull String inputId) {
TvInputSessionImpl session = new TvInputSessionImpl(this, inputId);
session.setOverlayViewEnabled(true);
returnsession; }}Copy the code
Here BaseTvInputService is also inherited from TvInputService. Then we need to copy the onCreateSession method, create our own Session to interact with TvInputManager, and configure it in the manifest file as follows:
<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>
Copy the code
Note the three changes above:
-
Add permission:
android:permission="android.permission.BIND_TV_INPUT" Copy the code
-
Add filter Action
<intent-filter> <action android:name="android.media.tv.TvInputService" /> </intent-filter> Copy the code
-
Add a meta – data
<meta-data android:name="android.media.tv.input" android:resource="@xml/richtvinputservice" /> Copy the code
<? 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" />
Copy the code
In the XML/RichtVinputService, configure two Activty, this is to provide LiveTv to open, for example, the first time to start the source, need to start to the activity specified by setupActivity, set to start to
For more details, see the official documentation for developing the TV input service. That’s not the point of this article.
Create the LiveTv
LiveTv is actually a system application, usually provided by the device manufacturer. There is a reference to the application LiveTv in AOSP.
The main logic of the LiveTv code is to read the CONTENT of the Tv Provider and interact with TvInput through the TvManager. See the TvInput framework for detailed documentation
This section describes the TIF workflow
With the introduction above, you have a general understanding of the steps of using TIF. How does it work and how does it work? An important class that we use in LiveTv and TvInput is TvInputManager. First look at the implementation
TvInputManager
TvInputManager access
TvInputManager tvInputManager = (TvInputManager) getSystemService(Context.TV_INPUT_SERVICE);
Copy the code
TvInputManager is just a proxy for our current process. The actual implementation of TvInputManager is actually a system Service, so we can know that the Service is actually implemented in the system_server process, in the TvInputManagerService class. Since this place is communicating across processes, it actually uses AIDL, so we can find the interface defined by TvInputManager in 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); // Omit some... . }Copy the code
Its implementation is in the BinderService inner class of TvInputManagerService.
# 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); }}... }Copy the code
So where is the BinderService instantiated? This involves starting the system service TvInputManagerService.
TvInputManagerService start
TvInputManagerService is started in SystemServer, in the startOtherServices method of the SystemServer class
# 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();
}
...
}
Copy the code
Instantiate TvInputManagerService through reflection
# 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);
}
}
Copy the code
Call the startService method of SystemServiceManager
# 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 {// onStart() of TvInputManagerService is called; } catch (RuntimeException ex) { throw new RuntimeException("Failed to start service " + service.getClass().getName() + ": onStart threw an exception", ex); } warnIfTooLong(SystemClock.elapsedRealtime() - time, service, "onStart"); }Copy the code
The onStart() method of the TvInputManagerService is then called
# frameworks/base/services/core/java/com/android/server/tv/TvInputManagerService.java
@Override
public void onStart() {
publishBinderService(Context.TV_INPUT_SERVICE, new BinderService());
}
Copy the code
See? BinderService is instantiated. Here is the actual implementation of our TvInputManager.
Binding TvInputService
In the previous section, when we created the TvInput application, we created a TvInputService. We also created a tvinputService. Session, and implemented the methods in the Session, such as providing players, etc. (players are provided by TvInput, But the page shown is in LiveTv). So how does TIF communicate with TvInputService? Now let’s look at the TvManagerService implementation.
Monitor the status of the installation package
The reason for monitoring the status of the installation package is that TvInput is often implemented as a third-party application. When the TvInput application is installed, TvInputManagerService checks whether the installation package contains 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 }); }... }Copy the code
When the installation package is installed, check whether there is TvInputService in it, and bind the Service if the permission matches.
# 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; // Check whether there is an 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)) {// Bind TvInputService updateServiceConnectionLocked(component, userId); notifyInputUpdatedLocked(userState, inputId); break; }}}}... }Copy the code
Bind a third-party 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 a third party application to a custom 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); }}Copy the code
In this case, TvInputManagerService is bound to the third-party application’s custom TvInputService
InputServiceConnection
TvInputService is now bound, so the logic for TvInputMangerService to interact with TvInputService is in ServiceConnection, which is implemented in InputServiceConnection. Now look at the logic in InputServiceConnection.
onServiceConnected
After onServiceConnected is successful, you can get a Binder object from the TvInputService.
@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);
}
}
}
}
}
Copy the code
After you connect to the TvInputService of the third party, you need to interact with them. To interact with them, you need to create a Session, which is tvinputService.session. The interaction in the Session is done by ITvInputSessionCallback, now, look at ITvInputSessionCallback aidl this file
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(a);
void onVideoUnavailable(int reason);
void onContentAllowed(a);
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);
}
Copy the code
This is the method we need to implement according to our requirements when we customize third-party TvInputService.
The interaction between TvInputManager and third-party TvInputService is complete.
TvProvider
Another way of interaction between LiveTv and TvInput is the TvProvider. The TvInput application will write its channel and program data to the database corresponding to the TvProvider. The database address is
/data/data/com.android.providers.tv/databases/tv.db
Copy the code
This allows LiveTv to read data from the TvProvider. Of course, other than LiveTv and the current TvInput application, no other application has permission to read this data.
Refer to the Demo
- input_demo
- LiveTv
conclusion
TIF is a specific part of Android TV, but it is similar to other modules in the Android Framework, such as AMS, WMS, and PMS. In the process of reading the source code, we can understand why it is designed the way it is. For example, TIF itself is designed so that third party TvInput can have a set of standard Api. Also, the design of TvInputService can give us a reference when designing Api for cross-process communication.