Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a0c68039 authored by Matthew Xie's avatar Matthew Xie
Browse files

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
parent b75a7984
Loading
Loading
Loading
Loading
+18 −1
Original line number Diff line number Diff line
@@ -423,6 +423,23 @@ public final class BluetoothA2dp implements BluetoothProfile {
        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.
     *
+64 −0
Original line number Diff line number Diff line
@@ -276,6 +276,70 @@ public final class BluetoothDevice implements Parcelable {
    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
+264 −11
Original line number Diff line number Diff line
@@ -22,9 +22,11 @@ import android.content.Intent;
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 final class BluetoothDeviceProfileState extends StateMachine {
    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 @@ public final class BluetoothDeviceProfileState extends StateMachine {
    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 @@ public final class BluetoothDeviceProfileState extends StateMachine {
            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 @@ public final class BluetoothDeviceProfileState extends StateMachine {
            } 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 @@ public final class BluetoothDeviceProfileState extends StateMachine {
                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,6 +180,13 @@ public final class BluetoothDeviceProfileState extends StateMachine {
                // 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);
            }
        }
    };
@@ -195,6 +230,7 @@ public final class BluetoothDeviceProfileState extends StateMachine {
        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 @@ public final class BluetoothDeviceProfileState extends StateMachine {
                                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 @@ public final class BluetoothDeviceProfileState extends StateMachine {
                    // 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 @@ public final class BluetoothDeviceProfileState extends StateMachine {
                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 @@ public final class BluetoothDeviceProfileState extends StateMachine {
              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 @@ public final class BluetoothDeviceProfileState extends StateMachine {
        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 @@ public final class BluetoothDeviceProfileState extends StateMachine {
            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 @@ public final class BluetoothDeviceProfileState extends StateMachine {
                }
                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 @@ public final class BluetoothDeviceProfileState extends StateMachine {
                }
                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 @@ public final class BluetoothDeviceProfileState extends StateMachine {
        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.
+17 −0
Original line number Diff line number Diff line
@@ -617,6 +617,23 @@ public final class BluetoothHeadset implements BluetoothProfile {
        return false;
    }

    /**
     * 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
+22 −0
Original line number Diff line number Diff line
@@ -308,6 +308,28 @@ public final class BluetoothInputDevice implements BluetoothProfile {
        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;
Loading