diff options
Diffstat (limited to 'core/java/android/server')
-rw-r--r-- | core/java/android/server/BluetoothA2dpService.java | 597 | ||||
-rw-r--r-- | core/java/android/server/BluetoothDeviceService.java | 1263 | ||||
-rw-r--r-- | core/java/android/server/BluetoothEventLoop.java | 513 | ||||
-rw-r--r-- | core/java/android/server/BluetoothService.java | 1673 | ||||
-rw-r--r-- | core/java/android/server/search/SearchDialogWrapper.java | 17 | ||||
-rw-r--r-- | core/java/android/server/search/SearchManagerService.java | 25 |
6 files changed, 2395 insertions, 1693 deletions
diff --git a/core/java/android/server/BluetoothA2dpService.java b/core/java/android/server/BluetoothA2dpService.java index 5c4e56d..ec3b2ff 100644 --- a/core/java/android/server/BluetoothA2dpService.java +++ b/core/java/android/server/BluetoothA2dpService.java @@ -23,17 +23,16 @@ package android.server; import android.bluetooth.BluetoothA2dp; +import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothError; -import android.bluetooth.BluetoothIntent; +import android.bluetooth.BluetoothUuid; import android.bluetooth.IBluetoothA2dp; +import android.os.ParcelUuid; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.PackageManager; import android.media.AudioManager; -import android.os.Binder; import android.os.Handler; import android.os.Message; import android.provider.Settings; @@ -41,9 +40,9 @@ import android.util.Log; import java.io.FileDescriptor; import java.io.PrintWriter; -import java.util.ArrayList; import java.util.HashMap; -import java.util.List; +import java.util.HashSet; +import java.util.Set; public class BluetoothA2dpService extends IBluetoothA2dp.Stub { private static final String TAG = "BluetoothA2dpService"; @@ -54,386 +53,423 @@ public class BluetoothA2dpService extends IBluetoothA2dp.Stub { private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN; private static final String BLUETOOTH_PERM = android.Manifest.permission.BLUETOOTH; - private static final String A2DP_SINK_ADDRESS = "a2dp_sink_address"; private static final String BLUETOOTH_ENABLED = "bluetooth_enabled"; private static final int MESSAGE_CONNECT_TO = 1; - private static final int MESSAGE_DISCONNECT = 2; - private final Context mContext; - private final IntentFilter mIntentFilter; - private HashMap<String, SinkState> mAudioDevices; - private final AudioManager mAudioManager; - private final BluetoothDevice mBluetooth; - - // list of disconnected sinks to process after a delay - private final ArrayList<String> mPendingDisconnects = new ArrayList<String>(); - // number of active sinks - private int mSinkCount = 0; - - private class SinkState { - public String address; - public int state; - public SinkState(String a, int s) {address = a; state = s;} - } - - public BluetoothA2dpService(Context context) { - mContext = context; - - mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - - mBluetooth = (BluetoothDevice) mContext.getSystemService(Context.BLUETOOTH_SERVICE); - if (mBluetooth == null) { - throw new RuntimeException("Platform does not support Bluetooth"); - } - - if (!initNative()) { - throw new RuntimeException("Could not init BluetoothA2dpService"); - } + private static final String PROPERTY_STATE = "State"; - mIntentFilter = new IntentFilter(BluetoothIntent.BLUETOOTH_STATE_CHANGED_ACTION); - mIntentFilter.addAction(BluetoothIntent.BOND_STATE_CHANGED_ACTION); - mIntentFilter.addAction(BluetoothIntent.REMOTE_DEVICE_CONNECTED_ACTION); - mContext.registerReceiver(mReceiver, mIntentFilter); + private static final String SINK_STATE_DISCONNECTED = "disconnected"; + private static final String SINK_STATE_CONNECTING = "connecting"; + private static final String SINK_STATE_CONNECTED = "connected"; + private static final String SINK_STATE_PLAYING = "playing"; - if (mBluetooth.isEnabled()) { - onBluetoothEnable(); - } - } + private static int mSinkCount; - @Override - protected void finalize() throws Throwable { - try { - cleanupNative(); - } finally { - super.finalize(); - } - } + private final Context mContext; + private final IntentFilter mIntentFilter; + private HashMap<BluetoothDevice, Integer> mAudioDevices; + private final AudioManager mAudioManager; + private final BluetoothService mBluetoothService; + private final BluetoothAdapter mAdapter; + private int mTargetA2dpState; private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); - String address = intent.getStringExtra(BluetoothIntent.ADDRESS); - if (action.equals(BluetoothIntent.BLUETOOTH_STATE_CHANGED_ACTION)) { - int state = intent.getIntExtra(BluetoothIntent.BLUETOOTH_STATE, - BluetoothError.ERROR); + BluetoothDevice device = + intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { + int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, + BluetoothAdapter.ERROR); switch (state) { - case BluetoothDevice.BLUETOOTH_STATE_ON: + case BluetoothAdapter.STATE_ON: onBluetoothEnable(); break; - case BluetoothDevice.BLUETOOTH_STATE_TURNING_OFF: + case BluetoothAdapter.STATE_TURNING_OFF: onBluetoothDisable(); break; } - } else if (action.equals(BluetoothIntent.BOND_STATE_CHANGED_ACTION)) { - int bondState = intent.getIntExtra(BluetoothIntent.BOND_STATE, - BluetoothError.ERROR); + } else if (action.equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) { + int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, + BluetoothDevice.ERROR); switch(bondState) { case BluetoothDevice.BOND_BONDED: - setSinkPriority(address, BluetoothA2dp.PRIORITY_AUTO); + setSinkPriority(device, BluetoothA2dp.PRIORITY_AUTO); break; case BluetoothDevice.BOND_BONDING: - case BluetoothDevice.BOND_NOT_BONDED: - setSinkPriority(address, BluetoothA2dp.PRIORITY_OFF); + case BluetoothDevice.BOND_NONE: + setSinkPriority(device, BluetoothA2dp.PRIORITY_OFF); break; } - } else if (action.equals(BluetoothIntent.REMOTE_DEVICE_CONNECTED_ACTION)) { - if (getSinkPriority(address) > BluetoothA2dp.PRIORITY_OFF) { + } else if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) { + if (getSinkPriority(device) > BluetoothA2dp.PRIORITY_OFF && + isSinkDevice(device)) { // This device is a preferred sink. Make an A2DP connection // after a delay. We delay to avoid connection collisions, // and to give other profiles such as HFP a chance to // connect first. - Message msg = Message.obtain(mHandler, MESSAGE_CONNECT_TO, address); + Message msg = Message.obtain(mHandler, MESSAGE_CONNECT_TO, device); mHandler.sendMessageDelayed(msg, 6000); } + } else if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) { + synchronized (this) { + if (mAudioDevices.containsKey(device)) { + int state = mAudioDevices.get(device); + handleSinkStateChange(device, state, BluetoothA2dp.STATE_DISCONNECTED); + } + } } } }; + public BluetoothA2dpService(Context context, BluetoothService bluetoothService) { + mContext = context; + + mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + + mBluetoothService = bluetoothService; + if (mBluetoothService == null) { + throw new RuntimeException("Platform does not support Bluetooth"); + } + + if (!initNative()) { + throw new RuntimeException("Could not init BluetoothA2dpService"); + } + + mAdapter = BluetoothAdapter.getDefaultAdapter(); + + mIntentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); + mIntentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); + mIntentFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); + mIntentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); + mContext.registerReceiver(mReceiver, mIntentFilter); + + mAudioDevices = new HashMap<BluetoothDevice, Integer>(); + + if (mBluetoothService.isEnabled()) + onBluetoothEnable(); + mTargetA2dpState = -1; + } + + @Override + protected void finalize() throws Throwable { + try { + cleanupNative(); + } finally { + super.finalize(); + } + } + private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case MESSAGE_CONNECT_TO: - String address = (String)msg.obj; + BluetoothDevice device = (BluetoothDevice) msg.obj; // check bluetooth is still on, device is still preferred, and // nothing is currently connected - if (mBluetooth.isEnabled() && - getSinkPriority(address) > BluetoothA2dp.PRIORITY_OFF && + if (mBluetoothService.isEnabled() && + getSinkPriority(device) > BluetoothA2dp.PRIORITY_OFF && lookupSinksMatchingStates(new int[] { BluetoothA2dp.STATE_CONNECTING, BluetoothA2dp.STATE_CONNECTED, BluetoothA2dp.STATE_PLAYING, BluetoothA2dp.STATE_DISCONNECTING}).size() == 0) { - log("Auto-connecting A2DP to sink " + address); - connectSink(address); + log("Auto-connecting A2DP to sink " + device); + connectSink(device); } break; - case MESSAGE_DISCONNECT: - handleDeferredDisconnect((String)msg.obj); - break; } } }; - private synchronized void onBluetoothEnable() { - mAudioDevices = new HashMap<String, SinkState>(); - String[] paths = (String[])listHeadsetsNative(); - if (paths != null) { - for (String path : paths) { - mAudioDevices.put(path, new SinkState(getAddressNative(path), - isSinkConnectedNative(path) ? BluetoothA2dp.STATE_CONNECTED : - BluetoothA2dp.STATE_DISCONNECTED)); + private int convertBluezSinkStringtoState(String value) { + if (value.equalsIgnoreCase("disconnected")) + return BluetoothA2dp.STATE_DISCONNECTED; + if (value.equalsIgnoreCase("connecting")) + return BluetoothA2dp.STATE_CONNECTING; + if (value.equalsIgnoreCase("connected")) + return BluetoothA2dp.STATE_CONNECTED; + if (value.equalsIgnoreCase("playing")) + return BluetoothA2dp.STATE_PLAYING; + return -1; + } + + private boolean isSinkDevice(BluetoothDevice device) { + ParcelUuid[] uuids = mBluetoothService.getRemoteUuids(device.getAddress()); + if (uuids != null && BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.AudioSink)) { + return true; + } + return false; + } + + private synchronized boolean addAudioSink (BluetoothDevice device) { + String path = mBluetoothService.getObjectPathFromAddress(device.getAddress()); + String propValues[] = (String []) getSinkPropertiesNative(path); + if (propValues == null) { + Log.e(TAG, "Error while getting AudioSink properties for device: " + device); + return false; + } + Integer state = null; + // Properties are name-value pairs + for (int i = 0; i < propValues.length; i+=2) { + if (propValues[i].equals(PROPERTY_STATE)) { + state = new Integer(convertBluezSinkStringtoState(propValues[i+1])); + break; } } - mAudioManager.setParameter(BLUETOOTH_ENABLED, "true"); + mAudioDevices.put(device, state); + handleSinkStateChange(device, BluetoothA2dp.STATE_DISCONNECTED, state); + return true; + } + + private synchronized void onBluetoothEnable() { + String devices = mBluetoothService.getProperty("Devices"); + mSinkCount = 0; + if (devices != null) { + String [] paths = devices.split(","); + for (String path: paths) { + String address = mBluetoothService.getAddressFromObjectPath(path); + BluetoothDevice device = mAdapter.getRemoteDevice(address); + ParcelUuid[] remoteUuids = mBluetoothService.getRemoteUuids(address); + if (remoteUuids != null) + if (BluetoothUuid.containsAnyUuid(remoteUuids, + new ParcelUuid[] {BluetoothUuid.AudioSink, + BluetoothUuid.AdvAudioDist})) { + addAudioSink(device); + } + } + } + mAudioManager.setParameters(BLUETOOTH_ENABLED+"=true"); + mAudioManager.setParameters("A2dpSuspended=false"); } private synchronized void onBluetoothDisable() { - if (mAudioDevices != null) { - // copy to allow modification during iteration - String[] paths = new String[mAudioDevices.size()]; - paths = mAudioDevices.keySet().toArray(paths); - for (String path : paths) { - switch (mAudioDevices.get(path).state) { + if (!mAudioDevices.isEmpty()) { + BluetoothDevice[] devices = new BluetoothDevice[mAudioDevices.size()]; + devices = mAudioDevices.keySet().toArray(devices); + for (BluetoothDevice device : devices) { + int state = getSinkState(device); + switch (state) { case BluetoothA2dp.STATE_CONNECTING: case BluetoothA2dp.STATE_CONNECTED: case BluetoothA2dp.STATE_PLAYING: - disconnectSinkNative(path); - updateState(path, BluetoothA2dp.STATE_DISCONNECTED); + disconnectSinkNative(mBluetoothService.getObjectPathFromAddress( + device.getAddress())); + handleSinkStateChange(device, state, BluetoothA2dp.STATE_DISCONNECTED); break; case BluetoothA2dp.STATE_DISCONNECTING: - updateState(path, BluetoothA2dp.STATE_DISCONNECTED); + handleSinkStateChange(device, BluetoothA2dp.STATE_DISCONNECTING, + BluetoothA2dp.STATE_DISCONNECTED); break; } } - mAudioDevices = null; + mAudioDevices.clear(); } - mAudioManager.setBluetoothA2dpOn(false); - mAudioManager.setParameter(BLUETOOTH_ENABLED, "false"); + + mAudioManager.setParameters(BLUETOOTH_ENABLED + "=false"); } - public synchronized int connectSink(String address) { + public synchronized boolean connectSink(BluetoothDevice device) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); - if (DBG) log("connectSink(" + address + ")"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return BluetoothError.ERROR; - } - if (mAudioDevices == null) { - return BluetoothError.ERROR; - } + if (DBG) log("connectSink(" + device + ")"); + // ignore if there are any active sinks if (lookupSinksMatchingStates(new int[] { BluetoothA2dp.STATE_CONNECTING, BluetoothA2dp.STATE_CONNECTED, BluetoothA2dp.STATE_PLAYING, BluetoothA2dp.STATE_DISCONNECTING}).size() != 0) { - return BluetoothError.ERROR; + return false; } - String path = lookupPath(address); - if (path == null) { - path = createHeadsetNative(address); - if (DBG) log("new bluez sink: " + address + " (" + path + ")"); - } - if (path == null) { - return BluetoothError.ERROR; - } + if (mAudioDevices.get(device) == null && !addAudioSink(device)) + return false; + + int state = mAudioDevices.get(device); - SinkState sink = mAudioDevices.get(path); - int state = BluetoothA2dp.STATE_DISCONNECTED; - if (sink != null) { - state = sink.state; - } switch (state) { case BluetoothA2dp.STATE_CONNECTED: case BluetoothA2dp.STATE_PLAYING: case BluetoothA2dp.STATE_DISCONNECTING: - return BluetoothError.ERROR; + return false; case BluetoothA2dp.STATE_CONNECTING: - return BluetoothError.SUCCESS; + return true; } + String path = mBluetoothService.getObjectPathFromAddress(device.getAddress()); + if (path == null) + return false; + // State is DISCONNECTED if (!connectSinkNative(path)) { - return BluetoothError.ERROR; + return false; } - updateState(path, BluetoothA2dp.STATE_CONNECTING); - return BluetoothError.SUCCESS; + return true; } - public synchronized int disconnectSink(String address) { + public synchronized boolean disconnectSink(BluetoothDevice device) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); - if (DBG) log("disconnectSink(" + address + ")"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return BluetoothError.ERROR; - } - if (mAudioDevices == null) { - return BluetoothError.ERROR; - } - String path = lookupPath(address); + if (DBG) log("disconnectSink(" + device + ")"); + + String path = mBluetoothService.getObjectPathFromAddress(device.getAddress()); if (path == null) { - return BluetoothError.ERROR; + return false; } - switch (mAudioDevices.get(path).state) { + + switch (getSinkState(device)) { case BluetoothA2dp.STATE_DISCONNECTED: - return BluetoothError.ERROR; + return false; case BluetoothA2dp.STATE_DISCONNECTING: - return BluetoothError.SUCCESS; + return true; } // State is CONNECTING or CONNECTED or PLAYING if (!disconnectSinkNative(path)) { - return BluetoothError.ERROR; + return false; } else { - updateState(path, BluetoothA2dp.STATE_DISCONNECTING); - return BluetoothError.SUCCESS; + return true; } } - public synchronized List<String> listConnectedSinks() { + public synchronized boolean suspendSink(BluetoothDevice device) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + if (DBG) log("suspendSink(" + device + "), mTargetA2dpState: "+mTargetA2dpState); + if (device == null || mAudioDevices == null) { + return false; + } + String path = mBluetoothService.getObjectPathFromAddress(device.getAddress()); + Integer state = mAudioDevices.get(device); + if (path == null || state == null) { + return false; + } + + mTargetA2dpState = BluetoothA2dp.STATE_CONNECTED; + return checkSinkSuspendState(state.intValue()); + } + + public synchronized boolean resumeSink(BluetoothDevice device) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + if (DBG) log("resumeSink(" + device + "), mTargetA2dpState: "+mTargetA2dpState); + if (device == null || mAudioDevices == null) { + return false; + } + String path = mBluetoothService.getObjectPathFromAddress(device.getAddress()); + Integer state = mAudioDevices.get(device); + if (path == null || state == null) { + return false; + } + mTargetA2dpState = BluetoothA2dp.STATE_PLAYING; + return checkSinkSuspendState(state.intValue()); + } + + public synchronized BluetoothDevice[] getConnectedSinks() { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return lookupSinksMatchingStates(new int[] {BluetoothA2dp.STATE_CONNECTED, - BluetoothA2dp.STATE_PLAYING}); + Set<BluetoothDevice> sinks = lookupSinksMatchingStates( + new int[] {BluetoothA2dp.STATE_CONNECTED, BluetoothA2dp.STATE_PLAYING}); + return sinks.toArray(new BluetoothDevice[sinks.size()]); } - public synchronized int getSinkState(String address) { + public synchronized int getSinkState(BluetoothDevice device) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return BluetoothError.ERROR; - } - if (mAudioDevices == null) { + Integer state = mAudioDevices.get(device); + if (state == null) return BluetoothA2dp.STATE_DISCONNECTED; - } - for (SinkState sink : mAudioDevices.values()) { - if (address.equals(sink.address)) { - return sink.state; - } - } - return BluetoothA2dp.STATE_DISCONNECTED; + return state; } - public synchronized int getSinkPriority(String address) { + public synchronized int getSinkPriority(BluetoothDevice device) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return BluetoothError.ERROR; - } return Settings.Secure.getInt(mContext.getContentResolver(), - Settings.Secure.getBluetoothA2dpSinkPriorityKey(address), + Settings.Secure.getBluetoothA2dpSinkPriorityKey(device.getAddress()), BluetoothA2dp.PRIORITY_OFF); } - public synchronized int setSinkPriority(String address, int priority) { + public synchronized boolean setSinkPriority(BluetoothDevice device, int priority) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return BluetoothError.ERROR; + if (!BluetoothAdapter.checkBluetoothAddress(device.getAddress())) { + return false; } return Settings.Secure.putInt(mContext.getContentResolver(), - Settings.Secure.getBluetoothA2dpSinkPriorityKey(address), priority) ? - BluetoothError.SUCCESS : BluetoothError.ERROR; + Settings.Secure.getBluetoothA2dpSinkPriorityKey(device.getAddress()), priority); } - private synchronized void onHeadsetCreated(String path) { - updateState(path, BluetoothA2dp.STATE_DISCONNECTED); - } - - private synchronized void onHeadsetRemoved(String path) { - if (mAudioDevices == null) return; - mAudioDevices.remove(path); - } - - private synchronized void onSinkConnected(String path) { - // if we are reconnected, do not process previous disconnect event. - mPendingDisconnects.remove(path); - - if (mAudioDevices == null) return; - // bluez 3.36 quietly disconnects the previous sink when a new sink - // is connected, so we need to mark all previously connected sinks as - // disconnected - - // copy to allow modification during iteration - String[] paths = new String[mAudioDevices.size()]; - paths = mAudioDevices.keySet().toArray(paths); - for (String oldPath : paths) { - if (path.equals(oldPath)) { - continue; - } - int state = mAudioDevices.get(oldPath).state; - if (state == BluetoothA2dp.STATE_CONNECTED || state == BluetoothA2dp.STATE_PLAYING) { - updateState(path, BluetoothA2dp.STATE_DISCONNECTED); - } + private synchronized void onSinkPropertyChanged(String path, String []propValues) { + if (!mBluetoothService.isEnabled()) { + return; } - updateState(path, BluetoothA2dp.STATE_CONNECTING); - mAudioManager.setParameter(A2DP_SINK_ADDRESS, lookupAddress(path)); - mAudioManager.setBluetoothA2dpOn(true); - updateState(path, BluetoothA2dp.STATE_CONNECTED); - } + String name = propValues[0]; + String address = mBluetoothService.getAddressFromObjectPath(path); + if (address == null) { + Log.e(TAG, "onSinkPropertyChanged: Address of the remote device in null"); + return; + } - private synchronized void onSinkDisconnected(String path) { - // This is to work around a problem in bluez that results - // sink disconnect events being sent, immediately followed by a reconnect. - // To avoid unnecessary audio routing changes, we defer handling - // sink disconnects until after a short delay. - mPendingDisconnects.add(path); - Message msg = Message.obtain(mHandler, MESSAGE_DISCONNECT, path); - mHandler.sendMessageDelayed(msg, 2000); - } + BluetoothDevice device = mAdapter.getRemoteDevice(address); - private synchronized void handleDeferredDisconnect(String path) { - if (mPendingDisconnects.contains(path)) { - mPendingDisconnects.remove(path); - if (mSinkCount == 1) { - mAudioManager.setBluetoothA2dpOn(false); + if (name.equals(PROPERTY_STATE)) { + int state = convertBluezSinkStringtoState(propValues[1]); + if (mAudioDevices.get(device) == null) { + // This is for an incoming connection for a device not known to us. + // We have authorized it and bluez state has changed. + addAudioSink(device); + } else { + int prevState = mAudioDevices.get(device); + handleSinkStateChange(device, prevState, state); } - updateState(path, BluetoothA2dp.STATE_DISCONNECTED); } } - private synchronized void onSinkPlaying(String path) { - updateState(path, BluetoothA2dp.STATE_PLAYING); - } - - private synchronized void onSinkStopped(String path) { - updateState(path, BluetoothA2dp.STATE_CONNECTED); - } - - private synchronized final String lookupAddress(String path) { - if (mAudioDevices == null) return null; - SinkState sink = mAudioDevices.get(path); - if (sink == null) { - Log.w(TAG, "lookupAddress() called for unknown device " + path); - updateState(path, BluetoothA2dp.STATE_DISCONNECTED); - } - String address = mAudioDevices.get(path).address; - if (address == null) Log.e(TAG, "Can't find address for " + path); - return address; - } + private void handleSinkStateChange(BluetoothDevice device, int prevState, int state) { + if (state != prevState) { + if (state == BluetoothA2dp.STATE_DISCONNECTED || + state == BluetoothA2dp.STATE_DISCONNECTING) { + if (prevState == BluetoothA2dp.STATE_CONNECTED || + prevState == BluetoothA2dp.STATE_PLAYING) { + // disconnecting or disconnected + Intent intent = new Intent(AudioManager.ACTION_AUDIO_BECOMING_NOISY); + mContext.sendBroadcast(intent); + } + mSinkCount--; + } else if (state == BluetoothA2dp.STATE_CONNECTED) { + mSinkCount ++; + } + mAudioDevices.put(device, state); - private synchronized final String lookupPath(String address) { - if (mAudioDevices == null) return null; + checkSinkSuspendState(state); + mTargetA2dpState = -1; - for (String path : mAudioDevices.keySet()) { - if (address.equals(mAudioDevices.get(path).address)) { - return path; + if (state == BluetoothA2dp.STATE_CONNECTING) { + mAudioManager.setParameters("A2dpSuspended=false"); } + Intent intent = new Intent(BluetoothA2dp.ACTION_SINK_STATE_CHANGED); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); + intent.putExtra(BluetoothA2dp.EXTRA_PREVIOUS_SINK_STATE, prevState); + intent.putExtra(BluetoothA2dp.EXTRA_SINK_STATE, state); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); + + if (DBG) log("A2DP state : device: " + device + " State:" + prevState + "->" + state); } - return null; } - private synchronized List<String> lookupSinksMatchingStates(int[] states) { - List<String> sinks = new ArrayList<String>(); - if (mAudioDevices == null) { + private synchronized Set<BluetoothDevice> lookupSinksMatchingStates(int[] states) { + Set<BluetoothDevice> sinks = new HashSet<BluetoothDevice>(); + if (mAudioDevices.isEmpty()) { return sinks; } - for (SinkState sink : mAudioDevices.values()) { + for (BluetoothDevice device: mAudioDevices.keySet()) { + int sinkState = getSinkState(device); for (int state : states) { - if (sink.state == state) { - sinks.add(sink.address); + if (state == sinkState) { + sinks.add(device); break; } } @@ -441,57 +477,30 @@ public class BluetoothA2dpService extends IBluetoothA2dp.Stub { return sinks; } - private synchronized void updateState(String path, int state) { - if (mAudioDevices == null) return; - - SinkState s = mAudioDevices.get(path); - int prevState; - String address; - if (s == null) { - address = getAddressNative(path); - mAudioDevices.put(path, new SinkState(address, state)); - prevState = BluetoothA2dp.STATE_DISCONNECTED; - } else { - address = lookupAddress(path); - prevState = s.state; - s.state = state; - } - - if (state != prevState) { - if (DBG) log("state " + address + " (" + path + ") " + prevState + "->" + state); - - // keep track of the number of active sinks - if (prevState == BluetoothA2dp.STATE_DISCONNECTED) { - mSinkCount++; - } else if (state == BluetoothA2dp.STATE_DISCONNECTED) { - mSinkCount--; - } - - Intent intent = new Intent(BluetoothA2dp.SINK_STATE_CHANGED_ACTION); - intent.putExtra(BluetoothIntent.ADDRESS, address); - intent.putExtra(BluetoothA2dp.SINK_PREVIOUS_STATE, prevState); - intent.putExtra(BluetoothA2dp.SINK_STATE, state); - mContext.sendBroadcast(intent, BLUETOOTH_PERM); - - if ((prevState == BluetoothA2dp.STATE_CONNECTED || - prevState == BluetoothA2dp.STATE_PLAYING) && - (state != BluetoothA2dp.STATE_CONNECTING && - state != BluetoothA2dp.STATE_CONNECTED && - state != BluetoothA2dp.STATE_PLAYING)) { - // disconnected - intent = new Intent(AudioManager.ACTION_AUDIO_BECOMING_NOISY); - mContext.sendBroadcast(intent); + private boolean checkSinkSuspendState(int state) { + boolean result = true; + + if (state != mTargetA2dpState) { + if (state == BluetoothA2dp.STATE_PLAYING && + mTargetA2dpState == BluetoothA2dp.STATE_CONNECTED) { + mAudioManager.setParameters("A2dpSuspended=true"); + } else if (state == BluetoothA2dp.STATE_CONNECTED && + mTargetA2dpState == BluetoothA2dp.STATE_PLAYING) { + mAudioManager.setParameters("A2dpSuspended=false"); + } else { + result = false; } } + return result; } @Override protected synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - if (mAudioDevices == null) return; + if (mAudioDevices.isEmpty()) return; pw.println("Cached audio devices:"); - for (String path : mAudioDevices.keySet()) { - SinkState sink = mAudioDevices.get(path); - pw.println(path + " " + sink.address + " " + BluetoothA2dp.stateToString(sink.state)); + for (BluetoothDevice device : mAudioDevices.keySet()) { + int state = mAudioDevices.get(device); + pw.println(device + " " + BluetoothA2dp.stateToString(state)); } } @@ -501,11 +510,9 @@ public class BluetoothA2dpService extends IBluetoothA2dp.Stub { private native boolean initNative(); private native void cleanupNative(); - private synchronized native String[] listHeadsetsNative(); - private synchronized native String createHeadsetNative(String address); - private synchronized native boolean removeHeadsetNative(String path); - private synchronized native String getAddressNative(String path); private synchronized native boolean connectSinkNative(String path); private synchronized native boolean disconnectSinkNative(String path); - private synchronized native boolean isSinkConnectedNative(String path); + private synchronized native boolean suspendSinkNative(String path); + private synchronized native boolean resumeSinkNative(String path); + private synchronized native Object []getSinkPropertiesNative(String path); } diff --git a/core/java/android/server/BluetoothDeviceService.java b/core/java/android/server/BluetoothDeviceService.java deleted file mode 100644 index 8c843ef..0000000 --- a/core/java/android/server/BluetoothDeviceService.java +++ /dev/null @@ -1,1263 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * TODO: Move this to - * java/services/com/android/server/BluetoothDeviceService.java - * and make the contructor package private again. - * - * @hide - */ - -package android.server; - -import android.bluetooth.BluetoothClass; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothError; -import android.bluetooth.BluetoothHeadset; -import android.bluetooth.BluetoothIntent; -import android.bluetooth.IBluetoothDevice; -import android.bluetooth.IBluetoothDeviceCallback; -import android.content.BroadcastReceiver; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Binder; -import android.os.Handler; -import android.os.IBinder; -import android.os.Message; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.SystemService; -import android.provider.Settings; -import android.util.Log; - -import java.io.FileDescriptor; -import java.io.PrintWriter; -import java.io.UnsupportedEncodingException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -import com.android.internal.app.IBatteryStats; - -public class BluetoothDeviceService extends IBluetoothDevice.Stub { - private static final String TAG = "BluetoothDeviceService"; - private static final boolean DBG = true; - - private int mNativeData; - private BluetoothEventLoop mEventLoop; - private IntentFilter mIntentFilter; - private boolean mIsAirplaneSensitive; - private int mBluetoothState; - private boolean mRestart = false; // need to call enable() after disable() - - private final BondState mBondState = new BondState(); // local cache of bondings - private boolean mIsDiscovering; - private final IBatteryStats mBatteryStats; - - private final Context mContext; - - private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN; - private static final String BLUETOOTH_PERM = android.Manifest.permission.BLUETOOTH; - - private static final int MESSAGE_REGISTER_SDP_RECORDS = 1; - private static final int MESSAGE_FINISH_DISABLE = 2; - - static { - classInitNative(); - } - private native static void classInitNative(); - - public BluetoothDeviceService(Context context) { - mContext = context; - - // Need to do this in place of: - // mBatteryStats = BatteryStatsService.getService(); - // Since we can not import BatteryStatsService from here. This class really needs to be - // moved to java/services/com/android/server/ - mBatteryStats = IBatteryStats.Stub.asInterface(ServiceManager.getService("batteryinfo")); - } - - /** Must be called after construction, and before any other method. - */ - public synchronized void init() { - initializeNativeDataNative(); - - if (isEnabledNative() == 1) { - Log.w(TAG, "Bluetooth daemons already running - runtime restart? "); - disableNative(); - } - - setBluetoothState(BluetoothDevice.BLUETOOTH_STATE_OFF); - mIsDiscovering = false; - mEventLoop = new BluetoothEventLoop(mContext, this); - registerForAirplaneMode(); - } - private native void initializeNativeDataNative(); - - @Override - protected void finalize() throws Throwable { - if (mIsAirplaneSensitive) { - mContext.unregisterReceiver(mReceiver); - } - try { - cleanupNativeDataNative(); - } finally { - super.finalize(); - } - } - private native void cleanupNativeDataNative(); - - public boolean isEnabled() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return mBluetoothState == BluetoothDevice.BLUETOOTH_STATE_ON; - } - private native int isEnabledNative(); - - public int getBluetoothState() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return mBluetoothState; - } - - - /** - * Bring down bluetooth and disable BT in settings. Returns true on success. - */ - public boolean disable() { - return disable(true); - } - - /** - * Bring down bluetooth. Returns true on success. - * - * @param saveSetting If true, disable BT in settings - */ - public synchronized boolean disable(boolean saveSetting) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - - switch (mBluetoothState) { - case BluetoothDevice.BLUETOOTH_STATE_OFF: - return true; - case BluetoothDevice.BLUETOOTH_STATE_ON: - break; - default: - return false; - } - if (mEnableThread != null && mEnableThread.isAlive()) { - return false; - } - setBluetoothState(BluetoothDevice.BLUETOOTH_STATE_TURNING_OFF); - - // Allow 3 seconds for profiles to gracefully disconnect - // TODO: Introduce a callback mechanism so that each profile can notify - // BluetoothDeviceService when it is done shutting down - mHandler.sendMessageDelayed( - mHandler.obtainMessage(MESSAGE_FINISH_DISABLE, saveSetting ? 1 : 0, 0), 3000); - return true; - } - - - private synchronized void finishDisable(boolean saveSetting) { - if (mBluetoothState != BluetoothDevice.BLUETOOTH_STATE_TURNING_OFF) { - return; - } - mEventLoop.stop(); - disableNative(); - - // mark in progress bondings as cancelled - for (String address : mBondState.listInState(BluetoothDevice.BOND_BONDING)) { - mBondState.setBondState(address, BluetoothDevice.BOND_NOT_BONDED, - BluetoothDevice.UNBOND_REASON_AUTH_CANCELED); - } - - // Remove remoteServiceChannelCallbacks - HashMap<String, IBluetoothDeviceCallback> callbacksMap = - mEventLoop.getRemoteServiceChannelCallbacks(); - - for (Iterator<String> i = callbacksMap.keySet().iterator(); i.hasNext();) { - String address = i.next(); - IBluetoothDeviceCallback callback = callbacksMap.get(address); - try { - callback.onGetRemoteServiceChannelResult(address, BluetoothError.ERROR_DISABLED); - } catch (RemoteException e) {} - i.remove(); - } - - // update mode - Intent intent = new Intent(BluetoothIntent.SCAN_MODE_CHANGED_ACTION); - intent.putExtra(BluetoothIntent.SCAN_MODE, BluetoothDevice.SCAN_MODE_NONE); - mContext.sendBroadcast(intent, BLUETOOTH_PERM); - - mIsDiscovering = false; - - if (saveSetting) { - persistBluetoothOnSetting(false); - } - - setBluetoothState(BluetoothDevice.BLUETOOTH_STATE_OFF); - - // Log bluetooth off to battery stats. - long ident = Binder.clearCallingIdentity(); - try { - mBatteryStats.noteBluetoothOff(); - } catch (RemoteException e) { - } finally { - Binder.restoreCallingIdentity(ident); - } - - if (mRestart) { - mRestart = false; - enable(); - } - } - - /** Bring up BT and persist BT on in settings */ - public boolean enable() { - return enable(true); - } - - /** - * Enable this Bluetooth device, asynchronously. - * This turns on/off the underlying hardware. - * - * @param saveSetting If true, persist the new state of BT in settings - * @return True on success (so far) - */ - public synchronized boolean enable(boolean saveSetting) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - - // Airplane mode can prevent Bluetooth radio from being turned on. - if (mIsAirplaneSensitive && isAirplaneModeOn()) { - return false; - } - if (mBluetoothState != BluetoothDevice.BLUETOOTH_STATE_OFF) { - return false; - } - if (mEnableThread != null && mEnableThread.isAlive()) { - return false; - } - setBluetoothState(BluetoothDevice.BLUETOOTH_STATE_TURNING_ON); - mEnableThread = new EnableThread(saveSetting); - mEnableThread.start(); - return true; - } - - /** Forcibly restart Bluetooth if it is on */ - /* package */ synchronized void restart() { - if (mBluetoothState != BluetoothDevice.BLUETOOTH_STATE_ON) { - return; - } - mRestart = true; - if (!disable(false)) { - mRestart = false; - } - } - - private synchronized void setBluetoothState(int state) { - if (state == mBluetoothState) { - return; - } - - if (DBG) log("Bluetooth state " + mBluetoothState + " -> " + state); - - Intent intent = new Intent(BluetoothIntent.BLUETOOTH_STATE_CHANGED_ACTION); - intent.putExtra(BluetoothIntent.BLUETOOTH_PREVIOUS_STATE, mBluetoothState); - intent.putExtra(BluetoothIntent.BLUETOOTH_STATE, state); - intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); - - mBluetoothState = state; - - mContext.sendBroadcast(intent, BLUETOOTH_PERM); - } - - private final Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MESSAGE_REGISTER_SDP_RECORDS: - //TODO: Don't assume HSP/HFP is running, don't use sdptool, - if (isEnabled()) { - SystemService.start("hsag"); - SystemService.start("hfag"); - } - break; - case MESSAGE_FINISH_DISABLE: - finishDisable(msg.arg1 != 0); - break; - } - } - }; - - private EnableThread mEnableThread; - - private class EnableThread extends Thread { - private final boolean mSaveSetting; - public EnableThread(boolean saveSetting) { - mSaveSetting = saveSetting; - } - public void run() { - boolean res = (enableNative() == 0); - if (res) { - int retryCount = 2; - boolean running = false; - while ((retryCount-- > 0) && !running) { - mEventLoop.start(); - // it may take a momement for the other thread to do its - // thing. Check periodically for a while. - int pollCount = 5; - while ((pollCount-- > 0) && !running) { - if (mEventLoop.isEventLoopRunning()) { - running = true; - break; - } - try { - Thread.sleep(100); - } catch (InterruptedException e) {} - } - } - if (!running) { - log("bt EnableThread giving up"); - res = false; - disableNative(); - } - } - - - if (res) { - if (mSaveSetting) { - persistBluetoothOnSetting(true); - } - mIsDiscovering = false; - mBondState.loadBondState(); - mHandler.sendMessageDelayed(mHandler.obtainMessage(MESSAGE_REGISTER_SDP_RECORDS), - 3000); - - // Log bluetooth on to battery stats. - long ident = Binder.clearCallingIdentity(); - try { - mBatteryStats.noteBluetoothOn(); - } catch (RemoteException e) { - } finally { - Binder.restoreCallingIdentity(ident); - } - } - - mEnableThread = null; - - setBluetoothState(res ? - BluetoothDevice.BLUETOOTH_STATE_ON : - BluetoothDevice.BLUETOOTH_STATE_OFF); - - if (res) { - // Update mode - mEventLoop.onModeChanged(getModeNative()); - } - - if (mIsAirplaneSensitive && isAirplaneModeOn()) { - disable(false); - } - - } - } - - private void persistBluetoothOnSetting(boolean bluetoothOn) { - long origCallerIdentityToken = Binder.clearCallingIdentity(); - Settings.Secure.putInt(mContext.getContentResolver(), Settings.Secure.BLUETOOTH_ON, - bluetoothOn ? 1 : 0); - Binder.restoreCallingIdentity(origCallerIdentityToken); - } - - private native int enableNative(); - private native int disableNative(); - - /* package */ BondState getBondState() { - return mBondState; - } - - /** local cache of bonding state. - /* we keep our own state to track the intermediate state BONDING, which - /* bluez does not track. - * All addreses must be passed in upper case. - */ - public class BondState { - private final HashMap<String, Integer> mState = new HashMap<String, Integer>(); - private final HashMap<String, Integer> mPinAttempt = new HashMap<String, Integer>(); - private final ArrayList<String> mAutoPairingFailures = new ArrayList<String>(); - // List of all the vendor_id prefix of Bluetooth addresses for - // which auto pairing is not attempted. - // The following companies are included in the list below: - // ALPS (lexus), Murata (Prius 2007, Nokia 616), TEMIC SDS (Porsche, Audi), - // Parrot, Zhongshan General K-mate Electronics, Great Well - // Electronics, Flaircomm Electronics, Jatty Electronics, Delphi, - // Clarion, Novero, Denso (Lexus, Toyota), Johnson Controls (Acura), - // Continental Automotive, Harman/Becker - private final ArrayList<String> mAutoPairingBlacklisted = - new ArrayList<String>(Arrays.asList( - "00:02:C7", "00:16:FE", "00:19:C1", "00:1B:FB", "00:1E:3D", "00:21:4F", - "00:23:06", "00:24:33", "00:A0:79", "00:0E:6D", "00:13:E0", "00:21:E8", - "00:60:57", "00:0E:9F", "00:12:1C", "00:18:91", "00:18:96", "00:13:04", - "00:16:FD", "00:22:A0", "00:0B:4C", "00:60:6F", "00:23:3D", "00:C0:59", - "00:0A:30", "00:1E:AE", "00:1C:D7" - )); - - public synchronized void loadBondState() { - if (mBluetoothState != BluetoothDevice.BLUETOOTH_STATE_TURNING_ON) { - return; - } - String[] bonds = listBondingsNative(); - if (bonds == null) { - return; - } - mState.clear(); - if (DBG) log("found " + bonds.length + " bonded devices"); - for (String address : bonds) { - mState.put(address.toUpperCase(), BluetoothDevice.BOND_BONDED); - } - } - - public synchronized void setBondState(String address, int state) { - setBondState(address, state, 0); - } - - /** reason is ignored unless state == BOND_NOT_BONDED */ - public synchronized void setBondState(String address, int state, int reason) { - int oldState = getBondState(address); - if (oldState == state) { - return; - } - if (DBG) log(address + " bond state " + oldState + " -> " + state + " (" + - reason + ")"); - Intent intent = new Intent(BluetoothIntent.BOND_STATE_CHANGED_ACTION); - intent.putExtra(BluetoothIntent.ADDRESS, address); - intent.putExtra(BluetoothIntent.BOND_STATE, state); - intent.putExtra(BluetoothIntent.BOND_PREVIOUS_STATE, oldState); - if (state == BluetoothDevice.BOND_NOT_BONDED) { - if (reason <= 0) { - Log.w(TAG, "setBondState() called to unbond device, but reason code is " + - "invalid. Overriding reason code with BOND_RESULT_REMOVED"); - reason = BluetoothDevice.UNBOND_REASON_REMOVED; - } - intent.putExtra(BluetoothIntent.REASON, reason); - mState.remove(address); - } else { - mState.put(address, state); - } - - mContext.sendBroadcast(intent, BLUETOOTH_PERM); - } - - public boolean isAutoPairingBlacklisted(String address) { - for (String blacklistAddress : mAutoPairingBlacklisted) { - if (address.startsWith(blacklistAddress)) return true; - } - return false; - } - - public synchronized int getBondState(String address) { - Integer state = mState.get(address); - if (state == null) { - return BluetoothDevice.BOND_NOT_BONDED; - } - return state.intValue(); - } - - private synchronized String[] listInState(int state) { - ArrayList<String> result = new ArrayList<String>(mState.size()); - for (Map.Entry<String, Integer> e : mState.entrySet()) { - if (e.getValue().intValue() == state) { - result.add(e.getKey()); - } - } - return result.toArray(new String[result.size()]); - } - - public synchronized void addAutoPairingFailure(String address) { - if (!mAutoPairingFailures.contains(address)) { - mAutoPairingFailures.add(address); - } - } - - public synchronized boolean isAutoPairingAttemptsInProgress(String address) { - return getAttempt(address) != 0; - } - - public synchronized void clearPinAttempts(String address) { - mPinAttempt.remove(address); - } - - public synchronized boolean hasAutoPairingFailed(String address) { - return mAutoPairingFailures.contains(address); - } - - public synchronized int getAttempt(String address) { - Integer attempt = mPinAttempt.get(address); - if (attempt == null) { - return 0; - } - return attempt.intValue(); - } - - public synchronized void attempt(String address) { - Integer attempt = mPinAttempt.get(address); - int newAttempt; - if (attempt == null) { - newAttempt = 1; - } else { - newAttempt = attempt.intValue() + 1; - } - mPinAttempt.put(address, new Integer(newAttempt)); - } - - } - private native String[] listBondingsNative(); - - private static String toBondStateString(int bondState) { - switch (bondState) { - case BluetoothDevice.BOND_NOT_BONDED: - return "not bonded"; - case BluetoothDevice.BOND_BONDING: - return "bonding"; - case BluetoothDevice.BOND_BONDED: - return "bonded"; - default: - return "??????"; - } - } - - public synchronized String getAddress() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return getAddressNative(); - } - private native String getAddressNative(); - - public synchronized String getName() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return getNameNative(); - } - private native String getNameNative(); - - public synchronized boolean setName(String name) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - if (name == null) { - return false; - } - // hcid handles persistance of the bluetooth name - return setNameNative(name); - } - private native boolean setNameNative(String name); - - /** - * Returns the user-friendly name of a remote device. This value is - * retrned from our local cache, which is updated during device discovery. - * Do not expect to retrieve the updated remote name immediately after - * changing the name on the remote device. - * - * @param address Bluetooth address of remote device. - * - * @return The user-friendly name of the specified remote device. - */ - public synchronized String getRemoteName(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return null; - } - return getRemoteNameNative(address); - } - private native String getRemoteNameNative(String address); - - /* pacakge */ native String getAdapterPathNative(); - - public synchronized boolean startDiscovery(boolean resolveNames) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - return startDiscoveryNative(resolveNames); - } - private native boolean startDiscoveryNative(boolean resolveNames); - - public synchronized boolean cancelDiscovery() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - return cancelDiscoveryNative(); - } - private native boolean cancelDiscoveryNative(); - - public synchronized boolean isDiscovering() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return mIsDiscovering; - } - - /* package */ void setIsDiscovering(boolean isDiscovering) { - mIsDiscovering = isDiscovering; - } - - public synchronized boolean startPeriodicDiscovery() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - return startPeriodicDiscoveryNative(); - } - private native boolean startPeriodicDiscoveryNative(); - - public synchronized boolean stopPeriodicDiscovery() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - return stopPeriodicDiscoveryNative(); - } - private native boolean stopPeriodicDiscoveryNative(); - - public synchronized boolean isPeriodicDiscovery() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return isPeriodicDiscoveryNative(); - } - private native boolean isPeriodicDiscoveryNative(); - - /** - * Set the discoverability window for the device. A timeout of zero - * makes the device permanently discoverable (if the device is - * discoverable). Setting the timeout to a nonzero value does not make - * a device discoverable; you need to call setMode() to make the device - * explicitly discoverable. - * - * @param timeout_s The discoverable timeout in seconds. - */ - public synchronized boolean setDiscoverableTimeout(int timeout) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - return setDiscoverableTimeoutNative(timeout); - } - private native boolean setDiscoverableTimeoutNative(int timeout_s); - - /** - * Get the discoverability window for the device. A timeout of zero - * means that the device is permanently discoverable (if the device is - * in the discoverable mode). - * - * @return The discoverability window of the device, in seconds. A negative - * value indicates an error. - */ - public synchronized int getDiscoverableTimeout() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return getDiscoverableTimeoutNative(); - } - private native int getDiscoverableTimeoutNative(); - - public synchronized boolean isAclConnected(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return false; - } - return isConnectedNative(address); - } - private native boolean isConnectedNative(String address); - - public synchronized int getScanMode() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return bluezStringToScanMode(getModeNative()); - } - private native String getModeNative(); - - public synchronized boolean setScanMode(int mode) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - String bluezMode = scanModeToBluezString(mode); - if (bluezMode != null) { - return setModeNative(bluezMode); - } - return false; - } - private native boolean setModeNative(String mode); - - public synchronized boolean disconnectRemoteDeviceAcl(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return false; - } - return disconnectRemoteDeviceNative(address); - } - private native boolean disconnectRemoteDeviceNative(String address); - - public synchronized boolean createBond(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return false; - } - address = address.toUpperCase(); - - String[] bonding = mBondState.listInState(BluetoothDevice.BOND_BONDING); - if (bonding.length > 0 && !bonding[0].equals(address)) { - log("Ignoring createBond(): another device is bonding"); - // a different device is currently bonding, fail - return false; - } - - // Check for bond state only if we are not performing auto - // pairing exponential back-off attempts. - if (!mBondState.isAutoPairingAttemptsInProgress(address) && - mBondState.getBondState(address) != BluetoothDevice.BOND_NOT_BONDED) { - log("Ignoring createBond(): this device is already bonding or bonded"); - return false; - } - - if (!createBondingNative(address, 60000 /* 1 minute */)) { - return false; - } - - mBondState.setBondState(address, BluetoothDevice.BOND_BONDING); - return true; - } - private native boolean createBondingNative(String address, int timeout_ms); - - public synchronized boolean cancelBondProcess(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return false; - } - address = address.toUpperCase(); - if (mBondState.getBondState(address) != BluetoothDevice.BOND_BONDING) { - return false; - } - - mBondState.setBondState(address, BluetoothDevice.BOND_NOT_BONDED, - BluetoothDevice.UNBOND_REASON_AUTH_CANCELED); - cancelBondingProcessNative(address); - return true; - } - private native boolean cancelBondingProcessNative(String address); - - public synchronized boolean removeBond(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return false; - } - return removeBondingNative(address); - } - private native boolean removeBondingNative(String address); - - public synchronized String[] listBonds() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return mBondState.listInState(BluetoothDevice.BOND_BONDED); - } - - public synchronized int getBondState(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return BluetoothError.ERROR; - } - return mBondState.getBondState(address.toUpperCase()); - } - - public synchronized String[] listAclConnections() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return listConnectionsNative(); - } - private native String[] listConnectionsNative(); - - /** - * This method lists all remote devices that this adapter is aware of. - * This is a list not only of all most-recently discovered devices, but of - * all devices discovered by this adapter up to some point in the past. - * Note that many of these devices may not be in the neighborhood anymore, - * and attempting to connect to them will result in an error. - * - * @return An array of strings representing the Bluetooth addresses of all - * remote devices that this adapter is aware of. - */ - public synchronized String[] listRemoteDevices() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return listRemoteDevicesNative(); - } - private native String[] listRemoteDevicesNative(); - - /** - * Returns the version of the Bluetooth chip. This version is compiled from - * the LMP version. In case of EDR the features attribute must be checked. - * Example: "Bluetooth 2.0 + EDR". - * - * @return a String representation of the this Adapter's underlying - * Bluetooth-chip version. - */ - public synchronized String getVersion() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return getVersionNative(); - } - private native String getVersionNative(); - - /** - * Returns the revision of the Bluetooth chip. This is a vendor-specific - * value and in most cases it represents the firmware version. This might - * derive from the HCI revision and LMP subversion values or via extra - * vendord specific commands. - * In case the revision of a chip is not available. This method should - * return the LMP subversion value as a string. - * Example: "HCI 19.2" - * - * @return The HCI revision of this adapter. - */ - public synchronized String getRevision() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return getRevisionNative(); - } - private native String getRevisionNative(); - - /** - * Returns the manufacturer of the Bluetooth chip. If the company id is not - * known the sting "Company ID %d" where %d should be replaced with the - * numeric value from the manufacturer field. - * Example: "Cambridge Silicon Radio" - * - * @return Manufacturer name. - */ - public synchronized String getManufacturer() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return getManufacturerNative(); - } - private native String getManufacturerNative(); - - /** - * Returns the company name from the OUI database of the Bluetooth device - * address. This function will need a valid and up-to-date oui.txt from - * the IEEE. This value will be different from the manufacturer string in - * the most cases. - * If the oui.txt file is not present or the OUI part of the Bluetooth - * address is not listed, it should return the string "OUI %s" where %s is - * the actual OUI. - * - * Example: "Apple Computer" - * - * @return company name - */ - public synchronized String getCompany() { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return getCompanyNative(); - } - private native String getCompanyNative(); - - /** - * Like getVersion(), but for a remote device. - * - * @param address The Bluetooth address of the remote device. - * - * @return remote-device Bluetooth version - * - * @see #getVersion - */ - public synchronized String getRemoteVersion(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return null; - } - return getRemoteVersionNative(address); - } - private native String getRemoteVersionNative(String address); - - /** - * Like getRevision(), but for a remote device. - * - * @param address The Bluetooth address of the remote device. - * - * @return remote-device HCI revision - * - * @see #getRevision - */ - public synchronized String getRemoteRevision(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return null; - } - return getRemoteRevisionNative(address); - } - private native String getRemoteRevisionNative(String address); - - /** - * Like getManufacturer(), but for a remote device. - * - * @param address The Bluetooth address of the remote device. - * - * @return remote-device Bluetooth chip manufacturer - * - * @see #getManufacturer - */ - public synchronized String getRemoteManufacturer(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return null; - } - return getRemoteManufacturerNative(address); - } - private native String getRemoteManufacturerNative(String address); - - /** - * Like getCompany(), but for a remote device. - * - * @param address The Bluetooth address of the remote device. - * - * @return remote-device company - * - * @see #getCompany - */ - public synchronized String getRemoteCompany(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return null; - } - return getRemoteCompanyNative(address); - } - private native String getRemoteCompanyNative(String address); - - /** - * Returns the date and time when the specified remote device has been seen - * by a discover procedure. - * Example: "2006-02-08 12:00:00 GMT" - * - * @return a String with the timestamp. - */ - public synchronized String lastSeen(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return null; - } - return lastSeenNative(address); - } - private native String lastSeenNative(String address); - - /** - * Returns the date and time when the specified remote device has last been - * connected to - * Example: "2006-02-08 12:00:00 GMT" - * - * @return a String with the timestamp. - */ - public synchronized String lastUsed(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return null; - } - return lastUsedNative(address); - } - private native String lastUsedNative(String address); - - /** - * Gets the remote major, minor, and service classes encoded as a 32-bit - * integer. - * - * Note: this value is retrieved from cache, because we get it during - * remote-device discovery. - * - * @return 32-bit integer encoding the remote major, minor, and service - * classes. - * - * @see #getRemoteMajorClass - * @see #getRemoteMinorClass - * @see #getRemoteServiceClasses - */ - public synchronized int getRemoteClass(String address) { - if (!BluetoothDevice.checkBluetoothAddress(address)) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return BluetoothClass.ERROR; - } - return getRemoteClassNative(address); - } - private native int getRemoteClassNative(String address); - - /** - * Gets the remote features encoded as bit mask. - * - * Note: This method may be obsoleted soon. - * - * @return byte array of features. - */ - public synchronized byte[] getRemoteFeatures(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return null; - } - return getRemoteFeaturesNative(address); - } - private native byte[] getRemoteFeaturesNative(String address); - - /** - * This method and {@link #getRemoteServiceRecord} query the SDP service - * on a remote device. They do not interpret the data, but simply return - * it raw to the user. To read more about SDP service handles and records, - * consult the Bluetooth core documentation (www.bluetooth.com). - * - * @param address Bluetooth address of remote device. - * @param match a String match to narrow down the service-handle search. - * The only supported value currently is "hsp" for the headset - * profile. To retrieve all service handles, simply pass an empty - * match string. - * - * @return all service handles corresponding to the string match. - * - * @see #getRemoteServiceRecord - */ - public synchronized int[] getRemoteServiceHandles(String address, String match) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return null; - } - if (match == null) { - match = ""; - } - return getRemoteServiceHandlesNative(address, match); - } - private native int[] getRemoteServiceHandlesNative(String address, String match); - - /** - * This method retrieves the service records corresponding to a given - * service handle (method {@link #getRemoteServiceHandles} retrieves the - * service handles.) - * - * This method and {@link #getRemoteServiceHandles} do not interpret their - * data, but simply return it raw to the user. To read more about SDP - * service handles and records, consult the Bluetooth core documentation - * (www.bluetooth.com). - * - * @param address Bluetooth address of remote device. - * @param handle Service handle returned by {@link #getRemoteServiceHandles} - * - * @return a byte array of all service records corresponding to the - * specified service handle. - * - * @see #getRemoteServiceHandles - */ - public synchronized byte[] getRemoteServiceRecord(String address, int handle) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return null; - } - return getRemoteServiceRecordNative(address, handle); - } - private native byte[] getRemoteServiceRecordNative(String address, int handle); - - private static final int MAX_OUTSTANDING_ASYNC = 32; - - // AIDL does not yet support short's - public synchronized boolean getRemoteServiceChannel(String address, int uuid16, - IBluetoothDeviceCallback callback) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return false; - } - HashMap<String, IBluetoothDeviceCallback> callbacks = - mEventLoop.getRemoteServiceChannelCallbacks(); - if (callbacks.containsKey(address)) { - Log.w(TAG, "SDP request already in progress for " + address); - return false; - } - // Protect from malicious clients - only allow 32 bonding requests per minute. - if (callbacks.size() > MAX_OUTSTANDING_ASYNC) { - Log.w(TAG, "Too many outstanding SDP requests, dropping request for " + address); - return false; - } - callbacks.put(address, callback); - - if (!getRemoteServiceChannelNative(address, (short)uuid16)) { - callbacks.remove(address); - return false; - } - return true; - } - private native boolean getRemoteServiceChannelNative(String address, short uuid16); - - public synchronized boolean setPin(String address, byte[] pin) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - if (pin == null || pin.length <= 0 || pin.length > 16 || - !BluetoothDevice.checkBluetoothAddress(address)) { - return false; - } - address = address.toUpperCase(); - Integer data = mEventLoop.getPasskeyAgentRequestData().remove(address); - if (data == null) { - Log.w(TAG, "setPin(" + address + ") called but no native data available, " + - "ignoring. Maybe the PasskeyAgent Request was cancelled by the remote device" + - " or by bluez.\n"); - return false; - } - // bluez API wants pin as a string - String pinString; - try { - pinString = new String(pin, "UTF8"); - } catch (UnsupportedEncodingException uee) { - Log.e(TAG, "UTF8 not supported?!?"); - return false; - } - return setPinNative(address, pinString, data.intValue()); - } - private native boolean setPinNative(String address, String pin, int nativeData); - - public synchronized boolean cancelPin(String address) { - mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, - "Need BLUETOOTH_ADMIN permission"); - if (!BluetoothDevice.checkBluetoothAddress(address)) { - return false; - } - address = address.toUpperCase(); - Integer data = mEventLoop.getPasskeyAgentRequestData().remove(address); - if (data == null) { - Log.w(TAG, "cancelPin(" + address + ") called but no native data available, " + - "ignoring. Maybe the PasskeyAgent Request was already cancelled by the remote " + - "or by bluez.\n"); - return false; - } - return cancelPinNative(address, data.intValue()); - } - private native boolean cancelPinNative(String address, int natveiData); - - private final BroadcastReceiver mReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (action.equals(Intent.ACTION_AIRPLANE_MODE_CHANGED)) { - ContentResolver resolver = context.getContentResolver(); - // Query the airplane mode from Settings.System just to make sure that - // some random app is not sending this intent and disabling bluetooth - boolean enabled = !isAirplaneModeOn(); - // If bluetooth is currently expected to be on, then enable or disable bluetooth - if (Settings.Secure.getInt(resolver, Settings.Secure.BLUETOOTH_ON, 0) > 0) { - if (enabled) { - enable(false); - } else { - disable(false); - } - } - } - } - }; - - private void registerForAirplaneMode() { - String airplaneModeRadios = Settings.System.getString(mContext.getContentResolver(), - Settings.System.AIRPLANE_MODE_RADIOS); - mIsAirplaneSensitive = airplaneModeRadios == null - ? true : airplaneModeRadios.contains(Settings.System.RADIO_BLUETOOTH); - if (mIsAirplaneSensitive) { - mIntentFilter = new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED); - mContext.registerReceiver(mReceiver, mIntentFilter); - } - } - - /* Returns true if airplane mode is currently on */ - private final boolean isAirplaneModeOn() { - return Settings.System.getInt(mContext.getContentResolver(), - Settings.System.AIRPLANE_MODE_ON, 0) == 1; - } - - @Override - protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - pw.println("\nmIsAirplaneSensitive = " + mIsAirplaneSensitive + "\n"); - - switch(mBluetoothState) { - case BluetoothDevice.BLUETOOTH_STATE_OFF: - pw.println("\nBluetooth OFF\n"); - return; - case BluetoothDevice.BLUETOOTH_STATE_TURNING_ON: - pw.println("\nBluetooth TURNING ON\n"); - return; - case BluetoothDevice.BLUETOOTH_STATE_TURNING_OFF: - pw.println("\nBluetooth TURNING OFF\n"); - return; - case BluetoothDevice.BLUETOOTH_STATE_ON: - pw.println("\nBluetooth ON\n"); - } - - pw.println("\nLocal address = " + getAddress()); - pw.println("\nLocal name = " + getName()); - pw.println("\nisDiscovering() = " + isDiscovering()); - - BluetoothHeadset headset = new BluetoothHeadset(mContext, null); - - String[] addresses = listRemoteDevices(); - - pw.println("\n--Known devices--"); - for (String address : addresses) { - pw.printf("%s %10s (%d) %s\n", address, - toBondStateString(mBondState.getBondState(address)), - mBondState.getAttempt(address), - getRemoteName(address)); - } - - addresses = listAclConnections(); - pw.println("\n--ACL connected devices--"); - for (String address : addresses) { - pw.println(address); - } - - // Rather not do this from here, but no-where else and I need this - // dump - pw.println("\n--Headset Service--"); - switch (headset.getState()) { - case BluetoothHeadset.STATE_DISCONNECTED: - pw.println("getState() = STATE_DISCONNECTED"); - break; - case BluetoothHeadset.STATE_CONNECTING: - pw.println("getState() = STATE_CONNECTING"); - break; - case BluetoothHeadset.STATE_CONNECTED: - pw.println("getState() = STATE_CONNECTED"); - break; - case BluetoothHeadset.STATE_ERROR: - pw.println("getState() = STATE_ERROR"); - break; - } - pw.println("getHeadsetAddress() = " + headset.getHeadsetAddress()); - pw.println("getBatteryUsageHint() = " + headset.getBatteryUsageHint()); - - headset.close(); - } - - /* package */ static int bluezStringToScanMode(String mode) { - if (mode == null) { - return BluetoothError.ERROR; - } - mode = mode.toLowerCase(); - if (mode.equals("off")) { - return BluetoothDevice.SCAN_MODE_NONE; - } else if (mode.equals("connectable")) { - return BluetoothDevice.SCAN_MODE_CONNECTABLE; - } else if (mode.equals("discoverable")) { - return BluetoothDevice.SCAN_MODE_CONNECTABLE_DISCOVERABLE; - } else { - return BluetoothError.ERROR; - } - } - - /* package */ static String scanModeToBluezString(int mode) { - switch (mode) { - case BluetoothDevice.SCAN_MODE_NONE: - return "off"; - case BluetoothDevice.SCAN_MODE_CONNECTABLE: - return "connectable"; - case BluetoothDevice.SCAN_MODE_CONNECTABLE_DISCOVERABLE: - return "discoverable"; - } - return null; - } - - private static void log(String msg) { - Log.d(TAG, msg); - } -} diff --git a/core/java/android/server/BluetoothEventLoop.java b/core/java/android/server/BluetoothEventLoop.java index 8cc229b..c0b9a68 100644 --- a/core/java/android/server/BluetoothEventLoop.java +++ b/core/java/android/server/BluetoothEventLoop.java @@ -17,16 +17,15 @@ package android.server; import android.bluetooth.BluetoothA2dp; +import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothError; -import android.bluetooth.BluetoothIntent; -import android.bluetooth.IBluetoothDeviceCallback; +import android.bluetooth.BluetoothUuid; +import android.os.ParcelUuid; import android.content.Context; import android.content.Intent; import android.os.Handler; import android.os.Message; -import android.os.RemoteException; import android.util.Log; import java.util.HashMap; @@ -46,13 +45,20 @@ class BluetoothEventLoop { private Thread mThread; private boolean mStarted; private boolean mInterrupted; + private final HashMap<String, Integer> mPasskeyAgentRequestData; - private final HashMap<String, IBluetoothDeviceCallback> mGetRemoteServiceChannelCallbacks; - private final BluetoothDeviceService mBluetoothService; + private final BluetoothService mBluetoothService; + private final BluetoothAdapter mAdapter; private final Context mContext; private static final int EVENT_AUTO_PAIRING_FAILURE_ATTEMPT_DELAY = 1; private static final int EVENT_RESTART_BLUETOOTH = 2; + private static final int EVENT_PAIRING_CONSENT_DELAYED_ACCEPT = 3; + private static final int EVENT_AGENT_CANCEL = 4; + + private static final int CREATE_DEVICE_ALREADY_EXISTS = 1; + private static final int CREATE_DEVICE_SUCCESS = 0; + private static final int CREATE_DEVICE_FAILED = -1; // The time (in millisecs) to delay the pairing attempt after the first // auto pairing attempt fails. We use an exponential delay with @@ -67,9 +73,10 @@ class BluetoothEventLoop { private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { + String address = null; switch (msg.what) { case EVENT_AUTO_PAIRING_FAILURE_ATTEMPT_DELAY: - String address = (String)msg.obj; + address = (String)msg.obj; if (address != null) { mBluetoothService.createBond(address); return; @@ -78,6 +85,28 @@ class BluetoothEventLoop { case EVENT_RESTART_BLUETOOTH: mBluetoothService.restart(); break; + case EVENT_PAIRING_CONSENT_DELAYED_ACCEPT: + address = (String)msg.obj; + if (address != null) { + mBluetoothService.setPairingConfirmation(address, true); + } + break; + case EVENT_AGENT_CANCEL: + // Set the Bond State to BOND_NONE. + // We always have only 1 device in BONDING state. + String[] devices = + mBluetoothService.getBondState().listInState(BluetoothDevice.BOND_BONDING); + if (devices.length == 0) { + break; + } else if (devices.length > 1) { + Log.e(TAG, " There is more than one device in the Bonding State"); + break; + } + address = devices[0]; + mBluetoothService.getBondState().setBondState(address, + BluetoothDevice.BOND_NONE, + BluetoothDevice.UNBOND_REASON_REMOTE_AUTH_CANCELED); + break; } } }; @@ -85,14 +114,14 @@ class BluetoothEventLoop { static { classInitNative(); } private static native void classInitNative(); - /* pacakge */ BluetoothEventLoop(Context context, BluetoothDeviceService bluetoothService) { + /* pacakge */ BluetoothEventLoop(Context context, BluetoothAdapter adapter, + BluetoothService bluetoothService) { mBluetoothService = bluetoothService; mContext = context; mPasskeyAgentRequestData = new HashMap(); - mGetRemoteServiceChannelCallbacks = new HashMap(); + mAdapter = adapter; initializeNativeDataNative(); } - private native void initializeNativeDataNative(); protected void finalize() throws Throwable { try { @@ -101,20 +130,11 @@ class BluetoothEventLoop { super.finalize(); } } - private native void cleanupNativeDataNative(); - - /* pacakge */ HashMap<String, IBluetoothDeviceCallback> getRemoteServiceChannelCallbacks() { - return mGetRemoteServiceChannelCallbacks; - } - /* pacakge */ HashMap<String, Integer> getPasskeyAgentRequestData() { + /* package */ HashMap<String, Integer> getPasskeyAgentRequestData() { return mPasskeyAgentRequestData; } - private native void startEventLoopNative(); - private native void stopEventLoopNative(); - private native boolean isEventLoopRunningNative(); - /* package */ void start() { if (!isEventLoopRunningNative()) { @@ -134,81 +154,61 @@ class BluetoothEventLoop { return isEventLoopRunningNative(); } - /*package*/ void onModeChanged(String bluezMode) { - int mode = BluetoothDeviceService.bluezStringToScanMode(bluezMode); - if (mode >= 0) { - Intent intent = new Intent(BluetoothIntent.SCAN_MODE_CHANGED_ACTION); - intent.putExtra(BluetoothIntent.SCAN_MODE, mode); - intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + private void addDevice(String address, String[] properties) { + mBluetoothService.addRemoteDeviceProperties(address, properties); + String rssi = mBluetoothService.getRemoteDeviceProperty(address, "RSSI"); + String classValue = mBluetoothService.getRemoteDeviceProperty(address, "Class"); + String name = mBluetoothService.getRemoteDeviceProperty(address, "Name"); + short rssiValue; + // For incoming connections, we don't get the RSSI value. Use a default of MIN_VALUE. + // If we accept the pairing, we will automatically show it at the top of the list. + if (rssi != null) { + rssiValue = (short)Integer.valueOf(rssi).intValue(); + } else { + rssiValue = Short.MIN_VALUE; + } + if (classValue != null) { + Intent intent = new Intent(BluetoothDevice.ACTION_FOUND); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); + intent.putExtra(BluetoothDevice.EXTRA_CLASS, + new BluetoothClass(Integer.valueOf(classValue))); + intent.putExtra(BluetoothDevice.EXTRA_RSSI, rssiValue); + intent.putExtra(BluetoothDevice.EXTRA_NAME, name); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); + } else { + log ("ClassValue: " + classValue + " for remote device: " + address + " is null"); } } - private void onDiscoveryStarted() { - mBluetoothService.setIsDiscovering(true); - Intent intent = new Intent(BluetoothIntent.DISCOVERY_STARTED_ACTION); - mContext.sendBroadcast(intent, BLUETOOTH_PERM); - } - private void onDiscoveryCompleted() { - mBluetoothService.setIsDiscovering(false); - Intent intent = new Intent(BluetoothIntent.DISCOVERY_COMPLETED_ACTION); - mContext.sendBroadcast(intent, BLUETOOTH_PERM); + private void onDeviceFound(String address, String[] properties) { + if (properties == null) { + Log.e(TAG, "ERROR: Remote device properties are null"); + return; + } + addDevice(address, properties); } - private void onRemoteDeviceFound(String address, int deviceClass, short rssi) { - Intent intent = new Intent(BluetoothIntent.REMOTE_DEVICE_FOUND_ACTION); - intent.putExtra(BluetoothIntent.ADDRESS, address); - intent.putExtra(BluetoothIntent.CLASS, deviceClass); - intent.putExtra(BluetoothIntent.RSSI, rssi); + private void onDeviceDisappeared(String address) { + Intent intent = new Intent(BluetoothDevice.ACTION_DISAPPEARED); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); mContext.sendBroadcast(intent, BLUETOOTH_PERM); } - private void onRemoteDeviceDisappeared(String address) { - Intent intent = new Intent(BluetoothIntent.REMOTE_DEVICE_DISAPPEARED_ACTION); - intent.putExtra(BluetoothIntent.ADDRESS, address); - mContext.sendBroadcast(intent, BLUETOOTH_PERM); - } - private void onRemoteClassUpdated(String address, int deviceClass) { - Intent intent = new Intent(BluetoothIntent.REMOTE_DEVICE_CLASS_UPDATED_ACTION); - intent.putExtra(BluetoothIntent.ADDRESS, address); - intent.putExtra(BluetoothIntent.CLASS, deviceClass); - mContext.sendBroadcast(intent, BLUETOOTH_PERM); - } - private void onRemoteDeviceConnected(String address) { - Intent intent = new Intent(BluetoothIntent.REMOTE_DEVICE_CONNECTED_ACTION); - intent.putExtra(BluetoothIntent.ADDRESS, address); - mContext.sendBroadcast(intent, BLUETOOTH_PERM); - } - private void onRemoteDeviceDisconnectRequested(String address) { - Intent intent = new Intent(BluetoothIntent.REMOTE_DEVICE_DISCONNECT_REQUESTED_ACTION); - intent.putExtra(BluetoothIntent.ADDRESS, address); - mContext.sendBroadcast(intent, BLUETOOTH_PERM); - } - private void onRemoteDeviceDisconnected(String address) { - Intent intent = new Intent(BluetoothIntent.REMOTE_DEVICE_DISCONNECTED_ACTION); - intent.putExtra(BluetoothIntent.ADDRESS, address); - mContext.sendBroadcast(intent, BLUETOOTH_PERM); - } - private void onRemoteNameUpdated(String address, String name) { - Intent intent = new Intent(BluetoothIntent.REMOTE_NAME_UPDATED_ACTION); - intent.putExtra(BluetoothIntent.ADDRESS, address); - intent.putExtra(BluetoothIntent.NAME, name); - mContext.sendBroadcast(intent, BLUETOOTH_PERM); - } - private void onRemoteNameFailed(String address) { - Intent intent = new Intent(BluetoothIntent.REMOTE_NAME_FAILED_ACTION); - intent.putExtra(BluetoothIntent.ADDRESS, address); - mContext.sendBroadcast(intent, BLUETOOTH_PERM); - } - private void onRemoteNameChanged(String address, String name) { - Intent intent = new Intent(BluetoothIntent.REMOTE_NAME_UPDATED_ACTION); - intent.putExtra(BluetoothIntent.ADDRESS, address); - intent.putExtra(BluetoothIntent.NAME, name); + + private void onDeviceDisconnectRequested(String deviceObjectPath) { + String address = mBluetoothService.getAddressFromObjectPath(deviceObjectPath); + if (address == null) { + Log.e(TAG, "onDeviceDisconnectRequested: Address of the remote device in null"); + return; + } + Intent intent = new Intent(BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); mContext.sendBroadcast(intent, BLUETOOTH_PERM); } - private void onCreateBondingResult(String address, int result) { + private void onCreatePairedDeviceResult(String address, int result) { address = address.toUpperCase(); - if (result == BluetoothError.SUCCESS) { + if (result == BluetoothDevice.BOND_SUCCESS) { mBluetoothService.getBondState().setBondState(address, BluetoothDevice.BOND_BONDED); if (mBluetoothService.getBondState().isAutoPairingAttemptsInProgress(address)) { mBluetoothService.getBondState().clearPinAttempts(address); @@ -222,7 +222,7 @@ class BluetoothEventLoop { pairingAttempt(address, result); } else { mBluetoothService.getBondState().setBondState(address, - BluetoothDevice.BOND_NOT_BONDED, result); + BluetoothDevice.BOND_NONE, result); if (mBluetoothService.getBondState().isAutoPairingAttemptsInProgress(address)) { mBluetoothService.getBondState().clearPinAttempts(address); } @@ -242,7 +242,7 @@ class BluetoothEventLoop { MAX_AUTO_PAIRING_FAILURE_ATTEMPT_DELAY) { mBluetoothService.getBondState().clearPinAttempts(address); mBluetoothService.getBondState().setBondState(address, - BluetoothDevice.BOND_NOT_BONDED, result); + BluetoothDevice.BOND_NONE, result); return; } @@ -253,45 +253,249 @@ class BluetoothEventLoop { if (!postResult) { mBluetoothService.getBondState().clearPinAttempts(address); mBluetoothService.getBondState().setBondState(address, - BluetoothDevice.BOND_NOT_BONDED, result); + BluetoothDevice.BOND_NONE, result); return; } mBluetoothService.getBondState().attempt(address); } - private void onBondingCreated(String address) { - mBluetoothService.getBondState().setBondState(address.toUpperCase(), - BluetoothDevice.BOND_BONDED); + private void onDeviceCreated(String deviceObjectPath) { + String address = mBluetoothService.getAddressFromObjectPath(deviceObjectPath); + if (!mBluetoothService.isRemoteDeviceInCache(address)) { + // Incoming connection, we haven't seen this device, add to cache. + String[] properties = mBluetoothService.getRemoteDeviceProperties(address); + if (properties != null) { + addDevice(address, properties); + } + } + return; } - private void onBondingRemoved(String address) { - mBluetoothService.getBondState().setBondState(address.toUpperCase(), - BluetoothDevice.BOND_NOT_BONDED, BluetoothDevice.UNBOND_REASON_REMOVED); + private void onDeviceRemoved(String deviceObjectPath) { + String address = mBluetoothService.getAddressFromObjectPath(deviceObjectPath); + if (address != null) + mBluetoothService.getBondState().setBondState(address.toUpperCase(), + BluetoothDevice.BOND_NONE, BluetoothDevice.UNBOND_REASON_REMOVED); } - private void onNameChanged(String name) { - Intent intent = new Intent(BluetoothIntent.NAME_CHANGED_ACTION); - intent.putExtra(BluetoothIntent.NAME, name); - mContext.sendBroadcast(intent, BLUETOOTH_PERM); + /*package*/ void onPropertyChanged(String[] propValues) { + if (mBluetoothService.isAdapterPropertiesEmpty()) { + // We have got a property change before + // we filled up our cache. + mBluetoothService.getAllProperties(); + } + String name = propValues[0]; + if (name.equals("Name")) { + Intent intent = new Intent(BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED); + intent.putExtra(BluetoothAdapter.EXTRA_LOCAL_NAME, propValues[1]); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); + mBluetoothService.setProperty(name, propValues[1]); + } else if (name.equals("Pairable") || name.equals("Discoverable")) { + String pairable = name.equals("Pairable") ? propValues[1] : + mBluetoothService.getProperty("Pairable"); + String discoverable = name.equals("Discoverable") ? propValues[1] : + mBluetoothService.getProperty("Discoverable"); + + // This shouldn't happen, unless Adapter Properties are null. + if (pairable == null || discoverable == null) + return; + + int mode = BluetoothService.bluezStringToScanMode( + pairable.equals("true"), + discoverable.equals("true")); + if (mode >= 0) { + Intent intent = new Intent(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED); + intent.putExtra(BluetoothAdapter.EXTRA_SCAN_MODE, mode); + intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); + } + mBluetoothService.setProperty(name, propValues[1]); + } else if (name.equals("Discovering")) { + Intent intent; + if (propValues[1].equals("true")) { + mBluetoothService.setIsDiscovering(true); + intent = new Intent(BluetoothAdapter.ACTION_DISCOVERY_STARTED); + } else { + // Stop the discovery. + mBluetoothService.cancelDiscovery(); + mBluetoothService.setIsDiscovering(false); + intent = new Intent(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); + } + mContext.sendBroadcast(intent, BLUETOOTH_PERM); + mBluetoothService.setProperty(name, propValues[1]); + } else if (name.equals("Devices")) { + String value = null; + int len = Integer.valueOf(propValues[1]); + if (len > 0) { + StringBuilder str = new StringBuilder(); + for (int i = 2; i < propValues.length; i++) { + str.append(propValues[i]); + str.append(","); + } + value = str.toString(); + } + mBluetoothService.setProperty(name, value); + } else if (name.equals("Powered")) { + // bluetoothd has restarted, re-read all our properties. + // Note: bluez only sends this property change when it restarts. + if (propValues[1].equals("true")) + onRestartRequired(); + } } - private void onPasskeyAgentRequest(String address, int nativeData) { + private void onDevicePropertyChanged(String deviceObjectPath, String[] propValues) { + String name = propValues[0]; + String address = mBluetoothService.getAddressFromObjectPath(deviceObjectPath); + if (address == null) { + Log.e(TAG, "onDevicePropertyChanged: Address of the remote device in null"); + return; + } + if (DBG) { + log("Device property changed:" + address + "property:" + name); + } + BluetoothDevice device = mAdapter.getRemoteDevice(address); + if (name.equals("Name")) { + Intent intent = new Intent(BluetoothDevice.ACTION_NAME_CHANGED); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); + intent.putExtra(BluetoothDevice.EXTRA_NAME, propValues[1]); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); + mBluetoothService.setRemoteDeviceProperty(address, name, propValues[1]); + } else if (name.equals("Class")) { + Intent intent = new Intent(BluetoothDevice.ACTION_CLASS_CHANGED); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); + intent.putExtra(BluetoothDevice.EXTRA_CLASS, + new BluetoothClass(Integer.valueOf(propValues[1]))); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); + mBluetoothService.setRemoteDeviceProperty(address, name, propValues[1]); + } else if (name.equals("Connected")) { + Intent intent = null; + if (propValues[1].equals("true")) { + intent = new Intent(BluetoothDevice.ACTION_ACL_CONNECTED); + } else { + intent = new Intent(BluetoothDevice.ACTION_ACL_DISCONNECTED); + } + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); + mBluetoothService.setRemoteDeviceProperty(address, name, propValues[1]); + } else if (name.equals("UUIDs")) { + String uuid = null; + int len = Integer.valueOf(propValues[1]); + if (len > 0) { + StringBuilder str = new StringBuilder(); + for (int i = 2; i < propValues.length; i++) { + str.append(propValues[i]); + str.append(","); + } + uuid = str.toString(); + } + mBluetoothService.setRemoteDeviceProperty(address, name, uuid); + + // UUIDs have changed, query remote service channel and update cache. + mBluetoothService.updateDeviceServiceChannelCache(address); + + mBluetoothService.sendUuidIntent(address); + } else if (name.equals("Paired")) { + if (propValues[1].equals("true")) { + mBluetoothService.getBondState().setBondState(address, BluetoothDevice.BOND_BONDED); + } else { + mBluetoothService.getBondState().setBondState(address, + BluetoothDevice.BOND_NONE); + mBluetoothService.setRemoteDeviceProperty(address, "Trusted", "false"); + } + } else if (name.equals("Trusted")) { + if (DBG) + log("set trust state succeded, value is " + propValues[1]); + mBluetoothService.setRemoteDeviceProperty(address, name, propValues[1]); + } + } + + private String checkPairingRequestAndGetAddress(String objectPath, int nativeData) { + String address = mBluetoothService.getAddressFromObjectPath(objectPath); + if (address == null) { + Log.e(TAG, "Unable to get device address in checkPairingRequestAndGetAddress, " + + "returning null"); + return null; + } address = address.toUpperCase(); mPasskeyAgentRequestData.put(address, new Integer(nativeData)); - if (mBluetoothService.getBluetoothState() == BluetoothDevice.BLUETOOTH_STATE_TURNING_OFF) { + if (mBluetoothService.getBluetoothState() == BluetoothAdapter.STATE_TURNING_OFF) { // shutdown path - mBluetoothService.cancelPin(address); + mBluetoothService.cancelPairingUserInput(address); + return null; + } + // Set state to BONDING. For incoming connections it will be set here. + // For outgoing connections, it gets set when we call createBond. + // Also set it only when the state is not already Bonded, we can sometimes + // get an authorization request from the remote end if it doesn't have the link key + // while we still have it. + if (mBluetoothService.getBondState().getBondState(address) != BluetoothDevice.BOND_BONDED) + mBluetoothService.getBondState().setBondState(address, BluetoothDevice.BOND_BONDING); + return address; + } + + private void onRequestPairingConsent(String objectPath, int nativeData) { + String address = checkPairingRequestAndGetAddress(objectPath, nativeData); + if (address == null) return; + + /* The link key will not be stored if the incoming request has MITM + * protection switched on. Unfortunately, some devices have MITM + * switched on even though their capabilities are NoInputNoOutput, + * so we may get this request many times. Also if we respond immediately, + * the other end is unable to handle it. Delay sending the message. + */ + if (mBluetoothService.getBondState().getBondState(address) == BluetoothDevice.BOND_BONDED) { + Message message = mHandler.obtainMessage(EVENT_PAIRING_CONSENT_DELAYED_ACCEPT); + message.obj = address; + mHandler.sendMessageDelayed(message, 1500); return; } - if (mBluetoothService.getBondState().getBondState(address) == - BluetoothDevice.BOND_BONDING) { + Intent intent = new Intent(BluetoothDevice.ACTION_PAIRING_REQUEST); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); + intent.putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, + BluetoothDevice.PAIRING_VARIANT_CONSENT); + mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); + return; + } + + private void onRequestPasskeyConfirmation(String objectPath, int passkey, int nativeData) { + String address = checkPairingRequestAndGetAddress(objectPath, nativeData); + if (address == null) return; + + Intent intent = new Intent(BluetoothDevice.ACTION_PAIRING_REQUEST); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); + intent.putExtra(BluetoothDevice.EXTRA_PASSKEY, passkey); + intent.putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, + BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION); + mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); + return; + } + + private void onRequestPasskey(String objectPath, int nativeData) { + String address = checkPairingRequestAndGetAddress(objectPath, nativeData); + if (address == null) return; + + Intent intent = new Intent(BluetoothDevice.ACTION_PAIRING_REQUEST); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); + intent.putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, + BluetoothDevice.PAIRING_VARIANT_PASSKEY); + mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); + return; + } + + private void onRequestPinCode(String objectPath, int nativeData) { + String address = checkPairingRequestAndGetAddress(objectPath, nativeData); + if (address == null) return; + + String pendingOutgoingAddress = + mBluetoothService.getBondState().getPendingOutgoingBonding(); + if (address.equals(pendingOutgoingAddress)) { // we initiated the bonding - int btClass = mBluetoothService.getRemoteClass(address); + BluetoothClass btClass = new BluetoothClass(mBluetoothService.getRemoteClass(address)); // try 0000 once if the device looks dumb - switch (BluetoothClass.Device.getDevice(btClass)) { + switch (btClass.getDeviceClass()) { case BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET: case BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE: case BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES: @@ -306,56 +510,99 @@ class BluetoothEventLoop { } } } - Intent intent = new Intent(BluetoothIntent.PAIRING_REQUEST_ACTION); - intent.putExtra(BluetoothIntent.ADDRESS, address); + Intent intent = new Intent(BluetoothDevice.ACTION_PAIRING_REQUEST); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); + intent.putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.PAIRING_VARIANT_PIN); mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); + return; } - private void onPasskeyAgentCancel(String address) { - address = address.toUpperCase(); - mBluetoothService.cancelPin(address); - Intent intent = new Intent(BluetoothIntent.PAIRING_CANCEL_ACTION); - intent.putExtra(BluetoothIntent.ADDRESS, address); + private void onDisplayPasskey(String objectPath, int passkey, int nativeData) { + String address = checkPairingRequestAndGetAddress(objectPath, nativeData); + if (address == null) return; + + Intent intent = new Intent(BluetoothDevice.ACTION_PAIRING_REQUEST); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); + intent.putExtra(BluetoothDevice.EXTRA_PASSKEY, passkey); + intent.putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, + BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY); mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); - mBluetoothService.getBondState().setBondState(address, BluetoothDevice.BOND_NOT_BONDED, - BluetoothDevice.UNBOND_REASON_AUTH_CANCELED); } - private boolean onAuthAgentAuthorize(String address, String service, String uuid) { + private boolean onAgentAuthorize(String objectPath, String deviceUuid) { + String address = mBluetoothService.getAddressFromObjectPath(objectPath); + if (address == null) { + Log.e(TAG, "Unable to get device address in onAuthAgentAuthorize"); + return false; + } + boolean authorized = false; - if (mBluetoothService.isEnabled() && service.endsWith("service_audio")) { + ParcelUuid uuid = ParcelUuid.fromString(deviceUuid); + // Bluez sends the UUID of the local service being accessed, _not_ the + // remote service + if (mBluetoothService.isEnabled() && + (BluetoothUuid.isAudioSource(uuid) || BluetoothUuid.isAvrcpTarget(uuid) + || BluetoothUuid.isAdvAudioDist(uuid))) { BluetoothA2dp a2dp = new BluetoothA2dp(mContext); - authorized = a2dp.getSinkPriority(address) > BluetoothA2dp.PRIORITY_OFF; + BluetoothDevice device = mAdapter.getRemoteDevice(address); + authorized = a2dp.getSinkPriority(device) > BluetoothA2dp.PRIORITY_OFF; if (authorized) { - Log.i(TAG, "Allowing incoming A2DP connection from " + address); + Log.i(TAG, "Allowing incoming A2DP / AVRCP connection from " + address); } else { - Log.i(TAG, "Rejecting incoming A2DP connection from " + address); + Log.i(TAG, "Rejecting incoming A2DP / AVRCP connection from " + address); } } else { - Log.i(TAG, "Rejecting incoming " + service + " connection from " + address); + Log.i(TAG, "Rejecting incoming " + deviceUuid + " connection from " + address); } + log("onAgentAuthorize(" + objectPath + ", " + deviceUuid + ") = " + authorized); return authorized; } - private void onAuthAgentCancel(String address, String service, String uuid) { - // We immediately response to DBUS Authorize() so this should not - // usually happen - log("onAuthAgentCancel(" + address + ", " + service + ", " + uuid + ")"); + private void onAgentCancel() { + Intent intent = new Intent(BluetoothDevice.ACTION_PAIRING_CANCEL); + mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); + + mHandler.sendMessageDelayed(mHandler.obtainMessage(EVENT_AGENT_CANCEL), + 1500); + + return; + } + + private void onDiscoverServicesResult(String deviceObjectPath, boolean result) { + String address = mBluetoothService.getAddressFromObjectPath(deviceObjectPath); + // We don't parse the xml here, instead just query Bluez for the properties. + if (result) { + mBluetoothService.updateRemoteDevicePropertiesCache(address); + } + mBluetoothService.sendUuidIntent(address); + mBluetoothService.makeServiceChannelCallbacks(address); } - private void onGetRemoteServiceChannelResult(String address, int channel) { - IBluetoothDeviceCallback callback = mGetRemoteServiceChannelCallbacks.get(address); - if (callback != null) { - mGetRemoteServiceChannelCallbacks.remove(address); - try { - callback.onGetRemoteServiceChannelResult(address, channel); - } catch (RemoteException e) {} + private void onCreateDeviceResult(String address, int result) { + if (DBG) log("Result of onCreateDeviceResult:" + result); + + switch (result) { + case CREATE_DEVICE_ALREADY_EXISTS: + String path = mBluetoothService.getObjectPathFromAddress(address); + if (path != null) { + mBluetoothService.discoverServicesNative(path, ""); + break; + } + Log.w(TAG, "Device exists, but we dont have the bluez path, failing"); + // fall-through + case CREATE_DEVICE_FAILED: + mBluetoothService.sendUuidIntent(address); + mBluetoothService.makeServiceChannelCallbacks(address); + break; + case CREATE_DEVICE_SUCCESS: + // nothing to do, UUID intent's will be sent via property changed } } private void onRestartRequired() { if (mBluetoothService.isEnabled()) { - Log.e(TAG, "*** A serious error occured (did hcid crash?) - restarting Bluetooth ***"); + Log.e(TAG, "*** A serious error occured (did bluetoothd crash?) - " + + "restarting Bluetooth ***"); mHandler.sendEmptyMessage(EVENT_RESTART_BLUETOOTH); } } @@ -363,4 +610,10 @@ class BluetoothEventLoop { private static void log(String msg) { Log.d(TAG, msg); } + + private native void initializeNativeDataNative(); + private native void startEventLoopNative(); + private native void stopEventLoopNative(); + private native boolean isEventLoopRunningNative(); + private native void cleanupNativeDataNative(); } diff --git a/core/java/android/server/BluetoothService.java b/core/java/android/server/BluetoothService.java new file mode 100644 index 0000000..d1dd311 --- /dev/null +++ b/core/java/android/server/BluetoothService.java @@ -0,0 +1,1673 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * TODO: Move this to + * java/services/com/android/server/BluetoothService.java + * and make the contructor package private again. + * + * @hide + */ + +package android.server; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothClass; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHeadset; +import android.bluetooth.BluetoothSocket; +import android.bluetooth.BluetoothUuid; +import android.bluetooth.IBluetooth; +import android.bluetooth.IBluetoothCallback; +import android.os.ParcelUuid; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Binder; +import android.os.IBinder; +import android.os.Handler; +import android.os.Message; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemService; +import android.provider.Settings; +import android.util.Log; + +import com.android.internal.app.IBatteryStats; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +public class BluetoothService extends IBluetooth.Stub { + private static final String TAG = "BluetoothService"; + private static final boolean DBG = false; + + private int mNativeData; + private BluetoothEventLoop mEventLoop; + private IntentFilter mIntentFilter; + private boolean mIsAirplaneSensitive; + private int mBluetoothState; + private boolean mRestart = false; // need to call enable() after disable() + private boolean mIsDiscovering; + + private BluetoothAdapter mAdapter; // constant after init() + private final BondState mBondState = new BondState(); // local cache of bondings + private final IBatteryStats mBatteryStats; + private final Context mContext; + + private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN; + private static final String BLUETOOTH_PERM = android.Manifest.permission.BLUETOOTH; + + private static final int MESSAGE_REGISTER_SDP_RECORDS = 1; + private static final int MESSAGE_FINISH_DISABLE = 2; + private static final int MESSAGE_UUID_INTENT = 3; + private static final int MESSAGE_DISCOVERABLE_TIMEOUT = 4; + + // The timeout used to sent the UUIDs Intent + // This timeout should be greater than the page timeout + private static final int UUID_INTENT_DELAY = 6000; + + /** Always retrieve RFCOMM channel for these SDP UUIDs */ + private static final ParcelUuid[] RFCOMM_UUIDS = { + BluetoothUuid.Handsfree, + BluetoothUuid.HSP, + BluetoothUuid.ObexObjectPush }; + + + private final Map<String, String> mAdapterProperties; + private final HashMap<String, Map<String, String>> mDeviceProperties; + + private final HashMap<String, Map<ParcelUuid, Integer>> mDeviceServiceChannelCache; + private final ArrayList<String> mUuidIntentTracker; + private final HashMap<RemoteService, IBluetoothCallback> mUuidCallbackTracker; + + private final HashMap<Integer, Integer> mServiceRecordToPid; + + private static class RemoteService { + public String address; + public ParcelUuid uuid; + public RemoteService(String address, ParcelUuid uuid) { + this.address = address; + this.uuid = uuid; + } + @Override + public boolean equals(Object o) { + if (o instanceof RemoteService) { + RemoteService service = (RemoteService)o; + return address.equals(service.address) && uuid.equals(service.uuid); + } + return false; + } + } + + static { + classInitNative(); + } + + public BluetoothService(Context context) { + mContext = context; + + // Need to do this in place of: + // mBatteryStats = BatteryStatsService.getService(); + // Since we can not import BatteryStatsService from here. This class really needs to be + // moved to java/services/com/android/server/ + mBatteryStats = IBatteryStats.Stub.asInterface(ServiceManager.getService("batteryinfo")); + + initializeNativeDataNative(); + + if (isEnabledNative() == 1) { + Log.w(TAG, "Bluetooth daemons already running - runtime restart? "); + disableNative(); + } + + mBluetoothState = BluetoothAdapter.STATE_OFF; + mIsDiscovering = false; + mAdapterProperties = new HashMap<String, String>(); + mDeviceProperties = new HashMap<String, Map<String,String>>(); + + mDeviceServiceChannelCache = new HashMap<String, Map<ParcelUuid, Integer>>(); + mUuidIntentTracker = new ArrayList<String>(); + mUuidCallbackTracker = new HashMap<RemoteService, IBluetoothCallback>(); + mServiceRecordToPid = new HashMap<Integer, Integer>(); + registerForAirplaneMode(); + } + + public synchronized void initAfterRegistration() { + mAdapter = BluetoothAdapter.getDefaultAdapter(); + mEventLoop = new BluetoothEventLoop(mContext, mAdapter, this); + } + + @Override + protected void finalize() throws Throwable { + if (mIsAirplaneSensitive) { + mContext.unregisterReceiver(mReceiver); + } + try { + cleanupNativeDataNative(); + } finally { + super.finalize(); + } + } + + public boolean isEnabled() { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return mBluetoothState == BluetoothAdapter.STATE_ON; + } + + public int getBluetoothState() { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return mBluetoothState; + } + + + /** + * Bring down bluetooth and disable BT in settings. Returns true on success. + */ + public boolean disable() { + return disable(true); + } + + /** + * Bring down bluetooth. Returns true on success. + * + * @param saveSetting If true, persist the new setting + */ + public synchronized boolean disable(boolean saveSetting) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); + + switch (mBluetoothState) { + case BluetoothAdapter.STATE_OFF: + return true; + case BluetoothAdapter.STATE_ON: + break; + default: + return false; + } + if (mEnableThread != null && mEnableThread.isAlive()) { + return false; + } + setBluetoothState(BluetoothAdapter.STATE_TURNING_OFF); + mHandler.removeMessages(MESSAGE_REGISTER_SDP_RECORDS); + + // Allow 3 seconds for profiles to gracefully disconnect + // TODO: Introduce a callback mechanism so that each profile can notify + // BluetoothService when it is done shutting down + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MESSAGE_FINISH_DISABLE, saveSetting ? 1 : 0, 0), 3000); + return true; + } + + + private synchronized void finishDisable(boolean saveSetting) { + if (mBluetoothState != BluetoothAdapter.STATE_TURNING_OFF) { + return; + } + mEventLoop.stop(); + tearDownNativeDataNative(); + disableNative(); + + // mark in progress bondings as cancelled + for (String address : mBondState.listInState(BluetoothDevice.BOND_BONDING)) { + mBondState.setBondState(address, BluetoothDevice.BOND_NONE, + BluetoothDevice.UNBOND_REASON_AUTH_CANCELED); + } + + // update mode + Intent intent = new Intent(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED); + intent.putExtra(BluetoothAdapter.EXTRA_SCAN_MODE, BluetoothAdapter.SCAN_MODE_NONE); + mContext.sendBroadcast(intent, BLUETOOTH_PERM); + + mIsDiscovering = false; + mAdapterProperties.clear(); + mServiceRecordToPid.clear(); + + if (saveSetting) { + persistBluetoothOnSetting(false); + } + + setBluetoothState(BluetoothAdapter.STATE_OFF); + + // Log bluetooth off to battery stats. + long ident = Binder.clearCallingIdentity(); + try { + mBatteryStats.noteBluetoothOff(); + } catch (RemoteException e) { + } finally { + Binder.restoreCallingIdentity(ident); + } + + if (mRestart) { + mRestart = false; + enable(); + } + } + + /** Bring up BT and persist BT on in settings */ + public boolean enable() { + return enable(true); + } + + /** + * Enable this Bluetooth device, asynchronously. + * This turns on/off the underlying hardware. + * + * @param saveSetting If true, persist the new state of BT in settings + * @return True on success (so far) + */ + public synchronized boolean enable(boolean saveSetting) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + + // Airplane mode can prevent Bluetooth radio from being turned on. + if (mIsAirplaneSensitive && isAirplaneModeOn()) { + return false; + } + if (mBluetoothState != BluetoothAdapter.STATE_OFF) { + return false; + } + if (mEnableThread != null && mEnableThread.isAlive()) { + return false; + } + setBluetoothState(BluetoothAdapter.STATE_TURNING_ON); + mEnableThread = new EnableThread(saveSetting); + mEnableThread.start(); + return true; + } + + /** Forcibly restart Bluetooth if it is on */ + /* package */ synchronized void restart() { + if (mBluetoothState != BluetoothAdapter.STATE_ON) { + return; + } + mRestart = true; + if (!disable(false)) { + mRestart = false; + } + } + + private synchronized void setBluetoothState(int state) { + if (state == mBluetoothState) { + return; + } + + if (DBG) log("Bluetooth state " + mBluetoothState + " -> " + state); + + Intent intent = new Intent(BluetoothAdapter.ACTION_STATE_CHANGED); + intent.putExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, mBluetoothState); + intent.putExtra(BluetoothAdapter.EXTRA_STATE, state); + intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + + mBluetoothState = state; + + mContext.sendBroadcast(intent, BLUETOOTH_PERM); + } + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_REGISTER_SDP_RECORDS: + if (!isEnabled()) { + return; + } + // SystemService.start() forks sdptool to register service + // records. It can fail to register some records if it is + // forked multiple times in a row, probably because there is + // some race in sdptool or bluez when operated in parallel. + // As a workaround, delay 500ms between each fork of sdptool. + // TODO: Don't fork sdptool in order to regsiter service + // records, use a DBUS call instead. + switch (msg.arg1) { + case 1: + Log.d(TAG, "Registering hsag record"); + SystemService.start("hsag"); + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MESSAGE_REGISTER_SDP_RECORDS, 2, -1), 500); + break; + case 2: + Log.d(TAG, "Registering hfag record"); + SystemService.start("hfag"); + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MESSAGE_REGISTER_SDP_RECORDS, 3, -1), 500); + break; + case 3: + Log.d(TAG, "Registering opush record"); + SystemService.start("opush"); + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MESSAGE_REGISTER_SDP_RECORDS, 4, -1), 500); + break; + case 4: + Log.d(TAG, "Registering pbap record"); + SystemService.start("pbap"); + break; + } + break; + case MESSAGE_FINISH_DISABLE: + finishDisable(msg.arg1 != 0); + break; + case MESSAGE_UUID_INTENT: + String address = (String)msg.obj; + if (address != null) { + sendUuidIntent(address); + makeServiceChannelCallbacks(address); + } + break; + case MESSAGE_DISCOVERABLE_TIMEOUT: + int mode = msg.arg1; + if (isEnabled()) { + // TODO: Switch back to the previous scan mode + // This is ok for now, because we only use + // CONNECTABLE and CONNECTABLE_DISCOVERABLE + setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE, -1); + } + break; + } + } + }; + + private EnableThread mEnableThread; + + private class EnableThread extends Thread { + private final boolean mSaveSetting; + public EnableThread(boolean saveSetting) { + mSaveSetting = saveSetting; + } + public void run() { + boolean res = (enableNative() == 0); + if (res) { + int retryCount = 2; + boolean running = false; + while ((retryCount-- > 0) && !running) { + mEventLoop.start(); + // it may take a momement for the other thread to do its + // thing. Check periodically for a while. + int pollCount = 5; + while ((pollCount-- > 0) && !running) { + if (mEventLoop.isEventLoopRunning()) { + running = true; + break; + } + try { + Thread.sleep(100); + } catch (InterruptedException e) {} + } + } + if (!running) { + log("bt EnableThread giving up"); + res = false; + disableNative(); + } + } + + + if (res) { + if (!setupNativeDataNative()) { + return; + } + if (mSaveSetting) { + persistBluetoothOnSetting(true); + } + mIsDiscovering = false; + mBondState.loadBondState(); + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MESSAGE_REGISTER_SDP_RECORDS, 1, -1), 3000); + + // Log bluetooth on to battery stats. + long ident = Binder.clearCallingIdentity(); + try { + mBatteryStats.noteBluetoothOn(); + } catch (RemoteException e) { + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + mEnableThread = null; + + setBluetoothState(res ? + BluetoothAdapter.STATE_ON : + BluetoothAdapter.STATE_OFF); + + if (res) { + // Update mode + String[] propVal = {"Pairable", getProperty("Pairable")}; + mEventLoop.onPropertyChanged(propVal); + } + + if (mIsAirplaneSensitive && isAirplaneModeOn()) { + disable(false); + } + + } + } + + private void persistBluetoothOnSetting(boolean bluetoothOn) { + long origCallerIdentityToken = Binder.clearCallingIdentity(); + Settings.Secure.putInt(mContext.getContentResolver(), Settings.Secure.BLUETOOTH_ON, + bluetoothOn ? 1 : 0); + Binder.restoreCallingIdentity(origCallerIdentityToken); + } + + /* package */ BondState getBondState() { + return mBondState; + } + + /** local cache of bonding state. + /* we keep our own state to track the intermediate state BONDING, which + /* bluez does not track. + * All addreses must be passed in upper case. + */ + public class BondState { + private final HashMap<String, Integer> mState = new HashMap<String, Integer>(); + private final HashMap<String, Integer> mPinAttempt = new HashMap<String, Integer>(); + private final ArrayList<String> mAutoPairingFailures = new ArrayList<String>(); + // List of all the vendor_id prefix of Bluetooth addresses for + // which auto pairing is not attempted. + // The following companies are included in the list below: + // ALPS (lexus), Murata (Prius 2007, Nokia 616), TEMIC SDS (Porsche, Audi), + // Parrot, Zhongshan General K-mate Electronics, Great Well + // Electronics, Flaircomm Electronics, Jatty Electronics, Delphi, + // Clarion, Novero, Denso (Lexus, Toyota), Johnson Controls (Acura), + // Continental Automotive, Harman/Becker, Panasonic/Kyushu Ten, + // BMW (Motorola PCS) + private final ArrayList<String> mAutoPairingAddressBlacklist = + new ArrayList<String>(Arrays.asList( + "00:02:C7", "00:16:FE", "00:19:C1", "00:1B:FB", "00:1E:3D", "00:21:4F", + "00:23:06", "00:24:33", "00:A0:79", "00:0E:6D", "00:13:E0", "00:21:E8", + "00:60:57", "00:0E:9F", "00:12:1C", "00:18:91", "00:18:96", "00:13:04", + "00:16:FD", "00:22:A0", "00:0B:4C", "00:60:6F", "00:23:3D", "00:C0:59", + "00:0A:30", "00:1E:AE", "00:1C:D7", "00:80:F0", "00:12:8A" + )); + + // List of names of Bluetooth devices for which auto pairing should be + // disabled. + private final ArrayList<String> mAutoPairingNameBlacklist = + new ArrayList<String>(Arrays.asList( + "Motorola IHF1000", "i.TechBlueBAND", "X5 Stereo v1.3")); + + // If this is an outgoing connection, store the address. + // There can be only 1 pending outgoing connection at a time, + private String mPendingOutgoingBonding; + + private synchronized void setPendingOutgoingBonding(String address) { + mPendingOutgoingBonding = address; + } + + public synchronized String getPendingOutgoingBonding() { + return mPendingOutgoingBonding; + } + + public synchronized void loadBondState() { + if (mBluetoothState != BluetoothAdapter.STATE_TURNING_ON) { + return; + } + String []bonds = null; + String val = getProperty("Devices"); + if (val != null) { + bonds = val.split(","); + } + if (bonds == null) { + return; + } + mState.clear(); + if (DBG) log("found " + bonds.length + " bonded devices"); + for (String device : bonds) { + mState.put(getAddressFromObjectPath(device).toUpperCase(), + BluetoothDevice.BOND_BONDED); + } + } + + public synchronized void setBondState(String address, int state) { + setBondState(address, state, 0); + } + + /** reason is ignored unless state == BOND_NOT_BONDED */ + public synchronized void setBondState(String address, int state, int reason) { + int oldState = getBondState(address); + if (oldState == state) { + return; + } + + // Check if this was an pending outgoing bonding. + // If yes, reset the state. + if (oldState == BluetoothDevice.BOND_BONDING) { + if (address.equals(mPendingOutgoingBonding)) { + mPendingOutgoingBonding = null; + } + } + + if (DBG) log(address + " bond state " + oldState + " -> " + state + " (" + + reason + ")"); + Intent intent = new Intent(BluetoothDevice.ACTION_BOND_STATE_CHANGED); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); + intent.putExtra(BluetoothDevice.EXTRA_BOND_STATE, state); + intent.putExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, oldState); + if (state == BluetoothDevice.BOND_NONE) { + if (reason <= 0) { + Log.w(TAG, "setBondState() called to unbond device, but reason code is " + + "invalid. Overriding reason code with BOND_RESULT_REMOVED"); + reason = BluetoothDevice.UNBOND_REASON_REMOVED; + } + intent.putExtra(BluetoothDevice.EXTRA_REASON, reason); + mState.remove(address); + } else { + mState.put(address, state); + } + + mContext.sendBroadcast(intent, BLUETOOTH_PERM); + } + + public boolean isAutoPairingBlacklisted(String address) { + for (String blacklistAddress : mAutoPairingAddressBlacklist) { + if (address.startsWith(blacklistAddress)) return true; + } + + String name = getRemoteName(address); + if (name != null) { + for (String blacklistName : mAutoPairingNameBlacklist) { + if (name.equals(blacklistName)) return true; + } + } + return false; + } + + public synchronized int getBondState(String address) { + Integer state = mState.get(address); + if (state == null) { + return BluetoothDevice.BOND_NONE; + } + return state.intValue(); + } + + /*package*/ synchronized String[] listInState(int state) { + ArrayList<String> result = new ArrayList<String>(mState.size()); + for (Map.Entry<String, Integer> e : mState.entrySet()) { + if (e.getValue().intValue() == state) { + result.add(e.getKey()); + } + } + return result.toArray(new String[result.size()]); + } + + public synchronized void addAutoPairingFailure(String address) { + if (!mAutoPairingFailures.contains(address)) { + mAutoPairingFailures.add(address); + } + } + + public synchronized boolean isAutoPairingAttemptsInProgress(String address) { + return getAttempt(address) != 0; + } + + public synchronized void clearPinAttempts(String address) { + mPinAttempt.remove(address); + } + + public synchronized boolean hasAutoPairingFailed(String address) { + return mAutoPairingFailures.contains(address); + } + + public synchronized int getAttempt(String address) { + Integer attempt = mPinAttempt.get(address); + if (attempt == null) { + return 0; + } + return attempt.intValue(); + } + + public synchronized void attempt(String address) { + Integer attempt = mPinAttempt.get(address); + int newAttempt; + if (attempt == null) { + newAttempt = 1; + } else { + newAttempt = attempt.intValue() + 1; + } + mPinAttempt.put(address, new Integer(newAttempt)); + } + + } + + private static String toBondStateString(int bondState) { + switch (bondState) { + case BluetoothDevice.BOND_NONE: + return "not bonded"; + case BluetoothDevice.BOND_BONDING: + return "bonding"; + case BluetoothDevice.BOND_BONDED: + return "bonded"; + default: + return "??????"; + } + } + + /*package*/ synchronized boolean isAdapterPropertiesEmpty() { + return mAdapterProperties.isEmpty(); + } + + /*package*/synchronized void getAllProperties() { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + mAdapterProperties.clear(); + + String properties[] = (String [])getAdapterPropertiesNative(); + // The String Array consists of key-value pairs. + if (properties == null) { + Log.e(TAG, "*Error*: GetAdapterProperties returned NULL"); + return; + } + + for (int i = 0; i < properties.length; i++) { + String name = properties[i]; + String newValue = null; + int len; + if (name == null) { + Log.e(TAG, "Error:Adapter Property at index" + i + "is null"); + continue; + } + if (name.equals("Devices")) { + StringBuilder str = new StringBuilder(); + len = Integer.valueOf(properties[++i]); + for (int j = 0; j < len; j++) { + str.append(properties[++i]); + str.append(","); + } + if (len > 0) { + newValue = str.toString(); + } + } else { + newValue = properties[++i]; + } + mAdapterProperties.put(name, newValue); + } + + // Add adapter object path property. + String adapterPath = getAdapterPathNative(); + if (adapterPath != null) + mAdapterProperties.put("ObjectPath", adapterPath + "/dev_"); + } + + /* package */ synchronized void setProperty(String name, String value) { + mAdapterProperties.put(name, value); + } + + public synchronized boolean setName(String name) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + if (name == null) { + return false; + } + return setPropertyString("Name", name); + } + + //TODO(): setPropertyString, setPropertyInteger, setPropertyBoolean + // Either have a single property function with Object as the parameter + // or have a function for each property and then obfuscate in the JNI layer. + // The following looks dirty. + private boolean setPropertyString(String key, String value) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return setAdapterPropertyStringNative(key, value); + } + + private boolean setPropertyInteger(String key, int value) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return setAdapterPropertyIntegerNative(key, value); + } + + private boolean setPropertyBoolean(String key, boolean value) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return setAdapterPropertyBooleanNative(key, value ? 1 : 0); + } + + /** + * Set the discoverability window for the device. A timeout of zero + * makes the device permanently discoverable (if the device is + * discoverable). Setting the timeout to a nonzero value does not make + * a device discoverable; you need to call setMode() to make the device + * explicitly discoverable. + * + * @param timeout_s The discoverable timeout in seconds. + */ + public synchronized boolean setDiscoverableTimeout(int timeout) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + return setPropertyInteger("DiscoverableTimeout", timeout); + } + + public synchronized boolean setScanMode(int mode, int duration) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS, + "Need WRITE_SECURE_SETTINGS permission"); + boolean pairable = false; + boolean discoverable = false; + + switch (mode) { + case BluetoothAdapter.SCAN_MODE_NONE: + mHandler.removeMessages(MESSAGE_DISCOVERABLE_TIMEOUT); + pairable = false; + discoverable = false; + break; + case BluetoothAdapter.SCAN_MODE_CONNECTABLE: + mHandler.removeMessages(MESSAGE_DISCOVERABLE_TIMEOUT); + pairable = true; + discoverable = false; + break; + case BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE: + mHandler.removeMessages(MESSAGE_DISCOVERABLE_TIMEOUT); + pairable = true; + discoverable = true; + Message msg = mHandler.obtainMessage(MESSAGE_DISCOVERABLE_TIMEOUT); + mHandler.sendMessageDelayed(msg, duration * 1000); + if (DBG) Log.d(TAG, "BT Discoverable for " + duration + " seconds"); + break; + default: + Log.w(TAG, "Requested invalid scan mode " + mode); + return false; + } + setPropertyBoolean("Pairable", pairable); + setPropertyBoolean("Discoverable", discoverable); + + return true; + } + + /*package*/ synchronized String getProperty (String name) { + if (!mAdapterProperties.isEmpty()) + return mAdapterProperties.get(name); + getAllProperties(); + return mAdapterProperties.get(name); + } + + public synchronized String getAddress() { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return getProperty("Address"); + } + + public synchronized String getName() { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return getProperty("Name"); + } + + /** + * Returns the user-friendly name of a remote device. This value is + * returned from our local cache, which is updated when onPropertyChange + * event is received. + * Do not expect to retrieve the updated remote name immediately after + * changing the name on the remote device. + * + * @param address Bluetooth address of remote device. + * + * @return The user-friendly name of the specified remote device. + */ + public synchronized String getRemoteName(String address) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + return null; + } + return getRemoteDeviceProperty(address, "Name"); + } + + /** + * Get the discoverability window for the device. A timeout of zero + * means that the device is permanently discoverable (if the device is + * in the discoverable mode). + * + * @return The discoverability window of the device, in seconds. A negative + * value indicates an error. + */ + public synchronized int getDiscoverableTimeout() { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + String timeout = getProperty("DiscoverableTimeout"); + if (timeout != null) + return Integer.valueOf(timeout); + else + return -1; + } + + public synchronized int getScanMode() { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + if (!isEnabled()) + return BluetoothAdapter.SCAN_MODE_NONE; + + boolean pairable = getProperty("Pairable").equals("true"); + boolean discoverable = getProperty("Discoverable").equals("true"); + return bluezStringToScanMode (pairable, discoverable); + } + + public synchronized boolean startDiscovery() { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + if (!isEnabled()) { + return false; + } + return startDiscoveryNative(); + } + + public synchronized boolean cancelDiscovery() { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + return stopDiscoveryNative(); + } + + public synchronized boolean isDiscovering() { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return mIsDiscovering; + } + + /* package */ void setIsDiscovering(boolean isDiscovering) { + mIsDiscovering = isDiscovering; + } + + public synchronized boolean createBond(String address) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + return false; + } + address = address.toUpperCase(); + + if (mBondState.getPendingOutgoingBonding() != null) { + log("Ignoring createBond(): another device is bonding"); + // a different device is currently bonding, fail + return false; + } + + // Check for bond state only if we are not performing auto + // pairing exponential back-off attempts. + if (!mBondState.isAutoPairingAttemptsInProgress(address) && + mBondState.getBondState(address) != BluetoothDevice.BOND_NONE) { + log("Ignoring createBond(): this device is already bonding or bonded"); + return false; + } + + if (!createPairedDeviceNative(address, 60000 /* 1 minute */)) { + return false; + } + + mBondState.setPendingOutgoingBonding(address); + mBondState.setBondState(address, BluetoothDevice.BOND_BONDING); + + return true; + } + + public synchronized boolean cancelBondProcess(String address) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + return false; + } + address = address.toUpperCase(); + if (mBondState.getBondState(address) != BluetoothDevice.BOND_BONDING) { + return false; + } + + mBondState.setBondState(address, BluetoothDevice.BOND_NONE, + BluetoothDevice.UNBOND_REASON_AUTH_CANCELED); + cancelDeviceCreationNative(address); + return true; + } + + public synchronized boolean removeBond(String address) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + return false; + } + return removeDeviceNative(getObjectPathFromAddress(address)); + } + + public synchronized String[] listBonds() { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return mBondState.listInState(BluetoothDevice.BOND_BONDED); + } + + public synchronized int getBondState(String address) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + return BluetoothDevice.ERROR; + } + return mBondState.getBondState(address.toUpperCase()); + } + + /*package*/ boolean isRemoteDeviceInCache(String address) { + return (mDeviceProperties.get(address) != null); + } + + /*package*/ String[] getRemoteDeviceProperties(String address) { + String objectPath = getObjectPathFromAddress(address); + return (String [])getDevicePropertiesNative(objectPath); + } + + /*package*/ synchronized String getRemoteDeviceProperty(String address, String property) { + Map<String, String> properties = mDeviceProperties.get(address); + if (properties != null) { + return properties.get(property); + } else { + // Query for remote device properties, again. + // We will need to reload the cache when we switch Bluetooth on / off + // or if we crash. + if (updateRemoteDevicePropertiesCache(address)) + return getRemoteDeviceProperty(address, property); + } + Log.e(TAG, "getRemoteDeviceProperty: " + property + "not present:" + address); + return null; + } + + /* package */ synchronized boolean updateRemoteDevicePropertiesCache(String address) { + String[] propValues = getRemoteDeviceProperties(address); + if (propValues != null) { + addRemoteDeviceProperties(address, propValues); + return true; + } + return false; + } + + /* package */ synchronized void addRemoteDeviceProperties(String address, String[] properties) { + /* + * We get a DeviceFound signal every time RSSI changes or name changes. + * Don't create a new Map object every time */ + Map<String, String> propertyValues = mDeviceProperties.get(address); + if (propertyValues == null) { + propertyValues = new HashMap<String, String>(); + } + + for (int i = 0; i < properties.length; i++) { + String name = properties[i]; + String newValue = null; + int len; + if (name == null) { + Log.e(TAG, "Error: Remote Device Property at index" + i + "is null"); + continue; + } + if (name.equals("UUIDs") || name.equals("Nodes")) { + StringBuilder str = new StringBuilder(); + len = Integer.valueOf(properties[++i]); + for (int j = 0; j < len; j++) { + str.append(properties[++i]); + str.append(","); + } + if (len > 0) { + newValue = str.toString(); + } + } else { + newValue = properties[++i]; + } + + propertyValues.put(name, newValue); + } + mDeviceProperties.put(address, propertyValues); + + // We have added a new remote device or updated its properties. + // Also update the serviceChannel cache. + updateDeviceServiceChannelCache(address); + } + + /* package */ void removeRemoteDeviceProperties(String address) { + mDeviceProperties.remove(address); + } + + /* package */ synchronized void setRemoteDeviceProperty(String address, String name, + String value) { + Map <String, String> propVal = mDeviceProperties.get(address); + if (propVal != null) { + propVal.put(name, value); + mDeviceProperties.put(address, propVal); + } else { + Log.e(TAG, "setRemoteDeviceProperty for a device not in cache:" + address); + } + } + + /** + * Sets the remote device trust state. + * + * @return boolean to indicate operation success or fail + */ + public synchronized boolean setTrust(String address, boolean value) { + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + return false; + } + + return setDevicePropertyBooleanNative(getObjectPathFromAddress(address), "Trusted", + value ? 1 : 0); + } + + /** + * Gets the remote device trust state as boolean. + * Note: this value may be + * retrieved from cache if we retrieved the data before * + * + * @return boolean to indicate trust or untrust state + */ + public synchronized boolean getTrustState(String address) { + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return false; + } + + String val = getRemoteDeviceProperty(address, "Trusted"); + if (val == null) { + return false; + } else { + return val.equals("true") ? true : false; + } + } + + /** + * Gets the remote major, minor classes encoded as a 32-bit + * integer. + * + * Note: this value is retrieved from cache, because we get it during + * remote-device discovery. + * + * @return 32-bit integer encoding the remote major, minor, and service + * classes. + */ + public synchronized int getRemoteClass(String address) { + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return BluetoothClass.ERROR; + } + String val = getRemoteDeviceProperty(address, "Class"); + if (val == null) + return BluetoothClass.ERROR; + else { + return Integer.valueOf(val); + } + } + + + /** + * Gets the UUIDs supported by the remote device + * + * @return array of 128bit ParcelUuids + */ + public synchronized ParcelUuid[] getRemoteUuids(String address) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + return null; + } + return getUuidFromCache(address); + } + + private ParcelUuid[] getUuidFromCache(String address) { + String value = getRemoteDeviceProperty(address, "UUIDs"); + if (value == null) return null; + + String[] uuidStrings = null; + // The UUIDs are stored as a "," separated string. + uuidStrings = value.split(","); + ParcelUuid[] uuids = new ParcelUuid[uuidStrings.length]; + + for (int i = 0; i < uuidStrings.length; i++) { + uuids[i] = ParcelUuid.fromString(uuidStrings[i]); + } + return uuids; + } + + /** + * Connect and fetch new UUID's using SDP. + * The UUID's found are broadcast as intents. + * Optionally takes a uuid and callback to fetch the RFCOMM channel for the + * a given uuid. + * TODO: Don't wait UUID_INTENT_DELAY to broadcast UUID intents on success + * TODO: Don't wait UUID_INTENT_DELAY to handle the failure case for + * callback and broadcast intents. + */ + public synchronized boolean fetchRemoteUuids(String address, ParcelUuid uuid, + IBluetoothCallback callback) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + return false; + } + + RemoteService service = new RemoteService(address, uuid); + if (uuid != null && mUuidCallbackTracker.get(service) != null) { + // An SDP query for this address & uuid is already in progress + // Do not add this callback for the uuid + return false; + } + + if (mUuidIntentTracker.contains(address)) { + // An SDP query for this address is already in progress + // Add this uuid onto the in-progress SDP query + if (uuid != null) { + mUuidCallbackTracker.put(new RemoteService(address, uuid), callback); + } + return true; + } + + boolean ret; + if (getBondState(address) == BluetoothDevice.BOND_BONDED) { + String path = getObjectPathFromAddress(address); + if (path == null) return false; + + // Use an empty string for the UUID pattern + ret = discoverServicesNative(path, ""); + } else { + ret = createDeviceNative(address); + } + + mUuidIntentTracker.add(address); + if (uuid != null) { + mUuidCallbackTracker.put(new RemoteService(address, uuid), callback); + } + + Message message = mHandler.obtainMessage(MESSAGE_UUID_INTENT); + message.obj = address; + mHandler.sendMessageDelayed(message, UUID_INTENT_DELAY); + return ret; + } + + /** + * Gets the rfcomm channel associated with the UUID. + * Pulls records from the cache only. + * + * @param address Address of the remote device + * @param uuid ParcelUuid of the service attribute + * + * @return rfcomm channel associated with the service attribute + * -1 on error + */ + public int getRemoteServiceChannel(String address, ParcelUuid uuid) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + return BluetoothDevice.ERROR; + } + // Check if we are recovering from a crash. + if (mDeviceProperties.isEmpty()) { + if (!updateRemoteDevicePropertiesCache(address)) + return -1; + } + + Map<ParcelUuid, Integer> value = mDeviceServiceChannelCache.get(address); + if (value != null && value.containsKey(uuid)) + return value.get(uuid); + return -1; + } + + public synchronized boolean setPin(String address, byte[] pin) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + if (pin == null || pin.length <= 0 || pin.length > 16 || + !BluetoothAdapter.checkBluetoothAddress(address)) { + return false; + } + address = address.toUpperCase(); + Integer data = mEventLoop.getPasskeyAgentRequestData().remove(address); + if (data == null) { + Log.w(TAG, "setPin(" + address + ") called but no native data available, " + + "ignoring. Maybe the PasskeyAgent Request was cancelled by the remote device" + + " or by bluez.\n"); + return false; + } + // bluez API wants pin as a string + String pinString; + try { + pinString = new String(pin, "UTF8"); + } catch (UnsupportedEncodingException uee) { + Log.e(TAG, "UTF8 not supported?!?"); + return false; + } + return setPinNative(address, pinString, data.intValue()); + } + + public synchronized boolean setPasskey(String address, int passkey) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + if (passkey < 0 || passkey > 999999 || !BluetoothAdapter.checkBluetoothAddress(address)) { + return false; + } + address = address.toUpperCase(); + Integer data = mEventLoop.getPasskeyAgentRequestData().remove(address); + if (data == null) { + Log.w(TAG, "setPasskey(" + address + ") called but no native data available, " + + "ignoring. Maybe the PasskeyAgent Request was cancelled by the remote device" + + " or by bluez.\n"); + return false; + } + return setPasskeyNative(address, passkey, data.intValue()); + } + + public synchronized boolean setPairingConfirmation(String address, boolean confirm) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + address = address.toUpperCase(); + Integer data = mEventLoop.getPasskeyAgentRequestData().remove(address); + if (data == null) { + Log.w(TAG, "setPasskey(" + address + ") called but no native data available, " + + "ignoring. Maybe the PasskeyAgent Request was cancelled by the remote device" + + " or by bluez.\n"); + return false; + } + return setPairingConfirmationNative(address, confirm, data.intValue()); + } + + public synchronized boolean cancelPairingUserInput(String address) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + if (!BluetoothAdapter.checkBluetoothAddress(address)) { + return false; + } + mBondState.setBondState(address, BluetoothDevice.BOND_NONE, + BluetoothDevice.UNBOND_REASON_AUTH_CANCELED); + address = address.toUpperCase(); + Integer data = mEventLoop.getPasskeyAgentRequestData().remove(address); + if (data == null) { + Log.w(TAG, "cancelUserInputNative(" + address + ") called but no native data " + + "available, ignoring. Maybe the PasskeyAgent Request was already cancelled " + + "by the remote or by bluez.\n"); + return false; + } + return cancelPairingUserInputNative(address, data.intValue()); + } + + public void updateDeviceServiceChannelCache(String address) { + ParcelUuid[] deviceUuids = getRemoteUuids(address); + // We are storing the rfcomm channel numbers only for the uuids + // we are interested in. + int channel; + if (DBG) log("updateDeviceServiceChannelCache(" + address + ")"); + + ArrayList<ParcelUuid> applicationUuids = new ArrayList(); + + synchronized (this) { + for (RemoteService service : mUuidCallbackTracker.keySet()) { + if (service.address.equals(address)) { + applicationUuids.add(service.uuid); + } + } + } + + Map <ParcelUuid, Integer> value = new HashMap<ParcelUuid, Integer>(); + + // Retrieve RFCOMM channel for default uuids + for (ParcelUuid uuid : RFCOMM_UUIDS) { + if (BluetoothUuid.isUuidPresent(deviceUuids, uuid)) { + channel = getDeviceServiceChannelNative(getObjectPathFromAddress(address), + uuid.toString(), 0x0004); + if (DBG) log("\tuuid(system): " + uuid + " " + channel); + value.put(uuid, channel); + } + } + // Retrieve RFCOMM channel for application requested uuids + for (ParcelUuid uuid : applicationUuids) { + if (BluetoothUuid.isUuidPresent(deviceUuids, uuid)) { + channel = getDeviceServiceChannelNative(getObjectPathFromAddress(address), + uuid.toString(), 0x0004); + if (DBG) log("\tuuid(application): " + uuid + " " + channel); + value.put(uuid, channel); + } + } + + synchronized (this) { + // Make application callbacks + for (Iterator<RemoteService> iter = mUuidCallbackTracker.keySet().iterator(); + iter.hasNext();) { + RemoteService service = iter.next(); + if (service.address.equals(address)) { + channel = -1; + if (value.get(service.uuid) != null) { + channel = value.get(service.uuid); + } + if (channel != -1) { + if (DBG) log("Making callback for " + service.uuid + " with result " + + channel); + IBluetoothCallback callback = mUuidCallbackTracker.get(service); + if (callback != null) { + try { + callback.onRfcommChannelFound(channel); + } catch (RemoteException e) {Log.e(TAG, "", e);} + } + + iter.remove(); + } + } + } + + // Update cache + mDeviceServiceChannelCache.put(address, value); + } + } + + /** + * b is a handle to a Binder instance, so that this service can be notified + * for Applications that terminate unexpectedly, to clean there service + * records + */ + public synchronized int addRfcommServiceRecord(String serviceName, ParcelUuid uuid, + int channel, IBinder b) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, + "Need BLUETOOTH permission"); + if (serviceName == null || uuid == null || channel < 1 || + channel > BluetoothSocket.MAX_RFCOMM_CHANNEL) { + return -1; + } + if (BluetoothUuid.isUuidPresent(BluetoothUuid.RESERVED_UUIDS, uuid)) { + Log.w(TAG, "Attempted to register a reserved UUID: " + uuid); + return -1; + } + int handle = addRfcommServiceRecordNative(serviceName, + uuid.getUuid().getMostSignificantBits(), uuid.getUuid().getLeastSignificantBits(), + (short)channel); + if (DBG) log("new handle " + Integer.toHexString(handle)); + if (handle == -1) { + return -1; + } + + int pid = Binder.getCallingPid(); + mServiceRecordToPid.put(new Integer(handle), new Integer(pid)); + try { + b.linkToDeath(new Reaper(handle, pid), 0); + } catch (RemoteException e) {} + return handle; + } + + public void removeServiceRecord(int handle) { + mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, + "Need BLUETOOTH permission"); + checkAndRemoveRecord(handle, Binder.getCallingPid()); + } + + private synchronized void checkAndRemoveRecord(int handle, int pid) { + Integer handleInt = new Integer(handle); + Integer owner = mServiceRecordToPid.get(handleInt); + if (owner != null && pid == owner.intValue()) { + if (DBG) log("Removing service record " + Integer.toHexString(handle) + " for pid " + + pid); + mServiceRecordToPid.remove(handleInt); + removeServiceRecordNative(handle); + } + } + + private class Reaper implements IBinder.DeathRecipient { + int pid; + int handle; + Reaper(int handle, int pid) { + this.pid = pid; + this.handle = handle; + } + public void binderDied() { + synchronized (BluetoothService.this) { + if (DBG) log("Tracked app " + pid + " died"); + checkAndRemoveRecord(handle, pid); + } + } + } + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(Intent.ACTION_AIRPLANE_MODE_CHANGED)) { + ContentResolver resolver = context.getContentResolver(); + // Query the airplane mode from Settings.System just to make sure that + // some random app is not sending this intent and disabling bluetooth + boolean enabled = !isAirplaneModeOn(); + // If bluetooth is currently expected to be on, then enable or disable bluetooth + if (Settings.Secure.getInt(resolver, Settings.Secure.BLUETOOTH_ON, 0) > 0) { + if (enabled) { + enable(false); + } else { + disable(false); + } + } + } + } + }; + + private void registerForAirplaneMode() { + String airplaneModeRadios = Settings.System.getString(mContext.getContentResolver(), + Settings.System.AIRPLANE_MODE_RADIOS); + mIsAirplaneSensitive = airplaneModeRadios == null + ? true : airplaneModeRadios.contains(Settings.System.RADIO_BLUETOOTH); + if (mIsAirplaneSensitive) { + mIntentFilter = new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED); + mContext.registerReceiver(mReceiver, mIntentFilter); + } + } + + /* Returns true if airplane mode is currently on */ + private final boolean isAirplaneModeOn() { + return Settings.System.getInt(mContext.getContentResolver(), + Settings.System.AIRPLANE_MODE_ON, 0) == 1; + } + + /* Broadcast the Uuid intent */ + /*package*/ synchronized void sendUuidIntent(String address) { + ParcelUuid[] uuid = getUuidFromCache(address); + Intent intent = new Intent(BluetoothDevice.ACTION_UUID); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mAdapter.getRemoteDevice(address)); + intent.putExtra(BluetoothDevice.EXTRA_UUID, uuid); + mContext.sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); + + if (mUuidIntentTracker.contains(address)) + mUuidIntentTracker.remove(address); + + } + + /*package*/ synchronized void makeServiceChannelCallbacks(String address) { + for (Iterator<RemoteService> iter = mUuidCallbackTracker.keySet().iterator(); + iter.hasNext();) { + RemoteService service = iter.next(); + if (service.address.equals(address)) { + if (DBG) log("Cleaning up failed UUID channel lookup: " + service.address + + " " + service.uuid); + IBluetoothCallback callback = mUuidCallbackTracker.get(service); + if (callback != null) { + try { + callback.onRfcommChannelFound(-1); + } catch (RemoteException e) {Log.e(TAG, "", e);} + } + + iter.remove(); + } + } + } + + @Override + protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + switch(mBluetoothState) { + case BluetoothAdapter.STATE_OFF: + pw.println("Bluetooth OFF\n"); + return; + case BluetoothAdapter.STATE_TURNING_ON: + pw.println("Bluetooth TURNING ON\n"); + return; + case BluetoothAdapter.STATE_TURNING_OFF: + pw.println("Bluetooth TURNING OFF\n"); + return; + case BluetoothAdapter.STATE_ON: + pw.println("Bluetooth ON\n"); + } + + pw.println("mIsAirplaneSensitive = " + mIsAirplaneSensitive); + + pw.println("Local address = " + getAddress()); + pw.println("Local name = " + getName()); + pw.println("isDiscovering() = " + isDiscovering()); + + BluetoothHeadset headset = new BluetoothHeadset(mContext, null); + + pw.println("\n--Known devices--"); + for (String address : mDeviceProperties.keySet()) { + int bondState = mBondState.getBondState(address); + pw.printf("%s %10s (%d) %s\n", address, + toBondStateString(bondState), + mBondState.getAttempt(address), + getRemoteName(address)); + + Map<ParcelUuid, Integer> uuidChannels = mDeviceServiceChannelCache.get(address); + if (uuidChannels == null) { + pw.println("\tuuids = null"); + } else { + for (ParcelUuid uuid : uuidChannels.keySet()) { + Integer channel = uuidChannels.get(uuid); + if (channel == null) { + pw.println("\t" + uuid); + } else { + pw.println("\t" + uuid + " RFCOMM channel = " + channel); + } + } + } + for (RemoteService service : mUuidCallbackTracker.keySet()) { + if (service.address.equals(address)) { + pw.println("\tPENDING CALLBACK: " + service.uuid); + } + } + } + + String value = getProperty("Devices"); + String[] devicesObjectPath = null; + if (value != null) { + devicesObjectPath = value.split(","); + } + pw.println("\n--ACL connected devices--"); + if (devicesObjectPath != null) { + for (String device : devicesObjectPath) { + pw.println(getAddressFromObjectPath(device)); + } + } + + // Rather not do this from here, but no-where else and I need this + // dump + pw.println("\n--Headset Service--"); + switch (headset.getState()) { + case BluetoothHeadset.STATE_DISCONNECTED: + pw.println("getState() = STATE_DISCONNECTED"); + break; + case BluetoothHeadset.STATE_CONNECTING: + pw.println("getState() = STATE_CONNECTING"); + break; + case BluetoothHeadset.STATE_CONNECTED: + pw.println("getState() = STATE_CONNECTED"); + break; + case BluetoothHeadset.STATE_ERROR: + pw.println("getState() = STATE_ERROR"); + break; + } + + pw.println("\ngetCurrentHeadset() = " + headset.getCurrentHeadset()); + pw.println("getBatteryUsageHint() = " + headset.getBatteryUsageHint()); + headset.close(); + pw.println("\n--Application Service Records--"); + for (Integer handle : mServiceRecordToPid.keySet()) { + Integer pid = mServiceRecordToPid.get(handle); + pw.println("\tpid " + pid + " handle " + Integer.toHexString(handle)); + } + } + + /* package */ static int bluezStringToScanMode(boolean pairable, boolean discoverable) { + if (pairable && discoverable) + return BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE; + else if (pairable && !discoverable) + return BluetoothAdapter.SCAN_MODE_CONNECTABLE; + else + return BluetoothAdapter.SCAN_MODE_NONE; + } + + /* package */ static String scanModeToBluezString(int mode) { + switch (mode) { + case BluetoothAdapter.SCAN_MODE_NONE: + return "off"; + case BluetoothAdapter.SCAN_MODE_CONNECTABLE: + return "connectable"; + case BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE: + return "discoverable"; + } + return null; + } + + /*package*/ String getAddressFromObjectPath(String objectPath) { + String adapterObjectPath = getProperty("ObjectPath"); + if (adapterObjectPath == null || objectPath == null) { + Log.e(TAG, "getAddressFromObjectPath: AdpaterObjectPath:" + adapterObjectPath + + " or deviceObjectPath:" + objectPath + " is null"); + return null; + } + if (!objectPath.startsWith(adapterObjectPath)) { + Log.e(TAG, "getAddressFromObjectPath: AdpaterObjectPath:" + adapterObjectPath + + " is not a prefix of deviceObjectPath:" + objectPath + + "bluetoothd crashed ?"); + return null; + } + String address = objectPath.substring(adapterObjectPath.length()); + if (address != null) return address.replace('_', ':'); + + Log.e(TAG, "getAddressFromObjectPath: Address being returned is null"); + return null; + } + + /*package*/ String getObjectPathFromAddress(String address) { + String path = getProperty("ObjectPath"); + if (path == null) { + Log.e(TAG, "Error: Object Path is null"); + return null; + } + path = path + address.replace(":", "_"); + return path; + } + + private static void log(String msg) { + Log.d(TAG, msg); + } + + private native static void classInitNative(); + private native void initializeNativeDataNative(); + private native boolean setupNativeDataNative(); + private native boolean tearDownNativeDataNative(); + private native void cleanupNativeDataNative(); + private native String getAdapterPathNative(); + + private native int isEnabledNative(); + private native int enableNative(); + private native int disableNative(); + + private native Object[] getAdapterPropertiesNative(); + private native Object[] getDevicePropertiesNative(String objectPath); + private native boolean setAdapterPropertyStringNative(String key, String value); + private native boolean setAdapterPropertyIntegerNative(String key, int value); + private native boolean setAdapterPropertyBooleanNative(String key, int value); + + private native boolean startDiscoveryNative(); + private native boolean stopDiscoveryNative(); + + private native boolean createPairedDeviceNative(String address, int timeout_ms); + private native boolean cancelDeviceCreationNative(String address); + private native boolean removeDeviceNative(String objectPath); + private native int getDeviceServiceChannelNative(String objectPath, String uuid, + int attributeId); + + private native boolean cancelPairingUserInputNative(String address, int nativeData); + private native boolean setPinNative(String address, String pin, int nativeData); + private native boolean setPasskeyNative(String address, int passkey, int nativeData); + private native boolean setPairingConfirmationNative(String address, boolean confirm, + int nativeData); + private native boolean setDevicePropertyBooleanNative(String objectPath, String key, + int value); + private native boolean createDeviceNative(String address); + /*package*/ native boolean discoverServicesNative(String objectPath, String pattern); + + private native int addRfcommServiceRecordNative(String name, long uuidMsb, long uuidLsb, + short channel); + private native boolean removeServiceRecordNative(int handle); +} diff --git a/core/java/android/server/search/SearchDialogWrapper.java b/core/java/android/server/search/SearchDialogWrapper.java index 49718cb..9ee64af 100644 --- a/core/java/android/server/search/SearchDialogWrapper.java +++ b/core/java/android/server/search/SearchDialogWrapper.java @@ -58,6 +58,7 @@ implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener { // data[KEY_INITIAL_QUERY]: initial query // data[KEY_LAUNCH_ACTIVITY]: launch activity // data[KEY_APP_SEARCH_DATA]: app search data + // data[KEY_TRIGGER]: 0 = false, 1 = true private static final int MSG_START_SEARCH = 1; // Takes no arguments private static final int MSG_STOP_SEARCH = 2; @@ -69,7 +70,8 @@ implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener { private static final String KEY_INITIAL_QUERY = "q"; private static final String KEY_LAUNCH_ACTIVITY = "a"; private static final String KEY_APP_SEARCH_DATA = "d"; - private static final String KEY_IDENT= "i"; + private static final String KEY_IDENT = "i"; + private static final String KEY_TRIGGER = "t"; // Context used for getting search UI resources private final Context mContext; @@ -173,7 +175,8 @@ implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener { final Bundle appSearchData, final boolean globalSearch, final ISearchManagerCallback searchManagerCallback, - int ident) { + int ident, + boolean trigger) { if (DBG) debug("startSearch()"); Message msg = Message.obtain(); msg.what = MSG_START_SEARCH; @@ -185,6 +188,7 @@ implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener { msgData.putParcelable(KEY_LAUNCH_ACTIVITY, launchActivity); msgData.putBundle(KEY_APP_SEARCH_DATA, appSearchData); msgData.putInt(KEY_IDENT, ident); + msgData.putInt(KEY_TRIGGER, trigger ? 1 : 0); mSearchUiThread.sendMessage(msg); // be a little more eager in setting this so isVisible will return the correct value if // called immediately after startSearch @@ -268,8 +272,9 @@ implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener { boolean globalSearch = msg.arg2 != 0; ISearchManagerCallback searchManagerCallback = (ISearchManagerCallback) msg.obj; int ident = msgData.getInt(KEY_IDENT); + boolean trigger = msgData.getInt(KEY_TRIGGER) != 0; performStartSearch(initialQuery, selectInitialQuery, launchActivity, - appSearchData, globalSearch, searchManagerCallback, ident); + appSearchData, globalSearch, searchManagerCallback, ident, trigger); } } @@ -284,7 +289,8 @@ implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener { Bundle appSearchData, boolean globalSearch, ISearchManagerCallback searchManagerCallback, - int ident) { + int ident, + boolean trigger) { if (DBG) debug("performStartSearch()"); registerBroadcastReceiver(); @@ -301,6 +307,9 @@ implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener { mSearchDialog.show(initialQuery, selectInitialQuery, launchActivity, appSearchData, globalSearch); mVisible = true; + if (trigger) { + mSearchDialog.launchQuerySearch(); + } } /** diff --git a/core/java/android/server/search/SearchManagerService.java b/core/java/android/server/search/SearchManagerService.java index afed4a4..78ea2e3 100644 --- a/core/java/android/server/search/SearchManagerService.java +++ b/core/java/android/server/search/SearchManagerService.java @@ -233,7 +233,30 @@ public class SearchManagerService extends ISearchManager.Stub { appSearchData, globalSearch, searchManagerCallback, - ident); + ident, + false); // don't trigger + } + + /** + * Launches the search UI and triggers the search, as if the user had clicked on the + * search button within the dialog. + * + * @see SearchManager#triggerSearch(String, android.content.ComponentName, android.os.Bundle) + */ + public void triggerSearch(String query, + ComponentName launchActivity, + Bundle appSearchData, + ISearchManagerCallback searchManagerCallback, + int ident) { + getSearchDialog().startSearch( + query, + false, + launchActivity, + appSearchData, + false, + searchManagerCallback, + ident, + true); // triger search after launching } /** |