Incoming Bluetooth Connection requests - dialog.

This sends the intents to the Settings app to show
the dialogs for the incoming connection requests.
Includes down merged contributions from Jaikumar Ganesh.

Change-Id: Ic8b857aad3554315aae39a0e871eb94d0ac98a91
diff --git a/core/java/android/bluetooth/BluetoothA2dp.java b/core/java/android/bluetooth/BluetoothA2dp.java
index 61d3707..96f3290 100644
--- a/core/java/android/bluetooth/BluetoothA2dp.java
+++ b/core/java/android/bluetooth/BluetoothA2dp.java
@@ -423,7 +423,24 @@
         return false;
     }
 
-     /**
+    /**
+     * Allow or disallow incoming connection
+     * @param device Sink
+     * @param value True / False
+     * @return Success or Failure of the binder call.
+     * @hide
+     */
+    public boolean allowIncomingConnect(BluetoothDevice device, boolean value) {
+        if (DBG) log("allowIncomingConnect(" + device + ":" + value + ")");
+        try {
+            return mService.allowIncomingConnect(device, value);
+        } catch (RemoteException e) {
+            Log.e(TAG, "", e);
+            return false;
+        }
+    }
+
+    /**
      * Helper for converting a state to a string.
      *
      * For debug use only - strings are not internationalized.
diff --git a/core/java/android/bluetooth/BluetoothDevice.java b/core/java/android/bluetooth/BluetoothDevice.java
index 254e2f8..d9525a3 100644
--- a/core/java/android/bluetooth/BluetoothDevice.java
+++ b/core/java/android/bluetooth/BluetoothDevice.java
@@ -276,6 +276,70 @@
     public static final String ACTION_PAIRING_CANCEL =
             "android.bluetooth.device.action.PAIRING_CANCEL";
 
+    /** @hide */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_CONNECTION_ACCESS_REQUEST =
+            "android.bluetooth.device.action.CONNECTION_ACCESS_REQUEST";
+
+    /** @hide */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_CONNECTION_ACCESS_REPLY =
+            "android.bluetooth.device.action.CONNECTION_ACCESS_REPLY";
+
+    /** @hide */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_CONNECTION_ACCESS_CANCEL =
+            "android.bluetooth.device.action.CONNECTION_ACCESS_CANCEL";
+
+    /**
+     * Used as an extra field in {@link #ACTION_CONNECTION_ACCESS_REQUEST} intent.
+     * @hide
+     */
+    public static final String EXTRA_ACCESS_REQUEST_TYPE =
+        "android.bluetooth.device.extra.ACCESS_REQUEST_TYPE";
+
+    /**@hide*/
+    public static final int REQUEST_TYPE_PROFILE_CONNECTION = 1;
+
+    /**@hide*/
+    public static final int REQUEST_TYPE_PHONEBOOK_ACCESS = 2;
+
+    /**
+     * Used as an extra field in {@link #ACTION_CONNECTION_ACCESS_REQUEST} intents,
+     * Contains package name to return reply intent to.
+     * @hide
+     */
+    public static final String EXTRA_PACKAGE_NAME = "android.bluetooth.device.extra.PACKAGE_NAME";
+
+    /**
+     * Used as an extra field in {@link #ACTION_CONNECTION_ACCESS_REQUEST} intents,
+     * Contains class name to return reply intent to.
+     * @hide
+     */
+    public static final String EXTRA_CLASS_NAME = "android.bluetooth.device.extra.CLASS_NAME";
+
+    /**
+     * Used as an extra field in {@link #ACTION_CONNECTION_ACCESS_REPLY} intent.
+     * @hide
+     */
+    public static final String EXTRA_CONNECTION_ACCESS_RESULT =
+        "android.bluetooth.device.extra.CONNECTION_ACCESS_RESULT";
+
+    /**@hide*/
+    public static final int CONNECTION_ACCESS_YES = 1;
+
+    /**@hide*/
+    public static final int CONNECTION_ACCESS_NO = 2;
+
+    /**
+     * Used as an extra field in {@link #ACTION_CONNECTION_ACCESS_REPLY} intents,
+     * Contains boolean to indicate if the allowed response is once-for-all so that
+     * next request will be granted without asking user again.
+     * @hide
+     */
+    public static final String EXTRA_ALWAYS_ALLOWED =
+        "android.bluetooth.device.extra.ALWAYS_ALLOWED";
+
     /**
      * A bond attempt succeeded
      * @hide
diff --git a/core/java/android/bluetooth/BluetoothDeviceProfileState.java b/core/java/android/bluetooth/BluetoothDeviceProfileState.java
index 56f236d..ab3a426 100644
--- a/core/java/android/bluetooth/BluetoothDeviceProfileState.java
+++ b/core/java/android/bluetooth/BluetoothDeviceProfileState.java
@@ -22,9 +22,11 @@
 import android.content.IntentFilter;
 import android.os.Message;
 import android.bluetooth.BluetoothAdapter;
+import android.os.PowerManager;
 import android.server.BluetoothA2dpService;
 import android.server.BluetoothService;
 import android.util.Log;
+import android.util.Pair;
 
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
@@ -81,8 +83,18 @@
     public static final int AUTO_CONNECT_PROFILES = 101;
     public static final int TRANSITION_TO_STABLE = 102;
     public static final int CONNECT_OTHER_PROFILES = 103;
+    private static final int CONNECTION_ACCESS_REQUEST_REPLY = 104;
+    private static final int CONNECTION_ACCESS_REQUEST_EXPIRY = 105;
 
     private static final int CONNECT_OTHER_PROFILES_DELAY = 4000; // 4 secs
+    private static final int CONNECTION_ACCESS_REQUEST_EXPIRY_TIMEOUT = 7000; // 7 secs
+    private static final int CONNECTION_ACCESS_UNDEFINED = -1;
+    private static final long INIT_INCOMING_REJECT_TIMER = 1000; // 1 sec
+    private static final long MAX_INCOMING_REJECT_TIMER = 3600 * 1000 * 4; // 4 hours
+
+    private static final String ACCESS_AUTHORITY_PACKAGE = "com.android.settings";
+    private static final String ACCESS_AUTHORITY_CLASS =
+        "com.android.settings.bluetooth.BluetoothPermissionRequest";
 
     private BondedDevice mBondedDevice = new BondedDevice();
     private OutgoingHandsfree mOutgoingHandsfree = new OutgoingHandsfree();
@@ -98,10 +110,16 @@
     private BluetoothHeadset  mHeadsetService;
     private BluetoothPbap     mPbapService;
     private boolean mPbapServiceConnected;
+    private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN;
 
     private BluetoothDevice mDevice;
     private int mHeadsetState = BluetoothProfile.STATE_DISCONNECTED;
     private int mA2dpState = BluetoothProfile.STATE_DISCONNECTED;
+    private long mIncomingRejectTimer;
+    private boolean mConnectionAccessReplyReceived = false;
+    private Pair<Integer, String> mIncomingConnections;
+    private PowerManager.WakeLock mWakeLock;
+    private PowerManager mPowerManager;
 
     private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
         @Override
@@ -113,6 +131,10 @@
             if (action.equals(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)) {
                 int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0);
                 int oldState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, 0);
+                // We trust this device now
+                if (newState == BluetoothHeadset.STATE_CONNECTED) {
+                    setTrust(BluetoothDevice.CONNECTION_ACCESS_YES);
+                }
                 mA2dpState = newState;
                 if (oldState == BluetoothA2dp.STATE_CONNECTED &&
                     newState == BluetoothA2dp.STATE_DISCONNECTED) {
@@ -125,7 +147,10 @@
             } else if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
                 int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0);
                 int oldState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, 0);
-
+                // We trust this device now
+                if (newState == BluetoothHeadset.STATE_CONNECTED) {
+                    setTrust(BluetoothDevice.CONNECTION_ACCESS_YES);
+                }
                 mHeadsetState = newState;
                 if (oldState == BluetoothHeadset.STATE_CONNECTED &&
                     newState == BluetoothHeadset.STATE_DISCONNECTED) {
@@ -139,7 +164,10 @@
                 int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0);
                 int oldState =
                     intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, 0);
-
+                // We trust this device now
+                if (newState == BluetoothHeadset.STATE_CONNECTED) {
+                    setTrust(BluetoothDevice.CONNECTION_ACCESS_YES);
+                }
                 if (oldState == BluetoothProfile.STATE_CONNECTED &&
                     newState == BluetoothProfile.STATE_DISCONNECTED) {
                     sendMessage(DISCONNECT_HID_INCOMING);
@@ -152,8 +180,15 @@
                 // This is technically not needed, but we can get stuck sometimes.
                 // For example, if incoming A2DP fails, we are not informed by Bluez
                 sendMessage(TRANSITION_TO_STABLE);
+            } else if (action.equals(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY)) {
+                mWakeLock.release();
+                int val = intent.getIntExtra(BluetoothDevice.EXTRA_CONNECTION_ACCESS_RESULT,
+                                             BluetoothDevice.CONNECTION_ACCESS_NO);
+                Message msg = obtainMessage(CONNECTION_ACCESS_REQUEST_REPLY);
+                msg.arg1 = val;
+                sendMessage(msg);
             }
-      }
+        }
     };
 
     private boolean isPhoneDocked(BluetoothDevice autoConnectDevice) {
@@ -195,6 +230,7 @@
         filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
         filter.addAction(BluetoothInputDevice.ACTION_CONNECTION_STATE_CHANGED);
         filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
+        filter.addAction(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY);
 
         mContext.registerReceiver(mBroadcastReceiver, filter);
 
@@ -203,6 +239,14 @@
                                 BluetoothProfile.HEADSET);
         // TODO(): Convert PBAP to the new Profile APIs.
         PbapServiceListener p = new PbapServiceListener();
+
+        mIncomingConnections = mService.getIncomingState(address);
+        mIncomingRejectTimer = readTimerValue();
+        mPowerManager = (PowerManager)mContext.getSystemService(Context.POWER_SERVICE);
+        mWakeLock = mPowerManager.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK |
+                                              PowerManager.ACQUIRE_CAUSES_WAKEUP |
+                                              PowerManager.ON_AFTER_RELEASE, TAG);
+        mWakeLock.setReferenceCounted(false);
     }
 
     private BluetoothProfile.ServiceListener mBluetoothProfileServiceListener =
@@ -497,6 +541,24 @@
                     // Ignore
                     Log.e(TAG, "Error: Incoming connection with a pending incoming connection");
                     break;
+                case CONNECTION_ACCESS_REQUEST_REPLY:
+                    int val = message.arg1;
+                    mConnectionAccessReplyReceived = true;
+                    boolean value = false;
+                    if (val == BluetoothDevice.CONNECTION_ACCESS_YES) {
+                        value = true;
+                    }
+                    setTrust(val);
+
+                    handleIncomingConnection(CONNECT_HFP_INCOMING, value);
+                    break;
+                case CONNECTION_ACCESS_REQUEST_EXPIRY:
+                    if (!mConnectionAccessReplyReceived) {
+                        handleIncomingConnection(CONNECT_HFP_INCOMING, false);
+                        sendConnectionAccessRemovalIntent();
+                        sendMessage(TRANSITION_TO_STABLE);
+                    }
+                    break;
                 case CONNECT_A2DP_INCOMING:
                     // Serialize the commands.
                     deferMessage(message);
@@ -690,6 +752,25 @@
                 case CONNECT_A2DP_INCOMING:
                     // ignore
                     break;
+                case CONNECTION_ACCESS_REQUEST_REPLY:
+                    int val = message.arg1;
+                    mConnectionAccessReplyReceived = true;
+                    boolean value = false;
+                    if (val == BluetoothDevice.CONNECTION_ACCESS_YES) {
+                        value = true;
+                    }
+                    setTrust(val);
+                    handleIncomingConnection(CONNECT_A2DP_INCOMING, value);
+                    break;
+                case CONNECTION_ACCESS_REQUEST_EXPIRY:
+                    // The check protects the race condition between REQUEST_REPLY
+                    // and the timer expiry.
+                    if (!mConnectionAccessReplyReceived) {
+                        handleIncomingConnection(CONNECT_A2DP_INCOMING, false);
+                        sendConnectionAccessRemovalIntent();
+                        sendMessage(TRANSITION_TO_STABLE);
+                    }
+                    break;
                 case CONNECT_A2DP_OUTGOING:
                     // Defer message and retry
                     deferMessage(message);
@@ -847,6 +928,20 @@
               case DISCONNECT_HID_OUTGOING:
                   deferMessage(message);
                   break;
+              case CONNECTION_ACCESS_REQUEST_REPLY:
+                  mConnectionAccessReplyReceived = true;
+                  int val = message.arg1;
+                  setTrust(val);
+                  handleIncomingConnection(CONNECT_HID_INCOMING,
+                      val == BluetoothDevice.CONNECTION_ACCESS_YES);
+                  break;
+              case CONNECTION_ACCESS_REQUEST_EXPIRY:
+                  if (!mConnectionAccessReplyReceived) {
+                      handleIncomingConnection(CONNECT_HID_INCOMING, false);
+                      sendConnectionAccessRemovalIntent();
+                      sendMessage(TRANSITION_TO_STABLE);
+                  }
+                  break;
               case DISCONNECT_HFP_INCOMING:
                   // Shouldn't happen but if does, we can handle it.
                   // Depends if the headset can handle it.
@@ -891,8 +986,150 @@
         deferMessage(msg);
     }
 
+    private void updateIncomingAllowedTimer() {
+        // Not doing a perfect exponential backoff because
+        // we want two different rates. For all practical
+        // purposes, this is good enough.
+        if (mIncomingRejectTimer == 0) mIncomingRejectTimer = INIT_INCOMING_REJECT_TIMER;
+
+        mIncomingRejectTimer *= 5;
+        if (mIncomingRejectTimer > MAX_INCOMING_REJECT_TIMER) {
+            mIncomingRejectTimer = MAX_INCOMING_REJECT_TIMER;
+        }
+        writeTimerValue(mIncomingRejectTimer);
+    }
+
+    private boolean handleIncomingConnection(int command, boolean accept) {
+        boolean ret = false;
+        Log.i(TAG, "handleIncomingConnection:" + command + ":" + accept);
+        switch (command) {
+            case CONNECT_HFP_INCOMING:
+                if (!accept) {
+                    ret = mHeadsetService.rejectIncomingConnect(mDevice);
+                    sendMessage(TRANSITION_TO_STABLE);
+                    updateIncomingAllowedTimer();
+                } else if (mHeadsetState == BluetoothHeadset.STATE_CONNECTING) {
+                    writeTimerValue(0);
+                    ret =  mHeadsetService.acceptIncomingConnect(mDevice);
+                } else if (mHeadsetState == BluetoothHeadset.STATE_DISCONNECTED) {
+                    writeTimerValue(0);
+                    handleConnectionOfOtherProfiles(command);
+                    ret = mHeadsetService.createIncomingConnect(mDevice);
+                }
+                break;
+            case CONNECT_A2DP_INCOMING:
+                if (!accept) {
+                    ret = mA2dpService.allowIncomingConnect(mDevice, false);
+                    sendMessage(TRANSITION_TO_STABLE);
+                    updateIncomingAllowedTimer();
+                } else {
+                    writeTimerValue(0);
+                    ret = mA2dpService.allowIncomingConnect(mDevice, true);
+                    handleConnectionOfOtherProfiles(command);
+                }
+                break;
+            case CONNECT_HID_INCOMING:
+                if (!accept) {
+                    ret = mService.allowIncomingHidConnect(mDevice, false);
+                    sendMessage(TRANSITION_TO_STABLE);
+                    updateIncomingAllowedTimer();
+                } else {
+                    writeTimerValue(0);
+                    ret = mService.allowIncomingHidConnect(mDevice, true);
+                }
+                break;
+            default:
+                Log.e(TAG, "Waiting for incoming connection but state changed to:" + command);
+                break;
+       }
+       return ret;
+    }
+
+    private void sendConnectionAccessIntent() {
+        mConnectionAccessReplyReceived = false;
+
+        if (!mPowerManager.isScreenOn()) mWakeLock.acquire();
+
+        Intent intent = new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_REQUEST);
+        intent.setClassName(ACCESS_AUTHORITY_PACKAGE, ACCESS_AUTHORITY_CLASS);
+        intent.putExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE,
+                        BluetoothDevice.REQUEST_TYPE_PROFILE_CONNECTION);
+        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
+        mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM);
+    }
+
+    private void sendConnectionAccessRemovalIntent() {
+        mWakeLock.release();
+        Intent intent = new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_CANCEL);
+        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
+        mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM);
+    }
+
+    private int getTrust() {
+        String address = mDevice.getAddress();
+        if (mIncomingConnections != null) return mIncomingConnections.first;
+        return CONNECTION_ACCESS_UNDEFINED;
+    }
+
+
+    private String getStringValue(long value) {
+        StringBuilder sbr = new StringBuilder();
+        sbr.append(Long.toString(System.currentTimeMillis()));
+        sbr.append("-");
+        sbr.append(Long.toString(value));
+        return sbr.toString();
+    }
+
+    private void setTrust(int value) {
+        String second;
+        if (mIncomingConnections == null) {
+            second = getStringValue(INIT_INCOMING_REJECT_TIMER);
+        } else {
+            second = mIncomingConnections.second;
+        }
+
+        mIncomingConnections = new Pair(value, second);
+        mService.writeIncomingConnectionState(mDevice.getAddress(), mIncomingConnections);
+    }
+
+    private void writeTimerValue(long value) {
+        Integer first;
+        if (mIncomingConnections == null) {
+            first = CONNECTION_ACCESS_UNDEFINED;
+        } else {
+            first = mIncomingConnections.first;
+        }
+        mIncomingConnections = new Pair(first, getStringValue(value));
+        mService.writeIncomingConnectionState(mDevice.getAddress(), mIncomingConnections);
+    }
+
+    private long readTimerValue() {
+        if (mIncomingConnections == null)
+            return 0;
+        String value = mIncomingConnections.second;
+        String[] splits = value.split("-");
+        if (splits != null && splits.length == 2) {
+            return Long.parseLong(splits[1]);
+        }
+        return 0;
+    }
+
+    private boolean readIncomingAllowedValue() {
+        if (readTimerValue() == 0) return true;
+        String value = mIncomingConnections.second;
+        String[] splits = value.split("-");
+        if (splits != null && splits.length == 2) {
+            long val1 = Long.parseLong(splits[0]);
+            long val2 = Long.parseLong(splits[1]);
+            if (val1 + val2 <= System.currentTimeMillis()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     synchronized boolean processCommand(int command) {
-        Log.i(TAG, "Processing command:" + command);
+        log("Processing command:" + command);
         switch(command) {
             case  CONNECT_HFP_OUTGOING:
                 if (mHeadsetService == null) {
@@ -904,11 +1141,9 @@
             case CONNECT_HFP_INCOMING:
                 if (mHeadsetService == null) {
                     deferProfileServiceMessage(command);
-                } else if (mHeadsetState == BluetoothHeadset.STATE_CONNECTING) {
-                    return mHeadsetService.acceptIncomingConnect(mDevice);
-                } else if (mHeadsetState == BluetoothHeadset.STATE_DISCONNECTED) {
-                    handleConnectionOfOtherProfiles(command);
-                    return mHeadsetService.createIncomingConnect(mDevice);
+                } else {
+                    processIncomingConnectCommand(command);
+                    return true;
                 }
                 break;
             case CONNECT_A2DP_OUTGOING:
@@ -917,12 +1152,12 @@
                 }
                 break;
             case CONNECT_A2DP_INCOMING:
-                handleConnectionOfOtherProfiles(command);
-                // ignore, Bluez takes care
+                processIncomingConnectCommand(command);
                 return true;
             case CONNECT_HID_OUTGOING:
                 return mService.connectInputDeviceInternal(mDevice);
             case CONNECT_HID_INCOMING:
+                processIncomingConnectCommand(command);
                 return true;
             case DISCONNECT_HFP_OUTGOING:
                 if (mHeadsetService == null) {
@@ -972,6 +1207,8 @@
                 }
                 break;
             case UNPAIR:
+                writeTimerValue(INIT_INCOMING_REJECT_TIMER);
+                setTrust(CONNECTION_ACCESS_UNDEFINED);
                 return mService.removeBondInternal(mDevice.getAddress());
             default:
                 Log.e(TAG, "Error: Unknown Command");
@@ -979,6 +1216,22 @@
         return false;
     }
 
+    private void processIncomingConnectCommand(int command) {
+        // Check if device is already trusted
+        int access = getTrust();
+        if (access == BluetoothDevice.CONNECTION_ACCESS_YES) {
+            handleIncomingConnection(command, true);
+        } else if (access == BluetoothDevice.CONNECTION_ACCESS_NO &&
+                   !readIncomingAllowedValue()) {
+            handleIncomingConnection(command, false);
+        } else {
+            sendConnectionAccessIntent();
+            Message msg = obtainMessage(CONNECTION_ACCESS_REQUEST_EXPIRY);
+            sendMessageDelayed(msg,
+                               CONNECTION_ACCESS_REQUEST_EXPIRY_TIMEOUT);
+        }
+    }
+
     private void handleConnectionOfOtherProfiles(int command) {
         // The white paper recommendations mentions that when there is a
         // link loss, it is the responsibility of the remote device to connect.
diff --git a/core/java/android/bluetooth/BluetoothHeadset.java b/core/java/android/bluetooth/BluetoothHeadset.java
index 23724f2..3284361 100644
--- a/core/java/android/bluetooth/BluetoothHeadset.java
+++ b/core/java/android/bluetooth/BluetoothHeadset.java
@@ -618,6 +618,23 @@
     }
 
     /**
+     * Reject the incoming connection.
+     * @hide
+     */
+    public boolean rejectIncomingConnect(BluetoothDevice device) {
+        if (DBG) log("rejectIncomingConnect");
+        if (mService != null) {
+            try {
+                return mService.rejectIncomingConnect(device);
+            } catch (RemoteException e) {Log.e(TAG, e.toString());}
+        } else {
+            Log.w(TAG, "Proxy not attached to service");
+            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
+        }
+        return false;
+    }
+
+    /**
      * Connect to a Bluetooth Headset.
      * Note: This is an internal function and shouldn't be exposed
      *
diff --git a/core/java/android/bluetooth/BluetoothInputDevice.java b/core/java/android/bluetooth/BluetoothInputDevice.java
index 282b70a..f6757d9 100644
--- a/core/java/android/bluetooth/BluetoothInputDevice.java
+++ b/core/java/android/bluetooth/BluetoothInputDevice.java
@@ -308,6 +308,28 @@
         return BluetoothProfile.PRIORITY_OFF;
     }
 
+    /**
+     * Allow or disallow incoming connection
+     * @param device Input device
+     * @param allow true / false
+     * @return Success or Failure of the operation
+     * @hide
+     */
+    public boolean allowIncomingConnect(BluetoothDevice device, boolean allow) {
+        if (DBG) log("allowIncomingConnect(" + device + ", " + allow + ")");
+
+        if (mService == null || !isEnabled() || !isValidDevice(device)) {
+            return false;
+        }
+        try {
+            mService.allowIncomingHidConnect(device, allow);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+            return false;
+        }
+        return true;
+    }
+
     private boolean isEnabled() {
        if (mAdapter.getState() == BluetoothAdapter.STATE_ON) return true;
        return false;
diff --git a/core/java/android/bluetooth/IBluetooth.aidl b/core/java/android/bluetooth/IBluetooth.aidl
index 28b09b6..6ca6c2e 100644
--- a/core/java/android/bluetooth/IBluetooth.aidl
+++ b/core/java/android/bluetooth/IBluetooth.aidl
@@ -91,6 +91,7 @@
     int getInputDeviceConnectionState(in BluetoothDevice device);
     boolean setInputDevicePriority(in BluetoothDevice device, int priority);
     int getInputDevicePriority(in BluetoothDevice device);
+    boolean allowIncomingHidConnect(in BluetoothDevice device, boolean value);
 
     boolean isTetheringOn();
     void setBluetoothTethering(boolean value);
diff --git a/core/java/android/bluetooth/IBluetoothA2dp.aidl b/core/java/android/bluetooth/IBluetoothA2dp.aidl
index b4fc366..444dd1e 100644
--- a/core/java/android/bluetooth/IBluetoothA2dp.aidl
+++ b/core/java/android/bluetooth/IBluetoothA2dp.aidl
@@ -39,4 +39,6 @@
     boolean resumeSink(in BluetoothDevice device);
     boolean connectSinkInternal(in BluetoothDevice device);
     boolean disconnectSinkInternal(in BluetoothDevice device);
+    boolean allowIncomingConnect(in BluetoothDevice device, boolean value);
+
 }
