blob: 28947083854bdc4ff6fd8707bcfb134a89c83ac1 [file] [log] [blame]
/*
* Copyright (C) 2014 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 com.android.server.tv;
import static android.media.AudioManager.DEVICE_NONE;
import static android.media.tv.TvInputManager.INPUT_STATE_CONNECTED;
import static android.media.tv.TvInputManager.INPUT_STATE_CONNECTED_STANDBY;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.pm.UserInfo;
import android.graphics.Rect;
import android.hardware.hdmi.HdmiControlManager;
import android.hardware.hdmi.HdmiDeviceInfo;
import android.media.PlaybackParams;
import android.media.tv.DvbDeviceInfo;
import android.media.tv.ITvInputClient;
import android.media.tv.ITvInputHardware;
import android.media.tv.ITvInputHardwareCallback;
import android.media.tv.ITvInputManager;
import android.media.tv.ITvInputManagerCallback;
import android.media.tv.ITvInputService;
import android.media.tv.ITvInputServiceCallback;
import android.media.tv.ITvInputSession;
import android.media.tv.ITvInputSessionCallback;
import android.media.tv.TunedInfo;
import android.media.tv.TvContentRating;
import android.media.tv.TvContentRatingSystemInfo;
import android.media.tv.TvContract;
import android.media.tv.TvInputHardwareInfo;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager;
import android.media.tv.TvInputService;
import android.media.tv.TvStreamConfig;
import android.media.tv.TvTrackInfo;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
import android.view.InputChannel;
import android.view.Surface;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.content.PackageMonitor;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.IoThread;
import com.android.server.SystemService;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** This class provides a system service that manages television inputs. */
public final class TvInputManagerService extends SystemService {
private static final boolean DEBUG = false;
private static final String TAG = "TvInputManagerService";
private static final String DVB_DIRECTORY = "/dev/dvb";
private static final int APP_TAG_SELF = TunedInfo.APP_TAG_SELF;
private static final String PERMISSION_ACCESS_WATCHED_PROGRAMS =
"com.android.providers.tv.permission.ACCESS_WATCHED_PROGRAMS";
// There are two different formats of DVB frontend devices. One is /dev/dvb%d.frontend%d,
// another one is /dev/dvb/adapter%d/frontend%d. Followings are the patterns for selecting the
// DVB frontend devices from the list of files in the /dev and /dev/dvb/adapter%d directory.
private static final Pattern sFrontEndDevicePattern =
Pattern.compile("^dvb([0-9]+)\\.frontend([0-9]+)$");
private static final Pattern sAdapterDirPattern =
Pattern.compile("^adapter([0-9]+)$");
private static final Pattern sFrontEndInAdapterDirPattern =
Pattern.compile("^frontend([0-9]+)$");
private final Context mContext;
private final TvInputHardwareManager mTvInputHardwareManager;
private final UserManager mUserManager;
// A global lock.
private final Object mLock = new Object();
// ID of the current user.
@GuardedBy("mLock")
private int mCurrentUserId = UserHandle.USER_SYSTEM;
// IDs of the running profiles. Their parent user ID should be mCurrentUserId.
@GuardedBy("mLock")
private final Set<Integer> mRunningProfiles = new HashSet<>();
// A map from user id to UserState.
@GuardedBy("mLock")
private final SparseArray<UserState> mUserStates = new SparseArray<>();
// A map from session id to session state saved in userstate
@GuardedBy("mLock")
private final Map<String, SessionState> mSessionIdToSessionStateMap = new HashMap<>();
private final WatchLogHandler mWatchLogHandler;
private final ActivityManager mActivityManager;
public TvInputManagerService(Context context) {
super(context);
mContext = context;
mWatchLogHandler = new WatchLogHandler(mContext.getContentResolver(),
IoThread.get().getLooper());
mTvInputHardwareManager = new TvInputHardwareManager(context, new HardwareListener());
mActivityManager =
(ActivityManager) getContext().getSystemService(Context.ACTIVITY_SERVICE);
mUserManager = (UserManager) getContext().getSystemService(Context.USER_SERVICE);
synchronized (mLock) {
getOrCreateUserStateLocked(mCurrentUserId);
}
}
@Override
public void onStart() {
publishBinderService(Context.TV_INPUT_SERVICE, new BinderService());
}
@Override
public void onBootPhase(int phase) {
if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
registerBroadcastReceivers();
} else if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) {
synchronized (mLock) {
buildTvInputListLocked(mCurrentUserId, null);
buildTvContentRatingSystemListLocked(mCurrentUserId);
}
}
mTvInputHardwareManager.onBootPhase(phase);
}
@Override
public void onUserUnlocking(@NonNull TargetUser user) {
if (DEBUG) Slog.d(TAG, "onUnlockUser(user=" + user + ")");
synchronized (mLock) {
if (mCurrentUserId != user.getUserIdentifier()) {
return;
}
buildTvInputListLocked(mCurrentUserId, null);
buildTvContentRatingSystemListLocked(mCurrentUserId);
}
}
private void registerBroadcastReceivers() {
PackageMonitor monitor = new PackageMonitor() {
private void buildTvInputList(String[] packages) {
int userId = getChangingUserId();
synchronized (mLock) {
if (mCurrentUserId == userId || mRunningProfiles.contains(userId)) {
buildTvInputListLocked(userId, packages);
buildTvContentRatingSystemListLocked(userId);
}
}
}
@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 });
}
@Override
public void onPackagesAvailable(String[] packages) {
if (DEBUG) {
Slog.d(TAG, "onPackagesAvailable(packages=" + Arrays.toString(packages) + ")");
}
// This callback is invoked when the media on which some packages exist become
// available.
if (isReplacing()) {
buildTvInputList(packages);
}
}
@Override
public void onPackagesUnavailable(String[] packages) {
// This callback is invoked when the media on which some packages exist become
// unavailable.
if (DEBUG) {
Slog.d(TAG, "onPackagesUnavailable(packages=" + Arrays.toString(packages)
+ ")");
}
if (isReplacing()) {
buildTvInputList(packages);
}
}
@Override
public void onSomePackagesChanged() {
// TODO: Use finer-grained methods(e.g. onPackageAdded, onPackageRemoved) to manage
// the TV inputs.
if (DEBUG) Slog.d(TAG, "onSomePackagesChanged()");
if (isReplacing()) {
if (DEBUG) Slog.d(TAG, "Skipped building TV input list due to replacing");
// When the package is updated, buildTvInputListLocked is called in other
// methods instead.
return;
}
buildTvInputList(null);
}
@Override
public boolean onPackageChanged(String packageName, int uid, String[] components) {
// The input list needs to be updated in any cases, regardless of whether
// it happened to the whole package or a specific component. Returning true so that
// the update can be handled in {@link #onSomePackagesChanged}.
return true;
}
};
monitor.register(mContext, null, UserHandle.ALL, true);
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_USER_SWITCHED);
intentFilter.addAction(Intent.ACTION_USER_REMOVED);
intentFilter.addAction(Intent.ACTION_USER_STARTED);
intentFilter.addAction(Intent.ACTION_USER_STOPPED);
mContext.registerReceiverAsUser(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (Intent.ACTION_USER_SWITCHED.equals(action)) {
switchUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0));
} else if (Intent.ACTION_USER_REMOVED.equals(action)) {
removeUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0));
} else if (Intent.ACTION_USER_STARTED.equals(action)) {
int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0);
startUser(userId);
} else if (Intent.ACTION_USER_STOPPED.equals(action)) {
int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0);
stopUser(userId);
}
}
}, UserHandle.ALL, intentFilter, null, null);
}
private static boolean hasHardwarePermission(PackageManager pm, ComponentName component) {
return pm.checkPermission(android.Manifest.permission.TV_INPUT_HARDWARE,
component.getPackageName()) == PackageManager.PERMISSION_GRANTED;
}
@GuardedBy("mLock")
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;
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);
}
// sort the input list by input id so that TvInputState.inputNumber is stable.
Collections.sort(inputList, Comparator.comparing(TvInputInfo::getId));
Map<String, TvInputState> inputMap = new HashMap<>();
ArrayMap<String, Integer> tisInputCount = new ArrayMap<>(inputMap.size());
for (TvInputInfo info : inputList) {
String inputId = info.getId();
if (DEBUG) {
Slog.d(TAG, "add " + inputId);
}
// Running count of input for each input service
Integer count = tisInputCount.get(inputId);
count = count == null ? Integer.valueOf(1) : count + 1;
tisInputCount.put(inputId, count);
TvInputState inputState = userState.inputMap.get(inputId);
if (inputState == null) {
inputState = new TvInputState();
}
inputState.info = info;
inputState.uid = getInputUid(info);
inputMap.put(inputId, inputState);
inputState.inputNumber = count;
}
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)) {
updateServiceConnectionLocked(component, userId);
notifyInputUpdatedLocked(userState, inputId);
break;
}
}
}
}
for (String inputId : userState.inputMap.keySet()) {
if (!inputMap.containsKey(inputId)) {
TvInputInfo info = userState.inputMap.get(inputId).info;
ServiceState serviceState = userState.serviceStateMap.get(info.getComponent());
if (serviceState != null) {
abortPendingCreateSessionRequestsLocked(serviceState, inputId, userId);
}
notifyInputRemovedLocked(userState, inputId);
}
}
userState.inputMap.clear();
userState.inputMap = inputMap;
}
private int getInputUid(TvInputInfo info) {
try {
return getContext().getPackageManager().getApplicationInfo(
info.getServiceInfo().packageName, 0).uid;
} catch (NameNotFoundException e) {
Slog.w(TAG, "Unable to get UID for " + info, e);
return Process.INVALID_UID;
}
}
@GuardedBy("mLock")
private void buildTvContentRatingSystemListLocked(int userId) {
UserState userState = getOrCreateUserStateLocked(userId);
userState.contentRatingSystemList.clear();
final PackageManager pm = mContext.getPackageManager();
Intent intent = new Intent(TvInputManager.ACTION_QUERY_CONTENT_RATING_SYSTEMS);
for (ResolveInfo resolveInfo :
pm.queryBroadcastReceivers(intent, PackageManager.GET_META_DATA)) {
ActivityInfo receiver = resolveInfo.activityInfo;
Bundle metaData = receiver.metaData;
if (metaData == null) {
continue;
}
int xmlResId = metaData.getInt(TvInputManager.META_DATA_CONTENT_RATING_SYSTEMS);
if (xmlResId == 0) {
Slog.w(TAG, "Missing meta-data '"
+ TvInputManager.META_DATA_CONTENT_RATING_SYSTEMS + "' on receiver "
+ receiver.packageName + "/" + receiver.name);
continue;
}
userState.contentRatingSystemList.add(
TvContentRatingSystemInfo.createTvContentRatingSystemInfo(xmlResId,
receiver.applicationInfo));
}
}
private void startUser(int userId) {
synchronized (mLock) {
if (userId == mCurrentUserId || mRunningProfiles.contains(userId)) {
// user already started
return;
}
UserInfo userInfo = mUserManager.getUserInfo(userId);
UserInfo parentInfo = mUserManager.getProfileParent(userId);
if (userInfo.isProfile()
&& parentInfo != null
&& parentInfo.id == mCurrentUserId) {
// only the children of the current user can be started in background
startProfileLocked(userId);
}
}
}
private void stopUser(int userId) {
if (userId == mCurrentUserId) {
switchUser(ActivityManager.getCurrentUser());
return;
}
releaseSessionOfUserLocked(userId);
unbindServiceOfUserLocked(userId);
mRunningProfiles.remove(userId);
}
private void startProfileLocked(int userId) {
mRunningProfiles.add(userId);
buildTvInputListLocked(userId, null);
buildTvContentRatingSystemListLocked(userId);
}
private void switchUser(int userId) {
synchronized (mLock) {
if (mCurrentUserId == userId) {
return;
}
UserInfo userInfo = mUserManager.getUserInfo(userId);
if (userInfo.isProfile()) {
Slog.w(TAG, "cannot switch to a profile!");
return;
}
for (int runningId : mRunningProfiles) {
releaseSessionOfUserLocked(runningId);
unbindServiceOfUserLocked(runningId);
}
mRunningProfiles.clear();
releaseSessionOfUserLocked(mCurrentUserId);
unbindServiceOfUserLocked(mCurrentUserId);
mCurrentUserId = userId;
buildTvInputListLocked(userId, null);
buildTvContentRatingSystemListLocked(userId);
mWatchLogHandler.obtainMessage(WatchLogHandler.MSG_SWITCH_CONTENT_RESOLVER,
getContentResolverForUser(userId)).sendToTarget();
}
}
@GuardedBy("mLock")
private void releaseSessionOfUserLocked(int userId) {
UserState userState = getUserStateLocked(userId);
if (userState == null) {
return;
}
List<SessionState> sessionStatesToRelease = new ArrayList<>();
for (SessionState sessionState : userState.sessionStateMap.values()) {
if (sessionState.session != null && !sessionState.isRecordingSession) {
sessionStatesToRelease.add(sessionState);
}
}
boolean notifyInfoUpdated = false;
for (SessionState sessionState : sessionStatesToRelease) {
try {
sessionState.session.release();
sessionState.currentChannel = null;
if (sessionState.isCurrent) {
sessionState.isCurrent = false;
notifyInfoUpdated = true;
}
} catch (RemoteException e) {
Slog.e(TAG, "error in release", e);
} finally {
if (notifyInfoUpdated) {
notifyCurrentChannelInfosUpdatedLocked(userState);
}
}
clearSessionAndNotifyClientLocked(sessionState);
}
}
@GuardedBy("mLock")
private void unbindServiceOfUserLocked(int userId) {
UserState userState = getUserStateLocked(userId);
if (userState == null) {
return;
}
for (Iterator<ComponentName> it = userState.serviceStateMap.keySet().iterator();
it.hasNext(); ) {
ComponentName component = it.next();
ServiceState serviceState = userState.serviceStateMap.get(component);
if (serviceState != null && serviceState.sessionTokens.isEmpty()) {
if (serviceState.callback != null) {
try {
serviceState.service.unregisterCallback(serviceState.callback);
} catch (RemoteException e) {
Slog.e(TAG, "error in unregisterCallback", e);
}
}
mContext.unbindService(serviceState.connection);
it.remove();
}
}
}
@GuardedBy("mLock")
private void clearSessionAndNotifyClientLocked(SessionState state) {
if (state.client != null) {
try {
state.client.onSessionReleased(state.seq);
} catch(RemoteException e) {
Slog.e(TAG, "error in onSessionReleased", e);
}
}
// If there are any other sessions based on this session, they should be released.
UserState userState = getOrCreateUserStateLocked(state.userId);
for (SessionState sessionState : userState.sessionStateMap.values()) {
if (state.sessionToken == sessionState.hardwareSessionToken) {
releaseSessionLocked(sessionState.sessionToken, Process.SYSTEM_UID, state.userId);
try {
sessionState.client.onSessionReleased(sessionState.seq);
} catch (RemoteException e) {
Slog.e(TAG, "error in onSessionReleased", e);
}
}
}
removeSessionStateLocked(state.sessionToken, state.userId);
}
private void removeUser(int userId) {
synchronized (mLock) {
UserState userState = getUserStateLocked(userId);
if (userState == null) {
return;
}
// Release all created sessions.
boolean notifyInfoUpdated = false;
for (SessionState state : userState.sessionStateMap.values()) {
if (state.session != null) {
try {
state.session.release();
state.currentChannel = null;
if (state.isCurrent) {
state.isCurrent = false;
notifyInfoUpdated = true;
}
} catch (RemoteException e) {
Slog.e(TAG, "error in release", e);
} finally {
if (notifyInfoUpdated) {
notifyCurrentChannelInfosUpdatedLocked(userState);
}
}
}
}
userState.sessionStateMap.clear();
// Unregister all callbacks and unbind all services.
for (ServiceState serviceState : userState.serviceStateMap.values()) {
if (serviceState.service != null) {
if (serviceState.callback != null) {
try {
serviceState.service.unregisterCallback(serviceState.callback);
} catch (RemoteException e) {
Slog.e(TAG, "error in unregisterCallback", e);
}
}
mContext.unbindService(serviceState.connection);
}
}
userState.serviceStateMap.clear();
// Clear everything else.
userState.inputMap.clear();
userState.packageSet.clear();
userState.contentRatingSystemList.clear();
userState.clientStateMap.clear();
userState.mCallbacks.kill();
userState.mainSessionToken = null;
mRunningProfiles.remove(userId);
mUserStates.remove(userId);
if (userId == mCurrentUserId) {
switchUser(UserHandle.USER_SYSTEM);
}
}
}
private ContentResolver getContentResolverForUser(int userId) {
UserHandle user = new UserHandle(userId);
Context context;
try {
context = mContext.createPackageContextAsUser("android", 0, user);
} catch (NameNotFoundException e) {
Slog.e(TAG, "failed to create package context as user " + user);
context = mContext;
}
return context.getContentResolver();
}
@GuardedBy("mLock")
private UserState getOrCreateUserStateLocked(int userId) {
UserState userState = getUserStateLocked(userId);
if (userState == null) {
userState = new UserState(mContext, userId);
mUserStates.put(userId, userState);
}
return userState;
}
@GuardedBy("mLock")
private ServiceState getServiceStateLocked(ComponentName component, int userId) {
UserState userState = getOrCreateUserStateLocked(userId);
ServiceState serviceState = userState.serviceStateMap.get(component);
if (serviceState == null) {
throw new IllegalStateException("Service state not found for " + component + " (userId="
+ userId + ")");
}
return serviceState;
}
@GuardedBy("mLock")
private SessionState getSessionStateLocked(IBinder sessionToken, int callingUid, int userId) {
UserState userState = getOrCreateUserStateLocked(userId);
return getSessionStateLocked(sessionToken, callingUid, userState);
}
@GuardedBy("mLock")
private SessionState getSessionStateLocked(IBinder sessionToken,
int callingUid, UserState userState) {
SessionState sessionState = userState.sessionStateMap.get(sessionToken);
if (sessionState == null) {
throw new SessionNotFoundException("Session state not found for token " + sessionToken);
}
// Only the application that requested this session or the system can access it.
if (callingUid != Process.SYSTEM_UID && callingUid != sessionState.callingUid) {
throw new SecurityException("Illegal access to the session with token " + sessionToken
+ " from uid " + callingUid);
}
return sessionState;
}
@GuardedBy("mLock")
private ITvInputSession getSessionLocked(IBinder sessionToken, int callingUid, int userId) {
return getSessionLocked(getSessionStateLocked(sessionToken, callingUid, userId));
}
@GuardedBy("mLock")
private ITvInputSession getSessionLocked(SessionState sessionState) {
ITvInputSession session = sessionState.session;
if (session == null) {
throw new IllegalStateException("Session not yet created for token "
+ sessionState.sessionToken);
}
return session;
}
private int resolveCallingUserId(int callingPid, int callingUid, int requestedUserId,
String methodName) {
return ActivityManager.handleIncomingUser(callingPid, callingUid, requestedUserId, false,
false, methodName, null);
}
@GuardedBy("mLock")
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 || mRunningProfiles.contains(userId)) {
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 + ")");
}
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);
}
}
@GuardedBy("mLock")
private void abortPendingCreateSessionRequestsLocked(ServiceState serviceState,
String inputId, int userId) {
// Let clients know the create session requests are failed.
UserState userState = getOrCreateUserStateLocked(userId);
List<SessionState> sessionsToAbort = new ArrayList<>();
for (IBinder sessionToken : serviceState.sessionTokens) {
SessionState sessionState = userState.sessionStateMap.get(sessionToken);
if (sessionState.session == null && (inputId == null
|| sessionState.inputId.equals(inputId))) {
sessionsToAbort.add(sessionState);
}
}
for (SessionState sessionState : sessionsToAbort) {
removeSessionStateLocked(sessionState.sessionToken, sessionState.userId);
sendSessionTokenToClientLocked(sessionState.client,
sessionState.inputId, null, null, sessionState.seq);
}
updateServiceConnectionLocked(serviceState.component, userId);
}
@GuardedBy("mLock")
private boolean createSessionInternalLocked(ITvInputService service, IBinder sessionToken,
int userId) {
UserState userState = getOrCreateUserStateLocked(userId);
SessionState sessionState = userState.sessionStateMap.get(sessionToken);
if (DEBUG) {
Slog.d(TAG, "createSessionInternalLocked(inputId="
+ sessionState.inputId + ", sessionId=" + sessionState.sessionId + ")");
}
InputChannel[] channels = InputChannel.openInputChannelPair(sessionToken.toString());
// Set up a callback to send the session token.
ITvInputSessionCallback callback = new SessionCallback(sessionState, channels);
boolean created = true;
// Create a session. When failed, send a null token immediately.
try {
if (sessionState.isRecordingSession) {
service.createRecordingSession(
callback, sessionState.inputId, sessionState.sessionId);
} else {
service.createSession(
channels[1], callback, sessionState.inputId, sessionState.sessionId);
}
} catch (RemoteException e) {
Slog.e(TAG, "error in createSession", e);
sendSessionTokenToClientLocked(sessionState.client, sessionState.inputId, null,
null, sessionState.seq);
created = false;
}
channels[1].dispose();
return created;
}
@GuardedBy("mLock")
private void sendSessionTokenToClientLocked(ITvInputClient client, String inputId,
IBinder sessionToken, InputChannel channel, int seq) {
try {
client.onSessionCreated(inputId, sessionToken, channel, seq);
} catch (RemoteException e) {
Slog.e(TAG, "error in onSessionCreated", e);
}
}
@GuardedBy("mLock")
@Nullable
private SessionState releaseSessionLocked(IBinder sessionToken, int callingUid, int userId) {
SessionState sessionState = null;
try {
sessionState = getSessionStateLocked(sessionToken, callingUid, userId);
UserState userState = getOrCreateUserStateLocked(userId);
if (sessionState.session != null) {
if (sessionToken == userState.mainSessionToken) {
setMainLocked(sessionToken, false, callingUid, userId);
}
sessionState.session.asBinder().unlinkToDeath(sessionState, 0);
sessionState.session.release();
}
sessionState.currentChannel = null;
if (sessionState.isCurrent) {
sessionState.isCurrent = false;
notifyCurrentChannelInfosUpdatedLocked(userState);
}
} catch (RemoteException | SessionNotFoundException e) {
Slog.e(TAG, "error in releaseSession", e);
} finally {
if (sessionState != null) {
sessionState.session = null;
}
}
removeSessionStateLocked(sessionToken, userId);
return sessionState;
}
@GuardedBy("mLock")
private void removeSessionStateLocked(IBinder sessionToken, int userId) {
UserState userState = getOrCreateUserStateLocked(userId);
if (sessionToken == userState.mainSessionToken) {
if (DEBUG) {
Slog.d(TAG, "mainSessionToken=null");
}
userState.mainSessionToken = null;
}
// Remove the session state from the global session state map of the current user.
SessionState sessionState = userState.sessionStateMap.remove(sessionToken);
if (sessionState == null) {
Slog.e(TAG, "sessionState null, no more remove session action!");
return;
}
// Also remove the session token from the session token list of the current client and
// service.
ClientState clientState = userState.clientStateMap.get(sessionState.client.asBinder());
if (clientState != null) {
clientState.sessionTokens.remove(sessionToken);
if (clientState.isEmpty()) {
userState.clientStateMap.remove(sessionState.client.asBinder());
sessionState.client.asBinder().unlinkToDeath(clientState, 0);
}
}
mSessionIdToSessionStateMap.remove(sessionState.sessionId);
ServiceState serviceState = userState.serviceStateMap.get(sessionState.componentName);
if (serviceState != null) {
serviceState.sessionTokens.remove(sessionToken);
}
updateServiceConnectionLocked(sessionState.componentName, userId);
// Log the end of watch.
SomeArgs args = SomeArgs.obtain();
args.arg1 = sessionToken;
args.arg2 = System.currentTimeMillis();
mWatchLogHandler.obtainMessage(WatchLogHandler.MSG_LOG_WATCH_END, args).sendToTarget();
}
@GuardedBy("mLock")
private void setMainLocked(IBinder sessionToken, boolean isMain, int callingUid, int userId) {
try {
SessionState sessionState = getSessionStateLocked(sessionToken, callingUid, userId);
if (sessionState.hardwareSessionToken != null) {
sessionState = getSessionStateLocked(sessionState.hardwareSessionToken,
Process.SYSTEM_UID, userId);
}
ServiceState serviceState = getServiceStateLocked(sessionState.componentName, userId);
if (!serviceState.isHardware) {
return;
}
ITvInputSession session = getSessionLocked(sessionState);
session.setMain(isMain);
if (sessionState.isMainSession != isMain) {
UserState userState = getUserStateLocked(userId);
sessionState.isMainSession = isMain;
notifyCurrentChannelInfosUpdatedLocked(userState);
}
} catch (RemoteException | SessionNotFoundException e) {
Slog.e(TAG, "error in setMain", e);
}
}
@GuardedBy("mLock")
private void notifyInputAddedLocked(UserState userState, String inputId) {
if (DEBUG) {
Slog.d(TAG, "notifyInputAddedLocked(inputId=" + inputId + ")");
}
int n = userState.mCallbacks.beginBroadcast();
for (int i = 0; i < n; ++i) {
try {
userState.mCallbacks.getBroadcastItem(i).onInputAdded(inputId);
} catch (RemoteException e) {
Slog.e(TAG, "failed to report added input to callback", e);
}
}
userState.mCallbacks.finishBroadcast();
}
@GuardedBy("mLock")
private void notifyInputRemovedLocked(UserState userState, String inputId) {
if (DEBUG) {
Slog.d(TAG, "notifyInputRemovedLocked(inputId=" + inputId + ")");
}
int n = userState.mCallbacks.beginBroadcast();
for (int i = 0; i < n; ++i) {
try {
userState.mCallbacks.getBroadcastItem(i).onInputRemoved(inputId);
} catch (RemoteException e) {
Slog.e(TAG, "failed to report removed input to callback", e);
}
}
userState.mCallbacks.finishBroadcast();
}
@GuardedBy("mLock")
private void notifyInputUpdatedLocked(UserState userState, String inputId) {
if (DEBUG) {
Slog.d(TAG, "notifyInputUpdatedLocked(inputId=" + inputId + ")");
}
int n = userState.mCallbacks.beginBroadcast();
for (int i = 0; i < n; ++i) {
try {
userState.mCallbacks.getBroadcastItem(i).onInputUpdated(inputId);
} catch (RemoteException e) {
Slog.e(TAG, "failed to report updated input to callback", e);
}
}
userState.mCallbacks.finishBroadcast();
}
@GuardedBy("mLock")
private void notifyInputStateChangedLocked(UserState userState, String inputId,
int state, ITvInputManagerCallback targetCallback) {
if (DEBUG) {
Slog.d(TAG, "notifyInputStateChangedLocked(inputId=" + inputId
+ ", state=" + state + ")");
}
if (targetCallback == null) {
int n = userState.mCallbacks.beginBroadcast();
for (int i = 0; i < n; ++i) {
try {
userState.mCallbacks.getBroadcastItem(i).onInputStateChanged(inputId, state);
} catch (RemoteException e) {
Slog.e(TAG, "failed to report state change to callback", e);
}
}
userState.mCallbacks.finishBroadcast();
} else {
try {
targetCallback.onInputStateChanged(inputId, state);
} catch (RemoteException e) {
Slog.e(TAG, "failed to report state change to callback", e);
}
}
}
@GuardedBy("mLock")
private void notifyCurrentChannelInfosUpdatedLocked(UserState userState) {
if (DEBUG) {
Slog.d(TAG, "notifyCurrentChannelInfosUpdatedLocked");
}
int n = userState.mCallbacks.beginBroadcast();
for (int i = 0; i < n; ++i) {
try {
ITvInputManagerCallback callback = userState.mCallbacks.getBroadcastItem(i);
Pair<Integer, Integer> pidUid = userState.callbackPidUidMap.get(callback);
if (mContext.checkPermission(android.Manifest.permission.ACCESS_TUNED_INFO,
pidUid.first, pidUid.second) != PackageManager.PERMISSION_GRANTED) {
continue;
}
List<TunedInfo> infos = getCurrentTunedInfosInternalLocked(
userState, pidUid.first, pidUid.second);
callback.onCurrentTunedInfosUpdated(infos);
} catch (RemoteException e) {
Slog.e(TAG, "failed to report updated current channel infos to callback", e);
}
}
userState.mCallbacks.finishBroadcast();
}
@GuardedBy("mLock")
private void updateTvInputInfoLocked(UserState userState, TvInputInfo inputInfo) {
if (DEBUG) {
Slog.d(TAG, "updateTvInputInfoLocked(inputInfo=" + inputInfo + ")");
}
String inputId = inputInfo.getId();
TvInputState inputState = userState.inputMap.get(inputId);
if (inputState == null) {
Slog.e(TAG, "failed to set input info - unknown input id " + inputId);
return;
}
inputState.info = inputInfo;
inputState.uid = getInputUid(inputInfo);
int n = userState.mCallbacks.beginBroadcast();
for (int i = 0; i < n; ++i) {
try {
userState.mCallbacks.getBroadcastItem(i).onTvInputInfoUpdated(inputInfo);
} catch (RemoteException e) {
Slog.e(TAG, "failed to report updated input info to callback", e);
}
}
userState.mCallbacks.finishBroadcast();
}
@GuardedBy("mLock")
private void setStateLocked(String inputId, int state, int userId) {
UserState userState = getOrCreateUserStateLocked(userId);
TvInputState inputState = userState.inputMap.get(inputId);
if (inputState == null) {
Slog.e(TAG, "failed to setStateLocked - unknown input id " + inputId);
return;
}
ServiceState serviceState = userState.serviceStateMap.get(inputState.info.getComponent());
int oldState = inputState.state;
inputState.state = state;
if (serviceState != null && serviceState.service == null
&& (!serviceState.sessionTokens.isEmpty() || serviceState.isHardware)) {
// We don't notify state change while reconnecting. It should remain disconnected.
return;
}
if (oldState != state) {
notifyInputStateChangedLocked(userState, inputId, state, null);
}
}
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);
}
}
public void updateTvInputInfo(TvInputInfo inputInfo, int userId) {
String inputInfoPackageName = inputInfo.getServiceInfo().packageName;
String callingPackageName = getCallingPackageName();
if (!TextUtils.equals(inputInfoPackageName, callingPackageName)
&& mContext.checkCallingPermission(
android.Manifest.permission.WRITE_SECURE_SETTINGS)
!= PackageManager.PERMISSION_GRANTED) {
// Only the app owning the input and system settings are allowed to update info.
throw new IllegalArgumentException("calling package " + callingPackageName
+ " is not allowed to change TvInputInfo for " + inputInfoPackageName);
}
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
Binder.getCallingUid(), userId, "updateTvInputInfo");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
UserState userState = getOrCreateUserStateLocked(resolvedUserId);
updateTvInputInfoLocked(userState, inputInfo);
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
private String getCallingPackageName() {
final String[] packages = mContext.getPackageManager().getPackagesForUid(
Binder.getCallingUid());
if (packages != null && packages.length > 0) {
return packages[0];
}
return "unknown";
}
@Override
public int getTvInputState(String inputId, int userId) {
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
Binder.getCallingUid(), userId, "getTvInputState");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
UserState userState = getOrCreateUserStateLocked(resolvedUserId);
TvInputState state = userState.inputMap.get(inputId);
return state == null ? INPUT_STATE_CONNECTED : state.state;
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public List<TvContentRatingSystemInfo> getTvContentRatingSystemList(int userId) {
if (mContext.checkCallingPermission(
android.Manifest.permission.READ_CONTENT_RATING_SYSTEMS)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException(
"The caller does not have permission to read content rating systems");
}
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
Binder.getCallingUid(), userId, "getTvContentRatingSystemList");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
UserState userState = getOrCreateUserStateLocked(resolvedUserId);
return userState.contentRatingSystemList;
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void sendTvInputNotifyIntent(Intent intent, int userId) {
if (mContext.checkCallingPermission(android.Manifest.permission.NOTIFY_TV_INPUTS)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("The caller: " + getCallingPackageName()
+ " doesn't have permission: "
+ android.Manifest.permission.NOTIFY_TV_INPUTS);
}
if (TextUtils.isEmpty(intent.getPackage())) {
throw new IllegalArgumentException("Must specify package name to notify.");
}
switch (intent.getAction()) {
case TvContract.ACTION_PREVIEW_PROGRAM_BROWSABLE_DISABLED:
if (intent.getLongExtra(TvContract.EXTRA_PREVIEW_PROGRAM_ID, -1) < 0) {
throw new IllegalArgumentException("Invalid preview program ID.");
}
break;
case TvContract.ACTION_WATCH_NEXT_PROGRAM_BROWSABLE_DISABLED:
if (intent.getLongExtra(TvContract.EXTRA_WATCH_NEXT_PROGRAM_ID, -1) < 0) {
throw new IllegalArgumentException("Invalid watch next program ID.");
}
break;
case TvContract.ACTION_PREVIEW_PROGRAM_ADDED_TO_WATCH_NEXT:
if (intent.getLongExtra(TvContract.EXTRA_PREVIEW_PROGRAM_ID, -1) < 0) {
throw new IllegalArgumentException("Invalid preview program ID.");
}
if (intent.getLongExtra(TvContract.EXTRA_WATCH_NEXT_PROGRAM_ID, -1) < 0) {
throw new IllegalArgumentException("Invalid watch next program ID.");
}
break;
default:
throw new IllegalArgumentException("Invalid TV input notifying action: "
+ intent.getAction());
}
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
Binder.getCallingUid(), userId, "sendTvInputNotifyIntent");
final long identity = Binder.clearCallingIdentity();
try {
getContext().sendBroadcastAsUser(intent, new UserHandle(resolvedUserId));
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void registerCallback(final ITvInputManagerCallback callback, int userId) {
int callingPid = Binder.getCallingPid();
int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(callingPid, callingUid, userId,
"registerCallback");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
final UserState userState = getOrCreateUserStateLocked(resolvedUserId);
if (!userState.mCallbacks.register(callback)) {
Slog.e(TAG, "client process has already died");
} else {
userState.callbackPidUidMap.put(
callback, Pair.create(callingPid, callingUid));
}
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void unregisterCallback(ITvInputManagerCallback callback, int userId) {
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
Binder.getCallingUid(), userId, "unregisterCallback");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
UserState userState = getOrCreateUserStateLocked(resolvedUserId);
userState.mCallbacks.unregister(callback);
userState.callbackPidUidMap.remove(callback);
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public boolean isParentalControlsEnabled(int userId) {
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
Binder.getCallingUid(), userId, "isParentalControlsEnabled");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
UserState userState = getOrCreateUserStateLocked(resolvedUserId);
return userState.persistentDataStore.isParentalControlsEnabled();
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void setParentalControlsEnabled(boolean enabled, int userId) {
ensureParentalControlsPermission();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
Binder.getCallingUid(), userId, "setParentalControlsEnabled");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
UserState userState = getOrCreateUserStateLocked(resolvedUserId);
userState.persistentDataStore.setParentalControlsEnabled(enabled);
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public boolean isRatingBlocked(String rating, int userId) {
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
Binder.getCallingUid(), userId, "isRatingBlocked");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
UserState userState = getOrCreateUserStateLocked(resolvedUserId);
return userState.persistentDataStore.isRatingBlocked(
TvContentRating.unflattenFromString(rating));
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public List<String> getBlockedRatings(int userId) {
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
Binder.getCallingUid(), userId, "getBlockedRatings");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
UserState userState = getOrCreateUserStateLocked(resolvedUserId);
List<String> ratings = new ArrayList<>();
for (TvContentRating rating
: userState.persistentDataStore.getBlockedRatings()) {
ratings.add(rating.flattenToString());
}
return ratings;
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void addBlockedRating(String rating, int userId) {
ensureParentalControlsPermission();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
Binder.getCallingUid(), userId, "addBlockedRating");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
UserState userState = getOrCreateUserStateLocked(resolvedUserId);
userState.persistentDataStore.addBlockedRating(
TvContentRating.unflattenFromString(rating));
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void removeBlockedRating(String rating, int userId) {
ensureParentalControlsPermission();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
Binder.getCallingUid(), userId, "removeBlockedRating");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
UserState userState = getOrCreateUserStateLocked(resolvedUserId);
userState.persistentDataStore.removeBlockedRating(
TvContentRating.unflattenFromString(rating));
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
private void ensureParentalControlsPermission() {
if (mContext.checkCallingPermission(
android.Manifest.permission.MODIFY_PARENTAL_CONTROLS)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException(
"The caller does not have parental controls permission");
}
}
@Override
public void createSession(final ITvInputClient client, final String inputId,
boolean isRecordingSession, int seq, int userId) {
final int callingUid = Binder.getCallingUid();
final int callingPid = Binder.getCallingPid();
final int resolvedUserId = resolveCallingUserId(callingPid, callingUid,
userId, "createSession");
final long identity = Binder.clearCallingIdentity();
/**
* A randomly generated id for this this session.
*
* <p>This field contains no user or device reference and is large enough to be
* effectively globally unique.
*
* <p><b>WARNING</b> Any changes to this field should be carefully reviewed for privacy.
* Inspect the code at:
*
* <ul>
* <li>framework/base/cmds/statsd/src/atoms.proto#TifTuneState
* <li>{@link #logTuneStateChanged}
* <li>{@link TvInputManagerService.BinderService#createSession}
* <li>{@link SessionState#sessionId}
* </ul>
*/
String uniqueSessionId = UUID.randomUUID().toString();
try {
synchronized (mLock) {
if (userId != mCurrentUserId && !mRunningProfiles.contains(userId)
&& !isRecordingSession) {
// Only current user and its running profiles can create
// non-recording sessions.
// Let the client get onConnectionFailed callback for this case.
sendSessionTokenToClientLocked(client, inputId, null, null, seq);
return;
}
UserState userState = getOrCreateUserStateLocked(resolvedUserId);
TvInputState inputState = userState.inputMap.get(inputId);
if (inputState == null) {
Slog.w(TAG, "Failed to find input state for inputId=" + inputId);
sendSessionTokenToClientLocked(client, inputId, null, null, seq);
return;
}
TvInputInfo info = inputState.info;
ServiceState serviceState = userState.serviceStateMap.get(info.getComponent());
if (serviceState == null) {
int tisUid = PackageManager.getApplicationInfoAsUserCached(
info.getComponent().getPackageName(), 0, resolvedUserId).uid;
serviceState = new ServiceState(info.getComponent(), resolvedUserId);
userState.serviceStateMap.put(info.getComponent(), serviceState);
}
// Send a null token immediately while reconnecting.
if (serviceState.reconnecting) {
sendSessionTokenToClientLocked(client, inputId, null, null, seq);
return;
}
// Create a new session token and a session state.
IBinder sessionToken = new Binder();
SessionState sessionState = new SessionState(sessionToken, info.getId(),
info.getComponent(), isRecordingSession, client, seq, callingUid,
callingPid, resolvedUserId, uniqueSessionId);
// Add them to the global session state map of the current user.
userState.sessionStateMap.put(sessionToken, sessionState);
// Map the session id to the sessionStateMap in the user state
mSessionIdToSessionStateMap.put(uniqueSessionId, sessionState);
// Also, add them to the session state map of the current service.
serviceState.sessionTokens.add(sessionToken);
if (serviceState.service != null) {
if (!createSessionInternalLocked(serviceState.service, sessionToken,
resolvedUserId)) {
removeSessionStateLocked(sessionToken, resolvedUserId);
}
} else {
updateServiceConnectionLocked(info.getComponent(), resolvedUserId);
}
logTuneStateChanged(FrameworkStatsLog.TIF_TUNE_STATE_CHANGED__STATE__CREATED,
sessionState, inputState);
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void releaseSession(IBinder sessionToken, int userId) {
if (DEBUG) {
Slog.d(TAG, "releaseSession(sessionToken=" + sessionToken + ")");
}
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
userId, "releaseSession");
final long identity = Binder.clearCallingIdentity();
try {
SessionState sessionState = null;
UserState userState = null;
synchronized (mLock) {
sessionState = releaseSessionLocked(sessionToken, callingUid, resolvedUserId);
userState = getUserStateLocked(userId);
}
if (sessionState != null) {
TvInputState tvInputState = TvInputManagerService.getTvInputState(sessionState,
userState);
logTuneStateChanged(FrameworkStatsLog.TIF_TUNE_STATE_CHANGED__STATE__RELEASED,
sessionState, tvInputState);
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void setMainSession(IBinder sessionToken, int userId) {
if (mContext.checkCallingPermission(
android.Manifest.permission.CHANGE_HDMI_CEC_ACTIVE_SOURCE)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException(
"The caller does not have CHANGE_HDMI_CEC_ACTIVE_SOURCE permission");
}
if (DEBUG) {
Slog.d(TAG, "setMainSession(sessionToken=" + sessionToken + ")");
}
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
userId, "setMainSession");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
UserState userState = getOrCreateUserStateLocked(resolvedUserId);
if (userState.mainSessionToken == sessionToken) {
return;
}
if (DEBUG) {
Slog.d(TAG, "mainSessionToken=" + sessionToken);
}
IBinder oldMainSessionToken = userState.mainSessionToken;
userState.mainSessionToken = sessionToken;
// Inform the new main session first.
// See {@link TvInputService.Session#onSetMain}.
if (sessionToken != null) {
setMainLocked(sessionToken, true, callingUid, userId);
}
if (oldMainSessionToken != null) {
setMainLocked(oldMainSessionToken, false, Process.SYSTEM_UID, userId);
}
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void setSurface(IBinder sessionToken, Surface surface, int userId) {
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
userId, "setSurface");
final long identity = Binder.clearCallingIdentity();
SessionState sessionState = null;
UserState userState = null;
try {
synchronized (mLock) {
try {
userState = getUserStateLocked(userId);
sessionState = getSessionStateLocked(sessionToken, callingUid,
resolvedUserId);
if (sessionState.hardwareSessionToken == null) {
getSessionLocked(sessionState).setSurface(surface);
} else {
getSessionLocked(sessionState.hardwareSessionToken,
Process.SYSTEM_UID, resolvedUserId).setSurface(surface);
}
boolean isVisible = (surface == null);
if (sessionState.isVisible != isVisible) {
sessionState.isVisible = isVisible;
notifyCurrentChannelInfosUpdatedLocked(userState);
}
} catch (RemoteException | SessionNotFoundException e) {
Slog.e(TAG, "error in setSurface", e);
}
}
} finally {
if (surface != null) {
// surface is not used in TvInputManagerService.
surface.release();
}
if (sessionState != null) {
int state = surface == null
?
FrameworkStatsLog.TIF_TUNE_STATE_CHANGED__STATE__SURFACE_DETACHED
: FrameworkStatsLog.TIF_TUNE_STATE_CHANGED__STATE__SURFACE_ATTACHED;
logTuneStateChanged(state, sessionState,
TvInputManagerService.getTvInputState(sessionState, userState));
}
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void dispatchSurfaceChanged(IBinder sessionToken, int format, int width,
int height, int userId) {
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
userId, "dispatchSurfaceChanged");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
try {
SessionState sessionState = getSessionStateLocked(sessionToken, callingUid,
resolvedUserId);
getSessionLocked(sessionState).dispatchSurfaceChanged(format, width,
height);
if (sessionState.hardwareSessionToken != null) {
getSessionLocked(sessionState.hardwareSessionToken, Process.SYSTEM_UID,
resolvedUserId).dispatchSurfaceChanged(format, width, height);
}
} catch (RemoteException | SessionNotFoundException e) {
Slog.e(TAG, "error in dispatchSurfaceChanged", e);
}
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void setVolume(IBinder sessionToken, float volume, int userId) {
final float REMOTE_VOLUME_ON = 1.0f;
final float REMOTE_VOLUME_OFF = 0f;
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
userId, "setVolume");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
try {
SessionState sessionState = getSessionStateLocked(sessionToken, callingUid,
resolvedUserId);
getSessionLocked(sessionState).setVolume(volume);
if (sessionState.hardwareSessionToken != null) {
// Here, we let the hardware session know only whether volume is on or
// off to prevent that the volume is controlled in the both side.
getSessionLocked(sessionState.hardwareSessionToken,
Process.SYSTEM_UID, resolvedUserId).setVolume((volume > 0.0f)
? REMOTE_VOLUME_ON : REMOTE_VOLUME_OFF);
}
} catch (RemoteException | SessionNotFoundException e) {
Slog.e(TAG, "error in setVolume", e);
}
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void tune(IBinder sessionToken, final Uri channelUri, Bundle params, int userId) {
final int callingUid = Binder.getCallingUid();
final int callingPid = Binder.getCallingPid();
final int resolvedUserId = resolveCallingUserId(callingPid, callingUid, userId, "tune");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
try {
getSessionLocked(sessionToken, callingUid, resolvedUserId).tune(
channelUri, params);
UserState userState = getOrCreateUserStateLocked(resolvedUserId);
SessionState sessionState = getSessionStateLocked(sessionToken, callingUid,
userState);
if (!sessionState.isCurrent
|| !Objects.equals(sessionState.currentChannel, channelUri)) {
sessionState.isCurrent = true;
sessionState.currentChannel = channelUri;
notifyCurrentChannelInfosUpdatedLocked(userState);
}
if (TvContract.isChannelUriForPassthroughInput(channelUri)) {
// Do not log the watch history for passthrough inputs.
return;
}
if (sessionState.isRecordingSession) {
return;
}
logTuneStateChanged(
FrameworkStatsLog.TIF_TUNE_STATE_CHANGED__STATE__TUNE_STARTED,
sessionState,
TvInputManagerService.getTvInputState(sessionState, userState));
// Log the start of watch.
SomeArgs args = SomeArgs.obtain();
args.arg1 = sessionState.componentName.getPackageName();
args.arg2 = System.currentTimeMillis();
args.arg3 = ContentUris.parseId(channelUri);
args.arg4 = params;
args.arg5 = sessionToken;
mWatchLogHandler.obtainMessage(WatchLogHandler.MSG_LOG_WATCH_START, args)
.sendToTarget();
} catch (RemoteException | SessionNotFoundException e) {
Slog.e(TAG, "error in tune", e);
}
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void unblockContent(
IBinder sessionToken, String unblockedRating, int userId) {
ensureParentalControlsPermission();
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
userId, "unblockContent");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
try {
getSessionLocked(sessionToken, callingUid, resolvedUserId)
.unblockContent(unblockedRating);
} catch (RemoteException | SessionNotFoundException e) {
Slog.e(TAG, "error in unblockContent", e);
}
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void setCaptionEnabled(IBinder sessionToken, boolean enabled, int userId) {
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
userId, "setCaptionEnabled");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
try {
getSessionLocked(sessionToken, callingUid, resolvedUserId)
.setCaptionEnabled(enabled);
} catch (RemoteException | SessionNotFoundException e) {
Slog.e(TAG, "error in setCaptionEnabled", e);
}
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void selectTrack(IBinder sessionToken, int type, String trackId, int userId) {
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
userId, "selectTrack");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
try {
getSessionLocked(sessionToken, callingUid, resolvedUserId).selectTrack(
type, trackId);
} catch (RemoteException | SessionNotFoundException e) {
Slog.e(TAG, "error in selectTrack", e);
}
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void sendAppPrivateCommand(IBinder sessionToken, String command, Bundle data,
int userId) {
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
userId, "sendAppPrivateCommand");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
try {
getSessionLocked(sessionToken, callingUid, resolvedUserId)
.appPrivateCommand(command, data);
} catch (RemoteException | SessionNotFoundException e) {
Slog.e(TAG, "error in appPrivateCommand", e);
}
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void createOverlayView(IBinder sessionToken, IBinder windowToken, Rect frame,
int userId) {
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
userId, "createOverlayView");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
try {
getSessionLocked(sessionToken, callingUid, resolvedUserId)
.createOverlayView(windowToken, frame);
} catch (RemoteException | SessionNotFoundException e) {
Slog.e(TAG, "error in createOverlayView", e);
}
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void relayoutOverlayView(IBinder sessionToken, Rect frame, int userId) {
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
userId, "relayoutOverlayView");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
try {
getSessionLocked(sessionToken, callingUid, resolvedUserId)
.relayoutOverlayView(frame);
} catch (RemoteException | SessionNotFoundException e) {
Slog.e(TAG, "error in relayoutOverlayView", e);
}
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void removeOverlayView(IBinder sessionToken, int userId) {
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
userId, "removeOverlayView");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
try {
getSessionLocked(sessionToken, callingUid, resolvedUserId)
.removeOverlayView();
} catch (RemoteException | SessionNotFoundException e) {
Slog.e(TAG, "error in removeOverlayView", e);
}
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void timeShiftPlay(IBinder sessionToken, final Uri recordedProgramUri, int userId) {
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
userId, "timeShiftPlay");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
try {
getSessionLocked(sessionToken, callingUid, resolvedUserId).timeShiftPlay(
recordedProgramUri);
} catch (RemoteException | SessionNotFoundException e) {
Slog.e(TAG, "error in timeShiftPlay", e);
}
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void timeShiftPause(IBinder sessionToken, int userId) {
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
userId, "timeShiftPause");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
try {
getSessionLocked(sessionToken, callingUid, resolvedUserId).timeShiftPause();
} catch (RemoteException | SessionNotFoundException e) {
Slog.e(TAG, "error in timeShiftPause", e);
}
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void timeShiftResume(IBinder sessionToken, int userId) {
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
userId, "timeShiftResume");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
try {
getSessionLocked(sessionToken, callingUid, resolvedUserId)
.timeShiftResume();
} catch (RemoteException | SessionNotFoundException e) {
Slog.e(TAG, "error in timeShiftResume", e);
}
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void timeShiftSeekTo(IBinder sessionToken, long timeMs, int userId) {
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
userId, "timeShiftSeekTo");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
try {
getSessionLocked(sessionToken, callingUid, resolvedUserId)
.timeShiftSeekTo(timeMs);
} catch (RemoteException | SessionNotFoundException e) {
Slog.e(TAG, "error in timeShiftSeekTo", e);
}
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void timeShiftSetPlaybackParams(IBinder sessionToken, PlaybackParams params,
int userId) {
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
userId, "timeShiftSetPlaybackParams");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
try {
getSessionLocked(sessionToken, callingUid, resolvedUserId)
.timeShiftSetPlaybackParams(params);
} catch (RemoteException | SessionNotFoundException e) {
Slog.e(TAG, "error in timeShiftSetPlaybackParams", e);
}
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void timeShiftEnablePositionTracking(IBinder sessionToken, boolean enable,
int userId) {
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
userId, "timeShiftEnablePositionTracking");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
try {
getSessionLocked(sessionToken, callingUid, resolvedUserId)
.timeShiftEnablePositionTracking(enable);
} catch (RemoteException | SessionNotFoundException e) {
Slog.e(TAG, "error in timeShiftEnablePositionTracking", e);
}
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void startRecording(IBinder sessionToken, @Nullable Uri programUri,
@Nullable Bundle params, int userId) {
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
userId, "startRecording");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
try {
getSessionLocked(sessionToken, callingUid, resolvedUserId).startRecording(
programUri, params);
} catch (RemoteException | SessionNotFoundException e) {
Slog.e(TAG, "error in startRecording", e);
}
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void stopRecording(IBinder sessionToken, int userId) {
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
userId, "stopRecording");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
try {
getSessionLocked(sessionToken, callingUid, resolvedUserId).stopRecording();
} catch (RemoteException | SessionNotFoundException e) {
Slog.e(TAG, "error in stopRecording", e);
}
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void pauseRecording(IBinder sessionToken, @NonNull Bundle params, int userId) {
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
userId, "pauseRecording");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
try {
getSessionLocked(sessionToken, callingUid, resolvedUserId)
.pauseRecording(params);
} catch (RemoteException | SessionNotFoundException e) {
Slog.e(TAG, "error in pauseRecording", e);
}
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void resumeRecording(IBinder sessionToken, @NonNull Bundle params, int userId) {
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
userId, "resumeRecording");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
try {
getSessionLocked(sessionToken, callingUid, resolvedUserId)
.resumeRecording(params);
} catch (RemoteException | SessionNotFoundException e) {
Slog.e(TAG, "error in resumeRecording", e);
}
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public List<TvInputHardwareInfo> getHardwareList() throws RemoteException {
if (mContext.checkCallingPermission(android.Manifest.permission.TV_INPUT_HARDWARE)
!= PackageManager.PERMISSION_GRANTED) {
return null;
}
final long identity = Binder.clearCallingIdentity();
try {
return mTvInputHardwareManager.getHardwareList();
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public ITvInputHardware acquireTvInputHardware(int deviceId,
ITvInputHardwareCallback callback, TvInputInfo info, int userId,
String tvInputSessionId,
@TvInputService.PriorityHintUseCaseType int priorityHint) throws RemoteException {
if (mContext.checkCallingPermission(android.Manifest.permission.TV_INPUT_HARDWARE)
!= PackageManager.PERMISSION_GRANTED) {
return null;
}
final long identity = Binder.clearCallingIdentity();
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
userId, "acquireTvInputHardware");
try {
return mTvInputHardwareManager.acquireHardware(
deviceId, callback, info, callingUid, resolvedUserId,
tvInputSessionId, priorityHint);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void releaseTvInputHardware(int deviceId, ITvInputHardware hardware, int userId)
throws RemoteException {
if (mContext.checkCallingPermission(android.Manifest.permission.TV_INPUT_HARDWARE)
!= PackageManager.PERMISSION_GRANTED) {
return;
}
final long identity = Binder.clearCallingIdentity();
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
userId, "releaseTvInputHardware");
try {
mTvInputHardwareManager.releaseHardware(
deviceId, hardware, callingUid, resolvedUserId);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public List<DvbDeviceInfo> getDvbDeviceList() throws RemoteException {
if (mContext.checkCallingPermission(android.Manifest.permission.DVB_DEVICE)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("Requires DVB_DEVICE permission");
}
final long identity = Binder.clearCallingIdentity();
try {
// Pattern1: /dev/dvb%d.frontend%d
ArrayList<DvbDeviceInfo> deviceInfosFromPattern1 = new ArrayList<>();
File devDirectory = new File("/dev");
boolean dvbDirectoryFound = false;
for (String fileName : devDirectory.list()) {
Matcher matcher = sFrontEndDevicePattern.matcher(fileName);
if (matcher.find()) {
int adapterId = Integer.parseInt(matcher.group(1));
int deviceId = Integer.parseInt(matcher.group(2));
deviceInfosFromPattern1.add(new DvbDeviceInfo(adapterId, deviceId));
}
if (TextUtils.equals("dvb", fileName)) {
dvbDirectoryFound = true;
}
}
if (!dvbDirectoryFound) {
return Collections.unmodifiableList(deviceInfosFromPattern1);
}
File dvbDirectory = new File(DVB_DIRECTORY);
// Pattern2: /dev/dvb/adapter%d/frontend%d
ArrayList<DvbDeviceInfo> deviceInfosFromPattern2 = new ArrayList<>();
for (String fileNameInDvb : dvbDirectory.list()) {
Matcher adapterMatcher = sAdapterDirPattern.matcher(fileNameInDvb);
if (adapterMatcher.find()) {
int adapterId = Integer.parseInt(adapterMatcher.group(1));
File adapterDirectory = new File(DVB_DIRECTORY + "/" + fileNameInDvb);
for (String fileNameInAdapter : adapterDirectory.list()) {
Matcher frontendMatcher = sFrontEndInAdapterDirPattern.matcher(
fileNameInAdapter);
if (frontendMatcher.find()) {
int deviceId = Integer.parseInt(frontendMatcher.group(1));
deviceInfosFromPattern2.add(
new DvbDeviceInfo(adapterId, deviceId));
}
}
}
}
return deviceInfosFromPattern2.isEmpty()
? Collections.unmodifiableList(deviceInfosFromPattern1)
: Collections.unmodifiableList(deviceInfosFromPattern2);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public ParcelFileDescriptor openDvbDevice(DvbDeviceInfo info,
@TvInputManager.DvbDeviceType int deviceType) throws RemoteException {
if (mContext.checkCallingPermission(android.Manifest.permission.DVB_DEVICE)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("Requires DVB_DEVICE permission");
}
File devDirectory = new File("/dev");
boolean dvbDeviceFound = false;
for (String fileName : devDirectory.list()) {
if (TextUtils.equals("dvb", fileName)) {
File dvbDirectory = new File(DVB_DIRECTORY);
for (String fileNameInDvb : dvbDirectory.list()) {
Matcher adapterMatcher = sAdapterDirPattern.matcher(fileNameInDvb);
if (adapterMatcher.find()) {
File adapterDirectory = new File(DVB_DIRECTORY + "/" + fileNameInDvb);
for (String fileNameInAdapter : adapterDirectory.list()) {
Matcher frontendMatcher = sFrontEndInAdapterDirPattern.matcher(
fileNameInAdapter);
if (frontendMatcher.find()) {
dvbDeviceFound = true;
break;
}
}
}
if (dvbDeviceFound) {
break;
}
}
}
if (dvbDeviceFound) {
break;
}
}
final long identity = Binder.clearCallingIdentity();
try {
String deviceFileName;
switch (deviceType) {
case TvInputManager.DVB_DEVICE_DEMUX:
deviceFileName = String.format(dvbDeviceFound
? "/dev/dvb/adapter%d/demux%d" : "/dev/dvb%d.demux%d",
info.getAdapterId(), info.getDeviceId());
break;
case TvInputManager.DVB_DEVICE_DVR:
deviceFileName = String.format(dvbDeviceFound
? "/dev/dvb/adapter%d/dvr%d" : "/dev/dvb%d.dvr%d",
info.getAdapterId(), info.getDeviceId());
break;
case TvInputManager.DVB_DEVICE_FRONTEND:
deviceFileName = String.format(dvbDeviceFound
? "/dev/dvb/adapter%d/frontend%d" : "/dev/dvb%d.frontend%d",
info.getAdapterId(), info.getDeviceId());
break;
default:
throw new IllegalArgumentException("Invalid DVB device: " + deviceType);
}
try {
// The DVB frontend device only needs to be opened in read/write mode, which
// allows performing tuning operations. The DVB demux and DVR device are enough
// to be opened in read only mode.
return ParcelFileDescriptor.open(new File(deviceFileName),
TvInputManager.DVB_DEVICE_FRONTEND == deviceType
? ParcelFileDescriptor.MODE_READ_WRITE
: ParcelFileDescriptor.MODE_READ_ONLY);
} catch (FileNotFoundException e) {
return null;
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public List<TvStreamConfig> getAvailableTvStreamConfigList(String inputId, int userId)
throws RemoteException {
ensureCaptureTvInputPermission();
final long identity = Binder.clearCallingIdentity();
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
userId, "getAvailableTvStreamConfigList");
try {
return mTvInputHardwareManager.getAvailableTvStreamConfigList(
inputId, callingUid, resolvedUserId);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public boolean captureFrame(String inputId, Surface surface, TvStreamConfig config,
int userId)
throws RemoteException {
ensureCaptureTvInputPermission();
final long identity = Binder.clearCallingIdentity();
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
userId, "captureFrame");
try {
String hardwareInputId = null;
synchronized (mLock) {
UserState userState = getOrCreateUserStateLocked(resolvedUserId);
if (userState.inputMap.get(inputId) == null) {
Slog.e(TAG, "input not found for " + inputId);
return false;
}
for (SessionState sessionState : userState.sessionStateMap.values()) {
if (sessionState.inputId.equals(inputId)
&& sessionState.hardwareSessionToken != null) {
hardwareInputId = userState.sessionStateMap.get(
sessionState.hardwareSessionToken).inputId;
break;
}
}
}
return mTvInputHardwareManager.captureFrame(
(hardwareInputId != null) ? hardwareInputId : inputId,
surface, config, callingUid, resolvedUserId);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public boolean isSingleSessionActive(int userId) throws RemoteException {
ensureCaptureTvInputPermission();
final long identity = Binder.clearCallingIdentity();
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
userId, "isSingleSessionActive");
try {
synchronized (mLock) {
UserState userState = getOrCreateUserStateLocked(resolvedUserId);
if (userState.sessionStateMap.size() == 1) {
return true;
} else if (userState.sessionStateMap.size() == 2) {
SessionState[] sessionStates = userState.sessionStateMap.values().toArray(
new SessionState[2]);
// Check if there is a wrapper input.
return sessionStates[0].hardwareSessionToken != null
|| sessionStates[1].hardwareSessionToken != null;
}
return false;
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
private void ensureCaptureTvInputPermission() {
if (mContext.checkCallingPermission(
android.Manifest.permission.CAPTURE_TV_INPUT)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("Requires CAPTURE_TV_INPUT permission");
}
}
@Override
public void requestChannelBrowsable(Uri channelUri, int userId)
throws RemoteException {
final String callingPackageName = getCallingPackageName();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
Binder.getCallingUid(), userId, "requestChannelBrowsable");
final long identity = Binder.clearCallingIdentity();
try {
Intent intent = new Intent(TvContract.ACTION_CHANNEL_BROWSABLE_REQUESTED);
List<ResolveInfo> list = getContext().getPackageManager()
.queryBroadcastReceivers(intent, 0);
if (list != null) {
for (ResolveInfo info : list) {
String receiverPackageName = info.activityInfo.packageName;
intent.putExtra(TvContract.EXTRA_CHANNEL_ID, ContentUris.parseId(
channelUri));
intent.putExtra(TvContract.EXTRA_PACKAGE_NAME, callingPackageName);
intent.setPackage(receiverPackageName);
getContext().sendBroadcastAsUser(intent, new UserHandle(resolvedUserId));
}
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public int getClientPid(String sessionId) {
ensureTunerResourceAccessPermission();
final long identity = Binder.clearCallingIdentity();
int clientPid = TvInputManager.UNKNOWN_CLIENT_PID;
try {
synchronized (mLock) {
try {
clientPid = getClientPidLocked(sessionId);
} catch (ClientPidNotFoundException e) {
Slog.e(TAG, "error in getClientPid", e);
}
}
} finally {
Binder.restoreCallingIdentity(identity);
}
return clientPid;
}
@Override
public List<TunedInfo> getCurrentTunedInfos(@UserIdInt int userId) {
if (mContext.checkCallingPermission(android.Manifest.permission.ACCESS_TUNED_INFO)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException(
"The caller does not have access tuned info permission");
}
int callingPid = Binder.getCallingPid();
int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(callingPid, callingUid, userId,
"getTvCurrentChannelInfos");
synchronized (mLock) {
UserState userState = getOrCreateUserStateLocked(resolvedUserId);
return getCurrentTunedInfosInternalLocked(userState, callingPid, callingUid);
}
}
/**
* Add a hardware device in the TvInputHardwareManager for CTS testing
* purpose.
*
* @param deviceId the id of the adding hardware device.
*/
@Override
public void addHardwareDevice(int deviceId) {
TvInputHardwareInfo info = new TvInputHardwareInfo.Builder()
.deviceId(deviceId)
.type(TvInputHardwareInfo.TV_INPUT_TYPE_HDMI)
.audioType(DEVICE_NONE)
.audioAddress("0")
.hdmiPortId(0)
.build();
mTvInputHardwareManager.onDeviceAvailable(info, null);
}
/**
* Remove a hardware device in the TvInputHardwareManager for CTS testing
* purpose.
*
* @param deviceId the id of the removing hardware device.
*/
@Override
public void removeHardwareDevice(int deviceId) {
mTvInputHardwareManager.onDeviceUnavailable(deviceId);
}
@GuardedBy("mLock")
private int getClientPidLocked(String sessionId)
throws IllegalStateException {
if (mSessionIdToSessionStateMap.get(sessionId) == null) {
throw new IllegalStateException("Client Pid not found with sessionId "
+ sessionId);
}
return mSessionIdToSessionStateMap.get(sessionId).callingPid;
}
private void ensureTunerResourceAccessPermission() {
if (mContext.checkCallingPermission(
android.Manifest.permission.TUNER_RESOURCE_ACCESS)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("Requires TUNER_RESOURCE_ACCESS permission");
}
}
@Override
@SuppressWarnings("resource")
protected void dump(FileDescriptor fd, final PrintWriter writer, String[] args) {
final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ");
if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
synchronized (mLock) {
pw.println("User Ids (Current user: " + mCurrentUserId + "):");
pw.increaseIndent();
for (int i = 0; i < mUserStates.size(); i++) {
int userId = mUserStates.keyAt(i);
pw.println(Integer.valueOf(userId));
}
pw.decreaseIndent();
for (int i = 0; i < mUserStates.size(); i++) {
int userId = mUserStates.keyAt(i);
UserState userState = getOrCreateUserStateLocked(userId);
pw.println("UserState (" + userId + "):");
pw.increaseIndent();
pw.println("inputMap: inputId -> TvInputState");
pw.increaseIndent();
for (Map.Entry<String, TvInputState> entry: userState.inputMap.entrySet()) {
pw.println(entry.getKey() + ": " + entry.getValue());
}
pw.decreaseIndent();
pw.println("packageSet:");
pw.increaseIndent();
for (String packageName : userState.packageSet) {
pw.println(packageName);
}
pw.decreaseIndent();
pw.println("clientStateMap: ITvInputClient -> ClientState");
pw.increaseIndent();
for (Map.Entry<IBinder, ClientState> entry :
userState.clientStateMap.entrySet()) {
ClientState client = entry.getValue();
pw.println(entry.getKey() + ": " + client);
pw.increaseIndent();
pw.println("sessionTokens:");
pw.increaseIndent();
for (IBinder token : client.sessionTokens) {
pw.println("" + token);
}
pw.decreaseIndent();
pw.println("clientTokens: " + client.clientToken);
pw.println("userId: " + client.userId);
pw.decreaseIndent();
}
pw.decreaseIndent();
pw.println("serviceStateMap: ComponentName -> ServiceState");
pw.increaseIndent();
for (Map.Entry<ComponentName, ServiceState> entry :
userState.serviceStateMap.entrySet()) {
ServiceState service = entry.getValue();
pw.println(entry.getKey() + ": " + service);
pw.increaseIndent();
pw.println("sessionTokens:");
pw.increaseIndent();
for (IBinder token : service.sessionTokens) {
pw.println("" + token);
}
pw.decreaseIndent();
pw.println("service: " + service.service);
pw.println("callback: " + service.callback);
pw.println("bound: " + service.bound);
pw.println("reconnecting: " + service.reconnecting);
pw.decreaseIndent();
}
pw.decreaseIndent();
pw.println("sessionStateMap: ITvInputSession -> SessionState");
pw.increaseIndent();
for (Map.Entry<IBinder, SessionState> entry :
userState.sessionStateMap.entrySet()) {
SessionState session = entry.getValue();
pw.println(entry.getKey() + ": " + session);
pw.increaseIndent();
pw.println("inputId: " + session.inputId);
pw.println("sessionId: " + session.sessionId);
pw.println("client: " + session.client);
pw.println("seq: " + session.seq);
pw.println("callingUid: " + session.callingUid);
pw.println("callingPid: " + session.callingPid);
pw.println("userId: " + session.userId);
pw.println("sessionToken: " + session.sessionToken);
pw.println("session: " + session.session);
pw.println("hardwareSessionToken: " + session.hardwareSessionToken);
pw.decreaseIndent();
}
pw.decreaseIndent();
pw.println("mCallbacks:");
pw.increaseIndent();
int n = userState.mCallbacks.beginBroadcast();
for (int j = 0; j < n; ++j) {
pw.println(userState.mCallbacks.getRegisteredCallbackItem(j));
}
userState.mCallbacks.finishBroadcast();
pw.decreaseIndent();
pw.println("mainSessionToken: " + userState.mainSessionToken);
pw.decreaseIndent();
}
}
mTvInputHardwareManager.dump(fd, writer, args);
}
}
@Nullable
private static TvInputState getTvInputState(
SessionState sessionState,
@Nullable UserState userState) {
if (userState != null) {
return userState.inputMap.get(sessionState.inputId);
}
return null;
}
@GuardedBy("mLock")
private List<TunedInfo> getCurrentTunedInfosInternalLocked(
UserState userState, int callingPid, int callingUid) {
List<TunedInfo> channelInfos = new ArrayList<>();
boolean watchedProgramsAccess = hasAccessWatchedProgramsPermission(callingPid, callingUid);
for (SessionState state : userState.sessionStateMap.values()) {
if (state.isCurrent) {
Integer appTag;
int appType;
if (state.callingUid == callingUid) {
appTag = APP_TAG_SELF;
appType = TunedInfo.APP_TYPE_SELF;
} else {
appTag = userState.mAppTagMap.get(state.callingUid);
if (appTag == null) {
appTag = userState.mNextAppTag++;
userState.mAppTagMap.put(state.callingUid, appTag);
}
appType = isSystemApp(state.componentName.getPackageName())
? TunedInfo.APP_TYPE_SYSTEM
: TunedInfo.APP_TYPE_NON_SYSTEM;
}
channelInfos.add(new TunedInfo(
state.inputId,
watchedProgramsAccess ? state.currentChannel : null,
state.isRecordingSession,
state.isVisible,
state.isMainSession,
appType,
appTag));
}
}
return channelInfos;
}
private boolean hasAccessWatchedProgramsPermission(int callingPid, int callingUid) {
return mContext.checkPermission(PERMISSION_ACCESS_WATCHED_PROGRAMS, callingPid, callingUid)
== PackageManager.PERMISSION_GRANTED;
}
private boolean isSystemApp(String pkg) {
try {
return (mContext.getPackageManager().getApplicationInfo(pkg, 0).flags
& ApplicationInfo.FLAG_SYSTEM) != 0;
} catch (NameNotFoundException e) {
return false;
}
}
/**
* Log Tune state changes to {@link FrameworkStatsLog}.
*
* <p><b>WARNING</b> Any changes to this field should be carefully reviewed for privacy.
* Inspect the code at:
*
* <ul>
* <li>framework/base/cmds/statsd/src/atoms.proto#TifTuneState
* <li>{@link #logTuneStateChanged}
* <li>{@link TvInputManagerService.BinderService#createSession}
* <li>{@link SessionState#sessionId}
* </ul>
*/
private void logTuneStateChanged(int state, SessionState sessionState,
@Nullable TvInputState inputState) {
int tisUid = Process.INVALID_UID;
int inputType = FrameworkStatsLog.TIF_TUNE_STATE_CHANGED__TYPE__TIF_INPUT_TYPE_UNKNOWN;
int inputId = 0;
int hdmiPort = 0;
if (inputState != null) {
tisUid = inputState.uid;
inputType = inputState.info.getType();
if (inputType == TvInputInfo.TYPE_TUNER) {
inputType = FrameworkStatsLog.TIF_TUNE_STATE_CHANGED__TYPE__TUNER;
}
inputId = inputState.inputNumber;
HdmiDeviceInfo hdmiDeviceInfo = inputState.info.getHdmiDeviceInfo();
if (hdmiDeviceInfo != null) {
hdmiPort = hdmiDeviceInfo.getPortId();
}
}
FrameworkStatsLog.write(FrameworkStatsLog.TIF_TUNE_CHANGED,
new int[]{sessionState.callingUid,
tisUid},
new String[]{"tif_player", "tv_input_service"},
state,
sessionState.sessionId,
inputType,
inputId,
hdmiPort);
}
private static final class UserState {
// A mapping from the TV input id to its TvInputState.
private Map<String, TvInputState> inputMap = new HashMap<>();
// A set of all TV input packages.
private final Set<String> packageSet = new HashSet<>();
// A list of all TV content rating systems defined.
private final List<TvContentRatingSystemInfo>
contentRatingSystemList = new ArrayList<>();
// A mapping from the token of a client to its state.
private final Map<IBinder, ClientState> clientStateMap = new HashMap<>();
// A mapping from the name of a TV input service to its state.
private final Map<ComponentName, ServiceState> serviceStateMap = new HashMap<>();
// A mapping from the token of a TV input session to its state.
private final Map<IBinder, SessionState> sessionStateMap = new HashMap<>();
// A list of callbacks.
private final RemoteCallbackList<ITvInputManagerCallback> mCallbacks =
new RemoteCallbackList<>();
private final Map<ITvInputManagerCallback, Pair<Integer, Integer>> callbackPidUidMap =
new HashMap<>();
// The token of a "main" TV input session.
private IBinder mainSessionToken = null;
// Persistent data store for all internal settings maintained by the TV input manager
// service.
private final PersistentDataStore persistentDataStore;
@GuardedBy("TvInputManagerService.this.mLock")
private final Map<Integer, Integer> mAppTagMap = new HashMap<>();
@GuardedBy("TvInputManagerService.this.mLock")
private int mNextAppTag = 1;
private UserState(Context context, int userId) {
persistentDataStore = new PersistentDataStore(context, userId);
}
}
private final class ClientState implements IBinder.DeathRecipient {
private final List<IBinder> sessionTokens = new ArrayList<>();
private IBinder clientToken;
private final int userId;
ClientState(IBinder clientToken, int userId) {
this.clientToken = clientToken;
this.userId = userId;
}
public boolean isEmpty() {
return sessionTokens.isEmpty();
}
@Override
public void binderDied() {
synchronized (mLock) {
UserState userState = getOrCreateUserStateLocked(userId);
// DO NOT remove the client state of clientStateMap in this method. It will be
// removed in releaseSessionLocked().
ClientState clientState = userState.clientStateMap.get(clientToken);
if (clientState != null) {
while (clientState.sessionTokens.size() > 0) {
IBinder sessionToken = clientState.sessionTokens.get(0);
releaseSessionLocked(
sessionToken, Process.SYSTEM_UID, userId);
// the releaseSessionLocked function may return before the sessionToken
// is removed if the related sessionState is null. So need to check again
// to avoid death curculation.
if (clientState.sessionTokens.contains(sessionToken)) {
Slog.d(TAG, "remove sessionToken " + sessionToken + " for " + clientToken);
clientState.sessionTokens.remove(sessionToken);
}
}
}
clientToken = null;
}
}
}
private final class ServiceState {
private final List<IBinder> sessionTokens = new ArrayList<>();
private final ServiceConnection connection;
private final ComponentName component;
private final boolean isHardware;
private final Map<String, TvInputInfo> hardwareInputMap = new HashMap<>();
private ITvInputService service;
private ServiceCallback callback;
private boolean bound;
private boolean reconnecting;
private ServiceState(ComponentName component, int userId) {
this.component = component;
this.connection = new InputServiceConnection(component, userId);
this.isHardware = hasHardwarePermission(mContext.getPackageManager(), component);
}
}
private static final class TvInputState {
/** A TvInputInfo object which represents the TV input. */
private TvInputInfo info;
/**
* ID unique to a specific TvInputService.
*/
private int inputNumber;
/**
* The kernel user-ID that has been assigned to the application the TvInput is a part of.
*
* <p>
* Currently this is not a unique ID (multiple applications can have
* the same uid).
*/
private int uid;
/**
* The state of TV input.
*
* <p>
* Connected by default
*/
private int state = INPUT_STATE_CONNECTED;
@Override
public String toString() {
return "info: " + info + "; state: " + state;
}
}
private final class SessionState implements IBinder.DeathRecipient {
private final String inputId;
/**
* A randomly generated id for this this session.
*
* <p>This field contains no user or device reference and is large enough to be
* effectively globally unique.
*
* <p><b>WARNING</b> Any changes to this field should be carefully reviewed for privacy.
* Inspect the code at:
*
* <ul>
* <li>framework/base/cmds/statsd/src/atoms.proto#TifTuneState
* <li>{@link #logTuneStateChanged}
* <li>{@link TvInputManagerService.BinderService#createSession}
* <li>{@link SessionState#sessionId}
* </ul>
*/
private final String sessionId;
private final ComponentName componentName;
private final boolean isRecordingSession;
private final ITvInputClient client;
private final int seq;
/**
* The {code UID} of the application that created the session.
*
* <p>
* The application is usually the TIF Player.
*/
private final int callingUid;
/**
* The {@code PID} of the application that created the session.
*
* <p>
* The application is usually the TIF Player.
*/
private final int callingPid;
private final int userId;
private final IBinder sessionToken;
private ITvInputSession session;
// Not null if this session represents an external device connected to a hardware TV input.
private IBinder hardwareSessionToken;
private boolean isCurrent = false;
private Uri currentChannel = null;
private boolean isVisible = false;
private boolean isMainSession = false;
private SessionState(IBinder sessionToken, String inputId, ComponentName componentName,
boolean isRecordingSession, ITvInputClient client, int seq, int callingUid,
int callingPid, int userId, String sessionId) {
this.sessionToken = sessionToken;
this.inputId = inputId;
this.componentName = componentName;
this.isRecordingSession = isRecordingSession;
this.client = client;
this.seq = seq;
this.callingUid = callingUid;
this.callingPid = callingPid;
this.userId = userId;
this.sessionId = sessionId;
}
@Override
public void binderDied() {
synchronized (mLock) {
session = null;
clearSessionAndNotifyClientLocked(this);
}
}
}
private final class InputServiceConnection implements ServiceConnection {
private final ComponentName mComponent;
private final int mUserId;
private InputServiceConnection(ComponentName component, int userId) {
mComponent = component;
mUserId = userId;
}
@Override
public void onServiceConnected(ComponentName component, IBinder service) {
if (DEBUG) {
Slog.d(TAG, "onServiceConnected(component=" + component + ")");
}
synchronized (mLock) {
UserState userState = getUserStateLocked(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);
}
}
}
}
}
@Override
public void onServiceDisconnected(ComponentName component) {
if (DEBUG) {
Slog.d(TAG, "onServiceDisconnected(component=" + component + ")");
}
if (!mComponent.equals(component)) {
throw new IllegalArgumentException("Mismatched ComponentName: "
+ mComponent + " (expected), " + component + " (actual).");
}
synchronized (mLock) {
UserState userState = getOrCreateUserStateLocked(mUserId);
ServiceState serviceState = userState.serviceStateMap.get(mComponent);
if (serviceState != null) {
serviceState.reconnecting = true;
serviceState.bound = false;
serviceState.service = null;
serviceState.callback = null;
abortPendingCreateSessionRequestsLocked(serviceState, null, mUserId);
}
}
}
}
private final class ServiceCallback extends ITvInputServiceCallback.Stub {
private final ComponentName mComponent;
private final int mUserId;
ServiceCallback(ComponentName component, int userId) {
mComponent = component;
mUserId = userId;
}
private void ensureHardwarePermission() {
if (mContext.checkCallingPermission(android.Manifest.permission.TV_INPUT_HARDWARE)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("The caller does not have hardware permission");
}
}
private void ensureValidInput(TvInputInfo inputInfo) {
if (inputInfo.getId() == null || !mComponent.equals(inputInfo.getComponent())) {
throw new IllegalArgumentException("Invalid TvInputInfo");
}
}
@GuardedBy("mLock")
private void addHardwareInputLocked(TvInputInfo inputInfo) {
ServiceState serviceState = getServiceStateLocked(mComponent, mUserId);
serviceState.hardwareInputMap.put(inputInfo.getId(), inputInfo);
buildTvInputListLocked(mUserId, null);
}
public void addHardwareInput(int deviceId, TvInputInfo inputInfo) {
ensureHardwarePermission();
ensureValidInput(inputInfo);
synchronized (mLock) {
mTvInputHardwareManager.addHardwareInput(deviceId, inputInfo);
addHardwareInputLocked(inputInfo);
}
}
public void addHdmiInput(int id, TvInputInfo inputInfo) {
ensureHardwarePermission();
ensureValidInput(inputInfo);
synchronized (mLock) {
mTvInputHardwareManager.addHdmiInput(id, inputInfo);
addHardwareInputLocked(inputInfo);
}
}
public void removeHardwareInput(String inputId) {
ensureHardwarePermission();
synchronized (mLock) {
ServiceState serviceState = getServiceStateLocked(mComponent, mUserId);
boolean removed = serviceState.hardwareInputMap.remove(inputId) != null;
if (removed) {
buildTvInputListLocked(mUserId, null);
mTvInputHardwareManager.removeHardwareInput(inputId);
} else {
Slog.e(TAG, "failed to remove input " + inputId);
}
}
}
}
private final class SessionCallback extends ITvInputSessionCallback.Stub {
private final SessionState mSessionState;
private final InputChannel[] mChannels;
SessionCallback(SessionState sessionState, InputChannel[] channels) {
mSessionState = sessionState;
mChannels = channels;
}
@Override
public void onSessionCreated(ITvInputSession session, IBinder hardwareSessionToken) {
if (DEBUG) {
Slog.d(TAG, "onSessionCreated(inputId=" + mSessionState.inputId + ")");
}
synchronized (mLock) {
mSessionState.session = session;
mSessionState.hardwareSessionToken = hardwareSessionToken;
if (session != null && addSessionTokenToClientStateLocked(session)) {
sendSessionTokenToClientLocked(mSessionState.client,
mSessionState.inputId, mSessionState.sessionToken, mChannels[0],
mSessionState.seq);
} else {
removeSessionStateLocked(mSessionState.sessionToken, mSessionState.userId);
sendSessionTokenToClientLocked(mSessionState.client,
mSessionState.inputId, null, null, mSessionState.seq);
}
mChannels[0].dispose();
}
}
@GuardedBy("mLock")
private boolean addSessionTokenToClientStateLocked(ITvInputSession session) {
try {
session.asBinder().linkToDeath(mSessionState, 0);
} catch (RemoteException e) {
Slog.e(TAG, "session process has already died", e);
return false;
}
IBinder clientToken = mSessionState.client.asBinder();
UserState userState = getOrCreateUserStateLocked(mSessionState.userId);
ClientState clientState = userState.clientStateMap.get(clientToken);
if (clientState == null) {
clientState = new ClientState(clientToken, mSessionState.userId);
try {
clientToken.linkToDeath(clientState, 0);
} catch (RemoteException e) {
Slog.e(TAG, "client process has already died", e);
return false;
}
userState.clientStateMap.put(clientToken, clientState);
}
clientState.sessionTokens.add(mSessionState.sessionToken);
return true;
}
@Override
public void onChannelRetuned(Uri channelUri) {
synchronized (mLock) {
if (DEBUG) {
Slog.d(TAG, "onChannelRetuned(" + channelUri + ")");
}
if (mSessionState.session == null || mSessionState.client == null) {
return;
}
try {
// TODO: Consider adding this channel change in the watch log. When we do
// that, how we can protect the watch log from malicious tv inputs should
// be addressed. e.g. add a field which represents where the channel change
// originated from.
mSessionState.client.onChannelRetuned(channelUri, mSessionState.seq);
if (!mSessionState.isCurrent
|| !Objects.equals(mSessionState.currentChannel, channelUri)) {
UserState userState = getOrCreateUserStateLocked(mSessionState.userId);
mSessionState.isCurrent = true;
mSessionState.currentChannel = channelUri;
notifyCurrentChannelInfosUpdatedLocked(userState);
}
} catch (RemoteException e) {
Slog.e(TAG, "error in onChannelRetuned", e);
}
}
}
@Override
public void onTracksChanged(List<TvTrackInfo> tracks) {
synchronized (mLock) {
if (DEBUG) {
Slog.d(TAG, "onTracksChanged(" + tracks + ")");
}
if (mSessionState.session == null || mSessionState.client == null) {
return;
}
try {
mSessionState.client.onTracksChanged(tracks, mSessionState.seq);
} catch (RemoteException e) {
Slog.e(TAG, "error in onTracksChanged", e);
}
}
}
@Override
public void onTrackSelected(int type, String trackId) {
synchronized (mLock) {
if (DEBUG) {
Slog.d(TAG, "onTrackSelected(type=" + type + ", trackId=" + trackId + ")");
}
if (mSessionState.session == null || mSessionState.client == null) {
return;
}
try {
mSessionState.client.onTrackSelected(type, trackId, mSessionState.seq);
} catch (RemoteException e) {
Slog.e(TAG, "error in onTrackSelected", e);
}
}
}
@Override
public void onVideoAvailable() {
synchronized (mLock) {
if (DEBUG) {
Slog.d(TAG, "onVideoAvailable()");
}
if (mSessionState.session == null || mSessionState.client == null) {
return;
}
TvInputState tvInputState = getTvInputState(mSessionState,
getUserStateLocked(mCurrentUserId));
try {
mSessionState.client.onVideoAvailable(mSessionState.seq);
logTuneStateChanged(
FrameworkStatsLog.TIF_TUNE_STATE_CHANGED__STATE__VIDEO_AVAILABLE,
mSessionState, tvInputState);
} catch (RemoteException e) {
Slog.e(TAG, "error in onVideoAvailable", e);
}
}
}
@Override
public void onVideoUnavailable(int reason) {
synchronized (mLock) {
if (DEBUG) {
Slog.d(TAG, "onVideoUnavailable(" + reason + ")");
}
if (mSessionState.session == null || mSessionState.client == null) {
return;
}
TvInputState tvInputState = getTvInputState(mSessionState,
getUserStateLocked(mCurrentUserId));
try {
mSessionState.client.onVideoUnavailable(reason, mSessionState.seq);
int loggedReason = getVideoUnavailableReasonForStatsd(reason);
logTuneStateChanged(loggedReason, mSessionState, tvInputState);
} catch (RemoteException e) {
Slog.e(TAG, "error in onVideoUnavailable", e);
}
}
}
@Override
public void onContentAllowed() {
synchronized (mLock) {
if (DEBUG) {
Slog.d(TAG, "onContentAllowed()");
}
if (mSessionState.session == null || mSessionState.client == null) {
return;
}
try {
mSessionState.client.onContentAllowed(mSessionState.seq);
} catch (RemoteException e) {
Slog.e(TAG, "error in onContentAllowed", e);
}
}
}
@Override
public void onContentBlocked(String rating) {
synchronized (mLock) {
if (DEBUG) {
Slog.d(TAG, "onContentBlocked()");
}
if (mSessionState.session == null || mSessionState.client == null) {
return;
}
try {
mSessionState.client.onContentBlocked(rating, mSessionState.seq);
} catch (RemoteException e) {
Slog.e(TAG, "error in onContentBlocked", e);
}
}
}
@Override
public void onLayoutSurface(int left, int top, int right, int bottom) {
synchronized (mLock) {
if (DEBUG) {
Slog.d(TAG, "onLayoutSurface (left=" + left + ", top=" + top
+ ", right=" + right + ", bottom=" + bottom + ",)");
}
if (mSessionState.session == null || mSessionState.client == null) {
return;
}
try {
mSessionState.client.onLayoutSurface(left, top, right, bottom,
mSessionState.seq);
} catch (RemoteException e) {
Slog.e(TAG, "error in onLayoutSurface", e);
}
}
}
@Override
public void onSessionEvent(String eventType, Bundle eventArgs) {
synchronized (mLock) {
if (DEBUG) {
Slog.d(TAG, "onEvent(eventType=" + eventType + ", eventArgs=" + eventArgs
+ ")");
}
if (mSessionState.session == null || mSessionState.client == null) {
return;
}
try {
mSessionState.client.onSessionEvent(eventType, eventArgs, mSessionState.seq);
} catch (RemoteException e) {
Slog.e(TAG, "error in onSessionEvent", e);
}
}
}
@Override
public void onTimeShiftStatusChanged(int status) {
synchronized (mLock) {
if (DEBUG) {
Slog.d(TAG, "onTimeShiftStatusChanged(status=" + status + ")");
}
if (mSessionState.session == null || mSessionState.client == null) {
return;
}
try {
mSessionState.client.onTimeShiftStatusChanged(status, mSessionState.seq);
} catch (RemoteException e) {
Slog.e(TAG, "error in onTimeShiftStatusChanged", e);
}
}
}
@Override
public void onTimeShiftStartPositionChanged(long timeMs) {
synchronized (mLock) {
if (DEBUG) {
Slog.d(TAG, "onTimeShiftStartPositionChanged(timeMs=" + timeMs + ")");
}
if (mSessionState.session == null || mSessionState.client == null) {
return;
}
try {
mSessionState.client.onTimeShiftStartPositionChanged(timeMs, mSessionState.seq);
} catch (RemoteException e) {
Slog.e(TAG, "error in onTimeShiftStartPositionChanged", e);
}
}
}
@Override
public void onTimeShiftCurrentPositionChanged(long timeMs) {
synchronized (mLock) {
if (DEBUG) {
Slog.d(TAG, "onTimeShiftCurrentPositionChanged(timeMs=" + timeMs + ")");
}
if (mSessionState.session == null || mSessionState.client == null) {
return;
}
try {
mSessionState.client.onTimeShiftCurrentPositionChanged(timeMs,
mSessionState.seq);
} catch (RemoteException e) {
Slog.e(TAG, "error in onTimeShiftCurrentPositionChanged", e);
}
}
}
// For the recording session only
@Override
public void onTuned(Uri channelUri) {
synchronized (mLock) {
if (DEBUG) {
Slog.d(TAG, "onTuned()");
}
if (mSessionState.session == null || mSessionState.client == null) {
return;
}
try {
mSessionState.client.onTuned(mSessionState.seq, channelUri);
} catch (RemoteException e) {
Slog.e(TAG, "error in onTuned", e);
}
}
}
// For the recording session only
@Override
public void onRecordingStopped(Uri recordedProgramUri) {
synchronized (mLock) {
if (DEBUG) {
Slog.d(TAG, "onRecordingStopped(recordedProgramUri=" + recordedProgramUri
+ ")");
}
if (mSessionState.session == null || mSessionState.client == null) {
return;
}
try {
mSessionState.client.onRecordingStopped(recordedProgramUri, mSessionState.seq);
} catch (RemoteException e) {
Slog.e(TAG, "error in onRecordingStopped", e);
}
}
}
// For the recording session only
@Override
public void onError(int error) {
synchronized (mLock) {
if (DEBUG) {
Slog.d(TAG, "onError(error=" + error + ")");
}
if (mSessionState.session == null || mSessionState.client == null) {
return;
}
try {
mSessionState.client.onError(error, mSessionState.seq);
} catch (RemoteException e) {
Slog.e(TAG, "error in onError", e);
}
}
}
}
@VisibleForTesting
static int getVideoUnavailableReasonForStatsd(
@TvInputManager.VideoUnavailableReason int reason) {
int loggedReason = reason + FrameworkStatsLog
.TIF_TUNE_STATE_CHANGED__STATE__VIDEO_UNAVAILABLE_REASON_UNKNOWN;
if (loggedReason < FrameworkStatsLog
.TIF_TUNE_STATE_CHANGED__STATE__VIDEO_UNAVAILABLE_REASON_UNKNOWN
|| loggedReason > FrameworkStatsLog
.TIF_TUNE_STATE_CHANGED__STATE__VIDEO_UNAVAILABLE_REASON_CAS_UNKNOWN) {
loggedReason = FrameworkStatsLog
.TIF_TUNE_STATE_CHANGED__STATE__VIDEO_UNAVAILABLE_REASON_UNKNOWN;
}
return loggedReason;
}
private UserState getUserStateLocked(int userId) {
return mUserStates.get(userId);
}
private static final class WatchLogHandler extends Handler {
// There are only two kinds of watch events that can happen on the system:
// 1. The current TV input session is tuned to a new channel.
// 2. The session is released for some reason.
// The former indicates the end of the previous log entry, if any, followed by the start of
// a new entry. The latter indicates the end of the most recent entry for the given session.
// Here the system supplies the database the smallest set of information only that is
// sufficient to consolidate the log entries while minimizing database operations in the
// system service.
static final int MSG_LOG_WATCH_START = 1;
static final int MSG_LOG_WATCH_END = 2;
static final int MSG_SWITCH_CONTENT_RESOLVER = 3;
private ContentResolver mContentResolver;
WatchLogHandler(ContentResolver contentResolver, Looper looper) {
super(looper);
mContentResolver = contentResolver;
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_LOG_WATCH_START: {
SomeArgs args = (SomeArgs) msg.obj;
String packageName = (String) args.arg1;
long watchStartTime = (long) args.arg2;
long channelId = (long) args.arg3;
Bundle tuneParams = (Bundle) args.arg4;
IBinder sessionToken = (IBinder) args.arg5;
ContentValues values = new ContentValues();
values.put(TvContract.BaseTvColumns.COLUMN_PACKAGE_NAME, packageName);
values.put(TvContract.WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS,
watchStartTime);
values.put(TvContract.WatchedPrograms.COLUMN_CHANNEL_ID, channelId);
if (tuneParams != null) {
values.put(TvContract.WatchedPrograms.COLUMN_INTERNAL_TUNE_PARAMS,
encodeTuneParams(tuneParams));
}
values.put(TvContract.WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN,
sessionToken.toString());
try{
mContentResolver.insert(TvContract.WatchedPrograms.CONTENT_URI, values);
}catch(IllegalArgumentException ex){
Slog.w(TAG, "error in insert db for MSG_LOG_WATCH_START", ex);
}
args.recycle();
break;
}
case MSG_LOG_WATCH_END: {
SomeArgs args = (SomeArgs) msg.obj;
IBinder sessionToken = (IBinder) args.arg1;
long watchEndTime = (long) args.arg2;
ContentValues values = new ContentValues();
values.put(TvContract.WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS,
watchEndTime);
values.put(TvContract.WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN,
sessionToken.toString());
try{
mContentResolver.insert(TvContract.WatchedPrograms.CONTENT_URI, values);
}catch(IllegalArgumentException ex){
Slog.w(TAG, "error in insert db for MSG_LOG_WATCH_END", ex);
}
args.recycle();
break;
}
case MSG_SWITCH_CONTENT_RESOLVER: {
mContentResolver = (ContentResolver) msg.obj;
break;
}
default: {
Slog.w(TAG, "unhandled message code: " + msg.what);
break;
}
}
}
private String encodeTuneParams(Bundle tuneParams) {
StringBuilder builder = new StringBuilder();
Set<String> keySet = tuneParams.keySet();
Iterator<String> it = keySet.iterator();
while (it.hasNext()) {
String key = it.next();
Object value = tuneParams.get(key);
if (value == null) {
continue;
}
builder.append(replaceEscapeCharacters(key));
builder.append("=");
builder.append(replaceEscapeCharacters(value.toString()));
if (it.hasNext()) {
builder.append(", ");
}
}
return builder.toString();
}
private String replaceEscapeCharacters(String src) {
final char ESCAPE_CHARACTER = '%';
final String ENCODING_TARGET_CHARACTERS = "%=,";
StringBuilder builder = new StringBuilder();
for (char ch : src.toCharArray()) {
if (ENCODING_TARGET_CHARACTERS.indexOf(ch) >= 0) {
builder.append(ESCAPE_CHARACTER);
}
builder.append(ch);
}
return builder.toString();
}
}
private final class HardwareListener implements TvInputHardwareManager.Listener {
@Override
public void onStateChanged(String inputId, int state) {
synchronized (mLock) {
setStateLocked(inputId, state, mCurrentUserId);
}
}
@Override
public void onHardwareDeviceAdded(TvInputHardwareInfo info) {
synchronized (mLock) {
UserState userState = getOrCreateUserStateLocked(mCurrentUserId);
// Broadcast the event to all hardware inputs.
for (ServiceState serviceState : userState.serviceStateMap.values()) {
if (!serviceState.isHardware || serviceState.service == null) continue;
try {
serviceState.service.notifyHardwareAdded(info);
} catch (RemoteException e) {
Slog.e(TAG, "error in notifyHardwareAdded", e);
}
}
}
}
@Override
public void onHardwareDeviceRemoved(TvInputHardwareInfo info) {
synchronized (mLock) {
UserState userState = getOrCreateUserStateLocked(mCurrentUserId);
// Broadcast the event to all hardware inputs.
for (ServiceState serviceState : userState.serviceStateMap.values()) {
if (!serviceState.isHardware || serviceState.service == null) continue;
try {
serviceState.service.notifyHardwareRemoved(info);
} catch (RemoteException e) {
Slog.e(TAG, "error in notifyHardwareRemoved", e);
}
}
}
}
@Override
public void onHdmiDeviceAdded(HdmiDeviceInfo deviceInfo) {
synchronized (mLock) {
UserState userState = getOrCreateUserStateLocked(mCurrentUserId);
// Broadcast the event to all hardware inputs.
for (ServiceState serviceState : userState.serviceStateMap.values()) {
if (!serviceState.isHardware || serviceState.service == null) continue;
try {
serviceState.service.notifyHdmiDeviceAdded(deviceInfo);
} catch (RemoteException e) {
Slog.e(TAG, "error in notifyHdmiDeviceAdded", e);
}
}
}
}
@Override
public void onHdmiDeviceRemoved(HdmiDeviceInfo deviceInfo) {
synchronized (mLock) {
UserState userState = getOrCreateUserStateLocked(mCurrentUserId);
// Broadcast the event to all hardware inputs.
for (ServiceState serviceState : userState.serviceStateMap.values()) {
if (!serviceState.isHardware || serviceState.service == null) continue;
try {
serviceState.service.notifyHdmiDeviceRemoved(deviceInfo);
} catch (RemoteException e) {
Slog.e(TAG, "error in notifyHdmiDeviceRemoved", e);
}
}
}
}
@Override
public void onHdmiDeviceUpdated(String inputId, HdmiDeviceInfo deviceInfo) {
synchronized (mLock) {
Integer state;
switch (deviceInfo.getDevicePowerStatus()) {
case HdmiControlManager.POWER_STATUS_ON:
state = INPUT_STATE_CONNECTED;
break;
case HdmiControlManager.POWER_STATUS_STANDBY:
case HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON:
case HdmiControlManager.POWER_STATUS_TRANSIENT_TO_STANDBY:
state = INPUT_STATE_CONNECTED_STANDBY;
break;
case HdmiControlManager.POWER_STATUS_UNKNOWN:
default:
state = null;
break;
}
if (state != null) {
setStateLocked(inputId, state, mCurrentUserId);
}
UserState userState = getOrCreateUserStateLocked(mCurrentUserId);
// Broadcast the event to all hardware inputs.
for (ServiceState serviceState : userState.serviceStateMap.values()) {
if (!serviceState.isHardware || serviceState.service == null) continue;
try {
serviceState.service.notifyHdmiDeviceUpdated(deviceInfo);
} catch (RemoteException e) {
Slog.e(TAG, "error in notifyHdmiDeviceUpdated", e);
}
}
}
}
}
private static class SessionNotFoundException extends IllegalArgumentException {
public SessionNotFoundException(String name) {
super(name);
}
}
private static class ClientPidNotFoundException extends IllegalArgumentException {
public ClientPidNotFoundException(String name) {
super(name);
}
}
}