HID profile.

Change-Id: I52e965a6537bce02c751ba26fe7b44dd03832510
diff --git a/core/java/android/bluetooth/BluetoothInputDevice.java b/core/java/android/bluetooth/BluetoothInputDevice.java
new file mode 100644
index 0000000..1793838
--- /dev/null
+++ b/core/java/android/bluetooth/BluetoothInputDevice.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2010 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.bluetooth;
+
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.content.Context;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Public API for controlling the Bluetooth HID (Input Device) Profile
+ *
+ * BluetoothInputDevice is a proxy object used to make calls to Bluetooth Service
+ * which handles the HID profile.
+ *
+ * Creating a BluetoothInputDevice object will initiate a binding with the
+ * Bluetooth service. Users of this object should call close() when they
+ * are finished, so that this proxy object can unbind from the service.
+ *
+ * Currently the Bluetooth service runs in the system server and this
+ * proxy object will be immediately bound to the service on construction.
+ *
+ *  @hide
+ */
+public final class BluetoothInputDevice {
+    private static final String TAG = "BluetoothInputDevice";
+    private static final boolean DBG = false;
+
+    /** int extra for ACTION_INPUT_DEVICE_STATE_CHANGED */
+    public static final String EXTRA_INPUT_DEVICE_STATE =
+        "android.bluetooth.inputdevice.extra.INPUT_DEVICE_STATE";
+    /** int extra for ACTION_INPUT_DEVICE_STATE_CHANGED */
+    public static final String EXTRA_PREVIOUS_INPUT_DEVICE_STATE =
+        "android.bluetooth.inputdevice.extra.PREVIOUS_INPUT_DEVICE_STATE";
+
+    /** Indicates the state of an input device has changed.
+     * This intent will always contain EXTRA_INPUT_DEVICE_STATE,
+     * EXTRA_PREVIOUS_INPUT_DEVICE_STATE and BluetoothDevice.EXTRA_DEVICE
+     * extras.
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_INPUT_DEVICE_STATE_CHANGED =
+        "android.bluetooth.inputdevice.action.INPUT_DEVICE_STATE_CHANGED";
+
+    public static final int STATE_DISCONNECTED = 0;
+    public static final int STATE_CONNECTING   = 1;
+    public static final int STATE_CONNECTED    = 2;
+    public static final int STATE_DISCONNECTING = 3;
+
+    /**
+     * Auto connection, incoming and outgoing connection are allowed at this
+     * priority level.
+     */
+    public static final int PRIORITY_AUTO_CONNECT = 1000;
+    /**
+     * Incoming and outgoing connection are allowed at this priority level
+     */
+    public static final int PRIORITY_ON = 100;
+    /**
+     * Connections to the device are not allowed at this priority level.
+     */
+    public static final int PRIORITY_OFF = 0;
+    /**
+     * Default priority level when the device is unpaired.
+     */
+    public static final int PRIORITY_UNDEFINED = -1;
+
+    private final IBluetooth mService;
+    private final Context mContext;
+
+    /**
+     * Create a BluetoothInputDevice proxy object for interacting with the local
+     * Bluetooth Service which handle the HID profile.
+     * @param c Context
+     */
+    public BluetoothInputDevice(Context c) {
+        mContext = c;
+
+        IBinder b = ServiceManager.getService(BluetoothAdapter.BLUETOOTH_SERVICE);
+        if (b != null) {
+            mService = IBluetooth.Stub.asInterface(b);
+        } else {
+            Log.w(TAG, "Bluetooth Service not available!");
+
+            // Instead of throwing an exception which prevents people from going
+            // into Wireless settings in the emulator. Let it crash later when it is actually used.
+            mService = null;
+        }
+    }
+
+    /** Initiate a connection to an Input device.
+     *
+     *  This function returns false on error and true if the connection
+     *  attempt is being made.
+     *
+     *  Listen for INPUT_DEVICE_STATE_CHANGED_ACTION to find out when the
+     *  connection is completed.
+     *  @param device Remote BT device.
+     *  @return false on immediate error, true otherwise
+     *  @hide
+     */
+    public boolean connectInputDevice(BluetoothDevice device) {
+        if (DBG) log("connectInputDevice(" + device + ")");
+        try {
+            return mService.connectInputDevice(device);
+        } catch (RemoteException e) {
+            Log.e(TAG, "", e);
+            return false;
+        }
+    }
+
+    /** Initiate disconnect from an Input Device.
+     *  This function return false on error and true if the disconnection
+     *  attempt is being made.
+     *
+     *  Listen for INPUT_DEVICE_STATE_CHANGED_ACTION to find out when
+     *  disconnect is completed.
+     *
+     *  @param device Remote BT device.
+     *  @return false on immediate error, true otherwise
+     *  @hide
+     */
+    public boolean disconnectInputDevice(BluetoothDevice device) {
+        if (DBG) log("disconnectInputDevice(" + device + ")");
+        try {
+            return mService.disconnectInputDevice(device);
+        } catch (RemoteException e) {
+            Log.e(TAG, "", e);
+            return false;
+        }
+    }
+
+    /** Check if a specified InputDevice is connected.
+     *
+     *  @param device Remote BT device.
+     *  @return True if connected , false otherwise and on error.
+     *  @hide
+     */
+    public boolean isInputDeviceConnected(BluetoothDevice device) {
+        if (DBG) log("isInputDeviceConnected(" + device + ")");
+        int state = getInputDeviceState(device);
+        if (state == STATE_CONNECTED) return true;
+        return false;
+    }
+
+    /** Check if any Input Device is connected.
+     *
+     * @return a unmodifiable set of connected Input Devices, or null on error.
+     * @hide
+     */
+    public Set<BluetoothDevice> getConnectedInputDevices() {
+        if (DBG) log("getConnectedInputDevices()");
+        try {
+            return Collections.unmodifiableSet(
+                    new HashSet<BluetoothDevice>(
+                        Arrays.asList(mService.getConnectedInputDevices())));
+        } catch (RemoteException e) {
+            Log.e(TAG, "", e);
+            return null;
+        }
+    }
+
+    /** Get the state of an Input Device.
+     *
+     *  @param device Remote BT device.
+     *  @return The current state of the Input Device
+     *  @hide
+     */
+    public int getInputDeviceState(BluetoothDevice device) {
+        if (DBG) log("getInputDeviceState(" + device + ")");
+        try {
+            return mService.getInputDeviceState(device);
+        } catch (RemoteException e) {
+            Log.e(TAG, "", e);
+            return STATE_DISCONNECTED;
+        }
+    }
+
+    /**
+     * Set priority of an input device.
+     *
+     * Priority is a non-negative integer. Priority can take the following
+     * values:
+     * {@link PRIORITY_ON}, {@link PRIORITY_OFF}, {@link PRIORITY_AUTO_CONNECT}
+     *
+     * @param device Paired device.
+     * @param priority Integer priority
+     * @return true if priority is set, false on error
+     */
+    public boolean setInputDevicePriority(BluetoothDevice device, int priority) {
+        if (DBG) log("setInputDevicePriority(" + device + ", " + priority + ")");
+        try {
+            return mService.setInputDevicePriority(device, priority);
+        } catch (RemoteException e) {
+            Log.e(TAG, "", e);
+            return false;
+        }
+    }
+
+    /**
+     * Get the priority associated with an Input Device.
+     *
+     * @param device Input Device
+     * @return non-negative priority, or negative error code on error.
+     */
+    public int getInputDevicePriority(BluetoothDevice device) {
+        if (DBG) log("getInputDevicePriority(" + device + ")");
+        try {
+            return mService.getInputDevicePriority(device);
+        } catch (RemoteException e) {
+            Log.e(TAG, "", e);
+            return PRIORITY_OFF;
+        }
+    }
+
+    private static void log(String msg) {
+        Log.d(TAG, msg);
+    }
+}
diff --git a/core/java/android/bluetooth/BluetoothUuid.java b/core/java/android/bluetooth/BluetoothUuid.java
index 4164a3d..1909e03 100644
--- a/core/java/android/bluetooth/BluetoothUuid.java
+++ b/core/java/android/bluetooth/BluetoothUuid.java
@@ -49,6 +49,8 @@
             ParcelUuid.fromString("0000110C-0000-1000-8000-00805F9B34FB");
     public static final ParcelUuid ObexObjectPush =
             ParcelUuid.fromString("00001105-0000-1000-8000-00805f9b34fb");