diff --git a/core/java/android/bluetooth/IBluetoothHeadset.aidl b/core/java/android/bluetooth/IBluetoothHeadset.aidl
index 273cda7..ec00527 100644
--- a/core/java/android/bluetooth/IBluetoothHeadset.aidl
+++ b/core/java/android/bluetooth/IBluetoothHeadset.aidl
@@ -42,6 +42,7 @@
     // Internal functions, not be made public
     boolean createIncomingConnect(in BluetoothDevice device);
     boolean acceptIncomingConnect(in BluetoothDevice device);
+    boolean rejectIncomingConnect(in BluetoothDevice device);
     boolean cancelConnectThread();
     boolean connectHeadsetInternal(in BluetoothDevice device);
     boolean disconnectHeadsetInternal(in BluetoothDevice device);
diff --git a/core/java/android/server/BluetoothA2dpService.java b/core/java/android/server/BluetoothA2dpService.java
index 8a6fdb4..fd277d0 100644
--- a/core/java/android/server/BluetoothA2dpService.java
+++ b/core/java/android/server/BluetoothA2dpService.java
@@ -449,6 +449,22 @@
                 Settings.Secure.getBluetoothA2dpSinkPriorityKey(device.getAddress()), priority);
     }
 
+    public synchronized boolean allowIncomingConnect(BluetoothDevice device, boolean value) {
+        mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
+                                                "Need BLUETOOTH_ADMIN permission");
+        String address = device.getAddress();
+        if (!BluetoothAdapter.checkBluetoothAddress(address)) {
+            return false;
+        }
+        Integer data = mBluetoothService.getAuthorizationAgentRequestData(address);
+        if (data == null) {
+            Log.w(TAG, "allowIncomingConnect(" + device + ") called but no native data available");
+            return false;
+        }
+        log("allowIncomingConnect: A2DP: " + device + ":" + value);
+        return mBluetoothService.setAuthorizationNative(address, value, data.intValue());
+    }
+
     /**
      * Called by native code on a PropertyChanged signal from
      * org.bluez.AudioSink.
diff --git a/core/java/android/server/BluetoothBondState.java b/core/java/android/server/BluetoothBondState.java
index 76e7885..75f38f9 100644
--- a/core/java/android/server/BluetoothBondState.java
+++ b/core/java/android/server/BluetoothBondState.java
@@ -121,6 +121,8 @@
 
     /** reason is ignored unless state == BOND_NOT_BONDED */
     public synchronized void setBondState(String address, int state, int reason) {
+        if (DBG) Log.d(TAG, "setBondState " + "address" + " " + state + "reason: " + reason);
+
         int oldState = getBondState(address);
         if (oldState == state) {
             return;
@@ -136,8 +138,10 @@
 
         if (state == BluetoothDevice.BOND_BONDED) {
             mService.addProfileState(address);
-        } else if (state == BluetoothDevice.BOND_NONE) {
-            mService.removeProfileState(address);
+        } else if (state == BluetoothDevice.BOND_BONDING) {
+            if (mA2dpProxy == null || mHeadsetProxy == null) {
+                getProfileProxy();
+            }
         }
 
         setProfilePriorities(address, state);
@@ -240,6 +244,8 @@
     }
 
     public synchronized void clearPinAttempts(String address) {
+        if (DBG) Log.d(TAG, "clearPinAttempts: " + address);
+
         mPinAttempt.remove(address);
     }
 
@@ -265,6 +271,8 @@
         } else {
             newAttempt = attempt.intValue() + 1;
         }
