diff options
author | Jae Seo <jaeseo@google.com> | 2014-02-20 18:23:25 -0800 |
---|---|---|
committer | Jae Seo <jaeseo@google.com> | 2014-04-08 13:35:21 -0700 |
commit | 3957091ba8f08c02b5e781098cb955a5f697a1ff (patch) | |
tree | c8739c677c87e62ea6c1e8bc45de027d1e65d87c /services | |
parent | 53c2cf799fddfae7f6fc9ca1840ea345308b79ee (diff) | |
download | frameworks_base-3957091ba8f08c02b5e781098cb955a5f697a1ff.zip frameworks_base-3957091ba8f08c02b5e781098cb955a5f697a1ff.tar.gz frameworks_base-3957091ba8f08c02b5e781098cb955a5f697a1ff.tar.bz2 |
Initial round of Television Input Framework
This provides APIs to control and create individual television inputs on
the system which will later be hosted by television applications.
Change-Id: I6866d28e78175a1bff2c32a85c5d77e94d0cd60c
Diffstat (limited to 'services')
-rw-r--r-- | services/core/java/com/android/server/tv/TvInputManagerService.java | 667 | ||||
-rw-r--r-- | services/java/com/android/server/SystemServer.java | 9 |
2 files changed, 676 insertions, 0 deletions
diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java new file mode 100644 index 0000000..972b088 --- /dev/null +++ b/services/core/java/com/android/server/tv/TvInputManagerService.java @@ -0,0 +1,667 @@ +/* + * 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 android.app.ActivityManager; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.net.Uri; +import android.os.Binder; +import android.os.IBinder; +import android.os.Process; +import android.os.RemoteException; +import android.os.UserHandle; +import android.tv.ITvInputClient; +import android.tv.ITvInputManager; +import android.tv.ITvInputService; +import android.tv.ITvInputServiceCallback; +import android.tv.ITvInputSession; +import android.tv.ITvInputSessionCallback; +import android.tv.TvInputInfo; +import android.tv.TvInputService; +import android.util.ArrayMap; +import android.util.Log; +import android.util.SparseArray; +import android.view.Surface; + +import com.android.internal.content.PackageMonitor; +import com.android.server.SystemService; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** This class provides a system service that manages television inputs. */ +public final class TvInputManagerService extends SystemService { + // STOPSHIP: Turn debugging off. + private static final boolean DEBUG = true; + private static final String TAG = "TvInputManagerService"; + + private final Context mContext; + + // A global lock. + private final Object mLock = new Object(); + + // ID of the current user. + private int mCurrentUserId = UserHandle.USER_OWNER; + + // A map from user id to UserState. + private final SparseArray<UserState> mUserStates = new SparseArray<UserState>(); + + public TvInputManagerService(Context context) { + super(context); + mContext = context; + registerBroadcastReceivers(); + synchronized (mLock) { + mUserStates.put(mCurrentUserId, new UserState()); + buildTvInputListLocked(mCurrentUserId); + } + } + + @Override + public void onStart() { + publishBinderService(Context.TV_INPUT_SERVICE, new BinderService()); + } + + private void registerBroadcastReceivers() { + PackageMonitor monitor = new PackageMonitor() { + @Override + public void onSomePackagesChanged() { + synchronized (mLock) { + buildTvInputListLocked(mCurrentUserId); + } + } + }; + monitor.register(mContext, null, UserHandle.ALL, true); + + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_USER_SWITCHED); + intentFilter.addAction(Intent.ACTION_USER_REMOVED); + 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)); + } + } + }, UserHandle.ALL, intentFilter, null, null); + } + + private void buildTvInputListLocked(int userId) { + UserState userState = getUserStateLocked(userId); + userState.inputList.clear(); + + PackageManager pm = mContext.getPackageManager(); + List<ResolveInfo> services = pm.queryIntentServices( + new Intent(TvInputService.SERVICE_INTERFACE), PackageManager.GET_SERVICES); + for (ResolveInfo ri : services) { + ServiceInfo si = ri.serviceInfo; + if (!android.Manifest.permission.BIND_TV_INPUT.equals(si.permission)) { + Log.w(TAG, "Skipping TV input " + si.name + ": it does not require the permission " + + android.Manifest.permission.BIND_TV_INPUT); + continue; + } + TvInputInfo info = new TvInputInfo(ri); + userState.inputList.add(info); + } + } + + private void switchUser(int userId) { + synchronized (mLock) { + if (mCurrentUserId == userId) { + return; + } + // final int oldUserId = mCurrentUserId; + // TODO: Release services and sessions in the old user state, if needed. + mCurrentUserId = userId; + + UserState userState = mUserStates.get(userId); + if (userState == null) { + userState = new UserState(); + } + mUserStates.put(userId, userState); + buildTvInputListLocked(userId); + } + } + + private void removeUser(int userId) { + synchronized (mLock) { + // Release created sessions. + UserState userState = getUserStateLocked(userId); + for (SessionState state : userState.sessionStateMap.values()) { + if (state.session != null) { + try { + state.session.release(); + } catch (RemoteException e) { + Log.e(TAG, "error in release", e); + } + } + } + userState.sessionStateMap.clear(); + + // Unregister all callbacks and unbind all services. + for (ServiceState serviceState : userState.serviceStateMap.values()) { + if (serviceState.callback != null) { + try { + serviceState.service.unregisterCallback(serviceState.callback); + } catch (RemoteException e) { + Log.e(TAG, "error in unregisterCallback", e); + } + } + serviceState.clients.clear(); + mContext.unbindService(serviceState.connection); + } + userState.serviceStateMap.clear(); + + mUserStates.remove(userId); + } + } + + private UserState getUserStateLocked(int userId) { + UserState userState = mUserStates.get(userId); + if (userState == null) { + throw new IllegalStateException("User state not found for user ID " + userId); + } + return userState; + } + + private ServiceState getServiceStateLocked(ComponentName name, int userId) { + UserState userState = getUserStateLocked(userId); + ServiceState serviceState = userState.serviceStateMap.get(name); + if (serviceState == null) { + throw new IllegalStateException("Service state not found for " + name + " (userId=" + + userId + ")"); + } + return serviceState; + } + + private ITvInputSession getSessionLocked(IBinder sessionToken, int callingUid, int userId) { + UserState userState = getUserStateLocked(userId); + SessionState sessionState = userState.sessionStateMap.get(sessionToken); + if (sessionState == null) { + throw new IllegalArgumentException("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); + } + ITvInputSession session = sessionState.session; + if (session == null) { + throw new IllegalStateException("Session not yet created for token " + sessionToken); + } + return session; + } + + private int resolveCallingUserId(int callingPid, int callingUid, int requestedUserId, + String methodName) { + return ActivityManager.handleIncomingUser(callingPid, callingUid, requestedUserId, false, + false, methodName, null); + } + + private void updateServiceConnectionLocked(ComponentName name, int userId) { + UserState userState = getUserStateLocked(userId); + ServiceState serviceState = userState.serviceStateMap.get(name); + if (serviceState == null) { + return; + } + boolean isStateEmpty = serviceState.clients.size() == 0 + && serviceState.sessionStateMap.size() == 0; + if (serviceState.service == null && !isStateEmpty && userId == mCurrentUserId) { + // 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) { + Log.i(TAG, "bindServiceAsUser(name=" + name.getClassName() + ", userId=" + userId + + ")"); + } + Intent i = new Intent(TvInputService.SERVICE_INTERFACE).setComponent(name); + mContext.bindServiceAsUser(i, serviceState.connection, Context.BIND_AUTO_CREATE, + new UserHandle(userId)); + serviceState.bound = true; + } else if (serviceState.service != null && isStateEmpty) { + // 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) { + Log.i(TAG, "unbindService(name=" + name.getClassName() + ")"); + } + mContext.unbindService(serviceState.connection); + userState.serviceStateMap.remove(name); + } + } + + private void createSessionInternalLocked(ITvInputService service, final IBinder sessionToken, + final SessionState sessionState, final int userId) { + if (DEBUG) { + Log.d(TAG, "createSessionInternalLocked(name=" + sessionState.name.getClassName() + + ")"); + } + // Set up a callback to send the session token. + ITvInputSessionCallback callback = new ITvInputSessionCallback.Stub() { + @Override + public void onSessionCreated(ITvInputSession session) { + if (DEBUG) { + Log.d(TAG, "onSessionCreated(name=" + sessionState.name.getClassName() + ")"); + } + synchronized (mLock) { + sessionState.session = session; + sendSessionTokenToClientLocked(sessionState.client, sessionState.name, + sessionToken, sessionState.seq, userId); + } + } + }; + + // Create a session. When failed, send a null token immediately. + try { + service.createSession(callback); + } catch (RemoteException e) { + Log.e(TAG, "error in createSession", e); + sendSessionTokenToClientLocked(sessionState.client, sessionState.name, null, + sessionState.seq, userId); + } + } + + private void sendSessionTokenToClientLocked(ITvInputClient client, ComponentName name, + IBinder sessionToken, int seq, int userId) { + try { + client.onSessionCreated(name, sessionToken, seq); + } catch (RemoteException exception) { + Log.e(TAG, "error in onSessionCreated", exception); + } + + if (sessionToken == null) { + // This means that the session creation failed. We might want to disconnect the service. + updateServiceConnectionLocked(name, userId); + } + } + + 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 = getUserStateLocked(resolvedUserId); + return new ArrayList<TvInputInfo>(userState.inputList); + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public boolean getAvailability(final ITvInputClient client, final ComponentName name, + int userId) { + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), + Binder.getCallingUid(), userId, "getAvailability"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + UserState userState = getUserStateLocked(resolvedUserId); + ServiceState serviceState = userState.serviceStateMap.get(name); + if (serviceState != null) { + // We already know the status of this input service. Return the cached + // status. + return serviceState.available; + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + return false; + } + + @Override + public void registerCallback(final ITvInputClient client, final ComponentName name, + int userId) { + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), + Binder.getCallingUid(), userId, "registerCallback"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + // Create a new service callback and add it to the callback map of the current + // service. + UserState userState = getUserStateLocked(resolvedUserId); + ServiceState serviceState = userState.serviceStateMap.get(name); + if (serviceState == null) { + serviceState = new ServiceState(name, resolvedUserId); + userState.serviceStateMap.put(name, serviceState); + } + IBinder iBinder = client.asBinder(); + if (!serviceState.clients.contains(iBinder)) { + serviceState.clients.add(iBinder); + } + if (serviceState.service != null) { + if (serviceState.callback != null) { + // We already handled. + return; + } + serviceState.callback = new ServiceCallback(resolvedUserId); + try { + serviceState.service.registerCallback(serviceState.callback); + } catch (RemoteException e) { + Log.e(TAG, "error in registerCallback", e); + } + } else { + updateServiceConnectionLocked(name, resolvedUserId); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void unregisterCallback(ITvInputClient client, ComponentName name, int userId) { + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), + Binder.getCallingUid(), userId, "unregisterCallback"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + UserState userState = getUserStateLocked(resolvedUserId); + ServiceState serviceState = userState.serviceStateMap.get(name); + if (serviceState == null) { + return; + } + + // Remove this client from the client list and unregister the callback. + serviceState.clients.remove(client.asBinder()); + if (!serviceState.clients.isEmpty()) { + // We have other clients who want to keep the callback. Do this later. + return; + } + if (serviceState.service == null || serviceState.callback == null) { + return; + } + try { + serviceState.service.unregisterCallback(serviceState.callback); + } catch (RemoteException e) { + Log.e(TAG, "error in unregisterCallback", e); + } finally { + serviceState.callback = null; + updateServiceConnectionLocked(name, resolvedUserId); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void createSession(final ITvInputClient client, final ComponentName name, + int seq, int userId) { + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "createSession"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + // Create a new session token and a session state. + IBinder sessionToken = new Binder(); + SessionState sessionState = new SessionState(name, client, seq, callingUid); + sessionState.session = null; + + // Add them to the global session state map of the current user. + UserState userState = getUserStateLocked(resolvedUserId); + userState.sessionStateMap.put(sessionToken, sessionState); + + // Also, add them to the session state map of the current service. + ServiceState serviceState = userState.serviceStateMap.get(name); + if (serviceState == null) { + serviceState = new ServiceState(name, resolvedUserId); + userState.serviceStateMap.put(name, serviceState); + } + serviceState.sessionStateMap.put(sessionToken, sessionState); + + if (serviceState.service != null) { + createSessionInternalLocked(serviceState.service, sessionToken, + sessionState, resolvedUserId); + } else { + updateServiceConnectionLocked(name, resolvedUserId); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void releaseSession(IBinder sessionToken, int userId) { + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "releaseSession"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + // Release the session. + try { + getSessionLocked(sessionToken, callingUid, resolvedUserId).release(); + } catch (RemoteException e) { + Log.e(TAG, "error in release", e); + } + + // Remove its state from the global session state map of the current user. + UserState userState = getUserStateLocked(resolvedUserId); + SessionState sessionState = userState.sessionStateMap.remove(sessionToken); + + // Also remove it from the session state map of the current service. + ServiceState serviceState = userState.serviceStateMap.get(sessionState.name); + if (serviceState != null) { + serviceState.sessionStateMap.remove(sessionToken); + } + + updateServiceConnectionLocked(sessionState.name, resolvedUserId); + } + } 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(); + try { + synchronized (mLock) { + try { + getSessionLocked(sessionToken, callingUid, resolvedUserId).setSurface( + surface); + } catch (RemoteException e) { + Log.e(TAG, "error in setSurface", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void setVolume(IBinder sessionToken, float volume, int userId) { + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "setVolume"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + getSessionLocked(sessionToken, callingUid, resolvedUserId).setVolume( + volume); + } catch (RemoteException e) { + Log.e(TAG, "error in setVolume", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void tune(IBinder sessionToken, final Uri channelUri, int userId) { + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "tune"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + SessionState sessionState = getUserStateLocked(resolvedUserId) + .sessionStateMap.get(sessionToken); + final String serviceName = sessionState.name.getClassName(); + try { + getSessionLocked(sessionToken, callingUid, resolvedUserId).tune(channelUri); + } catch (RemoteException e) { + Log.e(TAG, "error in tune", e); + return; + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + + private static final class UserState { + // A list of all known TV inputs on the system. + private final List<TvInputInfo> inputList = new ArrayList<TvInputInfo>(); + + // A mapping from the name of a TV input service to its state. + private final Map<ComponentName, ServiceState> serviceStateMap = + new HashMap<ComponentName, ServiceState>(); + + // A mapping from the token of a TV input session to its state. + private final Map<IBinder, SessionState> sessionStateMap = + new HashMap<IBinder, SessionState>(); + } + + private final class ServiceState { + private final List<IBinder> clients = new ArrayList<IBinder>(); + private final ArrayMap<IBinder, SessionState> sessionStateMap = new ArrayMap<IBinder, + SessionState>(); + private final ServiceConnection connection; + + private ITvInputService service; + private ServiceCallback callback; + private boolean bound; + private boolean available; + + private ServiceState(ComponentName name, int userId) { + this.connection = new InputServiceConnection(userId); + } + } + + private static final class SessionState { + private final ComponentName name; + private final ITvInputClient client; + private final int seq; + private final int callingUid; + + private ITvInputSession session; + + private SessionState(ComponentName name, ITvInputClient client, int seq, int callingUid) { + this.name = name; + this.client = client; + this.seq = seq; + this.callingUid = callingUid; + } + } + + private final class InputServiceConnection implements ServiceConnection { + private final int mUserId; + + private InputServiceConnection(int userId) { + mUserId = userId; + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + if (DEBUG) { + Log.d(TAG, "onServiceConnected(name=" + name.getClassName() + ")"); + } + synchronized (mLock) { + ServiceState serviceState = getServiceStateLocked(name, mUserId); + serviceState.service = ITvInputService.Stub.asInterface(service); + + // Register a callback, if we need to. + if (!serviceState.clients.isEmpty() && serviceState.callback == null) { + serviceState.callback = new ServiceCallback(mUserId); + try { + serviceState.service.registerCallback(serviceState.callback); + } catch (RemoteException e) { + Log.e(TAG, "error in registerCallback", e); + } + } + + // And create sessions, if any. + for (Map.Entry<IBinder, SessionState> entry : serviceState.sessionStateMap + .entrySet()) { + createSessionInternalLocked(serviceState.service, entry.getKey(), + entry.getValue(), mUserId); + } + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + if (DEBUG) { + Log.d(TAG, "onServiceDisconnected(name=" + name.getClassName() + ")"); + } + } + } + + private final class ServiceCallback extends ITvInputServiceCallback.Stub { + private final int mUserId; + + ServiceCallback(int userId) { + mUserId = userId; + } + + @Override + public void onAvailabilityChanged(ComponentName name, boolean isAvailable) + throws RemoteException { + if (DEBUG) { + Log.d(TAG, "onAvailabilityChanged(name=" + name.getClassName() + ", isAvailable=" + + isAvailable + ")"); + } + synchronized (mLock) { + ServiceState serviceState = getServiceStateLocked(name, mUserId); + serviceState.available = isAvailable; + for (IBinder iBinder : serviceState.clients) { + ITvInputClient client = ITvInputClient.Stub.asInterface(iBinder); + client.onAvailabilityChanged(name, isAvailable); + } + } + } + } +} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 8f2adc8..bf81686 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -42,6 +42,7 @@ import android.os.SystemClock; import android.os.SystemProperties; import android.os.UserHandle; import android.service.dreams.DreamService; +import android.tv.TvInputManager; import android.util.DisplayMetrics; import android.util.EventLog; import android.util.Log; @@ -78,6 +79,7 @@ import com.android.server.search.SearchManagerService; import com.android.server.statusbar.StatusBarManagerService; import com.android.server.storage.DeviceStorageMonitorService; import com.android.server.trust.TrustManagerService; +import com.android.server.tv.TvInputManagerService; import com.android.server.twilight.TwilightService; import com.android.server.usb.UsbService; import com.android.server.wallpaper.WallpaperManagerService; @@ -906,6 +908,13 @@ public final class SystemServer { reportWtf("starting HdmiCec Service", e); } + try { + Slog.i(TAG, "TvInputManagerService"); + mSystemServiceManager.startService(TvInputManagerService.class); + } catch (Throwable e) { + reportWtf("starting TvInputManagerService", e); + } + if (!disableNonCoreServices) { try { Slog.i(TAG, "Media Router Service"); |