+    public static final ParcelUuid Hid =
+      ParcelUuid.fromString("00000011-0000-1000-8000-00805f9b34fb");
 
     public static final ParcelUuid[] RESERVED_UUIDS = {
         AudioSink, AudioSource, AdvAudioDist, HSP, Handsfree, AvrcpController, AvrcpTarget,
@@ -82,6 +84,10 @@
         return uuid.equals(AvrcpTarget);
     }
 
+    public static boolean isInputDevice(ParcelUuid uuid) {
+        return uuid.equals(Hid);
+    }
+
     /**
      * Returns true if ParcelUuid is present in uuidArray
      *
diff --git a/core/java/android/bluetooth/IBluetooth.aidl b/core/java/android/bluetooth/IBluetooth.aidl
index ea71034..75f093c 100644
--- a/core/java/android/bluetooth/IBluetooth.aidl
+++ b/core/java/android/bluetooth/IBluetooth.aidl
@@ -17,6 +17,7 @@
 package android.bluetooth;
 
 import android.bluetooth.IBluetoothCallback;
+import android.bluetooth.BluetoothDevice;
 import android.os.ParcelUuid;
 
 /**
@@ -72,4 +73,12 @@
     boolean connectHeadset(String address);
     boolean disconnectHeadset(String address);
     boolean notifyIncomingConnection(String address);
+
+    // HID profile APIs
+    boolean connectInputDevice(in BluetoothDevice device);
+    boolean disconnectInputDevice(in BluetoothDevice device);
+    BluetoothDevice[] getConnectedInputDevices();  // change to Set<> once AIDL supports
+    int getInputDeviceState(in BluetoothDevice device);
+    boolean setInputDevicePriority(in BluetoothDevice device, int priority);
+    int getInputDevicePriority(in BluetoothDevice device);
 }
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index a66c9ed..1ab3931 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -1001,7 +1001,7 @@
         public static boolean hasInterestingConfigurationChanges(int changes) {
             return (changes&ActivityInfo.CONFIG_FONT_SCALE) != 0;
         }
-        
+
         public static boolean getShowGTalkServiceStatus(ContentResolver cr) {
             return getInt(cr, SHOW_GTALK_SERVICE_STATUS, 0) != 0;
         }
@@ -1208,7 +1208,7 @@
         public static final String LOCK_PATTERN_VISIBLE = "lock_pattern_visible_pattern";
 
         /**
-         * @deprecated Use 
+         * @deprecated Use
          * {@link android.provider.Settings.Secure#LOCK_PATTERN_TACTILE_FEEDBACK_ENABLED}
          * instead
          */
