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 {