diff options
21 files changed, 1910 insertions, 0 deletions
@@ -197,6 +197,12 @@ LOCAL_SRC_FILES += \ core/java/android/service/wallpaper/IWallpaperConnection.aidl \ core/java/android/service/wallpaper/IWallpaperEngine.aidl \ core/java/android/service/wallpaper/IWallpaperService.aidl \ + core/java/android/tv/ITvInputClient.aidl \ + core/java/android/tv/ITvInputManager.aidl \ + core/java/android/tv/ITvInputService.aidl \ + core/java/android/tv/ITvInputServiceCallback.aidl \ + core/java/android/tv/ITvInputSession.aidl \ + core/java/android/tv/ITvInputSessionCallback.aidl \ core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl\ core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl\ core/java/android/view/accessibility/IAccessibilityManager.aidl \ diff --git a/CleanSpec.mk b/CleanSpec.mk index 4f0e603..ffdac4e 100644 --- a/CleanSpec.mk +++ b/CleanSpec.mk @@ -188,6 +188,7 @@ $(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/framew $(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/services_intermediates) $(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/framework-base_intermediates/src/media/java/android/media/IMedia*) $(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/framework-base_intermediates/src/core/java/android/view/IMagnificationCallbacks*) +$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/framework-base_intermediates/src/core/java/android/tv/) # ************************************************ # NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST diff --git a/api/current.txt b/api/current.txt index 1dcef29..747b6f2 100644 --- a/api/current.txt +++ b/api/current.txt @@ -29,6 +29,7 @@ package android { field public static final java.lang.String BIND_REMOTEVIEWS = "android.permission.BIND_REMOTEVIEWS"; field public static final java.lang.String BIND_TEXT_SERVICE = "android.permission.BIND_TEXT_SERVICE"; field public static final java.lang.String BIND_TRUST_AGENT_SERVICE = "android.permission.BIND_TRUST_AGENT_SERVICE"; + field public static final java.lang.String BIND_TV_INPUT = "android.permission.BIND_TV_INPUT"; field public static final java.lang.String BIND_VPN_SERVICE = "android.permission.BIND_VPN_SERVICE"; field public static final java.lang.String BIND_WALLPAPER = "android.permission.BIND_WALLPAPER"; field public static final java.lang.String BLUETOOTH = "android.permission.BLUETOOTH"; @@ -6497,6 +6498,7 @@ package android.content { field public static final java.lang.String STORAGE_SERVICE = "storage"; field public static final java.lang.String TELEPHONY_SERVICE = "phone"; field public static final java.lang.String TEXT_SERVICES_MANAGER_SERVICE = "textservices"; + field public static final java.lang.String TV_INPUT_SERVICE = "tv_input"; field public static final java.lang.String UI_MODE_SERVICE = "uimode"; field public static final java.lang.String USB_SERVICE = "usb"; field public static final java.lang.String USER_SERVICE = "user"; @@ -27614,6 +27616,60 @@ package android.transition { } +package android.tv { + + public final class TvInputInfo implements android.os.Parcelable { + method public int describeContents(); + method public android.content.ComponentName getComponent(); + method public java.lang.String getId(); + method public java.lang.String getPackageName(); + method public java.lang.String getServiceName(); + method public java.lang.CharSequence loadLabel(android.content.pm.PackageManager); + method public void writeToParcel(android.os.Parcel, int); + } + + public final class TvInputManager { + method public void createSession(android.content.ComponentName, android.tv.TvInputManager.SessionCreateCallback, android.os.Handler); + method public boolean getAvailability(android.content.ComponentName); + method public java.util.List<android.tv.TvInputInfo> getTvInputList(); + method public void registerListener(android.content.ComponentName, android.tv.TvInputManager.TvInputListener, android.os.Handler); + method public void unregisterListener(android.content.ComponentName, android.tv.TvInputManager.TvInputListener); + } + + public static final class TvInputManager.Session { + method public void release(); + method public void setSurface(android.view.Surface); + method public void setVolume(float); + method public void tune(android.net.Uri); + } + + public static abstract interface TvInputManager.SessionCreateCallback { + method public abstract void onSessionCreated(android.tv.TvInputManager.Session); + } + + public static abstract class TvInputManager.TvInputListener { + ctor public TvInputManager.TvInputListener(); + method public void onAvailabilityChanged(android.content.ComponentName, boolean); + } + + public abstract class TvInputService extends android.app.Service { + ctor public TvInputService(); + method public final android.os.IBinder onBind(android.content.Intent); + method public abstract android.tv.TvInputService.TvInputSessionImpl onCreateSession(); + method public final void setAvailable(boolean); + field public static final java.lang.String SERVICE_INTERFACE = "android.tv.TvInputService"; + } + + public static abstract class TvInputService.TvInputSessionImpl { + ctor public TvInputService.TvInputSessionImpl(); + method public abstract void onRelease(); + method public abstract boolean onSetSurface(android.view.Surface); + method public abstract void onSetVolume(float); + method public abstract boolean onTune(android.net.Uri); + } + +} + package android.util { public class AndroidException extends java.lang.Exception { diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 7149ab9..e3b8d5c 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -103,6 +103,8 @@ import android.os.storage.StorageManager; import android.print.IPrintManager; import android.print.PrintManager; import android.telephony.TelephonyManager; +import android.tv.ITvInputManager; +import android.tv.TvInputManager; import android.content.ClipboardManager; import android.util.AndroidRuntimeException; import android.util.ArrayMap; @@ -629,6 +631,13 @@ class ContextImpl extends Context { return new TrustManager(b); } }); + + registerService(TV_INPUT_SERVICE, new ServiceFetcher() { + public Object createService(ContextImpl ctx) { + IBinder iBinder = ServiceManager.getService(TV_INPUT_SERVICE); + ITvInputManager service = ITvInputManager.Stub.asInterface(iBinder); + return new TvInputManager(service, UserHandle.myUserId()); + }}); } static ContextImpl getImpl(Context context) { diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 27e526b..17b57bb 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -2609,6 +2609,16 @@ public abstract class Context { public static final String TRUST_SERVICE = "trust"; /** + * Use with {@link #getSystemService} to retrieve a + * {@link android.tv.TvInputManager} for interacting with TV inputs on the + * device. + * + * @see #getSystemService + * @see android.tv.TvInputManager + */ + public static final String TV_INPUT_SERVICE = "tv_input"; + + /** * Determine whether the given permission is allowed for a particular * process and user ID running in the system. * diff --git a/core/java/android/tv/ITvInputClient.aidl b/core/java/android/tv/ITvInputClient.aidl new file mode 100644 index 0000000..43be6f0 --- /dev/null +++ b/core/java/android/tv/ITvInputClient.aidl @@ -0,0 +1,30 @@ +/* + * 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 android.tv; + +import android.content.ComponentName; +import android.tv.ITvInputSession; + +/** + * Interface a client of the ITvInputManager implements, to identify itself and receive information + * about changes to the state of each TV input service. + * @hide + */ +oneway interface ITvInputClient { + void onSessionCreated(in ComponentName name, IBinder token, int seq); + void onAvailabilityChanged(in ComponentName name, boolean isAvailable); +} diff --git a/core/java/android/tv/ITvInputManager.aidl b/core/java/android/tv/ITvInputManager.aidl new file mode 100644 index 0000000..a927dc9 --- /dev/null +++ b/core/java/android/tv/ITvInputManager.aidl @@ -0,0 +1,43 @@ +/* + * 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 android.tv; + +import android.content.ComponentName; +import android.net.Uri; +import android.tv.ITvInputClient; +import android.tv.TvInputInfo; +import android.view.Surface; + +/** + * Interface to the TV input manager service. + * @hide + */ +interface ITvInputManager { + List<TvInputInfo> getTvInputList(int userId); + + boolean getAvailability(in ITvInputClient client, in ComponentName name, int userId); + + void registerCallback(in ITvInputClient client, in ComponentName name, int userId); + void unregisterCallback(in ITvInputClient client, in ComponentName name, int userId); + + void createSession(in ITvInputClient client, in ComponentName name, int seq, int userId); + void releaseSession(in IBinder sessionToken, int userId); + + void setSurface(in IBinder sessionToken, in Surface surface, int userId); + void setVolume(in IBinder sessionToken, float volume, int userId); + void tune(in IBinder sessionToken, in Uri channelUri, int userId); +} diff --git a/core/java/android/tv/ITvInputService.aidl b/core/java/android/tv/ITvInputService.aidl new file mode 100644 index 0000000..d80f286 --- /dev/null +++ b/core/java/android/tv/ITvInputService.aidl @@ -0,0 +1,31 @@ +/* + * 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 android.tv; + +import android.tv.ITvInputServiceCallback; +import android.tv.ITvInputSession; +import android.tv.ITvInputSessionCallback; + +/** + * Top-level interface to a TV input component (implemented in a Service). + * @hide + */ +oneway interface ITvInputService { + void registerCallback(ITvInputServiceCallback callback); + void unregisterCallback(in ITvInputServiceCallback callback); + void createSession(ITvInputSessionCallback callback); +} diff --git a/core/java/android/tv/ITvInputServiceCallback.aidl b/core/java/android/tv/ITvInputServiceCallback.aidl new file mode 100644 index 0000000..e535c81 --- /dev/null +++ b/core/java/android/tv/ITvInputServiceCallback.aidl @@ -0,0 +1,28 @@ +/* + * 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 android.tv; + +import android.content.ComponentName; + +/** + * Helper interface for ITvInputService to allow the TV input to notify the client when its status + * has been changed. + * @hide + */ +oneway interface ITvInputServiceCallback { + void onAvailabilityChanged(in ComponentName name, boolean isAvailable); +} diff --git a/core/java/android/tv/ITvInputSession.aidl b/core/java/android/tv/ITvInputSession.aidl new file mode 100644 index 0000000..d379d2d --- /dev/null +++ b/core/java/android/tv/ITvInputSession.aidl @@ -0,0 +1,34 @@ +/* + * 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 android.tv; + +import android.net.Uri; +import android.view.Surface; + +/** + * Sub-interface of ITvInputService which is created per session and has its own context. + * @hide + */ +oneway interface ITvInputSession { + void release(); + + void setSurface(in Surface surface); + // TODO: Remove this once it becomes irrelevant for applications to handle audio focus. The plan + // is to introduce some new concepts that will solve a number of problems in audio policy today. + void setVolume(float volume); + void tune(in Uri channelUri); +} diff --git a/core/java/android/tv/ITvInputSessionCallback.aidl b/core/java/android/tv/ITvInputSessionCallback.aidl new file mode 100644 index 0000000..a2bd0d7 --- /dev/null +++ b/core/java/android/tv/ITvInputSessionCallback.aidl @@ -0,0 +1,28 @@ +/* + * 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 android.tv; + +import android.tv.ITvInputSession; + +/** + * Helper interface for ITvInputSession to allow the TV input to notify the system service when a + * new session has been created. + * @hide + */ +oneway interface ITvInputSessionCallback { + void onSessionCreated(ITvInputSession session); +} diff --git a/core/java/android/tv/ITvInputSessionWrapper.java b/core/java/android/tv/ITvInputSessionWrapper.java new file mode 100644 index 0000000..fd4e1e3 --- /dev/null +++ b/core/java/android/tv/ITvInputSessionWrapper.java @@ -0,0 +1,99 @@ +/* + * 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 android.tv; + +import android.content.Context; +import android.net.Uri; +import android.os.Message; +import android.util.Log; +import android.view.Surface; + +import com.android.internal.os.HandlerCaller; + +/** + * Implements the internal ITvInputSession interface to convert incoming calls on to it back to + * calls on the public TvInputSession interface, scheduling them on the main thread of the process. + * + * @hide + */ +public class ITvInputSessionWrapper extends ITvInputSession.Stub implements HandlerCaller.Callback { + private static final String TAG = "TvInputSessionWrapper"; + + private static final int DO_RELEASE = 1; + private static final int DO_SET_SURFACE = 2; + private static final int DO_SET_VOLUME = 3; + private static final int DO_TUNE = 4; + + private TvInputSession mTvInputSession; + private final HandlerCaller mCaller; + + public ITvInputSessionWrapper(Context context, TvInputSession session) { + mCaller = new HandlerCaller(context, null, this, true /* asyncHandler */); + mTvInputSession = session; + } + + @Override + public void executeMessage(Message msg) { + if (mTvInputSession == null) { + return; + } + + switch (msg.what) { + case DO_RELEASE: { + mTvInputSession.release(); + mTvInputSession = null; + return; + } + case DO_SET_SURFACE: { + mTvInputSession.setSurface((Surface) msg.obj); + return; + } + case DO_SET_VOLUME: { + mTvInputSession.setVolume((Float) msg.obj); + return; + } + case DO_TUNE: { + mTvInputSession.tune((Uri) msg.obj); + return; + } + default: { + Log.w(TAG, "Unhandled message code: " + msg.what); + return; + } + } + } + + @Override + public void release() { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_RELEASE)); + } + + @Override + public void setSurface(Surface surface) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_SURFACE, surface)); + } + + @Override + public final void setVolume(float volume) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_VOLUME, volume)); + } + + @Override + public void tune(Uri channelUri) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_TUNE, channelUri)); + } +} diff --git a/core/java/android/tv/TvInputInfo.aidl b/core/java/android/tv/TvInputInfo.aidl new file mode 100644 index 0000000..abc4b47 --- /dev/null +++ b/core/java/android/tv/TvInputInfo.aidl @@ -0,0 +1,19 @@ +/* + * 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 android.tv; + +parcelable TvInputInfo; diff --git a/core/java/android/tv/TvInputInfo.java b/core/java/android/tv/TvInputInfo.java new file mode 100644 index 0000000..90625d8 --- /dev/null +++ b/core/java/android/tv/TvInputInfo.java @@ -0,0 +1,124 @@ +/* + * 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 android.tv; + +import android.content.ComponentName; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * This class is used to specify meta information of a TV input. + */ +public final class TvInputInfo implements Parcelable { + private final ResolveInfo mService; + private final String mId; + + /** + * Constructor. + * + * @param service The ResolveInfo returned from the package manager about this TV input service. + * @hide + */ + public TvInputInfo(ResolveInfo service) { + mService = service; + ServiceInfo si = service.serviceInfo; + mId = new ComponentName(si.packageName, si.name).flattenToShortString(); + } + + /** + * Returns a unique ID for this TV input. The ID is generated from the package and class name + * implementing the TV input service. + */ + public String getId() { + return mId; + } + + /** + * Returns the .apk package that implements this TV input service. + */ + public String getPackageName() { + return mService.serviceInfo.packageName; + } + + /** + * Returns the class name of the service component that implements this TV input service. + */ + public String getServiceName() { + return mService.serviceInfo.name; + } + + /** + * Returns the component of the service that implements this TV input. + */ + public ComponentName getComponent() { + return new ComponentName(mService.serviceInfo.packageName, mService.serviceInfo.name); + } + + /** + * Loads the user-displayed label for this TV input service. + * + * @param pm Supplies a PackageManager used to load the TV input's resources. + * @return Returns a CharSequence containing the TV input's label. If the TV input does not have + * a label, its name is returned. + */ + public CharSequence loadLabel(PackageManager pm) { + return mService.loadLabel(pm); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * Used to package this object into a {@link Parcel}. + * + * @param dest The {@link Parcel} to be written. + * @param flags The flags used for parceling. + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mId); + mService.writeToParcel(dest, flags); + } + + /** + * Used to make this class parcelable. + * + * @hide + */ + public static final Parcelable.Creator<TvInputInfo> CREATOR = + new Parcelable.Creator<TvInputInfo>() { + @Override + public TvInputInfo createFromParcel(Parcel in) { + return new TvInputInfo(in); + } + + @Override + public TvInputInfo[] newArray(int size) { + return new TvInputInfo[size]; + } + }; + + private TvInputInfo(Parcel in) { + mId = in.readString(); + mService = ResolveInfo.CREATOR.createFromParcel(in); + } +} diff --git a/core/java/android/tv/TvInputManager.java b/core/java/android/tv/TvInputManager.java new file mode 100644 index 0000000..0b6ab64 --- /dev/null +++ b/core/java/android/tv/TvInputManager.java @@ -0,0 +1,392 @@ +/* + * 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 android.tv; + +import android.content.ComponentName; +import android.net.Uri; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import android.util.SparseArray; +import android.view.Surface; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Central system API to the overall TV input framework (TIF) architecture, which arbitrates + * interaction between applications and the selected TV inputs. + */ +public final class TvInputManager { + private static final String TAG = "TvInputManager"; + + private final ITvInputManager mService; + + // A mapping from an input to the list of its TvInputListenerRecords. + private final Map<ComponentName, List<TvInputListenerRecord>> mTvInputListenerRecordsMap = + new HashMap<ComponentName, List<TvInputListenerRecord>>(); + + // A mapping from the sequence number of a session to its SessionCreateCallbackRecord. + private final SparseArray<SessionCreateCallbackRecord> mSessionCreateCallbackRecordMap = + new SparseArray<SessionCreateCallbackRecord>(); + + // A sequence number for the next session to be created. Should be protected by a lock + // {@code mSessionCreateCallbackRecordMap}. + private int mNextSeq; + + private final ITvInputClient mClient; + + private final int mUserId; + + /** + * Interface used to receive the created session. + */ + public interface SessionCreateCallback { + /** + * This is called after {@link TvInputManager#createSession} has been processed. + * + * @param session A {@link TvInputManager.Session} instance created. This can be + * {@code null} if the creation request failed. + */ + void onSessionCreated(Session session); + } + + private static final class SessionCreateCallbackRecord { + private final SessionCreateCallback mSessionCreateCallback; + private final Handler mHandler; + + public SessionCreateCallbackRecord(SessionCreateCallback sessionCreateCallback, + Handler handler) { + mSessionCreateCallback = sessionCreateCallback; + mHandler = handler; + } + + public void postSessionCreated(final Session session) { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCreateCallback.onSessionCreated(session); + } + }); + } + } + + /** + * Interface used to monitor status of the TV input. + */ + public abstract static class TvInputListener { + /** + * This is called when the availability status of a given TV input is changed. + * + * @param name {@link ComponentName} of {@link android.app.Service} that implements the + * given TV input. + * @param isAvailable {@code true} if the given TV input is available to show TV programs. + * {@code false} otherwise. + */ + public void onAvailabilityChanged(ComponentName name, boolean isAvailable) { + } + } + + private static final class TvInputListenerRecord { + private final TvInputListener mListener; + private final Handler mHandler; + + public TvInputListenerRecord(TvInputListener listener, Handler handler) { + mListener = listener; + mHandler = handler; + } + + public TvInputListener getListener() { + return mListener; + } + + public void postAvailabilityChanged(final ComponentName name, final boolean isAvailable) { + mHandler.post(new Runnable() { + @Override + public void run() { + mListener.onAvailabilityChanged(name, isAvailable); + } + }); + } + } + + /** + * @hide + */ + public TvInputManager(ITvInputManager service, int userId) { + mService = service; + mUserId = userId; + mClient = new ITvInputClient.Stub() { + @Override + public void onSessionCreated(ComponentName name, IBinder token, int seq) { + synchronized (mSessionCreateCallbackRecordMap) { + SessionCreateCallbackRecord record = mSessionCreateCallbackRecordMap.get(seq); + mSessionCreateCallbackRecordMap.delete(seq); + if (record == null) { + Log.e(TAG, "Callback not found for " + token); + return; + } + Session session = null; + if (token != null) { + session = new Session(name, token, mService, mUserId); + } + record.postSessionCreated(session); + } + } + + @Override + public void onAvailabilityChanged(ComponentName name, boolean isAvailable) { + synchronized (mTvInputListenerRecordsMap) { + List<TvInputListenerRecord> records = mTvInputListenerRecordsMap.get(name); + if (records == null) { + // Silently ignore - no listener is registered yet. + return; + } + int recordsCount = records.size(); + for (int i = 0; i < recordsCount; i++) { + records.get(i).postAvailabilityChanged(name, isAvailable); + } + } + } + }; + } + + /** + * Returns the complete list of TV inputs on the system. + * + * @return List of {@link TvInputInfo} for each TV input that describes its meta information. + */ + public List<TvInputInfo> getTvInputList() { + try { + return mService.getTvInputList(mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * Returns the availability of a given TV input. + * + * @param name {@link ComponentName} of {@link android.app.Service} that implements the given TV + * input. + * @throws IllegalArgumentException if the argument is {@code null}. + * @throws IllegalStateException If there is no {@link TvInputListener} registered on the given + * TV input. + */ + public boolean getAvailability(ComponentName name) { + if (name == null) { + throw new IllegalArgumentException("name cannot be null"); + } + synchronized (mTvInputListenerRecordsMap) { + List<TvInputListenerRecord> records = mTvInputListenerRecordsMap.get(name); + if (records == null || records.size() == 0) { + throw new IllegalStateException("At least one listener should be registered."); + } + } + try { + return mService.getAvailability(mClient, name, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * Registers a {@link TvInputListener} for a given TV input. + * + * @param name {@link ComponentName} of {@link android.app.Service} that implements the given TV + * input. + * @param listener a listener used to monitor status of the given TV input. + * @param handler a {@link Handler} that the status change will be delivered to. + * @throws IllegalArgumentException if any of the arguments is {@code null}. + */ + public void registerListener(ComponentName name, TvInputListener listener, Handler handler) { + if (name == null) { + throw new IllegalArgumentException("name cannot be null"); + } + if (listener == null) { + throw new IllegalArgumentException("listener cannot be null"); + } + if (handler == null) { + throw new IllegalArgumentException("handler cannot be null"); + } + synchronized (mTvInputListenerRecordsMap) { + List<TvInputListenerRecord> records = mTvInputListenerRecordsMap.get(name); + if (records == null) { + records = new ArrayList<TvInputListenerRecord>(); + mTvInputListenerRecordsMap.put(name, records); + try { + mService.registerCallback(mClient, name, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + records.add(new TvInputListenerRecord(listener, handler)); + } + } + + /** + * Unregisters the existing {@link TvInputListener} for a given TV input. + * + * @param name {@link ComponentName} of {@link android.app.Service} that implements the given TV + * input. + * @param listener the existing listener to remove for the given TV input. + * @throws IllegalArgumentException if any of the arguments is {@code null}. + */ + public void unregisterListener(ComponentName name, final TvInputListener listener) { + if (name == null) { + throw new IllegalArgumentException("name cannot be null"); + } + if (listener == null) { + throw new IllegalArgumentException("listener cannot be null"); + } + synchronized (mTvInputListenerRecordsMap) { + List<TvInputListenerRecord> records = mTvInputListenerRecordsMap.get(name); + if (records == null) { + Log.e(TAG, "No listener found for " + name.getClassName()); + return; + } + for (Iterator<TvInputListenerRecord> it = records.iterator(); it.hasNext();) { + TvInputListenerRecord record = it.next(); + if (record.getListener() == listener) { + it.remove(); + } + } + if (records.isEmpty()) { + try { + mService.unregisterCallback(mClient, name, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } finally { + mTvInputListenerRecordsMap.remove(name); + } + } + } + } + + /** + * Creates a {@link TvInputSession} interface for a given TV input. + * <p> + * The number of sessions that can be created at the same time is limited by the capability of + * the given TV input. + * </p> + * + * @param name {@link ComponentName} of {@link android.app.Service} that implements the given TV + * input. + * @param callback a callback used to receive the created session. + * @param handler a {@link Handler} that the session creation will be delivered to. + * @throws IllegalArgumentException if any of the arguments is {@code null}. + */ + public void createSession(ComponentName name, final SessionCreateCallback callback, + Handler handler) { + if (name == null) { + throw new IllegalArgumentException("name cannot be null"); + } + if (callback == null) { + throw new IllegalArgumentException("callback cannot be null"); + } + if (handler == null) { + throw new IllegalArgumentException("handler cannot be null"); + } + SessionCreateCallbackRecord record = new SessionCreateCallbackRecord(callback, handler); + synchronized (mSessionCreateCallbackRecordMap) { + int seq = mNextSeq++; + mSessionCreateCallbackRecordMap.put(seq, record); + try { + mService.createSession(mClient, name, seq, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + } + + /** The Session provides the per-session functionality of TV inputs. */ + public static final class Session { + private final ITvInputManager mService; + private final IBinder mToken; + private final int mUserId; + + /** @hide */ + private Session(ComponentName name, IBinder token, ITvInputManager service, int userId) { + mToken = token; + mService = service; + mUserId = userId; + } + + /** + * Releases this session. + */ + public void release() { + try { + mService.releaseSession(mToken, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * Sets the {@link android.view.Surface} for this session. + * + * @param surface A {@link android.view.Surface} used to render video. + */ + public void setSurface(Surface surface) { + // surface can be null. + try { + mService.setSurface(mToken, surface, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * Sets the relative volume of this session to handle a change of audio focus. + * + * @param volume A volume value between 0.0f to 1.0f. + * @throws IllegalArgumentException if the volume value is out of range. + */ + public void setVolume(float volume) { + try { + if (volume < 0.0f || volume > 1.0f) { + throw new IllegalArgumentException("volume should be between 0.0f and 1.0f"); + } + mService.setVolume(mToken, volume, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * Tunes to a given channel. + * + * @param channelUri The URI of a channel. + * @throws IllegalArgumentException if the argument is {@code null}. + */ + public void tune(Uri channelUri) { + if (channelUri == null) { + throw new IllegalArgumentException("channelUri cannot be null"); + } + try { + mService.tune(mToken, channelUri, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/core/java/android/tv/TvInputService.java b/core/java/android/tv/TvInputService.java new file mode 100644 index 0000000..e43cc95 --- /dev/null +++ b/core/java/android/tv/TvInputService.java @@ -0,0 +1,254 @@ +/* + * 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 android.tv; + +import android.app.Service; +import android.content.ComponentName; +import android.content.Intent; +import android.net.Uri; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.util.Log; +import android.view.Surface; + +import com.android.internal.annotations.VisibleForTesting; + +/** + * A base class for implementing television input service. + */ +public abstract class TvInputService extends Service { + // STOPSHIP: Turn debugging off. + private static final boolean DEBUG = true; + private static final String TAG = "TvInputService"; + + /** + * This is the interface name that a service implementing a TV input should say that it support + * -- that is, this is the action it uses for its intent filter. To be supported, the service + * must also require the {@link android.Manifest.permission#BIND_TV_INPUT} permission so that + * other applications cannot abuse it. + */ + public static final String SERVICE_INTERFACE = "android.tv.TvInputService"; + + private ComponentName mComponentName; + private final Handler mHandler = new ServiceHandler(); + private final RemoteCallbackList<ITvInputServiceCallback> mCallbacks = + new RemoteCallbackList<ITvInputServiceCallback>(); + private boolean mAvailable; + + @Override + public void onCreate() { + super.onCreate(); + mComponentName = new ComponentName(getPackageName(), getClass().getName()); + } + + @Override + public final IBinder onBind(Intent intent) { + return new ITvInputService.Stub() { + @Override + public void registerCallback(ITvInputServiceCallback cb) { + if (cb != null) { + mCallbacks.register(cb); + // The first time a callback is registered, the service needs to report its + // availability status so that the system can know its initial value. + try { + cb.onAvailabilityChanged(mComponentName, mAvailable); + } catch (RemoteException e) { + Log.e(TAG, "error in onAvailabilityChanged", e); + } + } + } + + @Override + public void unregisterCallback(ITvInputServiceCallback cb) { + if (cb != null) { + mCallbacks.unregister(cb); + } + } + + @Override + public void createSession(ITvInputSessionCallback cb) { + if (cb != null) { + mHandler.obtainMessage(ServiceHandler.DO_CREATE_SESSION, cb).sendToTarget(); + } + } + }; + } + + /** + * Convenience method to notify an availability change of this TV input service. + * + * @param available {@code true} if the input service is available to show TV programs. + */ + public final void setAvailable(boolean available) { + if (available != mAvailable) { + mAvailable = available; + mHandler.obtainMessage(ServiceHandler.DO_BROADCAST_AVAILABILITY_CHANGE, available) + .sendToTarget(); + } + } + + /** + * Get the number of callbacks that are registered. + * + * @hide + */ + @VisibleForTesting + public final int getRegisteredCallbackCount() { + return mCallbacks.getRegisteredCallbackCount(); + } + + /** + * Returns a concrete implementation of {@link TvInputSessionImpl}. + * <p> + * May return {@code null} if this TV input service fails to create a session for some reason. + * </p> + */ + public abstract TvInputSessionImpl onCreateSession(); + + /** + * Base class for derived classes to implement to provide {@link TvInputSession}. + */ + public abstract static class TvInputSessionImpl { + /** + * Called when the session is released. + */ + public abstract void onRelease(); + + /** + * Sets the {@link Surface} for the current input session on which the TV input renders + * video. + * + * @param surface {@link Surface} an application passes to this TV input session. + * @return {@code true} if the surface was set, {@code false} otherwise. + */ + public abstract boolean onSetSurface(Surface surface); + + /** + * Sets the relative volume of the current TV input session to handle the change of audio + * focus by setting. + * + * @param volume Volume scale from 0.0 to 1.0. + */ + public abstract void onSetVolume(float volume); + + /** + * Tunes to a given channel. + * + * @param channelUri The URI of the channel. + * @return {@code true} the tuning was successful, {@code false} otherwise. + */ + public abstract boolean onTune(Uri channelUri); + } + + /** + * Internal implementation of {@link TvInputSession}. This takes care of basic maintenance of + * the TV input session but most behavior must be implemented in {@link TvInputSessionImpl} + * returned by {@link TvInputService#onCreateSession}. + */ + private static class TvInputSessionImplInternal extends TvInputSession { + private final TvInputSessionImpl mSessionImpl; + + public TvInputSessionImplInternal(TvInputSessionImpl sessionImpl) { + mSessionImpl = sessionImpl; + } + + /** + * This method is called when the application would like to stop using the current input + * session. + */ + @Override + public final void release() { + mSessionImpl.onRelease(); + } + + /** + * Calls {@link TvInputSessionImpl#onSetSurface}. + */ + @Override + public final void setSurface(Surface surface) { + mSessionImpl.onSetSurface(surface); + // TODO: Handle failure. + } + + /** + * Calls {@link TvInputSessionImpl#onSetVolume}. + */ + @Override + public final void setVolume(float volume) { + mSessionImpl.onSetVolume(volume); + } + + /** + * Calls {@link TvInputSessionImpl#onTune}. + */ + @Override + public final void tune(Uri channelUri) { + mSessionImpl.onTune(channelUri); + // TODO: Handle failure. + } + } + + private final class ServiceHandler extends Handler { + private static final int DO_CREATE_SESSION = 1; + private static final int DO_BROADCAST_AVAILABILITY_CHANGE = 2; + + @Override + public final void handleMessage(Message msg) { + switch (msg.what) { + case DO_CREATE_SESSION: { + ITvInputSessionCallback cb = (ITvInputSessionCallback) msg.obj; + try { + TvInputSessionImpl sessionImpl = onCreateSession(); + if (sessionImpl == null) { + // Failed to create a session. + cb.onSessionCreated(null); + return; + } + ITvInputSession stub = new ITvInputSessionWrapper(TvInputService.this, + new TvInputSessionImplInternal(sessionImpl)); + cb.onSessionCreated(stub); + } catch (RemoteException e) { + Log.e(TAG, "error in onSessionCreated"); + } + return; + } + case DO_BROADCAST_AVAILABILITY_CHANGE: { + boolean isAvailable = (Boolean) msg.obj; + int n = mCallbacks.beginBroadcast(); + try { + for (int i = 0; i < n; i++) { + mCallbacks.getBroadcastItem(i).onAvailabilityChanged(mComponentName, + isAvailable); + } + } catch (RemoteException e) { + Log.e(TAG, "Unexpected exception", e); + } finally { + mCallbacks.finishBroadcast(); + } + return; + } + default: { + Log.w(TAG, "Unhandled message code: " + msg.what); + return; + } + } + } + } +} diff --git a/core/java/android/tv/TvInputSession.java b/core/java/android/tv/TvInputSession.java new file mode 100644 index 0000000..5b70a0b --- /dev/null +++ b/core/java/android/tv/TvInputSession.java @@ -0,0 +1,57 @@ +/* + * 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 android.tv; + +import android.net.Uri; +import android.view.Surface; + +/** + * The TvInputSession provides the per-session functionality of TvInputService. + * + * @hide + */ +public abstract class TvInputSession { + /** + * This method is called when the application would like to stop using the current input + * session. + */ + public void release() { } + + /** + * Sets the {@link Surface} for the current input session on which the TV input renders video. + * + * @param surface {@link Surface} to be used for the video playback of this session. + */ + public void setSurface(Surface surface) { } + + /** + * This method is called when the application needs to handle the change of audio focus by + * setting the relative volume of the current TV input service session. + * + * @param volume Volume scale from 0.0 to 1.0. + */ + // TODO: Remove this once it becomes irrelevant for applications to handle audio focus. The plan + // is to introduce some new concepts that will solve a number of problems in audio policy today. + public void setVolume(float volume) { } + + /** + * Tunes to a given channel. + * + * @param channelUri The URI of the channel. + */ + public void tune(Uri channelUri) { } +} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 3857cd1..0f772f1 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -2036,6 +2036,13 @@ android:description="@string/permdesc_bindRemoteDisplay" android:protectionLevel="signature" /> + <!-- Must be required by a {@link android.tv.TvInputService} + to ensure that only the system can bind to it. --> + <permission android:name="android.permission.BIND_TV_INPUT" + android:label="@string/permlab_bindTvInput" + android:description="@string/permdesc_bindTvInput" + android:protectionLevel="signature|system" /> + <!-- Must be required by device administration receiver, to ensure that only the system can interact with it. --> <permission android:name="android.permission.BIND_DEVICE_ADMIN" diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 902aea8..4a121d1 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -1056,6 +1056,12 @@ a device administrator. Should never be needed for normal apps.</string> <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permlab_bindTvInput">bind to a TV input</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_bindTvInput">Allows the holder to bind to the top-level + interface of a TV input. Should never be needed for normal apps.</string> + + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> <string name="permlab_manageDeviceAdmins">add or remove a device admin</string> <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> <string name="permdesc_manageDeviceAdmins">Allows the holder to add or remove active device 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"); |