@@ -2282,6 +2282,14 @@
         }
 
         /**
+         * Get the key that retrieves a bluetooth Input Device's priority.
+         * @hide
+         */
+        public static final String getBluetoothInputDevicePriorityKey(String address) {
+            return ("bluetooth_input_device_priority_" + address.toUpperCase());
+        }
+
+        /**
          * Whether or not data roaming is enabled. (0 = false, 1 = true)
          */
         public static final String DATA_ROAMING = "data_roaming";
diff --git a/core/java/android/server/BluetoothEventLoop.java b/core/java/android/server/BluetoothEventLoop.java
index 35a582d..9b7a73d 100644
--- a/core/java/android/server/BluetoothEventLoop.java
+++ b/core/java/android/server/BluetoothEventLoop.java
@@ -20,6 +20,7 @@
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothClass;
 import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothInputDevice;
 import android.bluetooth.BluetoothUuid;
 import android.content.Context;
 import android.content.Intent;
@@ -427,6 +428,20 @@
         }
     }
 
+    private void onInputDevicePropertyChanged(String path, String[] propValues) {
+        String address = mBluetoothService.getAddressFromObjectPath(path);
+        if (address == null) {
+            Log.e(TAG, "onInputDevicePropertyChanged: Address of the remote device in null");
+            return;
+        }
+        log(" Input Device : Name of Property is:" + propValues[0]);
+        boolean state = false;
+        if (propValues[1].equals("true")) {
+            state = true;
+        }
+        mBluetoothService.handleInputDevicePropertyChange(address, state);
+    }
+
     private String checkPairingRequestAndGetAddress(String objectPath, int nativeData) {
         String address = mBluetoothService.getAddressFromObjectPath(objectPath);
         if (address == null) {
@@ -573,6 +588,8 @@
     }
 
     private boolean onAgentAuthorize(String objectPath, String deviceUuid) {
+        if (!mBluetoothService.isEnabled()) return false;
+
         String address = mBluetoothService.getAddressFromObjectPath(objectPath);
         if (address == null) {
             Log.e(TAG, "Unable to get device address in onAuthAgentAuthorize");
@@ -581,15 +598,15 @@
 
         boolean authorized = false;
         ParcelUuid uuid = ParcelUuid.fromString(deviceUuid);
-        BluetoothA2dp a2dp = new BluetoothA2dp(mContext);
+        BluetoothDevice device = mAdapter.getRemoteDevice(address);
 
         // Bluez sends the UUID of the local service being accessed, _not_ the
         // remote service
-        if (mBluetoothService.isEnabled() &&
-                (BluetoothUuid.isAudioSource(uuid) || BluetoothUuid.isAvrcpTarget(uuid)
-                        || BluetoothUuid.isAdvAudioDist(uuid)) &&
-                        !isOtherSinkInNonDisconnectingState(address)) {
-            BluetoothDevice device = mAdapter.getRemoteDevice(address);
+        if ((BluetoothUuid.isAudioSource(uuid) || BluetoothUuid.isAvrcpTarget(uuid)
+              || BluetoothUuid.isAdvAudioDist(uuid)) &&
+              !isOtherSinkInNonDisconnectingState(address)) {
+            BluetoothA2dp a2dp = new BluetoothA2dp(mContext);
+
             authorized = a2dp.getSinkPriority(device) > BluetoothA2dp.PRIORITY_OFF;
             if (authorized) {
                 Log.i(TAG, "Allowing incoming A2DP / AVRCP connection from " + address);
@@ -597,6 +614,15 @@
             } else {
                 Log.i(TAG, "Rejecting incoming A2DP / AVRCP connection from " + address);
             }
+        } else if (BluetoothUuid.isInputDevice(uuid) && !isOtherInputDeviceConnected(address)) {
+            BluetoothInputDevice inputDevice = new BluetoothInputDevice(mContext);
+            authorized = inputDevice.getInputDevicePriority(device) >
+                         BluetoothInputDevice.PRIORITY_OFF;
+             if (authorized) {
+                 Log.i(TAG, "Allowing incoming HID connection from " + address);
+             } else {
+                 Log.i(TAG, "Rejecting incoming HID connection from " + address);
+             }
         } else {
             Log.i(TAG, "Rejecting incoming " + deviceUuid + " connection from " + address);
         }
@@ -604,7 +630,19 @@
         return authorized;
     }
 
-    boolean isOtherSinkInNonDisconnectingState(String address) {
+    private boolean isOtherInputDeviceConnected(String address) {
+        Set<BluetoothDevice> devices =
+            mBluetoothService.lookupInputDevicesMatchingStates(new int[] {
+                                                BluetoothInputDevice.STATE_CONNECTING,
+                                                BluetoothInputDevice.STATE_CONNECTED});
+
+        for (BluetoothDevice device : devices) {
+            if (!device.getAddress().equals(address)) return true;
+        }
+        return false;
+    }
+
+    private boolean isOtherSinkInNonDisconnectingState(String address) {
         BluetoothA2dp a2dp = new BluetoothA2dp(mContext);
         Set<BluetoothDevice> devices = a2dp.getNonDisconnectedSinks();
         if (devices.size() == 0) return false;
diff --git a/core/java/android/server/BluetoothService.java b/core/java/android/server/BluetoothService.java
index 31e5a7b..e68632d 100644
--- a/core/java/android/server/BluetoothService.java
+++ b/core/java/android/server/BluetoothService.java
@@ -24,16 +24,19 @@
 
 package android.server;
 
+import android.bluetooth.BluetoothA2dp;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothClass;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
 import android.bluetooth.BluetoothDeviceProfileState;
 import android.bluetooth.BluetoothProfileState;
+import android.bluetooth.BluetoothInputDevice;
 import android.bluetooth.BluetoothSocket;
 import android.bluetooth.BluetoothUuid;
 import android.bluetooth.IBluetooth;
 import android.bluetooth.IBluetoothCallback;
+import android.bluetooth.IBluetoothHeadset;
 import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -70,8 +73,10 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Map;
+import java.util.Set;
 
 public class BluetoothService extends IBluetooth.Stub {
     private static final String TAG = "BluetoothService";
@@ -129,6 +134,8 @@
     private final BluetoothProfileState mHfpProfileState;
 
     private BluetoothA2dpService mA2dpService;
+    private final HashMap<BluetoothDevice, Integer> mInputDevices;
+
     private static String mDockAddress;
     private String mDockPin;
 
@@ -198,6 +205,7 @@
 
         filter.addAction(Intent.ACTION_DOCK_EVENT);
         mContext.registerReceiver(mReceiver, filter);
+        mInputDevices = new HashMap<BluetoothDevice, Integer>();
     }
 
     public static synchronized String readDockBluetoothAddress() {
@@ -1220,6 +1228,127 @@
         return sp.contains(SHARED_PREFERENCE_DOCK_ADDRESS + address);
     }
 
+    public synchronized boolean connectInputDevice(BluetoothDevice device) {
+        mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
+                                                "Need BLUETOOTH_ADMIN permission");
+
+        String objectPath = getObjectPathFromAddress(device.getAddress());
+        if (objectPath == null || getConnectedInputDevices().length != 0 ||
+            getInputDevicePriority(device) == BluetoothInputDevice.PRIORITY_OFF) {
+            return false;
+        }
+        if(connectInputDeviceNative(objectPath)) {
+            handleInputDeviceStateChange(device, BluetoothInputDevice.STATE_CONNECTING);
+            return true;
+        }
+        return false;
+    }
+
+    public synchronized boolean disconnectInputDevice(BluetoothDevice device) {
+        mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
+                                                "Need BLUETOOTH_ADMIN permission");
+
+        String objectPath = getObjectPathFromAddress(device.getAddress());
+        if (objectPath == null || getConnectedInputDevices().length == 0) {
+            return false;
+        }
+        if(disconnectInputDeviceNative(objectPath)) {
+            handleInputDeviceStateChange(device, BluetoothInputDevice.STATE_DISCONNECTING);
+            return true;
+        }
+        return false;
+    }
+
+    public synchronized int getInputDeviceState(BluetoothDevice device) {
+        mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
+
+        if (mInputDevices.get(device) == null) {
+            return BluetoothInputDevice.STATE_DISCONNECTED;
+        }
+        return mInputDevices.get(device.getAddress());
+    }
+
+    public synchronized BluetoothDevice[] getConnectedInputDevices() {
+        mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
+        Set<BluetoothDevice> devices = lookupInputDevicesMatchingStates(
+            new int[] {BluetoothInputDevice.STATE_CONNECTED});
+        return devices.toArray(new BluetoothDevice[devices.size()]);
+    }
+
+    public synchronized int getInputDevicePriority(BluetoothDevice device) {
+        mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
+        return Settings.Secure.getInt(mContext.getContentResolver(),
+                Settings.Secure.getBluetoothInputDevicePriorityKey(device.getAddress()),
+                BluetoothInputDevice.PRIORITY_UNDEFINED);
+    }
+
+    public synchronized boolean setInputDevicePriority(BluetoothDevice device, int priority) {
+        mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
+                                                "Need BLUETOOTH_ADMIN permission");
+        if (!BluetoothAdapter.checkBluetoothAddress(device.getAddress())) {
+            return false;
+        }
+        return Settings.Secure.putInt(mContext.getContentResolver(),
+                Settings.Secure.getBluetoothInputDevicePriorityKey(device.getAddress()),
+                priority);
+    }
+
+    /*package*/synchronized Set<BluetoothDevice> lookupInputDevicesMatchingStates(int[] states) {
+        Set<BluetoothDevice> inputDevices = new HashSet<BluetoothDevice>();
+        if (mInputDevices.isEmpty()) {
+            return inputDevices;
+        }
+        for (BluetoothDevice device: mInputDevices.keySet()) {
+            int inputDeviceState = getInputDeviceState(device);
+            for (int state : states) {
+                if (state == inputDeviceState) {
+                    inputDevices.add(device);
+                    break;
+                }
+            }
+        }
+        return inputDevices;
+    }
+
+    private synchronized void handleInputDeviceStateChange(BluetoothDevice device, int state) {
+        int prevState;
+        if (mInputDevices.get(device) == null) {
+            prevState = BluetoothInputDevice.STATE_DISCONNECTED;
+        } else {
+            prevState = mInputDevices.get(device);
+        }
+        if (prevState == state) return;
+
+        mInputDevices.put(device, state);
+
+        if (getInputDevicePriority(device) >
+              BluetoothInputDevice.PRIORITY_OFF &&
+            state == BluetoothInputDevice.STATE_CONNECTING ||
+            state == BluetoothInputDevice.STATE_CONNECTED) {
+            // We have connected or attempting to connect.
+            // Bump priority
+            setInputDevicePriority(device, BluetoothInputDevice.PRIORITY_AUTO_CONNECT);
+        }
+
+        Intent intent = new Intent(BluetoothInputDevice.ACTION_INPUT_DEVICE_STATE_CHANGED);
+        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
+        intent.putExtra(BluetoothInputDevice.EXTRA_PREVIOUS_INPUT_DEVICE_STATE, prevState);
+        intent.putExtra(BluetoothInputDevice.EXTRA_INPUT_DEVICE_STATE, state);
+        mContext.sendBroadcast(intent, BLUETOOTH_PERM);
+
+        if (DBG) log("InputDevice state : device: " + device + " State:" + prevState + "->" + state);
+
+    }
+
+    /*package*/ void handleInputDevicePropertyChange(String path, boolean connected) {
+        String address = getAddressFromObjectPath(path);
+        if (address == null) return;
+        int state = connected ? BluetoothInputDevice.STATE_CONNECTED :
+            BluetoothInputDevice.STATE_DISCONNECTED;
+        BluetoothDevice device = mAdapter.getRemoteDevice(address);
+        handleInputDeviceStateChange(device, state);
+    }
+
     /*package*/ boolean isRemoteDeviceInCache(String address) {
         return (mDeviceProperties.get(address) != null);
     }
@@ -2103,4 +2232,6 @@
             short channel);
     private native boolean removeServiceRecordNative(int handle);
     private native boolean setLinkTimeoutNative(String path, int num_slots);
+    private native boolean connectInputDeviceNative(String path);
+    private native boolean disconnectInputDeviceNative(String path);
 }