+        if (DBG) Log.d(TAG, "attemp newAttempt: " + newAttempt);
+
         mPinAttempt.put(address, new Integer(newAttempt));
     }
 
diff --git a/core/java/android/server/BluetoothEventLoop.java b/core/java/android/server/BluetoothEventLoop.java
index a220007..63c420a 100644
--- a/core/java/android/server/BluetoothEventLoop.java
+++ b/core/java/android/server/BluetoothEventLoop.java
@@ -49,6 +49,7 @@
     private boolean mInterrupted;
 
     private final HashMap<String, Integer> mPasskeyAgentRequestData;
+    private final HashMap<String, Integer> mAuthorizationAgentRequestData;
     private final BluetoothService mBluetoothService;
     private final BluetoothAdapter mAdapter;
     private BluetoothA2dp mA2dp;
@@ -110,6 +111,7 @@
         mBluetoothService = bluetoothService;
         mContext = context;
         mPasskeyAgentRequestData = new HashMap<String, Integer>();
+        mAuthorizationAgentRequestData = new HashMap<String, Integer>();
         mAdapter = adapter;
         //WakeLock instantiation in BluetoothEventLoop class
         PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
@@ -155,6 +157,10 @@
         return mPasskeyAgentRequestData;
     }
 
+    /* package */ HashMap<String, Integer> getAuthorizationAgentRequestData() {
+        return mAuthorizationAgentRequestData;
+    }
+
     /* package */ void start() {
 
         if (!isEventLoopRunningNative()) {
@@ -747,20 +753,22 @@
      *
      * @param objectPath the path of the device requesting to be authorized
      * @param deviceUuid the UUID of the requesting device
-     * @return true if the authorization is allowed; false if not allowed
+     * @param nativeData reference for native data
      */
-    private boolean onAgentAuthorize(String objectPath, String deviceUuid) {
-        if (!mBluetoothService.isEnabled()) return false;
+    private void  onAgentAuthorize(String objectPath, String deviceUuid, int nativeData) {
+        if (!mBluetoothService.isEnabled()) return;
 
         String address = mBluetoothService.getAddressFromObjectPath(objectPath);
         if (address == null) {
             Log.e(TAG, "Unable to get device address in onAuthAgentAuthorize");
-            return false;
+            return;
         }
 
         boolean authorized = false;
         ParcelUuid uuid = ParcelUuid.fromString(deviceUuid);
+
         BluetoothDevice device = mAdapter.getRemoteDevice(address);
+        mAuthorizationAgentRequestData.put(address, new Integer(nativeData));
 
         // Bluez sends the UUID of the local service being accessed, _not_ the
         // remote service
@@ -769,26 +777,29 @@
               || BluetoothUuid.isAdvAudioDist(uuid)) &&
               !isOtherSinkInNonDisconnectedState(address)) {
             authorized = mA2dp.getPriority(device) > BluetoothProfile.PRIORITY_OFF;
-            if (authorized) {
-                Log.i(TAG, "Allowing incoming A2DP / AVRCP connection from " + address);
+            if (authorized && !BluetoothUuid.isAvrcpTarget(uuid)) {
+                Log.i(TAG, "First check pass for incoming A2DP / AVRCP connection from " + address);
                 // Some headsets try to connect AVCTP before AVDTP - against the recommendation
                 // If AVCTP connection fails, we get stuck in IncomingA2DP state in the state
                 // machine.  We don't handle AVCTP signals currently. We only send
                 // intents for AVDTP state changes. We need to handle both of them in
                 // some cases. For now, just don't move to incoming state in this case.
-                if (!BluetoothUuid.isAvrcpTarget(uuid)) {
-                    mBluetoothService.notifyIncomingA2dpConnection(address);
-                }
+                mBluetoothService.notifyIncomingA2dpConnection(address);
             } else {
-                Log.i(TAG, "Rejecting incoming A2DP / AVRCP connection from " + address);
+                Log.i(TAG, "" + authorized +
+                      "Incoming A2DP / AVRCP connection from " + address);
+                mA2dp.allowIncomingConnect(device, authorized);
             }
         } else if (mInputDevice != null && BluetoothUuid.isInputDevice(uuid)) {
             // We can have more than 1 input device connected.
             authorized = mInputDevice.getPriority(device) > BluetoothInputDevice.PRIORITY_OFF;
              if (authorized) {
-                 Log.i(TAG, "Allowing incoming HID connection from " + address);
+                 Log.i(TAG, "First check pass for incoming HID connection from " + address);
+                 // notify profile state change
+                 mBluetoothService.notifyIncomingHidConnection(address);
              } else {
                  Log.i(TAG, "Rejecting incoming HID connection from " + address);
+                 mBluetoothService.allowIncomingHidConnect(device, authorized);
              }
         } else if (BluetoothUuid.isBnep(uuid) && mBluetoothService.allowIncomingTethering()){
             authorized = true;
@@ -796,7 +807,6 @@
             Log.i(TAG, "Rejecting incoming " + deviceUuid + " connection from " + address);
         }
         log("onAgentAuthorize(" + objectPath + ", " + deviceUuid + ") = " + authorized);
-        return authorized;
     }
 
     private boolean onAgentOutOfBandDataAvailable(String objectPath) {
diff --git a/core/java/android/server/BluetoothService.java b/core/java/android/server/BluetoothService.java
index b5ae298..9839f76 100644
--- a/core/java/android/server/BluetoothService.java
+++ b/core/java/android/server/BluetoothService.java
@@ -59,13 +59,18 @@
 import android.util.Pair;
 
 import java.io.BufferedInputStream;
+import java.io.BufferedReader;
 import java.io.BufferedWriter;
+import java.io.DataInputStream;
+import java.io.File;
 import java.io.FileDescriptor;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileWriter;
+import java.io.InputStreamReader;
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.io.RandomAccessFile;
 import java.io.UnsupportedEncodingException;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -151,6 +156,9 @@
     private BluetoothPanProfileHandler mBluetoothPanProfileHandler;
     private BluetoothInputProfileHandler mBluetoothInputProfileHandler;
     private BluetoothHealthProfileHandler mBluetoothHealthProfileHandler;
+    private static final String INCOMING_CONNECTION_FILE =
+      "/data/misc/bluetooth/incoming_connection.conf";
+    private HashMap<String, Pair<Integer, String>> mIncomingConnections;
 
     private static class RemoteService {
         public String address;
@@ -224,6 +232,7 @@
         mBluetoothInputProfileHandler = BluetoothInputProfileHandler.getInstance(mContext, this);
         mBluetoothPanProfileHandler = BluetoothPanProfileHandler.getInstance(mContext, this);
         mBluetoothHealthProfileHandler = BluetoothHealthProfileHandler.getInstance(mContext, this);
+        mIncomingConnections = new HashMap<String, Pair<Integer, String>>();
     }
 
     public static synchronized String readDockBluetoothAddress() {
@@ -2069,6 +2078,24 @@
         }
     }
 
+    public boolean allowIncomingHidConnect(BluetoothDevice device, boolean allow) {
+        mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
+                                                "Need BLUETOOTH_ADMIN permission");
+        String address = device.getAddress();
+        if (!BluetoothAdapter.checkBluetoothAddress(address)) {
+            return false;
+        }
+
+        Integer data = getAuthorizationAgentRequestData(address);
+        if (data == null) {
+            Log.w(TAG, "allowIncomingHidConnect(" + device +
+                  ") called but no native data available");
+            return false;
+        }
+        if (DBG) log("allowIncomingHidConnect: " + device + " : " + allow + " : " + data);
+        return setAuthorizationNative(address, allow, data.intValue());
+    }
+
     /*package*/List<BluetoothDevice> lookupInputDevicesMatchingStates(int[] states) {
         synchronized (mBluetoothInputProfileHandler) {
             return mBluetoothInputProfileHandler.lookupInputDevicesMatchingStates(states);
@@ -2181,6 +2208,17 @@
         }
     }
 
+    /*package*/boolean notifyIncomingHidConnection(String address) {
+        BluetoothDeviceProfileState state = mDeviceProfileState.get(address);
+        if (state == null) {
+            return false;
+        }
+        Message msg = new Message();
+        msg.what = BluetoothDeviceProfileState.CONNECT_HID_INCOMING;
+        state.sendMessage(msg);
+        return true;
+    }
+
     public boolean connectHeadset(String address) {
         if (getBondState(address) != BluetoothDevice.BOND_BONDED) return false;
 
@@ -2319,6 +2357,11 @@
         mA2dpService = a2dpService;
     }
 
+    /*package*/ Integer getAuthorizationAgentRequestData(String address) {
+        Integer data = mEventLoop.getAuthorizationAgentRequestData().remove(address);
+        return data;
+    }
+
     public void sendProfileStateMessage(int profile, int cmd) {
         Message msg = new Message();
         msg.what = cmd;
@@ -2422,6 +2465,120 @@
         }
     }
 
