1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.telecom; 18 19 import android.bluetooth.BluetoothDevice; 20 import android.bluetooth.BluetoothHeadset; 21 import android.bluetooth.BluetoothProfile; 22 import android.content.BroadcastReceiver; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.IntentFilter; 26 import android.os.Handler; 27 import android.os.Looper; 28 import android.os.SystemClock; 29 30 import com.android.internal.annotations.VisibleForTesting; 31 import com.android.internal.util.IndentingPrintWriter; 32 33 import java.util.List; 34 35 /** 36 * Listens to and caches bluetooth headset state. Used By the CallAudioManager for maintaining 37 * overall audio state. Also provides method for connecting the bluetooth headset to the phone call. 38 */ 39 public class BluetoothManager { 40 public static final int BLUETOOTH_UNINITIALIZED = 0; 41 public static final int BLUETOOTH_DISCONNECTED = 1; 42 public static final int BLUETOOTH_DEVICE_CONNECTED = 2; 43 public static final int BLUETOOTH_AUDIO_PENDING = 3; 44 public static final int BLUETOOTH_AUDIO_CONNECTED = 4; 45 46 public interface BluetoothStateListener { 47 void onBluetoothStateChange(int oldState, int newState); 48 } 49 50 private final BluetoothProfile.ServiceListener mBluetoothProfileServiceListener = 51 new BluetoothProfile.ServiceListener() { 52 @Override 53 public void onServiceConnected(int profile, BluetoothProfile proxy) { 54 Log.startSession("BMSL.oSC"); 55 try { 56 if (profile == BluetoothProfile.HEADSET) { 57 mBluetoothHeadset = new BluetoothHeadsetProxy((BluetoothHeadset) proxy); 58 Log.v(this, "- Got BluetoothHeadset: " + mBluetoothHeadset); 59 } else { 60 Log.w(this, "Connected to non-headset bluetooth service. Not changing" + 61 " bluetooth headset."); 62 } 63 updateListenerOfBluetoothState(true); 64 } finally { 65 Log.endSession(); 66 } 67 } 68 69 @Override 70 public void onServiceDisconnected(int profile) { 71 Log.startSession("BMSL.oSD"); 72 try { 73 mBluetoothHeadset = null; 74 Log.v(this, "Lost BluetoothHeadset: " + mBluetoothHeadset); 75 updateListenerOfBluetoothState(false); 76 } finally { 77 Log.endSession(); 78 } 79 } 80 }; 81 82 /** 83 * Receiver for misc intent broadcasts the BluetoothManager cares about. 84 */ 85 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 86 @Override 87 public void onReceive(Context context, Intent intent) { 88 Log.startSession("BM.oR"); 89 try { 90 String action = intent.getAction(); 91 92 if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) { 93 int bluetoothHeadsetState = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, 94 BluetoothHeadset.STATE_DISCONNECTED); 95 Log.i(this, "mReceiver: HEADSET_STATE_CHANGED_ACTION"); 96 Log.i(this, "==> new state: %s ", bluetoothHeadsetState); 97 updateListenerOfBluetoothState( 98 bluetoothHeadsetState == BluetoothHeadset.STATE_CONNECTING); 99 } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) { 100 int bluetoothHeadsetAudioState = 101 intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, 102 BluetoothHeadset.STATE_AUDIO_DISCONNECTED); 103 Log.i(this, "mReceiver: HEADSET_AUDIO_STATE_CHANGED_ACTION"); 104 Log.i(this, "==> new state: %s", bluetoothHeadsetAudioState); 105 updateListenerOfBluetoothState( 106 bluetoothHeadsetAudioState == 107 BluetoothHeadset.STATE_AUDIO_CONNECTING 108 || bluetoothHeadsetAudioState == 109 BluetoothHeadset.STATE_AUDIO_CONNECTED); 110 } 111 } finally { 112 Log.endSession(); 113 } 114 } 115 }; 116 117 private final Handler mHandler = new Handler(Looper.getMainLooper()); 118 119 private final BluetoothAdapterProxy mBluetoothAdapter; 120 private BluetoothStateListener mBluetoothStateListener; 121 122 private BluetoothHeadsetProxy mBluetoothHeadset; 123 private long mBluetoothConnectionRequestTime; 124 private final Runnable mBluetoothConnectionTimeout = new Runnable("BM.cBA", null /*lock*/) { 125 @Override 126 public void loggedRun() { 127 if (!isBluetoothAudioConnected()) { 128 Log.v(this, "Bluetooth audio inexplicably disconnected within 5 seconds of " + 129 "connection. Updating UI."); 130 } 131 updateListenerOfBluetoothState(false); 132 } 133 }; 134 135 private final Runnable mRetryConnectAudio = new Runnable("BM.rCA", null /*lock*/) { 136 @Override 137 public void loggedRun() { 138 Log.i(this, "Retrying connecting to bluetooth audio."); 139 if (!mBluetoothHeadset.connectAudio()) { 140 Log.w(this, "Retry of bluetooth audio connection failed. Giving up."); 141 } else { 142 setBluetoothStatePending(); 143 } 144 } 145 }; 146 147 private final Context mContext; 148 private int mBluetoothState = BLUETOOTH_UNINITIALIZED; 149 150 public BluetoothManager(Context context, BluetoothAdapterProxy bluetoothAdapterProxy) { 151 mBluetoothAdapter = bluetoothAdapterProxy; 152 mContext = context; 153 154 if (mBluetoothAdapter != null) { 155 mBluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener, 156 BluetoothProfile.HEADSET); 157 } 158 159 // Register for misc other intent broadcasts. 160 IntentFilter intentFilter = 161 new IntentFilter(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); 162 intentFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED); 163 context.registerReceiver(mReceiver, intentFilter); 164 } 165 166 public void setBluetoothStateListener(BluetoothStateListener bluetoothStateListener) { 167 mBluetoothStateListener = bluetoothStateListener; 168 } 169 170 // 171 // Bluetooth helper methods. 172 // 173 // - BluetoothAdapter is the Bluetooth system service. If 174 // getDefaultAdapter() returns null 175 // then the device is not BT capable. Use BluetoothDevice.isEnabled() 176 // to see if BT is enabled on the device. 177 // 178 // - BluetoothHeadset is the API for the control connection to a 179 // Bluetooth Headset. This lets you completely connect/disconnect a 180 // headset (which we don't do from the Phone UI!) but also lets you 181 // get the address of the currently active headset and see whether 182 // it's currently connected. 183 184 /** 185 * @return true if the Bluetooth on/off switch in the UI should be 186 * available to the user (i.e. if the device is BT-capable 187 * and a headset is connected.) 188 */ 189 @VisibleForTesting 190 public boolean isBluetoothAvailable() { 191 Log.v(this, "isBluetoothAvailable()..."); 192 193 // There's no need to ask the Bluetooth system service if BT is enabled: 194 // 195 // BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 196 // if ((adapter == null) || !adapter.isEnabled()) { 197 // Log.d(this, " ==> FALSE (BT not enabled)"); 198 // return false; 199 // } 200 // Log.d(this, " - BT enabled! device name " + adapter.getName() 201 // + ", address " + adapter.getAddress()); 202 // 203 // ...since we already have a BluetoothHeadset instance. We can just 204 // call isConnected() on that, and assume it'll be false if BT isn't 205 // enabled at all. 206 207 // Check if there's a connected headset, using the BluetoothHeadset API. 208 boolean isConnected = false; 209 if (mBluetoothHeadset != null) { 210 List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices(); 211 212 if (deviceList.size() > 0) { 213 isConnected = true; 214 for (int i = 0; i < deviceList.size(); i++) { 215 BluetoothDevice device = deviceList.get(i); 216 Log.v(this, "state = " + mBluetoothHeadset.getConnectionState(device) 217 + "for headset: " + device); 218 } 219 } 220 } 221 222 Log.v(this, " ==> " + isConnected); 223 return isConnected; 224 } 225 226 /** 227 * @return true if a BT Headset is available, and its audio is currently connected. 228 */ 229 @VisibleForTesting 230 public boolean isBluetoothAudioConnected() { 231 if (mBluetoothHeadset == null) { 232 Log.v(this, "isBluetoothAudioConnected: ==> FALSE (null mBluetoothHeadset)"); 233 return false; 234 } 235 List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices(); 236 237 if (deviceList.isEmpty()) { 238 return false; 239 } 240 for (int i = 0; i < deviceList.size(); i++) { 241 BluetoothDevice device = deviceList.get(i); 242 boolean isAudioOn = mBluetoothHeadset.isAudioConnected(device); 243 Log.v(this, "isBluetoothAudioConnected: ==> isAudioOn = " + isAudioOn 244 + "for headset: " + device); 245 if (isAudioOn) { 246 return true; 247 } 248 } 249 return false; 250 } 251 252 /** 253 * Helper method used to control the onscreen "Bluetooth" indication; 254 * 255 * @return true if a BT device is available and its audio is currently connected, 256 * <b>or</b> if we issued a BluetoothHeadset.connectAudio() 257 * call within the last 5 seconds (which presumably means 258 * that the BT audio connection is currently being set 259 * up, and will be connected soon.) 260 */ 261 @VisibleForTesting 262 public boolean isBluetoothAudioConnectedOrPending() { 263 if (isBluetoothAudioConnected()) { 264 Log.v(this, "isBluetoothAudioConnectedOrPending: ==> TRUE (really connected)"); 265 return true; 266 } 267 268 // If we issued a connectAudio() call "recently enough", even 269 // if BT isn't actually connected yet, let's still pretend BT is 270 // on. This makes the onscreen indication more responsive. 271 if (isBluetoothAudioPending()) { 272 long timeSinceRequest = 273 SystemClock.elapsedRealtime() - mBluetoothConnectionRequestTime; 274 Log.v(this, "isBluetoothAudioConnectedOrPending: ==> TRUE (requested " 275 + timeSinceRequest + " msec ago)"); 276 return true; 277 } 278 279 Log.v(this, "isBluetoothAudioConnectedOrPending: ==> FALSE"); 280 return false; 281 } 282 283 private boolean isBluetoothAudioPending() { 284 return mBluetoothState == BLUETOOTH_AUDIO_PENDING; 285 } 286 287 /** 288 * Notified audio manager of a change to the bluetooth state. 289 */ 290 private void updateListenerOfBluetoothState(boolean canBePending) { 291 int newState; 292 if (isBluetoothAudioConnected()) { 293 newState = BLUETOOTH_AUDIO_CONNECTED; 294 } else if (canBePending && isBluetoothAudioPending()) { 295 newState = BLUETOOTH_AUDIO_PENDING; 296 } else if (isBluetoothAvailable()) { 297 newState = BLUETOOTH_DEVICE_CONNECTED; 298 } else { 299 newState = BLUETOOTH_DISCONNECTED; 300 } 301 if (mBluetoothState != newState) { 302 mBluetoothStateListener.onBluetoothStateChange(mBluetoothState, newState); 303 mBluetoothState = newState; 304 } 305 } 306 307 @VisibleForTesting 308 public void connectBluetoothAudio() { 309 Log.v(this, "connectBluetoothAudio()..."); 310 if (mBluetoothHeadset != null) { 311 if (!mBluetoothHeadset.connectAudio()) { 312 mHandler.postDelayed(mRetryConnectAudio.prepare(), 313 Timeouts.getRetryBluetoothConnectAudioBackoffMillis( 314 mContext.getContentResolver())); 315 } 316 } 317 // The call to connectAudio is asynchronous and may take some time to complete. However, 318 // if connectAudio() returns false, we know that it has failed and therefore will 319 // schedule a retry to happen some time later. We set bluetooth state to pending now and 320 // show bluetooth as connected in the UI, but confirmation that we are connected will 321 // arrive through mReceiver. 322 setBluetoothStatePending(); 323 } 324 325 private void setBluetoothStatePending() { 326 mBluetoothState = BLUETOOTH_AUDIO_PENDING; 327 mBluetoothConnectionRequestTime = SystemClock.elapsedRealtime(); 328 mHandler.removeCallbacks(mBluetoothConnectionTimeout.getRunnableToCancel()); 329 mBluetoothConnectionTimeout.cancel(); 330 // If the mBluetoothConnectionTimeout runnable has run, the session had been cleared... 331 // Create a new Session before putting it back in the queue to possibly run again. 332 mHandler.postDelayed(mBluetoothConnectionTimeout.prepare(), 333 Timeouts.getBluetoothPendingTimeoutMillis(mContext.getContentResolver())); 334 } 335 336 @VisibleForTesting 337 public void disconnectBluetoothAudio() { 338 Log.v(this, "disconnectBluetoothAudio()..."); 339 if (mBluetoothHeadset != null) { 340 mBluetoothState = BLUETOOTH_DEVICE_CONNECTED; 341 mBluetoothHeadset.disconnectAudio(); 342 } else { 343 mBluetoothState = BLUETOOTH_DISCONNECTED; 344 } 345 mHandler.removeCallbacks(mBluetoothConnectionTimeout.getRunnableToCancel()); 346 mBluetoothConnectionTimeout.cancel(); 347 } 348 349 /** 350 * Dumps the state of the {@link BluetoothManager}. 351 * 352 * @param pw The {@code IndentingPrintWriter} to write the state to. 353 */ 354 public void dump(IndentingPrintWriter pw) { 355 pw.println("isBluetoothAvailable: " + isBluetoothAvailable()); 356 pw.println("isBluetoothAudioConnected: " + isBluetoothAudioConnected()); 357 pw.println("isBluetoothAudioConnectedOrPending: " + isBluetoothAudioConnectedOrPending()); 358 359 if (mBluetoothAdapter != null) { 360 if (mBluetoothHeadset != null) { 361 List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices(); 362 363 if (deviceList.size() > 0) { 364 BluetoothDevice device = deviceList.get(0); 365 pw.println("BluetoothHeadset.getCurrentDevice: " + device); 366 pw.println("BluetoothHeadset.State: " 367 + mBluetoothHeadset.getConnectionState(device)); 368 pw.println("BluetoothHeadset audio connected: " + 369 mBluetoothHeadset.isAudioConnected(device)); 370 } 371 } else { 372 pw.println("mBluetoothHeadset is null"); 373 } 374 } else { 375 pw.println("mBluetoothAdapter is null; device is not BT capable"); 376 } 377 } 378 379 /** 380 * Set the bluetooth headset proxy for testing purposes. 381 * @param bluetoothHeadsetProxy 382 */ 383 @VisibleForTesting 384 public void setBluetoothHeadsetForTesting(BluetoothHeadsetProxy bluetoothHeadsetProxy) { 385 mBluetoothHeadset = bluetoothHeadsetProxy; 386 } 387 388 /** 389 * Set mBluetoothState for testing. 390 * @param state 391 */ 392 @VisibleForTesting 393 public void setInternalBluetoothState(int state) { 394 mBluetoothState = state; 395 } 396 } 397