diff --git a/core/jni/android_bluetooth_common.cpp b/core/jni/android_bluetooth_common.cpp
index 9a8f1b8..53ac625 100644
--- a/core/jni/android_bluetooth_common.cpp
+++ b/core/jni/android_bluetooth_common.cpp
@@ -68,6 +68,10 @@
     {"UUIDs", DBUS_TYPE_ARRAY},
 };
 
+static Properties input_properties[] = {
+    {"Connected", DBUS_TYPE_BOOLEAN},
+};
+
 typedef union {
     char *str_val;
     int int_val;
@@ -698,6 +702,11 @@
                     sizeof(remote_device_properties) / sizeof(Properties));
 }
 
+jobjectArray parse_input_property_change(JNIEnv *env, DBusMessage *msg) {
+    return parse_property_change(env, msg, (Properties *) &input_properties,
+                    sizeof(input_properties) / sizeof(Properties));
+}
+
 jobjectArray parse_adapter_properties(JNIEnv *env, DBusMessageIter *iter) {
     return parse_properties(env, iter, (Properties *) &adapter_properties,
                             sizeof(adapter_properties) / sizeof(Properties));
@@ -708,6 +717,11 @@
                           sizeof(remote_device_properties) / sizeof(Properties));
 }
 
+jobjectArray parse_input_properties(JNIEnv *env, DBusMessageIter *iter) {
+    return parse_properties(env, iter, (Properties *) &input_properties,
+                          sizeof(input_properties) / sizeof(Properties));
+}
+
 int get_bdaddr(const char *str, bdaddr_t *ba) {
     char *d = ((char *)ba) + 5, *endp;
     int i;
diff --git a/core/jni/android_bluetooth_common.h b/core/jni/android_bluetooth_common.h
index 378bb6f..27a00ae 100644
--- a/core/jni/android_bluetooth_common.h
+++ b/core/jni/android_bluetooth_common.h
@@ -162,6 +162,8 @@
 jobjectArray parse_remote_device_properties(JNIEnv *env, DBusMessageIter *iter);
 jobjectArray parse_remote_device_property_change(JNIEnv *env, DBusMessage *msg);
 jobjectArray parse_adapter_property_change(JNIEnv *env, DBusMessage *msg);
+jobjectArray parse_input_properties(JNIEnv *env, DBusMessageIter *iter);
+jobjectArray parse_input_property_change(JNIEnv *env, DBusMessage *msg);
 void append_variant(DBusMessageIter *iter, int type, void *val);
 int get_bdaddr(const char *str, bdaddr_t *ba);
 void get_bdaddr_as_string(const bdaddr_t *ba, char *str);
diff --git a/core/jni/android_server_BluetoothEventLoop.cpp b/core/jni/android_server_BluetoothEventLoop.cpp
index 01b6711..3c88158 100644
--- a/core/jni/android_server_BluetoothEventLoop.cpp
+++ b/core/jni/android_server_BluetoothEventLoop.cpp
@@ -64,6 +64,8 @@
 static jmethodID method_onAgentAuthorize;
 static jmethodID method_onAgentCancel;
 
+static jmethodID method_onInputDevicePropertyChanged;
+
 typedef event_loop_native_data_t native_data_t;
 
 #define EVENT_LOOP_REFS 10
@@ -116,6 +118,9 @@
                                                "(Ljava/lang/String;I)V");
     method_onDisplayPasskey = env->GetMethodID(clazz, "onDisplayPasskey",
                                                "(Ljava/lang/String;II)V");