+    private void createIncomingConnectionStateFile() {
+        File f = new File(INCOMING_CONNECTION_FILE);
+        if (!f.exists()) {
+            try {
+                f.createNewFile();
+            } catch (IOException e) {
+                Log.e(TAG, "IOException: cannot create file");
+            }
+        }
+    }
+
+    /** @hide */
+    public Pair<Integer, String> getIncomingState(String address) {
+        if (mIncomingConnections.isEmpty()) {
+            createIncomingConnectionStateFile();
+            readIncomingConnectionState();
+        }
+        return mIncomingConnections.get(address);
+    }
+
+    private void readIncomingConnectionState() {
+        synchronized(mIncomingConnections) {
+            FileInputStream fstream = null;
+            try {
+              fstream = new FileInputStream(INCOMING_CONNECTION_FILE);
+              DataInputStream in = new DataInputStream(fstream);
+              BufferedReader file = new BufferedReader(new InputStreamReader(in));
+              String line;
+              while((line = file.readLine()) != null) {
+                  line = line.trim();
+                  if (line.length() == 0) continue;
+                  String[] value = line.split(",");
+                  if (value != null && value.length == 3) {
+                      Integer val1 = Integer.parseInt(value[1]);
+                      Pair<Integer, String> val = new Pair(val1, value[2]);
+                      mIncomingConnections.put(value[0], val);
+                  }
+              }
+            } catch (FileNotFoundException e) {
+                log("FileNotFoundException: readIncomingConnectionState" + e.toString());
+            } catch (IOException e) {
+                log("IOException: readIncomingConnectionState" + e.toString());
+            } finally {
+                if (fstream != null) {
+                    try {
+                        fstream.close();
+                    } catch (IOException e) {
+                        // Ignore
+                    }
+                }
+            }
+        }
+    }
+
+    private void truncateIncomingConnectionFile() {
+        RandomAccessFile r = null;
+        try {
+            r = new RandomAccessFile(INCOMING_CONNECTION_FILE, "rw");
+            r.setLength(0);
+        } catch (FileNotFoundException e) {
+            log("FileNotFoundException: truncateIncomingConnectionState" + e.toString());
+        } catch (IOException e) {
+            log("IOException: truncateIncomingConnectionState" + e.toString());
+        } finally {
+            if (r != null) {
+                try {
+                    r.close();
+                } catch (IOException e) {
+                    // ignore
+                 }
+            }
+        }
+    }
+
+    /** @hide */
+    public void writeIncomingConnectionState(String address, Pair<Integer, String> data) {
+        synchronized(mIncomingConnections) {
+            mIncomingConnections.put(address, data);
+
+            truncateIncomingConnectionFile();
+            BufferedWriter out = null;
+            StringBuilder value = new StringBuilder();
+            try {
+                out = new BufferedWriter(new FileWriter(INCOMING_CONNECTION_FILE, true));
+                for (String devAddress: mIncomingConnections.keySet()) {
+                  Pair<Integer, String> val = mIncomingConnections.get(devAddress);
+                  value.append(devAddress);
+                  value.append(",");
+                  value.append(val.first.toString());
+                  value.append(",");
+                  value.append(val.second);
+                  value.append("\n");
+                }
+                out.write(value.toString());
+            } catch (FileNotFoundException e) {
+                log("FileNotFoundException: writeIncomingConnectionState" + e.toString());
+            } catch (IOException e) {
+                log("IOException: writeIncomingConnectionState" + e.toString());
+            } finally {
+                if (out != null) {
+                    try {
+                        out.close();
+                    } catch (IOException e) {
+                        // Ignore
+                    }
+                }
+            }
+        }
+    }
+
+    private static void log(String msg) {
+        Log.d(TAG, msg);
+    }
+
     private native static void classInitNative();
     private native void initializeNativeDataNative();
     private native boolean setupNativeDataNative();
