Unbundle RemoteService on TV - part 3
- New service TVRemoteService triggered by SystemServer
- Provider service proxy and watcher for maintaining connections to unbundled
services which have the BIND_TV_REMOTE_SERVICE permission.
- Shared library to facilitate connections between unbundled service and
TVRemoteService.
- Unbundled service needs TV_VIRTUAL_REMOTE_CONTROLLER
permission to be fully functional.
b/23792608
Change-Id: Ief5c6995883d1f7268a73bdd0c920c4c3f42cddb
diff --git a/Android.mk b/Android.mk
index da53db7..1c5b08b 100644
--- a/Android.mk
+++ b/Android.mk
@@ -401,6 +401,8 @@
media/java/android/media/tv/ITvInputServiceCallback.aidl \
media/java/android/media/tv/ITvInputSession.aidl \
media/java/android/media/tv/ITvInputSessionCallback.aidl \
+ media/java/android/media/tv/ITvRemoteProvider.aidl \
+ media/java/android/media/tv/ITvRemoteServiceInput.aidl \
media/java/android/service/media/IMediaBrowserService.aidl \
media/java/android/service/media/IMediaBrowserServiceCallbacks.aidl \
telecomm/java/com/android/internal/telecom/ICallScreeningAdapter.aidl \
diff --git a/api/system-current.txt b/api/system-current.txt
index 06d95ef..ecc978d 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -51,6 +51,7 @@
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 = "android.permission.BIND_TRUST_AGENT";
field public static final java.lang.String BIND_TV_INPUT = "android.permission.BIND_TV_INPUT";
+ field public static final java.lang.String BIND_TV_REMOTE_SERVICE = "android.permission.BIND_TV_REMOTE_SERVICE";
field public static final java.lang.String BIND_VOICE_INTERACTION = "android.permission.BIND_VOICE_INTERACTION";
field public static final java.lang.String BIND_VPN_SERVICE = "android.permission.BIND_VPN_SERVICE";
field public static final java.lang.String BIND_VR_LISTENER_SERVICE = "android.permission.BIND_VR_LISTENER_SERVICE";
@@ -222,6 +223,7 @@
field public static final java.lang.String TETHER_PRIVILEGED = "android.permission.TETHER_PRIVILEGED";
field public static final java.lang.String TRANSMIT_IR = "android.permission.TRANSMIT_IR";
field public static final java.lang.String TV_INPUT_HARDWARE = "android.permission.TV_INPUT_HARDWARE";
+ field public static final java.lang.String TV_VIRTUAL_REMOTE_CONTROLLER = "android.permission.TV_VIRTUAL_REMOTE_CONTROLLER";
field public static final java.lang.String UNINSTALL_SHORTCUT = "com.android.launcher.permission.UNINSTALL_SHORTCUT";
field public static final java.lang.String UPDATE_APP_OPS_STATS = "android.permission.UPDATE_APP_OPS_STATS";
field public static final java.lang.String UPDATE_DEVICE_STATS = "android.permission.UPDATE_DEVICE_STATS";
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index eb0c742..0ef72cd 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2279,6 +2279,23 @@
<permission android:name="android.permission.BIND_TV_INPUT"
android:protectionLevel="signature|privileged" />
+ <!-- @SystemApi
+ Must be required by a {@link com.android.media.tv.remoteprovider.TvRemoteProvider}
+ to ensure that only the system can bind to it.
+ <p>Protection level: signature|privileged
+ <p>Not for use by third-party applications. </p>
+ @hide -->
+ <permission android:name="android.permission.BIND_TV_REMOTE_SERVICE"
+ android:protectionLevel="signature|privileged" />
+
+ <!-- @SystemApi
+ Must be required for a virtual remote controller for TV.
+ <p>Protection level: signature|privileged
+ <p>Not for use by third-party applications. </p>
+ @hide -->
+ <permission android:name="android.permission.TV_VIRTUAL_REMOTE_CONTROLLER"
+ android:protectionLevel="signature|privileged" />
+
<!-- @SystemApi Allows an application to modify parental controls
<p>Not for use by third-party applications.
@hide -->
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index aada05d..789a417 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2474,4 +2474,8 @@
-->
<integer name="config_externalHardKeyboardBehavior">0</integer>
+ <!-- Package of the unbundled tv remote service which can connect to tv
+ remote provider -->
+ <string name="config_tvRemoteServicePackage" translatable="false"></string>
+
</resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index adeaa63..95f65de 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2574,4 +2574,7 @@
<java-symbol type="dimen" name="input_extract_action_button_width" />
<java-symbol type="dimen" name="input_extract_action_button_height" />
+
+ <!-- TV Remote Service package -->
+ <java-symbol type="string" name="config_tvRemoteServicePackage" />
</resources>
diff --git a/media/java/android/media/tv/ITvRemoteProvider.aidl b/media/java/android/media/tv/ITvRemoteProvider.aidl
new file mode 100644
index 0000000..3d9619b
--- /dev/null
+++ b/media/java/android/media/tv/ITvRemoteProvider.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2016 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.media.tv;
+
+import android.media.tv.ITvRemoteServiceInput;
+
+/**
+ * {@hide}
+ */
+oneway interface ITvRemoteProvider {
+ void setRemoteServiceInputSink(in ITvRemoteServiceInput tvServiceInput);
+ void onInputBridgeConnected(IBinder token);
+}
\ No newline at end of file
diff --git a/media/java/android/media/tv/ITvRemoteServiceInput.aidl b/media/java/android/media/tv/ITvRemoteServiceInput.aidl
new file mode 100644
index 0000000..df39299
--- /dev/null
+++ b/media/java/android/media/tv/ITvRemoteServiceInput.aidl
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2016 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.media.tv;
+
+/**
+ * {@hide}
+ */
+oneway interface ITvRemoteServiceInput {
+ // InputBridge related
+ void openInputBridge(IBinder token, String name, int width, int height, int maxPointers);
+ void closeInputBridge(IBinder token);
+ void clearInputBridge(IBinder token);
+ void sendTimestamp(IBinder token, long timestamp);
+ void sendKeyDown(IBinder token, int keyCode);
+ void sendKeyUp(IBinder token, int keyCode);
+ void sendPointerDown(IBinder token, int pointerId, int x, int y);
+ void sendPointerUp(IBinder token, int pointerId);
+ void sendPointerSync(IBinder token);
+}
\ No newline at end of file
diff --git a/media/lib/tvremote/Android.mk b/media/lib/tvremote/Android.mk
new file mode 100644
index 0000000..06838c2
--- /dev/null
+++ b/media/lib/tvremote/Android.mk
@@ -0,0 +1,48 @@
+#
+# Copyright (C) 2016 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.
+#
+LOCAL_PATH := $(call my-dir)
+
+# the tvremoteprovider library
+# ============================================================
+include $(CLEAR_VARS)
+
+LOCAL_MODULE:= com.android.media.tv.remoteprovider
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_SRC_FILES := \
+ $(call all-java-files-under, java) \
+ $(call all-aidl-files-under, java)
+
+LOCAL_DEX_PREOPT := false
+
+include $(BUILD_JAVA_LIBRARY)
+
+
+# ==== com.android.media.tvremote.xml lib def ========================
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := com.android.media.tv.remoteprovider.xml
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_MODULE_CLASS := ETC
+
+# This will install the file in /system/etc/permissions
+#
+LOCAL_MODULE_PATH := $(TARGET_OUT_ETC)/permissions
+
+LOCAL_SRC_FILES := $(LOCAL_MODULE)
+
+include $(BUILD_PREBUILT)
\ No newline at end of file
diff --git a/media/lib/tvremote/README.txt b/media/lib/tvremote/README.txt
new file mode 100644
index 0000000..9375f02
--- /dev/null
+++ b/media/lib/tvremote/README.txt
@@ -0,0 +1,26 @@
+This library (com.android.media.tv.remoteprovider.jar) is a shared java library
+containing classes required by unbundled atv remote providers.
+
+--- Rules of this library ---
+o This library is effectively a System API for unbundled emote service provider
+ that may be distributed outside the system image. So it MUST BE API STABLE.
+ You can add but not remove. The rules are the same as for the
+ public platform SDK API.
+o This library can see and instantiate internal platform classes, but it must not
+ expose them in any public method (or by extending them via inheritance). This would
+ break clients of the library because they cannot see the internal platform classes.
+
+This library is distributed in the system image, and loaded as
+a shared library. So you can change the implementation, but not
+the interface. In this way it is like framework.jar.
+
+--- Why does this library exist? ---
+
+Unbundled atv remote providers (such as Emote app) cannot use internal
+platform classes.
+
+This library will eventually be replaced when the inputmanager
+infrastructure is ready with APIs allowing unbundled system apps to
+inject events into uhid.
+That API isn't ready yet so this library is a compromise to
+make new capabilities available to the system.
\ No newline at end of file
diff --git a/media/lib/tvremote/com.android.media.tv.remoteprovider.xml b/media/lib/tvremote/com.android.media.tv.remoteprovider.xml
new file mode 100644
index 0000000..dcf479a
--- /dev/null
+++ b/media/lib/tvremote/com.android.media.tv.remoteprovider.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<permissions>
+ <library name="com.android.media.tv.remoteprovider"
+ file="/system/framework/com.android.media.tv.remoteprovider.jar" />
+</permissions>
\ No newline at end of file
diff --git a/media/lib/tvremote/java/com/android/media/tv/remoteprovider/TvRemoteProvider.java b/media/lib/tvremote/java/com/android/media/tv/remoteprovider/TvRemoteProvider.java
new file mode 100644
index 0000000..35322ad
--- /dev/null
+++ b/media/lib/tvremote/java/com/android/media/tv/remoteprovider/TvRemoteProvider.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2016 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.media.tv.remoteprovider;
+
+import android.content.Context;
+import android.media.tv.ITvRemoteProvider;
+import android.media.tv.ITvRemoteServiceInput;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * Base class for emote providers implemented in unbundled service.
+ * <p/>
+ * This object is not thread safe. It is only intended to be accessed on the
+ * {@link Context#getMainLooper main looper thread} of an application.
+ * </p><p>
+ * IMPORTANT: This class is effectively a system API for unbundled emote service, and
+ * must remain API stable. See README.txt in the root of this package for more information.
+ * </p>
+ */
+
+
+public abstract class TvRemoteProvider {
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ * The service must also require the {@link android.Manifest.permission#BIND_TV_REMOTE_SERVICE}
+ * permission so that other applications cannot abuse it.
+ */
+ public static final String SERVICE_INTERFACE =
+ "com.android.media.tv.remoteprovider.TvRemoteProvider";
+
+ private static final String TAG = "TvRemoteProvider";
+ private static final boolean DEBUG_KEYS = false;
+ private static final int MSG_SET_SERVICE_INPUT = 1;
+ private static final int MSG_SEND_INPUTBRIDGE_CONNECTED = 2;
+ private final Context mContext;
+ private final ProviderStub mStub;
+ private final ProviderHandler mHandler;
+ private ITvRemoteServiceInput mRemoteServiceInput;
+
+ /**
+ * Creates a provider for an unbundled emote controller
+ * service allowing it to interface with the tv remote controller
+ * system service.
+ *
+ * @param context The application context for the remote provider.
+ */
+ public TvRemoteProvider(Context context) {
+ mContext = context.getApplicationContext();
+ mStub = new ProviderStub();
+ mHandler = new ProviderHandler(mContext.getMainLooper());
+ }
+
+ /**
+ * Gets the context of the remote service provider.
+ */
+ public final Context getContext() {
+ return mContext;
+ }
+
+
+ /**
+ * Gets the Binder associated with the provider.
+ * <p>
+ * This is intended to be used for the onBind() method of a service that implements
+ * a remote provider service.
+ * </p>
+ *
+ * @return The IBinder instance associated with the provider.
+ */
+ public IBinder getBinder() {
+ return mStub;
+ }
+
+ /**
+ * Information about the InputBridge connected status.
+ *
+ * @param token Identifier for the connection. Null, if failed.
+ */
+ public void onInputBridgeConnected(IBinder token) {
+ }
+
+ /**
+ * Set a sink for sending events to framework service.
+ *
+ * @param tvServiceInput sink defined in framework service
+ */
+ private void setRemoteServiceInputSink(ITvRemoteServiceInput tvServiceInput) {
+ mRemoteServiceInput = tvServiceInput;
+ }
+
+ /**
+ * openRemoteInputBridge : Open an input bridge for a particular device.
+ * Clients should pass in a token that can be used to match this request with a token that
+ * will be returned by {@link TvRemoteProvider#onInputBridgeConnected(IBinder token)}
+ * <p>
+ * The token should be used for subsequent calls.
+ * </p>
+ *
+ * @param name Device name
+ * @param token Identifier for this connection
+ * @param width Width of the device's virtual touchpad
+ * @param height Height of the device's virtual touchpad
+ * @param maxPointers Maximum supported pointers
+ * @throws RuntimeException
+ */
+ public void openRemoteInputBridge(IBinder token, String name, int width, int height,
+ int maxPointers) throws RuntimeException {
+ try {
+ mRemoteServiceInput.openInputBridge(token, name, width, height, maxPointers);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * closeInputBridge : Close input bridge for a device
+ *
+ * @param token identifier for this connection
+ * @throws RuntimeException
+ */
+ public void closeInputBridge(IBinder token) throws RuntimeException {
+ try {
+ mRemoteServiceInput.closeInputBridge(token);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * clearInputBridge : Clear out any existing key or pointer events in queue for this device by
+ * dropping them on the floor and sending an UP to all keys and pointer
+ * slots.
+ *
+ * @param token identifier for this connection
+ * @throws RuntimeException
+ */
+ public void clearInputBridge(IBinder token) throws RuntimeException {
+ if (DEBUG_KEYS) Log.d(TAG, "clearInputBridge() token " + token);
+ try {
+ mRemoteServiceInput.clearInputBridge(token);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * sendTimestamp : Send a timestamp for a set of pointer events
+ *
+ * @param token identifier for the device
+ * @param timestamp Timestamp to be used in
+ * {@link android.os.SystemClock#uptimeMillis} time base
+ * @throws RuntimeException
+ */
+ public void sendTimestamp(IBinder token, long timestamp) throws RuntimeException {
+ if (DEBUG_KEYS) Log.d(TAG, "sendTimestamp() token: " + token +
+ ", timestamp: " + timestamp);
+ try {
+ mRemoteServiceInput.sendTimestamp(token, timestamp);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * sendKeyUp : Send key up event for a device
+ *
+ * @param token identifier for this connection
+ * @param keyCode Key code to be sent
+ * @throws RuntimeException
+ */
+ public void sendKeyUp(IBinder token, int keyCode) throws RuntimeException {
+ if (DEBUG_KEYS) Log.d(TAG, "sendKeyUp() token: " + token + ", keyCode: " + keyCode);
+ try {
+ mRemoteServiceInput.sendKeyUp(token, keyCode);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * sendKeyDown : Send key down event for a device
+ *
+ * @param token identifier for this connection
+ * @param keyCode Key code to be sent
+ * @throws RuntimeException
+ */
+ public void sendKeyDown(IBinder token, int keyCode) throws RuntimeException {
+ if (DEBUG_KEYS) Log.d(TAG, "sendKeyDown() token: " + token +
+ ", keyCode: " + keyCode);
+ try {
+ mRemoteServiceInput.sendKeyDown(token, keyCode);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * sendPointerUp : Send pointer up event for a device
+ *
+ * @param token identifier for the device
+ * @param pointerId Pointer id to be used. Value may be from 0
+ * to {@link MotionEvent#getPointerCount()} -1
+ * @throws RuntimeException
+ */
+ public void sendPointerUp(IBinder token, int pointerId) throws RuntimeException {
+ if (DEBUG_KEYS) Log.d(TAG, "sendPointerUp() token: " + token +
+ ", pointerId: " + pointerId);
+ try {
+ mRemoteServiceInput.sendPointerUp(token, pointerId);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * sendPointerDown : Send pointer down event for a device
+ *
+ * @param token identifier for the device
+ * @param pointerId Pointer id to be used. Value may be from 0
+ * to {@link MotionEvent#getPointerCount()} -1
+ * @param x X co-ordinates in display pixels
+ * @param y Y co-ordinates in display pixels
+ * @throws RuntimeException
+ */
+ public void sendPointerDown(IBinder token, int pointerId, int x, int y)
+ throws RuntimeException {
+ if (DEBUG_KEYS) Log.d(TAG, "sendPointerDown() token: " + token +
+ ", pointerId: " + pointerId);
+ try {
+ mRemoteServiceInput.sendPointerDown(token, pointerId, x, y);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * sendPointerSync : Send pointer sync event for a device
+ *
+ * @param token identifier for the device
+ * @throws RuntimeException
+ */
+ public void sendPointerSync(IBinder token) throws RuntimeException {
+ if (DEBUG_KEYS) Log.d(TAG, "sendPointerSync() token: " + token);
+ try {
+ mRemoteServiceInput.sendPointerSync(token);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ private final class ProviderStub extends ITvRemoteProvider.Stub {
+ @Override
+ public void setRemoteServiceInputSink(ITvRemoteServiceInput tvServiceInput) {
+ mHandler.obtainMessage(MSG_SET_SERVICE_INPUT, tvServiceInput).sendToTarget();
+ }
+
+ @Override
+ public void onInputBridgeConnected(IBinder token) {
+ mHandler.obtainMessage(MSG_SEND_INPUTBRIDGE_CONNECTED, 0, 0,
+ (IBinder) token).sendToTarget();
+ }
+ }
+
+ private final class ProviderHandler extends Handler {
+ public ProviderHandler(Looper looper) {
+ super(looper, null, true);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_SET_SERVICE_INPUT: {
+ setRemoteServiceInputSink((ITvRemoteServiceInput) msg.obj);
+ break;
+ }
+ case MSG_SEND_INPUTBRIDGE_CONNECTED: {
+ onInputBridgeConnected((IBinder) msg.obj);
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/tv/TvRemoteProviderProxy.java b/services/core/java/com/android/server/tv/TvRemoteProviderProxy.java
new file mode 100644
index 0000000..557e9c8
--- /dev/null
+++ b/services/core/java/com/android/server/tv/TvRemoteProviderProxy.java
@@ -0,0 +1,647 @@
+/*
+ * Copyright (C) 2016 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.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.media.tv.ITvRemoteProvider;
+import android.media.tv.ITvRemoteServiceInput;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.Log;
+import android.util.Slog;
+
+import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+
+/**
+ * Maintains a connection to a tv remote provider service.
+ */
+final class TvRemoteProviderProxy implements ServiceConnection {
+ private static final String TAG = "TvRemoteProvProxy"; // max. 23 chars
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.VERBOSE);
+ private static final boolean DEBUG_KEY = false;
+
+
+ // This should match TvRemoteProvider.ACTION_TV_REMOTE_PROVIDER
+ protected static final String SERVICE_INTERFACE =
+ "com.android.media.tv.remoteprovider.TvRemoteProvider";
+ private final Context mContext;
+ private final ComponentName mComponentName;
+ private final int mUserId;
+ private final int mUid;
+ private final Handler mHandler;
+
+ /**
+ * State guarded by mLock.
+ * This is the first lock in sequence for an incoming call.
+ * The second lock is always {@link TvRemoteService#mLock}
+ *
+ * There are currently no methods that break this sequence.
+ */
+ private final Object mLock = new Object();
+
+ private ProviderMethods mProviderMethods;
+ // Connection state
+ private boolean mRunning;
+ private boolean mBound;
+ private Connection mActiveConnection;
+ private boolean mConnectionReady;
+
+ public TvRemoteProviderProxy(Context context, ComponentName componentName, int userId,
+ int uid) {
+ mContext = context;
+ mComponentName = componentName;
+ mUserId = userId;
+ mUid = uid;
+ mHandler = new Handler();
+ }
+
+ public void dump(PrintWriter pw, String prefix) {
+ pw.println(prefix + "Proxy");
+ pw.println(prefix + " mUserId=" + mUserId);
+ pw.println(prefix + " mRunning=" + mRunning);
+ pw.println(prefix + " mBound=" + mBound);
+ pw.println(prefix + " mActiveConnection=" + mActiveConnection);
+ pw.println(prefix + " mConnectionReady=" + mConnectionReady);
+ }
+
+ public void setProviderSink(ProviderMethods provider) {
+ mProviderMethods = provider;
+ }
+
+ public boolean hasComponentName(String packageName, String className) {
+ return mComponentName.getPackageName().equals(packageName)
+ && mComponentName.getClassName().equals(className);
+ }
+
+ public void start() {
+ if (!mRunning) {
+ if (DEBUG) {
+ Slog.d(TAG, this + ": Starting");
+ }
+
+ mRunning = true;
+ updateBinding();
+ }
+ }
+
+ public void stop() {
+ if (mRunning) {
+ if (DEBUG) {
+ Slog.d(TAG, this + ": Stopping");
+ }
+
+ mRunning = false;
+ updateBinding();
+ }
+ }
+
+ public void rebindIfDisconnected() {
+ synchronized (mLock) {
+ if (mActiveConnection == null && shouldBind()) {
+ unbind();
+ bind();
+ }
+ }
+ }
+
+ private void updateBinding() {
+ if (shouldBind()) {
+ bind();
+ } else {
+ unbind();
+ }
+ }
+
+ private boolean shouldBind() {
+ return mRunning;
+ }
+
+ private void bind() {
+ if (!mBound) {
+ if (DEBUG) {
+ Slog.d(TAG, this + ": Binding");
+ }
+
+ Intent service = new Intent(SERVICE_INTERFACE);
+ service.setComponent(mComponentName);
+ try {
+ mBound = mContext.bindServiceAsUser(service, this,
+ Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
+ new UserHandle(mUserId));
+ if (!mBound && DEBUG) {
+ Slog.d(TAG, this + ": Bind failed");
+ }
+ } catch (SecurityException ex) {
+ if (DEBUG) {
+ Slog.d(TAG, this + ": Bind failed", ex);
+ }
+ }
+ }
+ }
+
+ private void unbind() {
+ if (mBound) {
+ if (DEBUG) {
+ Slog.d(TAG, this + ": Unbinding");
+ }
+
+ mBound = false;
+ disconnect();
+ mContext.unbindService(this);
+ }
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ if (DEBUG) {
+ Slog.d(TAG, this + ": onServiceConnected()");
+ }
+
+ if (mBound) {
+ disconnect();
+
+ ITvRemoteProvider provider = ITvRemoteProvider.Stub.asInterface(service);
+ if (provider != null) {
+ Connection connection = new Connection(provider);
+ if (connection.register()) {
+ synchronized (mLock) {
+ mActiveConnection = connection;
+ }
+ if (DEBUG) {
+ Slog.d(TAG, this + ": Connected successfully.");
+ }
+ } else {
+ if (DEBUG) {
+ Slog.d(TAG, this + ": Registration failed");
+ }
+ }
+ } else {
+ Slog.e(TAG, this + ": Service returned invalid remote-control provider binder");
+ }
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ if (DEBUG) Slog.d(TAG, this + ": Service disconnected");
+ disconnect();
+ }
+
+
+ private void onConnectionReady(Connection connection) {
+ synchronized (mLock) {
+ if (DEBUG) Slog.d(TAG, "onConnectionReady");
+ if (mActiveConnection == connection) {
+ if (DEBUG) Slog.d(TAG, "mConnectionReady = true");
+ mConnectionReady = true;
+ }
+ }
+ }
+
+ private void onConnectionDied(Connection connection) {
+ if (mActiveConnection == connection) {
+ if (DEBUG) Slog.d(TAG, this + ": Service connection died");
+ disconnect();
+ }
+ }
+
+ private void disconnect() {
+ synchronized (mLock) {
+ if (mActiveConnection != null) {
+ mConnectionReady = false;
+ mActiveConnection.dispose();
+ mActiveConnection = null;
+ }
+ }
+ }
+
+ // Provider helpers
+ public void inputBridgeConnected(IBinder token) {
+ synchronized (mLock) {
+ if (DEBUG) Slog.d(TAG, this + ": inputBridgeConnected token: " + token);
+ if (mConnectionReady) {
+ mActiveConnection.onInputBridgeConnected(token);
+ }
+ }
+ }
+
+ public interface ProviderMethods {
+ // InputBridge
+ void openInputBridge(TvRemoteProviderProxy provider, IBinder token, String name,
+ int width, int height, int maxPointers);
+
+ void closeInputBridge(TvRemoteProviderProxy provider, IBinder token);
+
+ void clearInputBridge(TvRemoteProviderProxy provider, IBinder token);
+
+ void sendTimeStamp(TvRemoteProviderProxy provider, IBinder token, long timestamp);
+
+ void sendKeyDown(TvRemoteProviderProxy provider, IBinder token, int keyCode);
+
+ void sendKeyUp(TvRemoteProviderProxy provider, IBinder token, int keyCode);
+
+ void sendPointerDown(TvRemoteProviderProxy provider, IBinder token, int pointerId, int x,
+ int y);
+
+ void sendPointerUp(TvRemoteProviderProxy provider, IBinder token, int pointerId);
+
+ void sendPointerSync(TvRemoteProviderProxy provider, IBinder token);
+ }
+
+ private final class Connection implements IBinder.DeathRecipient {
+ private final ITvRemoteProvider mTvRemoteProvider;
+ private final RemoteServiceInputProvider mServiceInputProvider;
+
+ public Connection(ITvRemoteProvider provider) {
+ mTvRemoteProvider = provider;
+ mServiceInputProvider = new RemoteServiceInputProvider(this);
+ }
+
+ public boolean register() {
+ if (DEBUG) Slog.d(TAG, "Connection::register()");
+ try {
+ mTvRemoteProvider.asBinder().linkToDeath(this, 0);
+ mTvRemoteProvider.setRemoteServiceInputSink(mServiceInputProvider);
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ onConnectionReady(Connection.this);
+ }
+ });
+ return true;
+ } catch (RemoteException ex) {
+ binderDied();
+ }
+ return false;
+ }
+
+ public void dispose() {
+ if (DEBUG) Slog.d(TAG, "Connection::dispose()");
+ mTvRemoteProvider.asBinder().unlinkToDeath(this, 0);
+ mServiceInputProvider.dispose();
+ }
+
+
+ public void onInputBridgeConnected(IBinder token) {
+ if (DEBUG) Slog.d(TAG, this + ": onInputBridgeConnected");
+ try {
+ mTvRemoteProvider.onInputBridgeConnected(token);
+ } catch (RemoteException ex) {
+ Slog.e(TAG, "Failed to deliver onInputBridgeConnected. ", ex);
+ }
+ }
+
+ @Override
+ public void binderDied() {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ onConnectionDied(Connection.this);
+ }
+ });
+ }
+
+ void openInputBridge(final IBinder token, final String name, final int width,
+ final int height, final int maxPointers) {
+ synchronized (mLock) {
+ if (mActiveConnection == this && Binder.getCallingUid() == mUid) {
+ if (DEBUG) {
+ Slog.d(TAG, this + ": openInputBridge," +
+ " token=" + token + ", name=" + name);
+ }
+ final long idToken = Binder.clearCallingIdentity();
+ try {
+ if (mProviderMethods != null) {
+ mProviderMethods.openInputBridge(TvRemoteProviderProxy.this, token,
+ name, width, height, maxPointers);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(idToken);
+ }
+ } else {
+ if (DEBUG) {
+ Slog.w(TAG,
+ "openInputBridge, Invalid connection or incorrect uid: " + Binder
+ .getCallingUid());
+ }
+ }
+ }
+ }
+
+ void closeInputBridge(final IBinder token) {
+ synchronized (mLock) {
+ if (mActiveConnection == this && Binder.getCallingUid() == mUid) {
+ if (DEBUG) {
+ Slog.d(TAG, this + ": closeInputBridge," +
+ " token=" + token);
+ }
+ final long idToken = Binder.clearCallingIdentity();
+ try {
+ if (mProviderMethods != null) {
+ mProviderMethods.closeInputBridge(TvRemoteProviderProxy.this, token);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(idToken);
+ }
+ } else {
+ if (DEBUG) {
+ Slog.w(TAG,
+ "closeInputBridge, Invalid connection or incorrect uid: " +
+ Binder.getCallingUid());
+ }
+ }
+ }
+ }
+
+ void clearInputBridge(final IBinder token) {
+ synchronized (mLock) {
+ if (mActiveConnection == this && Binder.getCallingUid() == mUid) {
+ if (DEBUG) {
+ Slog.d(TAG, this + ": clearInputBridge," +
+ " token=" + token);
+ }
+ final long idToken = Binder.clearCallingIdentity();
+ try {
+ if (mProviderMethods != null) {
+ mProviderMethods.clearInputBridge(TvRemoteProviderProxy.this, token);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(idToken);
+ }
+ } else {
+ if (DEBUG) {
+ Slog.w(TAG,
+ "clearInputBridge, Invalid connection or incorrect uid: " +
+ Binder.getCallingUid());
+ }
+ }
+ }
+ }
+
+ void sendTimestamp(final IBinder token, final long timestamp) {
+ synchronized (mLock) {
+ if (mActiveConnection == this && Binder.getCallingUid() == mUid) {
+ final long idToken = Binder.clearCallingIdentity();
+ try {
+ if (mProviderMethods != null) {
+ mProviderMethods.sendTimeStamp(TvRemoteProviderProxy.this, token,
+ timestamp);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(idToken);
+ }
+ } else {
+ if (DEBUG) {
+ Slog.w(TAG,
+ "sendTimeStamp, Invalid connection or incorrect uid: " + Binder
+ .getCallingUid());
+ }
+ }
+ }
+ }
+
+ void sendKeyDown(final IBinder token, final int keyCode) {
+ synchronized (mLock) {
+ if (mActiveConnection == this && Binder.getCallingUid() == mUid) {
+ if (DEBUG_KEY) {
+ Slog.d(TAG, this + ": sendKeyDown," +
+ " token=" + token + ", keyCode=" + keyCode);
+ }
+ final long idToken = Binder.clearCallingIdentity();
+ try {
+ if (mProviderMethods != null) {
+ mProviderMethods.sendKeyDown(TvRemoteProviderProxy.this, token,
+ keyCode);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(idToken);
+ }
+ } else {
+ if (DEBUG) {
+ Slog.w(TAG,
+ "sendKeyDown, Invalid connection or incorrect uid: " + Binder
+ .getCallingUid());
+ }
+ }
+ }
+ }
+
+ void sendKeyUp(final IBinder token, final int keyCode) {
+ synchronized (mLock) {
+ if (mActiveConnection == this && Binder.getCallingUid() == mUid) {
+ if (DEBUG_KEY) {
+ Slog.d(TAG, this + ": sendKeyUp," +
+ " token=" + token + ", keyCode=" + keyCode);
+ }
+ final long idToken = Binder.clearCallingIdentity();
+ try {
+ if (mProviderMethods != null) {
+ mProviderMethods.sendKeyUp(TvRemoteProviderProxy.this, token, keyCode);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(idToken);
+ }
+ } else {
+ if (DEBUG) {
+ Slog.w(TAG,
+ "sendKeyUp, Invalid connection or incorrect uid: " + Binder
+ .getCallingUid());
+ }
+ }
+ }
+ }
+
+ void sendPointerDown(final IBinder token, final int pointerId, final int x, final int y) {
+ synchronized (mLock) {
+ if (mActiveConnection == this && Binder.getCallingUid() == mUid) {
+ if (DEBUG_KEY) {
+ Slog.d(TAG, this + ": sendPointerDown," +
+ " token=" + token + ", pointerId=" + pointerId);
+ }
+ final long idToken = Binder.clearCallingIdentity();
+ try {
+ if (mProviderMethods != null) {
+ mProviderMethods.sendPointerDown(TvRemoteProviderProxy.this, token,
+ pointerId, x, y);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(idToken);
+ }
+ } else {
+ if (DEBUG) {
+ Slog.w(TAG,
+ "sendPointerDown, Invalid connection or incorrect uid: " + Binder
+ .getCallingUid());
+ }
+ }
+ }
+ }
+
+ void sendPointerUp(final IBinder token, final int pointerId) {
+ synchronized (mLock) {
+ if (mActiveConnection == this && Binder.getCallingUid() == mUid) {
+ if (DEBUG_KEY) {
+ Slog.d(TAG, this + ": sendPointerUp," +
+ " token=" + token + ", pointerId=" + pointerId);
+ }
+ final long idToken = Binder.clearCallingIdentity();
+ try {
+ if (mProviderMethods != null) {
+ mProviderMethods.sendPointerUp(TvRemoteProviderProxy.this, token,
+ pointerId);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(idToken);
+ }
+ } else {
+ if (DEBUG) {
+ Slog.w(TAG,
+ "sendPointerUp, Invalid connection or incorrect uid: " + Binder
+ .getCallingUid());
+ }
+ }
+ }
+ }
+
+ void sendPointerSync(final IBinder token) {
+ synchronized (mLock) {
+ if (mActiveConnection == this && Binder.getCallingUid() == mUid) {
+ if (DEBUG_KEY) {
+ Slog.d(TAG, this + ": sendPointerSync," +
+ " token=" + token);
+ }
+ final long idToken = Binder.clearCallingIdentity();
+ try {
+ if (mProviderMethods != null) {
+ mProviderMethods.sendPointerSync(TvRemoteProviderProxy.this, token);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(idToken);
+ }
+ } else {
+ if (DEBUG) {
+ Slog.w(TAG,
+ "sendPointerSync, Invalid connection or incorrect uid: " + Binder
+ .getCallingUid());
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Receives events from the connected provider.
+ * <p>
+ * This inner class is static and only retains a weak reference to the connection
+ * to prevent the client from being leaked in case the service is holding an
+ * active reference to the client's callback.
+ * </p>
+ */
+ private static final class RemoteServiceInputProvider extends ITvRemoteServiceInput.Stub {
+ private final WeakReference<Connection> mConnectionRef;
+
+ public RemoteServiceInputProvider(Connection connection) {
+ mConnectionRef = new WeakReference<Connection>(connection);
+ }
+
+ public void dispose() {
+ // Terminate the connection.
+ mConnectionRef.clear();
+ }
+
+ @Override
+ public void openInputBridge(IBinder token, String name, int width,
+ int height, int maxPointers) throws RemoteException {
+ Connection connection = mConnectionRef.get();
+ if (connection != null) {
+ connection.openInputBridge(token, name, width, height, maxPointers);
+ }
+ }
+
+ @Override
+ public void closeInputBridge(IBinder token) throws RemoteException {
+ Connection connection = mConnectionRef.get();
+ if (connection != null) {
+ connection.closeInputBridge(token);
+ }
+ }
+
+ @Override
+ public void clearInputBridge(IBinder token) throws RemoteException {
+ Connection connection = mConnectionRef.get();
+ if (connection != null) {
+ connection.clearInputBridge(token);
+ }
+ }
+
+ @Override
+ public void sendTimestamp(IBinder token, long timestamp) throws RemoteException {
+ Connection connection = mConnectionRef.get();
+ if (connection != null) {
+ connection.sendTimestamp(token, timestamp);
+ }
+ }
+
+ @Override
+ public void sendKeyDown(IBinder token, int keyCode) throws RemoteException {
+ Connection connection = mConnectionRef.get();
+ if (connection != null) {
+ connection.sendKeyDown(token, keyCode);
+ }
+ }
+
+ @Override
+ public void sendKeyUp(IBinder token, int keyCode) throws RemoteException {
+ Connection connection = mConnectionRef.get();
+ if (connection != null) {
+ connection.sendKeyUp(token, keyCode);
+ }
+ }
+
+ @Override
+ public void sendPointerDown(IBinder token, int pointerId, int x, int y)
+ throws RemoteException {
+ Connection connection = mConnectionRef.get();
+ if (connection != null) {
+ connection.sendPointerDown(token, pointerId, x, y);
+ }
+ }
+
+ @Override
+ public void sendPointerUp(IBinder token, int pointerId) throws RemoteException {
+ Connection connection = mConnectionRef.get();
+ if (connection != null) {
+ connection.sendPointerUp(token, pointerId);
+ }
+ }
+
+ @Override
+ public void sendPointerSync(IBinder token) throws RemoteException {
+ Connection connection = mConnectionRef.get();
+ if (connection != null) {
+ connection.sendPointerSync(token);
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/tv/TvRemoteProviderWatcher.java b/services/core/java/com/android/server/tv/TvRemoteProviderWatcher.java
new file mode 100644
index 0000000..d27970f
--- /dev/null
+++ b/services/core/java/com/android/server/tv/TvRemoteProviderWatcher.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2016 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.Manifest;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.util.Log;
+import android.util.Slog;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ * Watches for emote provider services to be installed.
+ * Adds a provider for each registered service.
+ *
+ * @see TvRemoteProviderProxy
+ */
+final class TvRemoteProviderWatcher {
+
+ private static final String TAG = "TvRemoteProvWatcher"; // max. 23 chars
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.VERBOSE);
+
+ private final Context mContext;
+ private final ProviderMethods mProvider;
+ private final Handler mHandler;
+ private final PackageManager mPackageManager;
+ private final ArrayList<TvRemoteProviderProxy> mProviderProxies = new ArrayList<>();
+ private final int mUserId;
+ private final String mUnbundledServicePackage;
+
+ private boolean mRunning;
+
+ public TvRemoteProviderWatcher(Context context, ProviderMethods provider, Handler handler) {
+ mContext = context;
+ mProvider = provider;
+ mHandler = handler;
+ mUserId = UserHandle.myUserId();
+ mPackageManager = context.getPackageManager();
+ mUnbundledServicePackage = context.getString(
+ com.android.internal.R.string.config_tvRemoteServicePackage);
+ }
+
+ public void start() {
+ if (DEBUG) Slog.d(TAG, "start()");
+ if (!mRunning) {
+ mRunning = true;
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_PACKAGE_ADDED);
+ filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+ filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
+ filter.addAction(Intent.ACTION_PACKAGE_RESTARTED);
+ filter.addDataScheme("package");
+ mContext.registerReceiverAsUser(mScanPackagesReceiver,
+ new UserHandle(mUserId), filter, null, mHandler);
+
+ // Scan packages.
+ // Also has the side-effect of restarting providers if needed.
+ mHandler.post(mScanPackagesRunnable);
+ }
+ }
+
+ public void stop() {
+ if (mRunning) {
+ mRunning = false;
+
+ mContext.unregisterReceiver(mScanPackagesReceiver);
+ mHandler.removeCallbacks(mScanPackagesRunnable);
+
+ // Stop all providers.
+ for (int i = mProviderProxies.size() - 1; i >= 0; i--) {
+ mProviderProxies.get(i).stop();
+ }
+ }
+ }
+
+ private void scanPackages() {
+ if (!mRunning) {
+ return;
+ }
+
+ if (DEBUG) Log.d(TAG, "scanPackages()");
+ // Add providers for all new services.
+ // Reorder the list so that providers left at the end will be the ones to remove.
+ int targetIndex = 0;
+ Intent intent = new Intent(TvRemoteProviderProxy.SERVICE_INTERFACE);
+ for (ResolveInfo resolveInfo : mPackageManager.queryIntentServicesAsUser(
+ intent, 0, mUserId)) {
+ ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+ if (serviceInfo != null && verifyServiceTrusted(serviceInfo)) {
+ int sourceIndex = findProvider(serviceInfo.packageName, serviceInfo.name);
+ if (sourceIndex < 0) {
+ TvRemoteProviderProxy providerProxy =
+ new TvRemoteProviderProxy(mContext,
+ new ComponentName(serviceInfo.packageName, serviceInfo.name),
+ mUserId, serviceInfo.applicationInfo.uid);
+ providerProxy.start();
+ mProviderProxies.add(targetIndex++, providerProxy);
+ mProvider.addProvider(providerProxy);
+ } else if (sourceIndex >= targetIndex) {
+ TvRemoteProviderProxy provider = mProviderProxies.get(sourceIndex);
+ provider.start(); // restart the provider if needed
+ provider.rebindIfDisconnected();
+ Collections.swap(mProviderProxies, sourceIndex, targetIndex++);
+ }
+ }
+ }
+ if (DEBUG) Log.d(TAG, "scanPackages() targetIndex " + targetIndex);
+ // Remove providers for missing services.
+ if (targetIndex < mProviderProxies.size()) {
+ for (int i = mProviderProxies.size() - 1; i >= targetIndex; i--) {
+ TvRemoteProviderProxy providerProxy = mProviderProxies.get(i);
+ mProvider.removeProvider(providerProxy);
+ mProviderProxies.remove(providerProxy);
+ providerProxy.stop();
+ }
+ }
+ }
+
+ private boolean verifyServiceTrusted(ServiceInfo serviceInfo) {
+ if (serviceInfo.permission == null || !serviceInfo.permission.equals(
+ Manifest.permission.BIND_TV_REMOTE_SERVICE)) {
+ // If the service does not require this permission then any app could
+ // potentially bind to it and cause the atv remote provider service to
+ // misbehave. So we only want to trust providers that require the
+ // correct permissions.
+ Slog.w(TAG, "Ignoring atv remote provider service because it did not "
+ + "require the BIND_TV_REMOTE_SERVICE permission in its manifest: "
+ + serviceInfo.packageName + "/" + serviceInfo.name);
+ return false;
+ }
+
+ // Check if package name is white-listed here.
+ if (!serviceInfo.packageName.equals(mUnbundledServicePackage)) {
+ Slog.w(TAG, "Ignoring atv remote provider service because the package has not "
+ + "been set and/or whitelisted: "
+ + serviceInfo.packageName + "/" + serviceInfo.name);
+ return false;
+ }
+
+ if (!hasNecessaryPermissions(serviceInfo.packageName)) {
+ // If the service does not have permission to be
+ // a virtual tv remote controller, do not trust it.
+ Slog.w(TAG, "Ignoring atv remote provider service because its package does not "
+ + "have TV_VIRTUAL_REMOTE_CONTROLLER permission: " + serviceInfo.packageName);
+ return false;
+ }
+
+ // Looks good.
+ return true;
+ }
+
+ // Returns true only if these permissions are present in calling package.
+ // Manifest.permission.TV_VIRTUAL_REMOTE_CONTROLLER : virtual remote controller on TV
+ private boolean hasNecessaryPermissions(String packageName) {
+ if ((mPackageManager.checkPermission(Manifest.permission.TV_VIRTUAL_REMOTE_CONTROLLER,
+ packageName) == PackageManager.PERMISSION_GRANTED)) {
+ return true;
+ }
+ return false;
+ }
+
+ private int findProvider(String packageName, String className) {
+ int count = mProviderProxies.size();
+ for (int i = 0; i < count; i++) {
+ TvRemoteProviderProxy provider = mProviderProxies.get(i);
+ if (provider.hasComponentName(packageName, className)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private final BroadcastReceiver mScanPackagesReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (DEBUG) {
+ Slog.d(TAG, "Received package manager broadcast: " + intent);
+ }
+ mHandler.post(mScanPackagesRunnable);
+ }
+ };
+
+ private final Runnable mScanPackagesRunnable = new Runnable() {
+ @Override
+ public void run() {
+ scanPackages();
+ }
+ };
+
+ public interface ProviderMethods {
+ void addProvider(TvRemoteProviderProxy providerProxy);
+
+ void removeProvider(TvRemoteProviderProxy providerProxy);
+ }
+}
diff --git a/services/core/java/com/android/server/tv/TvRemoteService.java b/services/core/java/com/android/server/tv/TvRemoteService.java
new file mode 100644
index 0000000..961c992
--- /dev/null
+++ b/services/core/java/com/android/server/tv/TvRemoteService.java
@@ -0,0 +1,388 @@
+/*
+ * Copyright (C) 2016 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.content.Context;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.util.ArrayMap;
+import android.util.Slog;
+
+import com.android.server.SystemService;
+import com.android.server.Watchdog;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Map;
+
+/**
+ * TvRemoteService represents a system service that allows a connected
+ * remote control (emote) service to inject white-listed input events
+ * and call other specified methods for functioning as an emote service.
+ * <p/>
+ * This service is intended for use only by white-listed packages.
+ */
+public class TvRemoteService extends SystemService implements Watchdog.Monitor {
+ private static final String TAG = "TvRemoteService";
+ private static final boolean DEBUG = false;
+ private static final boolean DEBUG_KEYS = false;
+
+ private Map<IBinder, UinputBridge> mBridgeMap = new ArrayMap();
+ private Map<IBinder, TvRemoteProviderProxy> mProviderMap = new ArrayMap();
+ private ArrayList<TvRemoteProviderProxy> mProviderList = new ArrayList<>();
+
+ /**
+ * State guarded by mLock.
+ * This is the second lock in sequence for an incoming call.
+ * The first lock is always {@link TvRemoteProviderProxy#mLock}
+ *
+ * There are currently no methods that break this sequence.
+ * Special note:
+ * Outgoing call informInputBridgeConnected(), which is called from
+ * openInputBridgeInternalLocked() uses a handler thereby relinquishing held locks.
+ */
+ private final Object mLock = new Object();
+
+ public final UserHandler mHandler;
+
+ public TvRemoteService(Context context) {
+ super(context);
+ mHandler = new UserHandler(new UserProvider(TvRemoteService.this), context);
+ Watchdog.getInstance().addMonitor(this);
+ }
+
+ @Override
+ public void onStart() {
+ if (DEBUG) Slog.d(TAG, "onStart()");
+ }
+
+ @Override
+ public void monitor() {
+ synchronized (mLock) { /* check for deadlock */ }
+ }
+
+ @Override
+ public void onBootPhase(int phase) {
+ if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) {
+ if (DEBUG) Slog.d(TAG, "PHASE_THIRD_PARTY_APPS_CAN_START");
+ mHandler.sendEmptyMessage(UserHandler.MSG_START);
+ }
+ }
+
+ //Outgoing calls.
+ private void informInputBridgeConnected(IBinder token) {
+ mHandler.obtainMessage(UserHandler.MSG_INPUT_BRIDGE_CONNECTED, 0, 0, token).sendToTarget();
+ }
+
+ // Incoming calls.
+ private void openInputBridgeInternalLocked(TvRemoteProviderProxy provider, IBinder token,
+ String name, int width, int height,
+ int maxPointers) {
+ if (DEBUG) {
+ Slog.d(TAG, "openInputBridgeInternalLocked(), token: " + token + ", name: " + name +
+ ", width: " + width + ", height: " + height + ", maxPointers: " + maxPointers);
+ }
+
+ try {
+ //Create a new bridge, if one does not exist already
+ if (mBridgeMap.containsKey(token)) {
+ if (DEBUG) Slog.d(TAG, "RemoteBridge already exists");
+ // Respond back with success.
+ informInputBridgeConnected(token);
+ return;
+ }
+
+ UinputBridge inputBridge = new UinputBridge(token, name, width, height, maxPointers);
+
+ mBridgeMap.put(token, inputBridge);
+ mProviderMap.put(token, provider);
+
+ // Respond back with success.
+ informInputBridgeConnected(token);
+
+ } catch (IOException ioe) {
+ Slog.e(TAG, "Cannot create device for " + name);
+ }
+ }
+
+ private void closeInputBridgeInternalLocked(IBinder token) {
+ if (DEBUG) {
+ Slog.d(TAG, "closeInputBridgeInternalLocked(), token: " + token);
+ }
+
+ // Close an existing RemoteBridge
+ UinputBridge inputBridge = mBridgeMap.get(token);
+ if (inputBridge != null) {
+ inputBridge.close(token);
+ }
+
+ mBridgeMap.remove(token);
+ }
+
+
+ private void clearInputBridgeInternalLocked(IBinder token) {
+ if (DEBUG) {
+ Slog.d(TAG, "clearInputBridgeInternalLocked(), token: " + token);
+ }
+
+ UinputBridge inputBridge = mBridgeMap.get(token);
+ if (inputBridge != null) {
+ inputBridge.clear(token);
+ }
+ }
+
+ private void sendTimeStampInternalLocked(IBinder token, long timestamp) {
+ UinputBridge inputBridge = mBridgeMap.get(token);
+ if (inputBridge != null) {
+ inputBridge.sendTimestamp(token, timestamp);
+ }
+ }
+
+ private void sendKeyDownInternalLocked(IBinder token, int keyCode) {
+ if (DEBUG_KEYS) {
+ Slog.d(TAG, "sendKeyDownInternalLocked(), token: " + token + ", keyCode: " + keyCode);
+ }
+
+ UinputBridge inputBridge = mBridgeMap.get(token);
+ if (inputBridge != null) {
+ inputBridge.sendKeyDown(token, keyCode);
+ }
+ }
+
+ private void sendKeyUpInternalLocked(IBinder token, int keyCode) {
+ if (DEBUG_KEYS) {
+ Slog.d(TAG, "sendKeyUpInternalLocked(), token: " + token + ", keyCode: " + keyCode);
+ }
+
+ UinputBridge inputBridge = mBridgeMap.get(token);
+ if (inputBridge != null) {
+ inputBridge.sendKeyUp(token, keyCode);
+ }
+ }
+
+ private void sendPointerDownInternalLocked(IBinder token, int pointerId, int x, int y) {
+ if (DEBUG_KEYS) {
+ Slog.d(TAG, "sendPointerDownInternalLocked(), token: " + token + ", pointerId: " +
+ pointerId + ", x: " + x + ", y: " + y);
+ }
+
+ UinputBridge inputBridge = mBridgeMap.get(token);
+ if (inputBridge != null) {
+ inputBridge.sendPointerDown(token, pointerId, x, y);
+ }
+ }
+
+ private void sendPointerUpInternalLocked(IBinder token, int pointerId) {
+ if (DEBUG_KEYS) {
+ Slog.d(TAG, "sendPointerUpInternalLocked(), token: " + token + ", pointerId: " +
+ pointerId);
+ }
+
+ UinputBridge inputBridge = mBridgeMap.get(token);
+ if (inputBridge != null) {
+ inputBridge.sendPointerUp(token, pointerId);
+ }
+ }
+
+ private void sendPointerSyncInternalLocked(IBinder token) {
+ if (DEBUG_KEYS) {
+ Slog.d(TAG, "sendPointerSyncInternalLocked(), token: " + token);
+ }
+
+ UinputBridge inputBridge = mBridgeMap.get(token);
+ if (inputBridge != null) {
+ inputBridge.sendPointerSync(token);
+ }
+ }
+
+ private final class UserHandler extends Handler {
+
+ public static final int MSG_START = 1;
+ public static final int MSG_INPUT_BRIDGE_CONNECTED = 2;
+
+ private final TvRemoteProviderWatcher mWatcher;
+ private boolean mRunning;
+
+ public UserHandler(UserProvider provider, Context context) {
+ super(Looper.getMainLooper(), null, true);
+ mWatcher = new TvRemoteProviderWatcher(context, provider, this);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_START: {
+ start();
+ break;
+ }
+ case MSG_INPUT_BRIDGE_CONNECTED: {
+ IBinder token = (IBinder) msg.obj;
+ TvRemoteProviderProxy provider = mProviderMap.get(token);
+ if (provider != null) {
+ provider.inputBridgeConnected(token);
+ }
+ break;
+ }
+ }
+ }
+
+ private void start() {
+ if (!mRunning) {
+ mRunning = true;
+ mWatcher.start(); // also starts all providers
+ }
+ }
+ }
+
+ private final class UserProvider implements TvRemoteProviderWatcher.ProviderMethods,
+ TvRemoteProviderProxy.ProviderMethods {
+
+ private final TvRemoteService mService;
+
+ public UserProvider(TvRemoteService service) {
+ mService = service;
+ }
+
+ @Override
+ public void openInputBridge(TvRemoteProviderProxy provider, IBinder token, String name,
+ int width, int height, int maxPointers) {
+ if (DEBUG) {
+ Slog.d(TAG, "openInputBridge(), token: " + token +
+ ", name: " + name + ", width: " + width +
+ ", height: " + height + ", maxPointers: " + maxPointers);
+ }
+
+ synchronized (mLock) {
+ if (mProviderList.contains(provider)) {
+ mService.openInputBridgeInternalLocked(provider, token, name, width, height,
+ maxPointers);
+ }
+ }
+ }
+
+ @Override
+ public void closeInputBridge(TvRemoteProviderProxy provider, IBinder token) {
+ if (DEBUG) Slog.d(TAG, "closeInputBridge(), token: " + token);
+ synchronized (mLock) {
+ if (mProviderList.contains(provider)) {
+ mService.closeInputBridgeInternalLocked(token);
+ mProviderMap.remove(token);
+ }
+ }
+ }
+
+ @Override
+ public void clearInputBridge(TvRemoteProviderProxy provider, IBinder token) {
+ if (DEBUG) Slog.d(TAG, "clearInputBridge(), token: " + token);
+ synchronized (mLock) {
+ if (mProviderList.contains(provider)) {
+ mService.clearInputBridgeInternalLocked(token);
+ }
+ }
+ }
+
+ @Override
+ public void sendTimeStamp(TvRemoteProviderProxy provider, IBinder token, long timestamp) {
+ synchronized (mLock) {
+ if (mProviderList.contains(provider)) {
+ mService.sendTimeStampInternalLocked(token, timestamp);
+ }
+ }
+ }
+
+ @Override
+ public void sendKeyDown(TvRemoteProviderProxy provider, IBinder token, int keyCode) {
+ if (DEBUG_KEYS) {
+ Slog.d(TAG, "sendKeyDown(), token: " + token + ", keyCode: " + keyCode);
+ }
+ synchronized (mLock) {
+ if (mProviderList.contains(provider)) {
+ mService.sendKeyDownInternalLocked(token, keyCode);
+ }
+ }
+ }
+
+ @Override
+ public void sendKeyUp(TvRemoteProviderProxy provider, IBinder token, int keyCode) {
+ if (DEBUG_KEYS) {
+ Slog.d(TAG, "sendKeyUp(), token: " + token + ", keyCode: " + keyCode);
+ }
+ synchronized (mLock) {
+ if (mProviderList.contains(provider)) {
+ mService.sendKeyUpInternalLocked(token, keyCode);
+ }
+ }
+ }
+
+ @Override
+ public void sendPointerDown(TvRemoteProviderProxy provider, IBinder token, int pointerId,
+ int x, int y) {
+ if (DEBUG_KEYS) {
+ Slog.d(TAG, "sendPointerDown(), token: " + token + ", pointerId: " + pointerId);
+ }
+ synchronized (mLock) {
+ if (mProviderList.contains(provider)) {
+ mService.sendPointerDownInternalLocked(token, pointerId, x, y);
+ }
+ }
+ }
+
+ @Override
+ public void sendPointerUp(TvRemoteProviderProxy provider, IBinder token, int pointerId) {
+ if (DEBUG_KEYS) {
+ Slog.d(TAG, "sendPointerUp(), token: " + token + ", pointerId: " + pointerId);
+ }
+ synchronized (mLock) {
+ if (mProviderList.contains(provider)) {
+ mService.sendPointerUpInternalLocked(token, pointerId);
+ }
+ }
+ }
+
+ @Override
+ public void sendPointerSync(TvRemoteProviderProxy provider, IBinder token) {
+ if (DEBUG_KEYS) Slog.d(TAG, "sendPointerSync(), token: " + token);
+ synchronized (mLock) {
+ if (mProviderList.contains(provider)) {
+ mService.sendPointerSyncInternalLocked(token);
+ }
+ }
+ }
+
+ @Override
+ public void addProvider(TvRemoteProviderProxy provider) {
+ if (DEBUG) Slog.d(TAG, "addProvider " + provider);
+ synchronized (mLock) {
+ provider.setProviderSink(this);
+ mProviderList.add(provider);
+ Slog.d(TAG, "provider: " + provider.toString());
+ }
+ }
+
+ @Override
+ public void removeProvider(TvRemoteProviderProxy provider) {
+ if (DEBUG) Slog.d(TAG, "removeProvider " + provider);
+ synchronized (mLock) {
+ if (mProviderList.remove(provider) == false) {
+ Slog.e(TAG, "Unknown provider " + provider);
+ }
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/tv/UinputBridge.java b/services/core/java/com/android/server/tv/UinputBridge.java
new file mode 100644
index 0000000..f910332
--- /dev/null
+++ b/services/core/java/com/android/server/tv/UinputBridge.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2016 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.os.Binder;
+import android.os.IBinder;
+
+import java.io.IOException;
+
+import dalvik.system.CloseGuard;
+
+/**
+ * Sends the input event to the linux driver.
+ */
+public final class UinputBridge {
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+ private long mPtr;
+ private IBinder mToken = null;
+
+ private static native long nativeOpen(String name, String uniqueId, int width, int height,
+ int maxPointers);
+ private static native void nativeClose(long ptr);
+ private static native void nativeClear(long ptr);
+ private static native void nativeSendTimestamp(long ptr, long timestamp);
+ private static native void nativeSendKey(long ptr, int keyCode, boolean down);
+ private static native void nativeSendPointerDown(long ptr, int pointerId, int x, int y);
+ private static native void nativeSendPointerUp(long ptr, int pointerId);
+ private static native void nativeSendPointerSync(long ptr);
+
+ public UinputBridge(IBinder token, String name, int width, int height, int maxPointers)
+ throws IOException {
+ if (width < 1 || height < 1) {
+ throw new IllegalArgumentException("Touchpad must be at least 1x1.");
+ }
+ if (maxPointers < 1 || maxPointers > 32) {
+ throw new IllegalArgumentException("Touchpad must support between 1 and 32 pointers.");
+ }
+ if (token == null) {
+ throw new IllegalArgumentException("Token cannot be null");
+ }
+ mPtr = nativeOpen(name, token.toString(), width, height, maxPointers);
+ if (mPtr == 0) {
+ throw new IOException("Could not open uinput device " + name);
+ }
+ mToken = token;
+ mCloseGuard.open("close");
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mPtr != 0) {
+ mCloseGuard.warnIfOpen();
+ }
+ close(mToken);
+ } finally {
+ mToken = null;
+ super.finalize();
+ }
+ }
+
+ public void close(IBinder token) {
+ if (isTokenValid(token)) {
+ if (mPtr != 0) {
+ clear(token);
+ nativeClose(mPtr);
+
+ mPtr = 0;
+ mCloseGuard.close();
+ }
+ }
+ }
+
+ public IBinder getToken() {
+ return mToken;
+ }
+
+ protected boolean isTokenValid(IBinder token) {
+ return mToken.equals(token);
+ }
+
+ public void sendTimestamp(IBinder token, long timestamp) {
+ if (isTokenValid(token)) {
+ nativeSendTimestamp(mPtr, timestamp);
+ }
+ }
+
+ public void sendKeyDown(IBinder token, int keyCode) {
+ if (isTokenValid(token)) {
+ nativeSendKey(mPtr, keyCode, true /*down*/);
+ }
+ }
+
+ public void sendKeyUp(IBinder token, int keyCode) {
+ if (isTokenValid(token)) {
+ nativeSendKey(mPtr, keyCode, false /*down*/);
+ }
+ }
+
+ public void sendPointerDown(IBinder token, int pointerId, int x, int y) {
+ if (isTokenValid(token)) {
+ nativeSendPointerDown(mPtr, pointerId, x, y);
+ }
+ }
+
+ public void sendPointerUp(IBinder token, int pointerId) {
+ if (isTokenValid(token)) {
+ nativeSendPointerUp(mPtr, pointerId);
+ }
+ }
+
+ public void sendPointerSync(IBinder token) {
+ if (isTokenValid(token)) {
+ nativeSendPointerSync(mPtr);
+ }
+
+ }
+
+ public void clear(IBinder token) {
+ if (isTokenValid(token)) {
+ nativeClear(mPtr);
+ }
+ }
+
+}
diff --git a/services/core/jni/Android.mk b/services/core/jni/Android.mk
index 5e5c6d9..a502c3a 100644
--- a/services/core/jni/Android.mk
+++ b/services/core/jni/Android.mk
@@ -28,6 +28,7 @@
$(LOCAL_REL_DIR)/com_android_server_power_PowerManagerService.cpp \
$(LOCAL_REL_DIR)/com_android_server_SerialService.cpp \
$(LOCAL_REL_DIR)/com_android_server_SystemServer.cpp \
+ $(LOCAL_REL_DIR)/com_android_server_tv_TvUinputBridge.cpp \
$(LOCAL_REL_DIR)/com_android_server_tv_TvInputHal.cpp \
$(LOCAL_REL_DIR)/com_android_server_vr_VrManagerService.cpp \
$(LOCAL_REL_DIR)/com_android_server_UsbDeviceManager.cpp \
diff --git a/services/core/jni/com_android_server_tv_TvKeys.h b/services/core/jni/com_android_server_tv_TvKeys.h
new file mode 100644
index 0000000..4895f34
--- /dev/null
+++ b/services/core/jni/com_android_server_tv_TvKeys.h
@@ -0,0 +1,113 @@
+#ifndef ANDROIDTVREMOTE_SERVICE_JNI_KEYS_H_
+#define ANDROIDTVREMOTE_SERVICE_JNI_KEYS_H_
+
+#include <android/keycodes.h>
+#include <linux/input.h>
+
+namespace android {
+
+// Map the keys specified in virtual-remote.kl.
+// Only specify the keys actually used in the layout here.
+struct Key {
+ int linuxKeyCode;
+ int32_t androidKeyCode;
+};
+
+// List of all of the keycodes that the emote is capable of sending.
+static Key KEYS[] = {
+ // Volume Control
+ { KEY_VOLUMEDOWN, AKEYCODE_VOLUME_DOWN },
+ { KEY_VOLUMEUP, AKEYCODE_VOLUME_UP },
+ { KEY_MUTE, AKEYCODE_VOLUME_MUTE },
+ { KEY_MUTE, AKEYCODE_MUTE },
+
+ { KEY_POWER, AKEYCODE_POWER },
+ { KEY_HOMEPAGE, AKEYCODE_HOME },
+ { KEY_BACK, AKEYCODE_BACK },
+
+ // Media Control
+ { KEY_PLAYPAUSE, AKEYCODE_MEDIA_PLAY_PAUSE },
+ { KEY_PLAY, AKEYCODE_MEDIA_PLAY },
+ { KEY_PAUSECD, AKEYCODE_MEDIA_PAUSE },
+ { KEY_NEXTSONG, AKEYCODE_MEDIA_NEXT },
+ { KEY_PREVIOUSSONG, AKEYCODE_MEDIA_PREVIOUS },
+ { KEY_STOPCD, AKEYCODE_MEDIA_STOP },
+ { KEY_RECORD, AKEYCODE_MEDIA_RECORD },
+ { KEY_REWIND, AKEYCODE_MEDIA_REWIND },
+ { KEY_FASTFORWARD, AKEYCODE_MEDIA_FAST_FORWARD },
+
+ // TV Control
+ { KEY_0, AKEYCODE_0 },
+ { KEY_1, AKEYCODE_1 },
+ { KEY_2, AKEYCODE_2 },
+ { KEY_3, AKEYCODE_3 },
+ { KEY_4, AKEYCODE_4 },
+ { KEY_5, AKEYCODE_5 },
+ { KEY_6, AKEYCODE_6 },
+ { KEY_7, AKEYCODE_7 },
+ { KEY_8, AKEYCODE_8 },
+ { KEY_9, AKEYCODE_9 },
+ { KEY_BACKSPACE, AKEYCODE_DEL },
+ { KEY_ENTER, AKEYCODE_ENTER},
+ { KEY_CHANNELUP, AKEYCODE_CHANNEL_UP },
+ { KEY_CHANNELDOWN, AKEYCODE_CHANNEL_DOWN },
+
+ // Old School TV Controls
+ { KEY_F1, AKEYCODE_F1 },
+ { KEY_F2, AKEYCODE_F2 },
+ { KEY_F3, AKEYCODE_F3 },
+ { KEY_F4, AKEYCODE_F4 },
+ { KEY_F5, AKEYCODE_F5 },
+ { KEY_F6, AKEYCODE_F6 },
+ { KEY_F7, AKEYCODE_F7 },
+ { KEY_F8, AKEYCODE_F8 },
+ { KEY_F9, AKEYCODE_F9 },
+ { KEY_F10, AKEYCODE_F10 },
+ { KEY_F11, AKEYCODE_F11 },
+ { KEY_F12, AKEYCODE_F12 },
+ { KEY_FN_F1, AKEYCODE_F1 },
+ { KEY_FN_F2, AKEYCODE_F2 },
+ { KEY_FN_F3, AKEYCODE_F3 },
+ { KEY_FN_F4, AKEYCODE_F4 },
+ { KEY_FN_F5, AKEYCODE_F5 },
+ { KEY_FN_F6, AKEYCODE_F6 },
+ { KEY_FN_F7, AKEYCODE_F7 },
+ { KEY_FN_F8, AKEYCODE_F8 },
+ { KEY_FN_F9, AKEYCODE_F9 },
+ { KEY_FN_F10, AKEYCODE_F10 },
+ { KEY_FN_F11, AKEYCODE_F11 },
+ { KEY_FN_F12, AKEYCODE_F12 },
+ { KEY_TV, AKEYCODE_TV },
+ { KEY_RED, AKEYCODE_PROG_RED },
+ { KEY_GREEN, AKEYCODE_PROG_GREEN },
+ { KEY_YELLOW, AKEYCODE_PROG_YELLOW },
+ { KEY_BLUE, AKEYCODE_PROG_BLUE },
+
+ { KEY_FAVORITES, AKEYCODE_BUTTON_MODE},
+ { KEY_WWW, AKEYCODE_EXPLORER },
+ { KEY_MENU, AKEYCODE_MENU },
+ { KEY_INFO, AKEYCODE_INFO },
+ { KEY_EPG, AKEYCODE_GUIDE },
+ { KEY_TEXT, AKEYCODE_TV_TELETEXT },
+ { KEY_SUBTITLE, AKEYCODE_CAPTIONS },
+ { KEY_PVR, AKEYCODE_DVR},
+ { KEY_AUDIO, AKEYCODE_MEDIA_AUDIO_TRACK},
+ { KEY_OPTION, AKEYCODE_SETTINGS},
+
+ // Gamepad buttons
+ { KEY_UP, AKEYCODE_DPAD_UP },
+ { KEY_DOWN, AKEYCODE_DPAD_DOWN },
+ { KEY_LEFT, AKEYCODE_DPAD_LEFT },
+ { KEY_RIGHT, AKEYCODE_DPAD_RIGHT },
+ { KEY_SELECT, AKEYCODE_DPAD_CENTER },
+ { BTN_A, AKEYCODE_BUTTON_A },
+ { BTN_B, AKEYCODE_BUTTON_B },
+ { BTN_X, AKEYCODE_BUTTON_X },
+ { BTN_Y, AKEYCODE_BUTTON_Y },
+
+ { KEY_SEARCH, AKEYCODE_SEARCH },
+};
+
+} // namespace android
+
+#endif // SERVICE_JNI_KEYS_H_
diff --git a/services/core/jni/com_android_server_tv_TvUinputBridge.cpp b/services/core/jni/com_android_server_tv_TvUinputBridge.cpp
new file mode 100644
index 0000000..de115c8
--- /dev/null
+++ b/services/core/jni/com_android_server_tv_TvUinputBridge.cpp
@@ -0,0 +1,308 @@
+/*
+ * Copyright 2016, 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.
+ */
+
+#define LOG_TAG "TvRemote-native-uiBridge"
+
+#include "com_android_server_tv_TvKeys.h"
+
+#include "jni.h"
+#include <android_runtime/AndroidRuntime.h>
+#include <ScopedUtfChars.h>
+#include <android/keycodes.h>
+
+#include <utils/BitSet.h>
+#include <utils/Errors.h>
+#include <utils/misc.h>
+#include <utils/Log.h>
+#include <utils/String8.h>
+
+#include <ctype.h>
+#include <linux/input.h>
+#include <unistd.h>
+#include <sys/time.h>
+#include <time.h>
+#include <stdint.h>
+#include <map>
+#include <fcntl.h>
+#include <linux/uinput.h>
+#include <signal.h>
+#include <sys/inotify.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+// Refer to EventHub.h
+#define MSC_ANDROID_TIME_SEC 0x6
+#define MSC_ANDROID_TIME_USEC 0x7
+
+#define SLOT_UNKNOWN -1
+
+namespace android {
+
+static std::map<int32_t,int> keysMap;
+static std::map<int32_t,int32_t> slotsMap;
+static BitSet32 mtSlots;
+
+static void initKeysMap() {
+ if (keysMap.empty()) {
+ for (size_t i = 0; i < NELEM(KEYS); i++) {
+ keysMap[KEYS[i].androidKeyCode] = KEYS[i].linuxKeyCode;
+ }
+ }
+}
+
+static int32_t getLinuxKeyCode(int32_t androidKeyCode) {
+ std::map<int,int>::iterator it = keysMap.find(androidKeyCode);
+ if (it != keysMap.end()) {
+ return it->second;
+ }
+ return KEY_UNKNOWN;
+}
+
+static int findSlot(int32_t pointerId) {
+ std::map<int,int>::iterator it = slotsMap.find(pointerId);
+ if (it != slotsMap.end()) {
+ return it->second;
+ }
+ return SLOT_UNKNOWN;
+}
+
+static int assignSlot(int32_t pointerId) {
+ if (!mtSlots.isFull()) {
+ uint32_t slot = mtSlots.markFirstUnmarkedBit();
+ slotsMap[pointerId] = slot;
+ return slot;
+ }
+ return SLOT_UNKNOWN;
+}
+
+static void unassignSlot(int32_t pointerId) {
+ int slot = findSlot(pointerId);
+ if (slot != SLOT_UNKNOWN) {
+ mtSlots.clearBit(slot);
+ slotsMap.erase(pointerId);
+ }
+}
+
+class NativeConnection {
+public:
+ ~NativeConnection();
+
+ static NativeConnection* open(const char* name, const char* uniqueId,
+ int32_t width, int32_t height, int32_t maxPointerId);
+
+ void sendEvent(int32_t type, int32_t code, int32_t value);
+
+ int32_t getMaxPointers() const { return mMaxPointers; }
+
+private:
+ NativeConnection(int fd, int32_t maxPointers);
+
+ const int mFd;
+ const int32_t mMaxPointers;
+};
+
+NativeConnection::NativeConnection(int fd, int32_t maxPointers) :
+ mFd(fd), mMaxPointers(maxPointers) {
+}
+
+NativeConnection::~NativeConnection() {
+ ALOGI("Un-Registering uinput device %d.", mFd);
+ ioctl(mFd, UI_DEV_DESTROY);
+ close(mFd);
+}
+
+NativeConnection* NativeConnection::open(const char* name, const char* uniqueId,
+ int32_t width, int32_t height, int32_t maxPointers) {
+ ALOGI("Registering uinput device %s: touch pad size %dx%d, "
+ "max pointers %d.", name, width, height, maxPointers);
+
+ int fd = ::open("/dev/uinput", O_WRONLY | O_NDELAY);
+ if (fd < 0) {
+ ALOGE("Cannot open /dev/uinput: %s.", strerror(errno));
+ return nullptr;
+ }
+
+ struct uinput_user_dev uinp;
+ memset(&uinp, 0, sizeof(struct uinput_user_dev));
+ strlcpy(uinp.name, name, UINPUT_MAX_NAME_SIZE);
+ uinp.id.version = 1;
+ uinp.id.bustype = BUS_VIRTUAL;
+
+ // initialize keymap
+ initKeysMap();
+
+ // write device unique id to the phys property
+ ioctl(fd, UI_SET_PHYS, uniqueId);
+
+ // set the keys mapped
+ ioctl(fd, UI_SET_EVBIT, EV_KEY);
+ for (size_t i = 0; i < NELEM(KEYS); i++) {
+ ioctl(fd, UI_SET_KEYBIT, KEYS[i].linuxKeyCode);
+ }
+
+ // set the misc events maps
+ ioctl(fd, UI_SET_EVBIT, EV_MSC);
+ ioctl(fd, UI_SET_MSCBIT, MSC_ANDROID_TIME_SEC);
+ ioctl(fd, UI_SET_MSCBIT, MSC_ANDROID_TIME_USEC);
+
+ // register the input device
+ if (write(fd, &uinp, sizeof(uinp)) != sizeof(uinp)) {
+ ALOGE("Cannot write uinput_user_dev to fd %d: %s.", fd, strerror(errno));
+ close(fd);
+ return NULL;
+ }
+ if (ioctl(fd, UI_DEV_CREATE) != 0) {
+ ALOGE("Unable to create uinput device: %s.", strerror(errno));
+ close(fd);
+ return nullptr;
+ }
+
+ ALOGV("Created uinput device, fd=%d.", fd);
+ return new NativeConnection(fd, maxPointers);
+}
+
+void NativeConnection::sendEvent(int32_t type, int32_t code, int32_t value) {
+ struct input_event iev;
+ memset(&iev, 0, sizeof(iev));
+ iev.type = type;
+ iev.code = code;
+ iev.value = value;
+ write(mFd, &iev, sizeof(iev));
+}
+
+
+static jlong nativeOpen(JNIEnv* env, jclass clazz,
+ jstring nameStr, jstring uniqueIdStr,
+ jint width, jint height, jint maxPointers) {
+ ScopedUtfChars name(env, nameStr);
+ ScopedUtfChars uniqueId(env, uniqueIdStr);
+
+ NativeConnection* connection = NativeConnection::open(name.c_str(), uniqueId.c_str(),
+ width, height, maxPointers);
+ return reinterpret_cast<jlong>(connection);
+}
+
+static void nativeClose(JNIEnv* env, jclass clazz, jlong ptr) {
+ NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr);
+ delete connection;
+}
+
+static void nativeSendTimestamp(JNIEnv* env, jclass clazz, jlong ptr, jlong timestamp) {
+ NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr);
+
+ connection->sendEvent(EV_MSC, MSC_ANDROID_TIME_SEC, timestamp / 1000L);
+ connection->sendEvent(EV_MSC, MSC_ANDROID_TIME_USEC, (timestamp % 1000L) * 1000L);
+}
+
+static void nativeSendKey(JNIEnv* env, jclass clazz, jlong ptr, jint keyCode, jboolean down) {
+ int32_t code = getLinuxKeyCode(keyCode);
+ NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr);
+ if (code != KEY_UNKNOWN) {
+ connection->sendEvent(EV_KEY, code, down ? 1 : 0);
+ } else {
+ ALOGE("Received an unknown keycode of %d.", keyCode);
+ }
+}
+
+static void nativeSendPointerDown(JNIEnv* env, jclass clazz, jlong ptr,
+ jint pointerId, jint x, jint y) {
+ NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr);
+
+ int32_t slot = findSlot(pointerId);
+ if (slot == SLOT_UNKNOWN) {
+ slot = assignSlot(pointerId);
+ }
+ if (slot != SLOT_UNKNOWN) {
+ connection->sendEvent(EV_ABS, ABS_MT_SLOT, slot);
+ connection->sendEvent(EV_ABS, ABS_MT_TRACKING_ID, pointerId);
+ connection->sendEvent(EV_ABS, ABS_MT_POSITION_X, x);
+ connection->sendEvent(EV_ABS, ABS_MT_POSITION_Y, y);
+ }
+}
+
+static void nativeSendPointerUp(JNIEnv* env, jclass clazz, jlong ptr,
+ jint pointerId) {
+ NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr);
+
+ int32_t slot = findSlot(pointerId);
+ if (slot != SLOT_UNKNOWN) {
+ connection->sendEvent(EV_ABS, ABS_MT_SLOT, slot);
+ connection->sendEvent(EV_ABS, ABS_MT_TRACKING_ID, -1);
+ unassignSlot(pointerId);
+ }
+}
+
+static void nativeSendPointerSync(JNIEnv* env, jclass clazz, jlong ptr) {
+ NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr);
+ connection->sendEvent(EV_SYN, SYN_REPORT, 0);
+}
+
+static void nativeClear(JNIEnv* env, jclass clazz, jlong ptr) {
+ NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr);
+
+ // Clear keys.
+ for (size_t i = 0; i < NELEM(KEYS); i++) {
+ connection->sendEvent(EV_KEY, KEYS[i].linuxKeyCode, 0);
+ }
+
+ // Clear pointers.
+ int32_t slot = SLOT_UNKNOWN;
+ for (int32_t i = 0; i < connection->getMaxPointers(); i++) {
+ slot = findSlot(i);
+ if (slot != SLOT_UNKNOWN) {
+ connection->sendEvent(EV_ABS, ABS_MT_SLOT, slot);
+ connection->sendEvent(EV_ABS, ABS_MT_TRACKING_ID, -1);
+ }
+ }
+
+ // Sync pointer events
+ connection->sendEvent(EV_SYN, SYN_REPORT, 0);
+}
+
+/*
+ * JNI registration
+ */
+
+static JNINativeMethod gUinputBridgeMethods[] = {
+ { "nativeOpen", "(Ljava/lang/String;Ljava/lang/String;III)J",
+ (void*)nativeOpen },
+ { "nativeClose", "(J)V",
+ (void*)nativeClose },
+ { "nativeSendTimestamp", "(JJ)V",
+ (void*)nativeSendTimestamp },
+ { "nativeSendKey", "(JIZ)V",
+ (void*)nativeSendKey },
+ { "nativeSendPointerDown", "(JIII)V",
+ (void*)nativeSendPointerDown },
+ { "nativeSendPointerUp", "(JI)V",
+ (void*)nativeSendPointerUp },
+ { "nativeClear", "(J)V",
+ (void*)nativeClear },
+ { "nativeSendPointerSync", "(J)V",
+ (void*)nativeSendPointerSync },
+};
+
+int register_android_server_tv_TvUinputBridge(JNIEnv* env) {
+ int res = jniRegisterNativeMethods(env, "com/android/server/tv/UinputBridge",
+ gUinputBridgeMethods, NELEM(gUinputBridgeMethods));
+
+ LOG_FATAL_IF(res < 0, "Unable to register native methods.");
+ (void)res; // Don't complain about unused variable in the LOG_NDEBUG case
+
+ return 0;
+}
+
+} // namespace android
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index be99673..d3341e5 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -41,6 +41,7 @@
int register_android_server_location_FlpHardwareProvider(JNIEnv* env);
int register_android_server_connectivity_Vpn(JNIEnv* env);
int register_android_server_hdmi_HdmiCecController(JNIEnv* env);
+int register_android_server_tv_TvUinputBridge(JNIEnv* env);
int register_android_server_tv_TvInputHal(JNIEnv* env);
int register_android_server_PersistentDataBlockService(JNIEnv* env);
int register_android_server_Watchdog(JNIEnv* env);
@@ -81,6 +82,7 @@
register_android_server_ConsumerIrService(env);
register_android_server_BatteryStatsService(env);
register_android_server_hdmi_HdmiCecController(env);
+ register_android_server_tv_TvUinputBridge(env);
register_android_server_tv_TvInputHal(env);
register_android_server_PersistentDataBlockService(env);
register_android_server_Watchdog(env);
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 9f2ca59..00b83841 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -90,6 +90,7 @@
import com.android.server.storage.DeviceStorageMonitorService;
import com.android.server.telecom.TelecomLoaderService;
import com.android.server.trust.TrustManagerService;
+import com.android.server.tv.TvRemoteService;
import com.android.server.tv.TvInputManagerService;
import com.android.server.twilight.TwilightService;
import com.android.server.usage.UsageStatsService;
@@ -1111,6 +1112,10 @@
mSystemServiceManager.startService(MediaResourceMonitorService.class);
}
+ if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
+ mSystemServiceManager.startService(TvRemoteService.class);
+ }
+
if (!disableNonCoreServices) {
traceBeginAndSlog("StartMediaRouterService");
try {