+    method_onInputDevicePropertyChanged = env->GetMethodID(clazz, "onInputDevicePropertyChanged",
+                                                      "(Ljava/lang/String;[Ljava/lang/String;)V");
+
 
     field_mNativeData = env->GetFieldID(clazz, "mNativeData", "I");
 #endif
@@ -853,6 +858,22 @@
                             method_onDeviceDisconnectRequested,
                             env->NewStringUTF(remote_device_path));
         goto success;
+    } else if (dbus_message_is_signal(msg,
+                                      "org.bluez.Input",
+                                      "PropertyChanged")) {
+
+        jobjectArray str_array =
+                    parse_input_property_change(env, msg);
+        if (str_array != NULL) {
+            const char *c_path = dbus_message_get_path(msg);
+            env->CallVoidMethod(nat->me,
+                                method_onInputDevicePropertyChanged,
+                                env->NewStringUTF(c_path),
+                                str_array);
+        } else {
+            LOG_AND_FREE_DBUS_ERROR_WITH_MSG(&err, msg);
+        }
+        goto success;
     }
 
     ret = a2dp_event_filter(msg, env);
diff --git a/core/jni/android_server_BluetoothService.cpp b/core/jni/android_server_BluetoothService.cpp
index 4420aca..a52a74c 100644
--- a/core/jni/android_server_BluetoothService.cpp
+++ b/core/jni/android_server_BluetoothService.cpp
@@ -16,6 +16,8 @@
 
 #define DBUS_ADAPTER_IFACE BLUEZ_DBUS_BASE_IFC ".Adapter"
 #define DBUS_DEVICE_IFACE BLUEZ_DBUS_BASE_IFC ".Device"