@@ -2468,6 +2625,7 @@
             short channel);
     private native boolean removeServiceRecordNative(int handle);
     private native boolean setLinkTimeoutNative(String path, int num_slots);
+
     native boolean connectInputDeviceNative(String path);
     native boolean disconnectInputDeviceNative(String path);
 
@@ -2491,4 +2649,5 @@
     native String getChannelApplicationNative(String channelPath);
     native ParcelFileDescriptor getChannelFdNative(String channelPath);
     native boolean releaseChannelFdNative(String channelPath);
+    native boolean setAuthorizationNative(String address, boolean value, int data);
 }
diff --git a/core/jni/android_server_BluetoothEventLoop.cpp b/core/jni/android_server_BluetoothEventLoop.cpp
index fb25486..2b09442 100644
--- a/core/jni/android_server_BluetoothEventLoop.cpp
+++ b/core/jni/android_server_BluetoothEventLoop.cpp
@@ -119,7 +119,7 @@
                                                          "(Ljava/lang/String;Z)V");
 
     method_onAgentAuthorize = env->GetMethodID(clazz, "onAgentAuthorize",
-                                               "(Ljava/lang/String;Ljava/lang/String;)Z");
+                                               "(Ljava/lang/String;Ljava/lang/String;I)V");
     method_onAgentOutOfBandDataAvailable = env->GetMethodID(clazz, "onAgentOutOfBandDataAvailable",
                                                "(Ljava/lang/String;)Z");
     method_onAgentCancel = env->GetMethodID(clazz, "onAgentCancel", "()V");
