| /* |
| * Copyright (C) 2006 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.phone; |
| |
| import android.app.Service; |
| import android.bluetooth.BluetoothAudioGateway; |
| import android.bluetooth.BluetoothAdapter; |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.BluetoothHeadset; |
| import android.bluetooth.BluetoothUuid; |
| import android.bluetooth.HeadsetBase; |
| import android.bluetooth.IBluetoothHeadset; |
| import android.os.ParcelUuid; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.media.AudioManager; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.Message; |
| import android.os.PowerManager; |
| import android.os.RemoteException; |
| import android.os.SystemProperties; |
| import android.provider.Settings; |
| import android.util.Log; |
| |
| import com.android.internal.telephony.Call; |
| import com.android.internal.telephony.Phone; |
| import com.android.internal.telephony.PhoneFactory; |
| |
| import java.util.HashMap; |
| import java.util.LinkedList; |
| import java.util.ListIterator; |
| import java.util.Set; |
| |
| /** |
| * Provides Bluetooth Headset and Handsfree profile, as a service in |
| * the Phone application. |
| * @hide |
| */ |
| public class BluetoothHeadsetService extends Service { |
| private static final String TAG = "BT HSHFP"; |
| private static final boolean DBG = true; |
| |
| private static final String PREF_NAME = BluetoothHeadsetService.class.getSimpleName(); |
| private static final String PREF_LAST_HEADSET = "lastHeadsetAddress"; |
| |
| private static final int PHONE_STATE_CHANGED = 1; |
| |
| private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN; |
| private static final String BLUETOOTH_PERM = android.Manifest.permission.BLUETOOTH; |
| |
| private static boolean sHasStarted = false; |
| |
| private BluetoothDevice mDeviceSdpQuery; |
| private BluetoothAdapter mAdapter; |
| private PowerManager mPowerManager; |
| private BluetoothAudioGateway mAg; |
| private HeadsetBase mHeadset; |
| private int mState; |
| private int mHeadsetType; |
| private BluetoothHandsfree mBtHandsfree; |
| private BluetoothDevice mRemoteDevice; |
| private LinkedList<BluetoothDevice> mAutoConnectQueue; |
| private Call mForegroundCall; |
| private Call mRingingCall; |
| private Phone mPhone; |
| |
| private final HeadsetPriority mHeadsetPriority = new HeadsetPriority(); |
| |
| public BluetoothHeadsetService() { |
| mState = BluetoothHeadset.STATE_DISCONNECTED; |
| mHeadsetType = BluetoothHandsfree.TYPE_UNKNOWN; |
| } |
| |
| @Override |
| public void onCreate() { |
| super.onCreate(); |
| mAdapter = BluetoothAdapter.getDefaultAdapter(); |
| mPowerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); |
| mBtHandsfree = PhoneApp.getInstance().getBluetoothHandsfree(); |
| mAg = new BluetoothAudioGateway(mAdapter); |
| mPhone = PhoneFactory.getDefaultPhone(); |
| mRingingCall = mPhone.getRingingCall(); |
| mForegroundCall = mPhone.getForegroundCall(); |
| if (mAdapter.isEnabled()) { |
| mHeadsetPriority.load(); |
| } |
| IntentFilter filter = new IntentFilter( |
| BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED); |
| filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); |
| filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); |
| filter.addAction(AudioManager.VOLUME_CHANGED_ACTION); |
| filter.addAction(BluetoothDevice.ACTION_UUID); |
| registerReceiver(mBluetoothReceiver, filter); |
| } |
| |
| @Override |
| public void onStart(Intent intent, int startId) { |
| if (mAdapter == null) { |
| Log.w(TAG, "Stopping BluetoothHeadsetService: device does not have BT"); |
| stopSelf(); |
| } else { |
| if (!sHasStarted) { |
| if (DBG) log("Starting BluetoothHeadsetService"); |
| if (mAdapter.isEnabled()) { |
| mAg.start(mIncomingConnectionHandler); |
| mBtHandsfree.onBluetoothEnabled(); |
| // BT might have only just started, wait 6 seconds until |
| // SDP records are registered before reconnecting headset |
| mHandler.sendMessageDelayed(mHandler.obtainMessage(RECONNECT_LAST_HEADSET), |
| 6000); |
| } |
| sHasStarted = true; |
| } |
| } |
| } |
| |
| private final Handler mIncomingConnectionHandler = new Handler() { |
| @Override |
| public void handleMessage(Message msg) { |
| BluetoothAudioGateway.IncomingConnectionInfo info = |
| (BluetoothAudioGateway.IncomingConnectionInfo)msg.obj; |
| int type = BluetoothHandsfree.TYPE_UNKNOWN; |
| switch(msg.what) { |
| case BluetoothAudioGateway.MSG_INCOMING_HEADSET_CONNECTION: |
| type = BluetoothHandsfree.TYPE_HEADSET; |
| break; |
| case BluetoothAudioGateway.MSG_INCOMING_HANDSFREE_CONNECTION: |
| type = BluetoothHandsfree.TYPE_HANDSFREE; |
| break; |
| } |
| |
| Log.i(TAG, "Incoming rfcomm (" + BluetoothHandsfree.typeToString(type) + |
| ") connection from " + info.mRemoteDevice + "on channel " + info.mRfcommChan); |
| |
| int priority = BluetoothHeadset.PRIORITY_OFF; |
| HeadsetBase headset; |
| try { |
| priority = mBinder.getPriority(info.mRemoteDevice); |
| } catch (RemoteException e) {} |
| if (priority <= BluetoothHeadset.PRIORITY_OFF) { |
| Log.i(TAG, "Rejecting incoming connection because priority = " + priority); |
| |
| headset = new HeadsetBase(mPowerManager, mAdapter, info.mRemoteDevice, |
| info.mSocketFd, info.mRfcommChan, null); |
| headset.disconnect(); |
| return; |
| } |
| switch (mState) { |
| case BluetoothHeadset.STATE_DISCONNECTED: |
| // headset connecting us, lets join |
| mRemoteDevice = info.mRemoteDevice; |
| setState(BluetoothHeadset.STATE_CONNECTING); |
| headset = new HeadsetBase(mPowerManager, mAdapter, mRemoteDevice, info.mSocketFd, |
| info.mRfcommChan, mConnectedStatusHandler); |
| mHeadsetType = type; |
| |
| mConnectingStatusHandler.obtainMessage(RFCOMM_CONNECTED, headset).sendToTarget(); |
| |
| break; |
| case BluetoothHeadset.STATE_CONNECTING: |
| if (!info.mRemoteDevice.equals(mRemoteDevice)) { |
| // different headset, ignoring |
| Log.i(TAG, "Already attempting connect to " + mRemoteDevice + |
| ", disconnecting " + info.mRemoteDevice); |
| |
| headset = new HeadsetBase(mPowerManager, mAdapter, info.mRemoteDevice, |
| info.mSocketFd, info.mRfcommChan, null); |
| headset.disconnect(); |
| } |
| // If we are here, we are in danger of a race condition |
| // incoming rfcomm connection, but we are also attempting an |
| // outgoing connection. Lets try and interrupt the outgoing |
| // connection. |
| Log.i(TAG, "Incoming and outgoing connections to " + info.mRemoteDevice + |
| ". Cancel outgoing connection."); |
| if (mConnectThread != null) { |
| mConnectThread.interrupt(); |
| mConnectThread = null; |
| } |
| |
| // Now continue with new connection, including calling callback |
| mHeadset = new HeadsetBase(mPowerManager, mAdapter, mRemoteDevice, |
| info.mSocketFd, info.mRfcommChan, mConnectedStatusHandler); |
| mHeadsetType = type; |
| |
| setState(BluetoothHeadset.STATE_CONNECTED, BluetoothHeadset.RESULT_SUCCESS); |
| mBtHandsfree.connectHeadset(mHeadset, mHeadsetType); |
| |
| if (DBG) log("Successfully used incoming connection"); |
| break; |
| case BluetoothHeadset.STATE_CONNECTED: |
| Log.i(TAG, "Already connected to " + mRemoteDevice + ", disconnecting " + |
| info.mRemoteDevice); |
| |
| headset = new HeadsetBase(mPowerManager, mAdapter, info.mRemoteDevice, |
| info.mSocketFd, info.mRfcommChan, null); |
| headset.disconnect(); |
| break; |
| } |
| } |
| }; |
| |
| private synchronized void autoConnectHeadset() { |
| if (DBG && debugDontReconnect()) { |
| return; |
| } |
| if (mAdapter.isEnabled()) { |
| try { |
| mBinder.connectHeadset(null); |
| } catch (RemoteException e) {} |
| } |
| } |
| |
| private final BroadcastReceiver mBluetoothReceiver = new BroadcastReceiver() { |
| |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| String action = intent.getAction(); |
| BluetoothDevice device = |
| intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); |
| |
| if ((mState == BluetoothHeadset.STATE_CONNECTED || |
| mState == BluetoothHeadset.STATE_CONNECTING) && |
| action.equals(BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED) && |
| device.equals(mRemoteDevice)) { |
| try { |
| mBinder.disconnectHeadset(); |
| } catch (RemoteException e) {} |
| } else if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { |
| switch (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, |
| BluetoothAdapter.ERROR)) { |
| case BluetoothAdapter.STATE_ON: |
| mHeadsetPriority.load(); |
| mHandler.sendMessageDelayed(mHandler.obtainMessage(RECONNECT_LAST_HEADSET), 8000); |
| mAg.start(mIncomingConnectionHandler); |
| mBtHandsfree.onBluetoothEnabled(); |
| break; |
| case BluetoothAdapter.STATE_TURNING_OFF: |
| mBtHandsfree.onBluetoothDisabled(); |
| mAg.stop(); |
| setState(BluetoothHeadset.STATE_DISCONNECTED, BluetoothHeadset.RESULT_FAILURE, |
| BluetoothHeadset.LOCAL_DISCONNECT); |
| break; |
| } |
| } 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: |
| if (mHeadsetPriority.get(device) == BluetoothHeadset.PRIORITY_UNDEFINED) { |
| mHeadsetPriority.set(device, BluetoothHeadset.PRIORITY_ON); |
| } |
| break; |
| case BluetoothDevice.BOND_NONE: |
| mHeadsetPriority.set(device, BluetoothHeadset.PRIORITY_UNDEFINED); |
| break; |
| } |
| } else if (action.equals(AudioManager.VOLUME_CHANGED_ACTION)) { |
| int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1); |
| if (streamType == AudioManager.STREAM_BLUETOOTH_SCO) { |
| mBtHandsfree.sendScoGainUpdate(intent.getIntExtra( |
| AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0)); |
| } |
| |
| } else if (action.equals(BluetoothDevice.ACTION_UUID)) { |
| if (device.equals(mDeviceSdpQuery)) { |
| // We have got SDP records for the device we are interested in. |
| getSdpRecordsAndConnect(); |
| } |
| } |
| } |
| }; |
| |
| private static final int RECONNECT_LAST_HEADSET = 1; |
| private static final int CONNECT_HEADSET_DELAYED = 2; |
| private Handler mHandler = new Handler() { |
| @Override |
| public void handleMessage(Message msg) { |
| switch (msg.what) { |
| case RECONNECT_LAST_HEADSET: |
| autoConnectHeadset(); |
| break; |
| case CONNECT_HEADSET_DELAYED: |
| getSdpRecordsAndConnect(); |
| break; |
| } |
| } |
| }; |
| |
| @Override |
| public IBinder onBind(Intent intent) { |
| return mBinder; |
| } |
| |
| // ------------------------------------------------------------------ |
| // Bluetooth Headset Connect |
| // ------------------------------------------------------------------ |
| private static final int RFCOMM_CONNECTED = 1; |
| private static final int RFCOMM_ERROR = 2; |
| |
| private long mTimestamp; |
| |
| /** |
| * Thread for RFCOMM connection |
| * Messages are sent to mConnectingStatusHandler as connection progresses. |
| */ |
| private RfcommConnectThread mConnectThread; |
| private class RfcommConnectThread extends Thread { |
| private BluetoothDevice device; |
| private int channel; |
| private int type; |
| |
| private static final int EINTERRUPT = -1000; |
| private static final int ECONNREFUSED = -111; |
| |
| public RfcommConnectThread(BluetoothDevice device, int channel, int type) { |
| super(); |
| this.device = device; |
| this.channel = channel; |
| this.type = type; |
| } |
| |
| private int waitForConnect(HeadsetBase headset) { |
| // Try to connect for 20 seconds |
| int result = 0; |
| for (int i=0; i < 40 && result == 0; i++) { |
| // waitForAsyncConnect returns 0 on timeout, 1 on success, < 0 on error. |
| result = headset.waitForAsyncConnect(500, mConnectedStatusHandler); |
| if (isInterrupted()) { |
| headset.disconnect(); |
| return EINTERRUPT; |
| } |
| } |
| return result; |
| } |
| |
| @Override |
| public void run() { |
| long timestamp; |
| |
| timestamp = System.currentTimeMillis(); |
| HeadsetBase headset = new HeadsetBase(mPowerManager, mAdapter, device, channel); |
| |
| int result = waitForConnect(headset); |
| |
| if (result != EINTERRUPT && result != 1) { |
| if (result == ECONNREFUSED && mDeviceSdpQuery == null) { |
| // The rfcomm channel number might have changed, do SDP |
| // query and try to connect again. |
| mDeviceSdpQuery = mRemoteDevice; |
| device.fetchUuidsWithSdp(); |
| mConnectThread = null; |
| return; |
| } else { |
| Log.i(TAG, "Trying to connect to rfcomm socket again after 1 sec"); |
| try { |
| sleep(1000); // 1 second |
| } catch (InterruptedException e) {} |
| } |
| result = waitForConnect(headset); |
| } |
| |
| mDeviceSdpQuery = null; |
| if (result == EINTERRUPT) return; |
| |
| if (DBG) log("RFCOMM connection attempt took " + |
| (System.currentTimeMillis() - timestamp) + " ms"); |
| if (isInterrupted()) { |
| headset.disconnect(); |
| return; |
| } |
| if (result < 0) { |
| Log.w(TAG, "headset.waitForAsyncConnect() error: " + result); |
| mConnectingStatusHandler.obtainMessage(RFCOMM_ERROR).sendToTarget(); |
| return; |
| } else if (result == 0) { |
| mConnectingStatusHandler.obtainMessage(RFCOMM_ERROR).sendToTarget(); |
| Log.w(TAG, "mHeadset.waitForAsyncConnect() error: " + result + "(timeout)"); |
| return; |
| } else { |
| mConnectingStatusHandler.obtainMessage(RFCOMM_CONNECTED, headset).sendToTarget(); |
| } |
| } |
| } |
| |
| /** |
| * Receives events from mConnectThread back in the main thread. |
| */ |
| private final Handler mConnectingStatusHandler = new Handler() { |
| @Override |
| public void handleMessage(Message msg) { |
| if (mState != BluetoothHeadset.STATE_CONNECTING) { |
| return; // stale events |
| } |
| |
| switch (msg.what) { |
| case RFCOMM_ERROR: |
| if (DBG) log("Rfcomm error"); |
| mConnectThread = null; |
| setState(BluetoothHeadset.STATE_DISCONNECTED, BluetoothHeadset.RESULT_FAILURE, |
| BluetoothHeadset.LOCAL_DISCONNECT); |
| break; |
| case RFCOMM_CONNECTED: |
| if (DBG) log("Rfcomm connected"); |
| mConnectThread = null; |
| mHeadset = (HeadsetBase)msg.obj; |
| setState(BluetoothHeadset.STATE_CONNECTED, BluetoothHeadset.RESULT_SUCCESS); |
| |
| mBtHandsfree.connectHeadset(mHeadset, mHeadsetType); |
| break; |
| } |
| } |
| }; |
| |
| /** |
| * Receives events from a connected RFCOMM socket back in the main thread. |
| */ |
| private final Handler mConnectedStatusHandler = new Handler() { |
| @Override |
| public void handleMessage(Message msg) { |
| switch (msg.what) { |
| case HeadsetBase.RFCOMM_DISCONNECTED: |
| mBtHandsfree.resetAtState(); |
| setState(BluetoothHeadset.STATE_DISCONNECTED, BluetoothHeadset.RESULT_FAILURE, |
| BluetoothHeadset.REMOTE_DISCONNECT); |
| break; |
| } |
| } |
| }; |
| |
| private void setState(int state) { |
| setState(state, BluetoothHeadset.RESULT_SUCCESS); |
| } |
| |
| private void setState(int state, int result) { |
| setState(state, result, -1); |
| } |
| |
| private synchronized void setState(int state, int result, int initiator) { |
| if (state != mState) { |
| if (DBG) log("Headset state " + mState + " -> " + state + ", result = " + result); |
| if (mState == BluetoothHeadset.STATE_CONNECTED) { |
| mBtHandsfree.disconnectHeadset(); |
| } |
| Intent intent = new Intent(BluetoothHeadset.ACTION_STATE_CHANGED); |
| intent.putExtra(BluetoothHeadset.EXTRA_PREVIOUS_STATE, mState); |
| mState = state; |
| intent.putExtra(BluetoothHeadset.EXTRA_STATE, mState); |
| intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mRemoteDevice); |
| // Add Extra EXTRA_DISCONNECT_INITIATOR for DISCONNECTED state |
| if (mState == BluetoothHeadset.STATE_DISCONNECTED) { |
| if (initiator == -1) { |
| log("Headset Disconnected Intent without Disconnect Initiator extra"); |
| } else { |
| intent.putExtra(BluetoothHeadset.EXTRA_DISCONNECT_INITIATOR, |
| initiator); |
| } |
| } |
| sendBroadcast(intent, BLUETOOTH_PERM); |
| if (mState == BluetoothHeadset.STATE_DISCONNECTED) { |
| mHeadset = null; |
| mRemoteDevice = null; |
| mHeadsetType = BluetoothHandsfree.TYPE_UNKNOWN; |
| if (mAutoConnectQueue != null) { |
| doNextAutoConnect(); |
| } |
| } else if (mState == BluetoothHeadset.STATE_CONNECTING) { |
| // Set the priority to AUTO_CONNECT |
| mHeadsetPriority.set(mRemoteDevice, BluetoothHeadset.PRIORITY_AUTO_CONNECT); |
| } else if (mState == BluetoothHeadset.STATE_CONNECTED) { |
| mAutoConnectQueue = null; // cancel further auto-connection |
| mHeadsetPriority.bump(mRemoteDevice); |
| } |
| } |
| } |
| |
| private void getSdpRecordsAndConnect() { |
| ParcelUuid[] uuids = mRemoteDevice.getUuids(); |
| if (uuids != null) { |
| if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Handsfree)) { |
| log("SDP UUID: TYPE_HANDSFREE"); |
| mHeadsetType = BluetoothHandsfree.TYPE_HANDSFREE; |
| int channel = mRemoteDevice.getServiceChannel(BluetoothUuid.Handsfree); |
| mConnectThread = new RfcommConnectThread(mRemoteDevice, channel, mHeadsetType); |
| mConnectThread.start(); |
| return; |
| } else if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.HSP)) { |
| log("SDP UUID: TYPE_HEADSET"); |
| mHeadsetType = BluetoothHandsfree.TYPE_HEADSET; |
| int channel = mRemoteDevice.getServiceChannel(BluetoothUuid.HSP); |
| mConnectThread = new RfcommConnectThread(mRemoteDevice, channel, mHeadsetType); |
| mConnectThread.start(); |
| return; |
| } |
| } |
| log("SDP UUID: TYPE_UNKNOWN"); |
| mHeadsetType = BluetoothHandsfree.TYPE_UNKNOWN; |
| setState(BluetoothHeadset.STATE_DISCONNECTED, |
| BluetoothHeadset.RESULT_FAILURE, BluetoothHeadset.LOCAL_DISCONNECT); |
| return; |
| } |
| |
| private synchronized boolean doNextAutoConnect() { |
| if (mAutoConnectQueue == null || mAutoConnectQueue.size() == 0) { |
| mAutoConnectQueue = null; |
| return false; |
| } |
| mRemoteDevice = mAutoConnectQueue.removeFirst(); |
| // Don't auto connect with docks if we are docked with the dock. |
| if (isPhoneDocked(mRemoteDevice)) return doNextAutoConnect(); |
| |
| if (DBG) log("pulled " + mRemoteDevice + " off auto-connect queue"); |
| setState(BluetoothHeadset.STATE_CONNECTING); |
| getSdpRecordsAndConnect(); |
| |
| return true; |
| } |
| |
| private boolean isPhoneDocked(BluetoothDevice autoConnectDevice) { |
| // This works only because these broadcast intents are "sticky" |
| Intent i = registerReceiver(null, new IntentFilter(Intent.ACTION_DOCK_EVENT)); |
| if (i != null) { |
| int state = i.getIntExtra(Intent.EXTRA_DOCK_STATE, Intent.EXTRA_DOCK_STATE_UNDOCKED); |
| if (state != Intent.EXTRA_DOCK_STATE_UNDOCKED) { |
| BluetoothDevice device = i.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); |
| if (device != null && autoConnectDevice.equals(device)) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Handlers for incoming service calls |
| */ |
| private final IBluetoothHeadset.Stub mBinder = new IBluetoothHeadset.Stub() { |
| public int getState() { |
| enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); |
| return mState; |
| } |
| public BluetoothDevice getCurrentHeadset() { |
| enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); |
| if (mState == BluetoothHeadset.STATE_DISCONNECTED) { |
| return null; |
| } |
| return mRemoteDevice; |
| } |
| public boolean connectHeadset(BluetoothDevice device) { |
| enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, |
| "Need BLUETOOTH_ADMIN permission"); |
| synchronized (BluetoothHeadsetService.this) { |
| if (mState == BluetoothHeadset.STATE_CONNECTED || |
| mState == BluetoothHeadset.STATE_CONNECTING) { |
| Log.w(TAG, "connectHeadset(" + device + "): failed: already in state " + |
| mState + " with headset " + mRemoteDevice); |
| return false; |
| } |
| if (device == null) { |
| mAutoConnectQueue = mHeadsetPriority.getSorted(); |
| return doNextAutoConnect(); |
| } |
| mRemoteDevice = device; |
| setState(BluetoothHeadset.STATE_CONNECTING); |
| if (mRemoteDevice.getUuids() == null) { |
| // We might not have got the UUID change notification from |
| // Bluez yet, if we have just paired. Try after 1.5 secs. |
| mHandler.sendMessageDelayed( |
| mHandler.obtainMessage(CONNECT_HEADSET_DELAYED), 1500); |
| } else { |
| getSdpRecordsAndConnect(); |
| } |
| } |
| return true; |
| } |
| public boolean isConnected(BluetoothDevice device) { |
| enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); |
| return mState == BluetoothHeadset.STATE_CONNECTED && mRemoteDevice.equals(device); |
| } |
| public void disconnectHeadset() { |
| enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, |
| "Need BLUETOOTH_ADMIN permission"); |
| synchronized (BluetoothHeadsetService.this) { |
| switch (mState) { |
| case BluetoothHeadset.STATE_CONNECTING: |
| if (mConnectThread != null) { |
| // cancel the connection thread |
| mConnectThread.interrupt(); |
| try { |
| mConnectThread.join(); |
| } catch (InterruptedException e) { |
| Log.e(TAG, "Connection cancelled twice?", e); |
| } |
| mConnectThread = null; |
| } |
| if (mHeadset != null) { |
| mHeadset.disconnect(); |
| mHeadset = null; |
| } |
| setState(BluetoothHeadset.STATE_DISCONNECTED, |
| BluetoothHeadset.RESULT_CANCELED, |
| BluetoothHeadset.LOCAL_DISCONNECT); |
| break; |
| case BluetoothHeadset.STATE_CONNECTED: |
| // Send a dummy battery level message to force headset |
| // out of sniff mode so that it will immediately notice |
| // the disconnection. We are currently sending it for |
| // handsfree only. |
| // TODO: Call hci_conn_enter_active_mode() from |
| // rfcomm_send_disc() in the kernel instead. |
| // See http://b/1716887 |
| if (mHeadsetType == BluetoothHandsfree.TYPE_HANDSFREE) { |
| mHeadset.sendURC("+CIEV: 7,3"); |
| } |
| |
| if (mHeadset != null) { |
| mHeadset.disconnect(); |
| mHeadset = null; |
| } |
| setState(BluetoothHeadset.STATE_DISCONNECTED, |
| BluetoothHeadset.RESULT_CANCELED, |
| BluetoothHeadset.LOCAL_DISCONNECT); |
| break; |
| } |
| } |
| } |
| public boolean startVoiceRecognition() { |
| enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); |
| synchronized (BluetoothHeadsetService.this) { |
| if (mState != BluetoothHeadset.STATE_CONNECTED) { |
| return false; |
| } |
| return mBtHandsfree.startVoiceRecognition(); |
| } |
| } |
| public boolean stopVoiceRecognition() { |
| enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); |
| synchronized (BluetoothHeadsetService.this) { |
| if (mState != BluetoothHeadset.STATE_CONNECTED) { |
| return false; |
| } |
| return mBtHandsfree.stopVoiceRecognition(); |
| } |
| } |
| public boolean setPriority(BluetoothDevice device, int priority) { |
| enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, |
| "Need BLUETOOTH_ADMIN permission"); |
| if (priority < BluetoothHeadset.PRIORITY_OFF) { |
| return false; |
| } |
| mHeadsetPriority.set(device, priority); |
| return true; |
| } |
| public int getPriority(BluetoothDevice device) { |
| enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); |
| |
| return mHeadsetPriority.get(device); |
| } |
| public int getBatteryUsageHint() { |
| enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); |
| |
| return HeadsetBase.getAtInputCount(); |
| } |
| }; |
| |
| @Override |
| public void onDestroy() { |
| super.onDestroy(); |
| if (DBG) log("Stopping BluetoothHeadsetService"); |
| unregisterReceiver(mBluetoothReceiver); |
| mBtHandsfree.onBluetoothDisabled(); |
| mAg.stop(); |
| sHasStarted = false; |
| setState(BluetoothHeadset.STATE_DISCONNECTED, |
| BluetoothHeadset.RESULT_CANCELED, |
| BluetoothHeadset.LOCAL_DISCONNECT); |
| mHeadsetType = BluetoothHandsfree.TYPE_UNKNOWN; |
| } |
| |
| private class HeadsetPriority { |
| private HashMap<BluetoothDevice, Integer> mPriority = |
| new HashMap<BluetoothDevice, Integer>(); |
| |
| public synchronized boolean load() { |
| Set<BluetoothDevice> devices = mAdapter.getBondedDevices(); |
| if (devices == null) { |
| return false; // for example, bluetooth is off |
| } |
| for (BluetoothDevice device : devices) { |
| load(device); |
| } |
| return true; |
| } |
| |
| private synchronized int load(BluetoothDevice device) { |
| int priority = Settings.Secure.getInt(getContentResolver(), |
| Settings.Secure.getBluetoothHeadsetPriorityKey(device.getAddress()), |
| BluetoothHeadset.PRIORITY_UNDEFINED); |
| mPriority.put(device, new Integer(priority)); |
| if (DBG) log("Loaded priority " + device + " = " + priority); |
| return priority; |
| } |
| |
| public synchronized int get(BluetoothDevice device) { |
| Integer priority = mPriority.get(device); |
| if (priority == null) { |
| return load(device); |
| } |
| return priority.intValue(); |
| } |
| |
| public synchronized void set(BluetoothDevice device, int priority) { |
| int oldPriority = get(device); |
| if (oldPriority == priority) { |
| return; |
| } |
| mPriority.put(device, new Integer(priority)); |
| Settings.Secure.putInt(getContentResolver(), |
| Settings.Secure.getBluetoothHeadsetPriorityKey(device.getAddress()), |
| priority); |
| if (DBG) log("Saved priority " + device + " = " + priority); |
| } |
| |
| /** Mark this headset as highest priority */ |
| public synchronized void bump(BluetoothDevice device) { |
| int oldPriority = get(device); |
| int maxPriority = BluetoothHeadset.PRIORITY_AUTO_CONNECT; |
| |
| // Find max, not including given address |
| for (BluetoothDevice d : mPriority.keySet()) { |
| if (device.equals(d)) continue; |
| int p = mPriority.get(d).intValue(); |
| if (p > maxPriority) { |
| maxPriority = p; |
| } |
| } |
| if (maxPriority >= oldPriority) { |
| int p = maxPriority + 1; |
| set(device, p); |
| if (p >= Integer.MAX_VALUE) { |
| rebalance(); |
| } |
| } |
| } |
| |
| /** shifts all non-zero priorities to be monotonically increasing from |
| * PRIORITY_AUTO_CONNECT */ |
| private synchronized void rebalance() { |
| LinkedList<BluetoothDevice> sorted = getSorted(); |
| if (DBG) log("Rebalancing " + sorted.size() + " headset priorities"); |
| |
| ListIterator<BluetoothDevice> li = sorted.listIterator(sorted.size()); |
| int priority = BluetoothHeadset.PRIORITY_AUTO_CONNECT; |
| while (li.hasPrevious()) { |
| BluetoothDevice device = li.previous(); |
| set(device, priority); |
| priority++; |
| } |
| } |
| |
| /** Get list of headsets sorted by decreasing priority. |
| * Headsets with priority less than AUTO_CONNECT are not included */ |
| public synchronized LinkedList<BluetoothDevice> getSorted() { |
| LinkedList<BluetoothDevice> sorted = new LinkedList<BluetoothDevice>(); |
| HashMap<BluetoothDevice, Integer> toSort = |
| new HashMap<BluetoothDevice, Integer>(mPriority); |
| |
| // add in sorted order. this could be more efficient. |
| while (true) { |
| BluetoothDevice maxDevice = null; |
| int maxPriority = BluetoothHeadset.PRIORITY_AUTO_CONNECT; |
| for (BluetoothDevice device : toSort.keySet()) { |
| int priority = toSort.get(device).intValue(); |
| if (priority >= maxPriority) { |
| maxDevice = device; |
| maxPriority = priority; |
| } |
| } |
| if (maxDevice == null) { |
| break; |
| } |
| sorted.addLast(maxDevice); |
| toSort.remove(maxDevice); |
| } |
| return sorted; |
| } |
| } |
| |
| /** If this property is false, then don't auto-reconnect BT headset */ |
| private static final String DEBUG_AUTO_RECONNECT = "debug.bt.hshfp.auto_reconnect"; |
| |
| private boolean debugDontReconnect() { |
| return (!SystemProperties.getBoolean(DEBUG_AUTO_RECONNECT, true)); |
| } |
| |
| private static void log(String msg) { |
| Log.d(TAG, msg); |
| } |
| } |