+#define DBUS_INPUT_IFACE BLUEZ_DBUS_BASE_IFC ".Input"
+
 #define LOG_TAG "BluetoothService.cpp"
 
 #include "android_bluetooth_common.h"
@@ -881,6 +883,43 @@
     return JNI_FALSE;
 }
 
+static jboolean connectInputDeviceNative(JNIEnv *env, jobject object, jstring path) {
+    LOGV(__FUNCTION__);
+#ifdef HAVE_BLUETOOTH
+    native_data_t *nat = get_native_data(env, object);
+    if (nat) {
+        const char *c_path = env->GetStringUTFChars(path, NULL);
+
+        bool ret = dbus_func_args_async(env, nat->conn, -1, NULL, NULL, nat,
+                                    c_path, DBUS_INPUT_IFACE, "Connect",
+                                    DBUS_TYPE_INVALID);
+
+        env->ReleaseStringUTFChars(path, c_path);
+        return ret ? JNI_TRUE : JNI_FALSE;
+    }
+#endif
+    return JNI_FALSE;
+}
+
+static jboolean disconnectInputDeviceNative(JNIEnv *env, jobject object,
+                                     jstring path) {
+    LOGV(__FUNCTION__);
+#ifdef HAVE_BLUETOOTH
+    native_data_t *nat = get_native_data(env, object);
+    if (nat) {
+        const char *c_path = env->GetStringUTFChars(path, NULL);
+
+        bool ret = dbus_func_args_async(env, nat->conn, -1, NULL, NULL, nat,
+                                    c_path, DBUS_INPUT_IFACE, "Disconnect",
+                                    DBUS_TYPE_INVALID);
+
+        env->ReleaseStringUTFChars(path, c_path);
+        return ret ? JNI_TRUE : JNI_FALSE;
+    }
+#endif
+    return JNI_FALSE;
+}
+
 static JNINativeMethod sMethods[] = {
      /* name, signature, funcPtr */
     {"classInitNative", "()V", (void*)classInitNative},
@@ -926,6 +965,9 @@
     {"addRfcommServiceRecordNative", "(Ljava/lang/String;JJS)I", (void *)addRfcommServiceRecordNative},
     {"removeServiceRecordNative", "(I)Z", (void *)removeServiceRecordNative},
     {"setLinkTimeoutNative", "(Ljava/lang/String;I)Z", (void *)setLinkTimeoutNative},
+    // HID functions
+    {"connectInputDeviceNative", "(Ljava/lang/String;)Z", (void *)connectInputDeviceNative},
+    {"disconnectInputDeviceNative", "(Ljava/lang/String;)Z", (void *)disconnectInputDeviceNative},
 };
 
 int register_android_server_BluetoothService(JNIEnv *env) {