@@ -1114,29 +1114,11 @@
         LOGV("... object_path = %s", object_path);
         LOGV("... uuid = %s", uuid);
 
-        bool auth_granted =
-            env->CallBooleanMethod(nat->me, method_onAgentAuthorize,
-                env->NewStringUTF(object_path), env->NewStringUTF(uuid));
+        dbus_message_ref(msg);  // increment refcount because we pass to java
+        env->CallBooleanMethod(nat->me, method_onAgentAuthorize,
+                env->NewStringUTF(object_path), env->NewStringUTF(uuid),
+                int(msg));
 
-        // reply
-        if (auth_granted) {
-            DBusMessage *reply = dbus_message_new_method_return(msg);
-            if (!reply) {
-                LOGE("%s: Cannot create message reply\n", __FUNCTION__);
-                goto failure;
-            }
-            dbus_connection_send(nat->conn, reply, NULL);
-            dbus_message_unref(reply);
-        } else {
-            DBusMessage *reply = dbus_message_new_error(msg,
-                    "org.bluez.Error.Rejected", "Authorization rejected");
-            if (!reply) {
-                LOGE("%s: Cannot create message reply\n", __FUNCTION__);
-                goto failure;
-            }
-            dbus_connection_send(nat->conn, reply, NULL);
-            dbus_message_unref(reply);
-        }
         goto success;
     } else if (dbus_message_is_method_call(msg,
             "org.bluez.Agent", "OutOfBandAvailable")) {
diff --git a/core/jni/android_server_BluetoothService.cpp b/core/jni/android_server_BluetoothService.cpp
index 036c34c..1166ae4 100644
--- a/core/jni/android_server_BluetoothService.cpp
+++ b/core/jni/android_server_BluetoothService.cpp
@@ -255,7 +255,7 @@
 }
 
 static jboolean stopDiscoveryNative(JNIEnv *env, jobject object) {
-    LOGV(__FUNCTION__);
+    LOGV("%s", __FUNCTION__);
 #ifdef HAVE_BLUETOOTH
     DBusMessage *msg = NULL;
     DBusMessage *reply = NULL;
@@ -616,6 +616,35 @@
     return JNI_FALSE;
 }
 
