diff options
-rw-r--r-- | core/java/android/bluetooth/BluetoothA2dp.java | 19 | ||||
-rw-r--r-- | core/java/android/bluetooth/BluetoothDevice.java | 64 | ||||
-rw-r--r-- | core/java/android/bluetooth/BluetoothDeviceProfileState.java | 275 | ||||
-rw-r--r-- | core/java/android/bluetooth/BluetoothHeadset.java | 17 | ||||
-rw-r--r-- | core/java/android/bluetooth/BluetoothInputDevice.java | 22 | ||||
-rw-r--r-- | core/java/android/bluetooth/IBluetooth.aidl | 1 | ||||
-rw-r--r-- | core/java/android/bluetooth/IBluetoothA2dp.aidl | 2 | ||||
-rw-r--r-- | core/java/android/bluetooth/IBluetoothHeadset.aidl | 1 | ||||
-rw-r--r-- | core/java/android/server/BluetoothA2dpService.java | 16 | ||||
-rw-r--r-- | core/java/android/server/BluetoothBondState.java | 12 | ||||
-rw-r--r-- | core/java/android/server/BluetoothEventLoop.java | 34 | ||||
-rw-r--r-- | core/java/android/server/BluetoothService.java | 159 | ||||
-rw-r--r-- | core/jni/android_server_BluetoothEventLoop.cpp | 28 | ||||
-rw-r--r-- | core/jni/android_server_BluetoothService.cpp | 32 |
14 files changed, 632 insertions, 50 deletions
diff --git a/core/java/android/bluetooth/BluetoothA2dp.java b/core/java/android/bluetooth/BluetoothA2dp.java index 61d3707f8c64..96f3290160f0 100644 --- a/core/java/android/bluetooth/BluetoothA2dp.java +++ b/core/java/android/bluetooth/BluetoothA2dp.java @@ -423,7 +423,24 @@ 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. * * 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 254e2f813a04..d9525a347fac 100644 --- a/core/java/android/bluetooth/BluetoothDevice.java +++ b/core/java/android/bluetooth/BluetoothDevice.java @@ -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 diff --git a/core/java/android/bluetooth/BluetoothDeviceProfileState.java b/core/java/android/bluetooth/BluetoothDeviceProfileState.java index 56f236d164b9..ab3a4267a7f8 100644 --- a/core/java/android/bluetooth/BluetoothDeviceProfileState.java +++ b/core/java/android/bluetooth/BluetoothDeviceProfileState.java @@ -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,8 +180,15 @@ 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); } - } + } }; private boolean isPhoneDocked(BluetoothDevice autoConnectDevice) { @@ -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. diff --git a/core/java/android/bluetooth/BluetoothHeadset.java b/core/java/android/bluetooth/BluetoothHeadset.java index 23724f245cab..32843618dcdf 100644 --- a/core/java/android/bluetooth/BluetoothHeadset.java +++ b/core/java/android/bluetooth/BluetoothHeadset.java @@ -618,6 +618,23 @@ public final class BluetoothHeadset implements BluetoothProfile { } /** + * 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 282b70a42c81..f6757d9bd360 100644 --- a/core/java/android/bluetooth/BluetoothInputDevice.java +++ b/core/java/android/bluetooth/BluetoothInputDevice.java @@ -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; diff --git a/core/java/android/bluetooth/IBluetooth.aidl b/core/java/android/bluetooth/IBluetooth.aidl index 28b09b6db21d..6ca6c2e1037e 100644 --- a/core/java/android/bluetooth/IBluetooth.aidl +++ b/core/java/android/bluetooth/IBluetooth.aidl @@ -91,6 +91,7 @@ interface IBluetooth 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 b4fc36660031..444dd1e95bc7 100644 --- a/core/java/android/bluetooth/IBluetoothA2dp.aidl +++ b/core/java/android/bluetooth/IBluetoothA2dp.aidl @@ -39,4 +39,6 @@ interface IBluetoothA2dp { 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 273cda73bea5..ec005275bbc5 100644 --- a/core/java/android/bluetooth/IBluetoothHeadset.aidl +++ b/core/java/android/bluetooth/IBluetoothHeadset.aidl @@ -42,6 +42,7 @@ interface IBluetoothHeadset { // 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 8a6fdb4fb325..fd277d0cac26 100644 --- a/core/java/android/server/BluetoothA2dpService.java +++ b/core/java/android/server/BluetoothA2dpService.java @@ -449,6 +449,22 @@ public class BluetoothA2dpService extends IBluetoothA2dp.Stub { 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 76e78858e029..75f38f934f48 100644 --- a/core/java/android/server/BluetoothBondState.java +++ b/core/java/android/server/BluetoothBondState.java @@ -121,6 +121,8 @@ class BluetoothBondState { /** 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 @@ class BluetoothBondState { 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 @@ class BluetoothBondState { } public synchronized void clearPinAttempts(String address) { + if (DBG) Log.d(TAG, "clearPinAttempts: " + address); + mPinAttempt.remove(address); } @@ -265,6 +271,8 @@ class BluetoothBondState { } 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 a2200073de5d..63c420aab613 100644 --- a/core/java/android/server/BluetoothEventLoop.java +++ b/core/java/android/server/BluetoothEventLoop.java @@ -49,6 +49,7 @@ class BluetoothEventLoop { 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 @@ class BluetoothEventLoop { 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 @@ class BluetoothEventLoop { return mPasskeyAgentRequestData; } + /* package */ HashMap<String, Integer> getAuthorizationAgentRequestData() { + return mAuthorizationAgentRequestData; + } + /* package */ void start() { if (!isEventLoopRunningNative()) { @@ -747,20 +753,22 @@ class BluetoothEventLoop { * * @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 @@ class BluetoothEventLoop { || 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 @@ class BluetoothEventLoop { 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 b5ae298f36e6..9839f763e479 100644 --- a/core/java/android/server/BluetoothService.java +++ b/core/java/android/server/BluetoothService.java @@ -59,13 +59,18 @@ import android.util.Log; 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 @@ public class BluetoothService extends IBluetooth.Stub { 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 @@ public class BluetoothService extends IBluetooth.Stub { 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 class BluetoothService extends IBluetooth.Stub { } } + 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 @@ public class BluetoothService extends IBluetooth.Stub { } } + /*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 @@ public class BluetoothService extends IBluetooth.Stub { 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 @@ public class BluetoothService extends IBluetooth.Stub { } } + 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 @@ public class BluetoothService extends IBluetooth.Stub { 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 @@ public class BluetoothService extends IBluetooth.Stub { 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 fb25486249b7..2b09442afb6a 100644 --- a/core/jni/android_server_BluetoothEventLoop.cpp +++ b/core/jni/android_server_BluetoothEventLoop.cpp @@ -119,7 +119,7 @@ static void classInitNative(JNIEnv* env, jclass clazz) { "(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 @@ DBusHandlerResult agent_event_filter(DBusConnection *conn, 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 036c34c17666..1166ae426bc5 100644 --- a/core/jni/android_server_BluetoothService.cpp +++ b/core/jni/android_server_BluetoothService.cpp @@ -255,7 +255,7 @@ done: } 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 @@ static jboolean setRemoteOutOfBandDataNative(JNIEnv *env, jobject object, jstrin 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 @@ static JNINativeMethod sMethods[] = { (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}, |