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