+static jboolean setAuthorizationNative(JNIEnv *env, jobject object, jstring address,
+                         jboolean val, int nativeData) {
+#ifdef HAVE_BLUETOOTH
+  LOGV("%s", __FUNCTION__);
+    native_data_t *nat = get_native_data(env, object);
+    if (nat) {
+        DBusMessage *msg = (DBusMessage *)nativeData;
+        DBusMessage *reply;
+        if (val) {
+            reply = dbus_message_new_method_return(msg);
+        } else {
+            reply = dbus_message_new_error(msg,
+                    "org.bluez.Error.Rejected", "Authorization rejected");
+        }
+        if (!reply) {
+            LOGE("%s: Cannot create message reply D-Bus\n", __FUNCTION__);
+            dbus_message_unref(msg);
+            return JNI_FALSE;
+        }
+
+        dbus_connection_send(nat->conn, reply, NULL);
+        dbus_message_unref(msg);
+        dbus_message_unref(reply);
+        return JNI_TRUE;
+    }
+#endif
+    return JNI_FALSE;
+}
+
 static jboolean setPinNative(JNIEnv *env, jobject object, jstring address,
                          jstring pin, int nativeData) {
 #ifdef HAVE_BLUETOOTH
@@ -1692,6 +1721,7 @@
             (void *)setPairingConfirmationNative},
     {"setPasskeyNative", "(Ljava/lang/String;II)Z", (void *)setPasskeyNative},
     {"setRemoteOutOfBandDataNative", "(Ljava/lang/String;[B[BI)Z", (void *)setRemoteOutOfBandDataNative},
+    {"setAuthorizationNative", "(Ljava/lang/String;ZI)Z", (void *)setAuthorizationNative},
     {"setPinNative", "(Ljava/lang/String;Ljava/lang/String;I)Z", (void *)setPinNative},
     {"cancelPairingUserInputNative", "(Ljava/lang/String;I)Z",
             (void *)cancelPairingUserInputNative},