1 /* 2 * Copyright (C) 2018 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.bluetooth.btservice; 18 19 import android.annotation.RequiresPermission; 20 import android.annotation.SuppressLint; 21 import android.bluetooth.BluetoothA2dp; 22 import android.bluetooth.BluetoothAdapter; 23 import android.bluetooth.BluetoothDevice; 24 import android.bluetooth.BluetoothHeadset; 25 import android.bluetooth.BluetoothHearingAid; 26 import android.bluetooth.BluetoothProfile; 27 import android.content.BroadcastReceiver; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.IntentFilter; 31 import android.media.AudioDeviceCallback; 32 import android.media.AudioDeviceInfo; 33 import android.media.AudioManager; 34 import android.os.Handler; 35 import android.os.HandlerThread; 36 import android.os.Looper; 37 import android.os.Message; 38 import android.util.Log; 39 40 import com.android.bluetooth.a2dp.A2dpService; 41 import com.android.bluetooth.hearingaid.HearingAidService; 42 import com.android.bluetooth.hfp.HeadsetService; 43 import com.android.internal.annotations.VisibleForTesting; 44 45 import java.util.LinkedList; 46 import java.util.List; 47 import java.util.Objects; 48 49 /** 50 * The active device manager is responsible for keeping track of the 51 * connected A2DP/HFP/AVRCP/HearingAid devices and select which device is 52 * active (for each profile). 53 * 54 * Current policy (subject to change): 55 * 1) If the maximum number of connected devices is one, the manager doesn't 56 * do anything. Each profile is responsible for automatically selecting 57 * the connected device as active. Only if the maximum number of connected 58 * devices is more than one, the rules below will apply. 59 * 2) The selected A2DP active device is the one used for AVRCP as well. 60 * 3) The HFP active device might be different from the A2DP active device. 61 * 4) The Active Device Manager always listens for ACTION_ACTIVE_DEVICE_CHANGED 62 * broadcasts for each profile: 63 * - BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED for A2DP 64 * - BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED for HFP 65 * - BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED for HearingAid 66 * If such broadcast is received (e.g., triggered indirectly by user 67 * action on the UI), the device in the received broacast is marked 68 * as the current active device for that profile. 69 * 5) If there is a HearingAid active device, then A2DP and HFP active devices 70 * must be set to null (i.e., A2DP and HFP cannot have active devices). 71 * The reason is because A2DP or HFP cannot be used together with HearingAid. 72 * 6) If there are no connected devices (e.g., during startup, or after all 73 * devices have been disconnected, the active device per profile 74 * (A2DP/HFP/HearingAid) is selected as follows: 75 * 6.1) The last connected HearingAid device is selected as active. 76 * If there is an active A2DP or HFP device, those must be set to null. 77 * 6.2) The last connected A2DP or HFP device is selected as active. 78 * However, if there is an active HearingAid device, then the 79 * A2DP or HFP active device is not set (must remain null). 80 * 7) If the currently active device (per profile) is disconnected, the 81 * Active Device Manager just marks that the profile has no active device, 82 * but does not attempt to select a new one. Currently, the expectation is 83 * that the user will explicitly select the new active device. 84 * 8) If there is already an active device, and the corresponding 85 * ACTION_ACTIVE_DEVICE_CHANGED broadcast is received, the device 86 * contained in the broadcast is marked as active. However, if 87 * the contained device is null, the corresponding profile is marked 88 * as having no active device. 89 * 9) If a wired audio device is connected, the audio output is switched 90 * by the Audio Framework itself to that device. We detect this here, 91 * and the active device for each profile (A2DP/HFP/HearingAid) is set 92 * to null to reflect the output device state change. However, if the 93 * wired audio device is disconnected, we don't do anything explicit 94 * and apply the default behavior instead: 95 * 9.1) If the wired headset is still the selected output device (i.e. the 96 * active device is set to null), the Phone itself will become the output 97 * device (i.e., the active device will remain null). If music was 98 * playing, it will stop. 99 * 9.2) If one of the Bluetooth devices is the selected active device 100 * (e.g., by the user in the UI), disconnecting the wired audio device 101 * will have no impact. E.g., music will continue streaming over the 102 * active Bluetooth device. 103 */ 104 class ActiveDeviceManager { 105 private static final boolean DBG = true; 106 private static final String TAG = "BluetoothActiveDeviceManager"; 107 108 // Message types for the handler 109 private static final int MESSAGE_ADAPTER_ACTION_STATE_CHANGED = 1; 110 private static final int MESSAGE_A2DP_ACTION_CONNECTION_STATE_CHANGED = 2; 111 private static final int MESSAGE_A2DP_ACTION_ACTIVE_DEVICE_CHANGED = 3; 112 private static final int MESSAGE_HFP_ACTION_CONNECTION_STATE_CHANGED = 4; 113 private static final int MESSAGE_HFP_ACTION_ACTIVE_DEVICE_CHANGED = 5; 114 private static final int MESSAGE_HEARING_AID_ACTION_ACTIVE_DEVICE_CHANGED = 6; 115 116 private final AdapterService mAdapterService; 117 private final ServiceFactory mFactory; 118 private HandlerThread mHandlerThread = null; 119 private Handler mHandler = null; 120 private final AudioManager mAudioManager; 121 private final AudioManagerAudioDeviceCallback mAudioManagerAudioDeviceCallback; 122 123 private final List<BluetoothDevice> mA2dpConnectedDevices = new LinkedList<>(); 124 private final List<BluetoothDevice> mHfpConnectedDevices = new LinkedList<>(); 125 private BluetoothDevice mA2dpActiveDevice = null; 126 private BluetoothDevice mHfpActiveDevice = null; 127 private BluetoothDevice mHearingAidActiveDevice = null; 128 129 // Broadcast receiver for all changes 130 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 131 @Override 132 public void onReceive(Context context, Intent intent) { 133 String action = intent.getAction(); 134 if (action == null) { 135 Log.e(TAG, "Received intent with null action"); 136 return; 137 } 138 switch (action) { 139 case BluetoothAdapter.ACTION_STATE_CHANGED: 140 mHandler.obtainMessage(MESSAGE_ADAPTER_ACTION_STATE_CHANGED, 141 intent).sendToTarget(); 142 break; 143 case BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED: 144 mHandler.obtainMessage(MESSAGE_A2DP_ACTION_CONNECTION_STATE_CHANGED, 145 intent).sendToTarget(); 146 break; 147 case BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED: 148 mHandler.obtainMessage(MESSAGE_A2DP_ACTION_ACTIVE_DEVICE_CHANGED, 149 intent).sendToTarget(); 150 break; 151 case BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED: 152 mHandler.obtainMessage(MESSAGE_HFP_ACTION_CONNECTION_STATE_CHANGED, 153 intent).sendToTarget(); 154 break; 155 case BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED: 156 mHandler.obtainMessage(MESSAGE_HFP_ACTION_ACTIVE_DEVICE_CHANGED, 157 intent).sendToTarget(); 158 break; 159 case BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED: 160 mHandler.obtainMessage(MESSAGE_HEARING_AID_ACTION_ACTIVE_DEVICE_CHANGED, 161 intent).sendToTarget(); 162 break; 163 default: 164 Log.e(TAG, "Received unexpected intent, action=" + action); 165 break; 166 } 167 } 168 }; 169 170 class ActiveDeviceManagerHandler extends Handler { ActiveDeviceManagerHandler(Looper looper)171 ActiveDeviceManagerHandler(Looper looper) { 172 super(looper); 173 } 174 175 @Override handleMessage(Message msg)176 public void handleMessage(Message msg) { 177 switch (msg.what) { 178 case MESSAGE_ADAPTER_ACTION_STATE_CHANGED: { 179 Intent intent = (Intent) msg.obj; 180 int newState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1); 181 if (DBG) { 182 Log.d(TAG, "handleMessage(MESSAGE_ADAPTER_ACTION_STATE_CHANGED): newState=" 183 + newState); 184 } 185 if (newState == BluetoothAdapter.STATE_ON) { 186 resetState(); 187 } 188 } 189 break; 190 191 case MESSAGE_A2DP_ACTION_CONNECTION_STATE_CHANGED: { 192 Intent intent = (Intent) msg.obj; 193 BluetoothDevice device = 194 intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 195 int prevState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1); 196 int nextState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); 197 if (prevState == nextState) { 198 // Nothing has changed 199 break; 200 } 201 if (nextState == BluetoothProfile.STATE_CONNECTED) { 202 // Device connected 203 if (DBG) { 204 Log.d(TAG, 205 "handleMessage(MESSAGE_A2DP_ACTION_CONNECTION_STATE_CHANGED): " 206 + "device " + device + " connected"); 207 } 208 if (mA2dpConnectedDevices.contains(device)) { 209 break; // The device is already connected 210 } 211 mA2dpConnectedDevices.add(device); 212 if (mHearingAidActiveDevice == null) { 213 // New connected device: select it as active 214 setA2dpActiveDevice(device); 215 break; 216 } 217 break; 218 } 219 if (prevState == BluetoothProfile.STATE_CONNECTED) { 220 // Device disconnected 221 if (DBG) { 222 Log.d(TAG, 223 "handleMessage(MESSAGE_A2DP_ACTION_CONNECTION_STATE_CHANGED): " 224 + "device " + device + " disconnected"); 225 } 226 mA2dpConnectedDevices.remove(device); 227 if (Objects.equals(mA2dpActiveDevice, device)) { 228 setA2dpActiveDevice(null); 229 } 230 } 231 } 232 break; 233 234 case MESSAGE_A2DP_ACTION_ACTIVE_DEVICE_CHANGED: { 235 Intent intent = (Intent) msg.obj; 236 BluetoothDevice device = 237 intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 238 if (DBG) { 239 Log.d(TAG, "handleMessage(MESSAGE_A2DP_ACTION_ACTIVE_DEVICE_CHANGED): " 240 + "device= " + device); 241 } 242 if (device != null && !Objects.equals(mA2dpActiveDevice, device)) { 243 setHearingAidActiveDevice(null); 244 } 245 // Just assign locally the new value 246 mA2dpActiveDevice = device; 247 } 248 break; 249 250 case MESSAGE_HFP_ACTION_CONNECTION_STATE_CHANGED: { 251 Intent intent = (Intent) msg.obj; 252 BluetoothDevice device = 253 intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 254 int prevState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1); 255 int nextState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); 256 if (prevState == nextState) { 257 // Nothing has changed 258 break; 259 } 260 if (nextState == BluetoothProfile.STATE_CONNECTED) { 261 // Device connected 262 if (DBG) { 263 Log.d(TAG, 264 "handleMessage(MESSAGE_HFP_ACTION_CONNECTION_STATE_CHANGED): " 265 + "device " + device + " connected"); 266 } 267 if (mHfpConnectedDevices.contains(device)) { 268 break; // The device is already connected 269 } 270 mHfpConnectedDevices.add(device); 271 if (mHearingAidActiveDevice == null) { 272 // New connected device: select it as active 273 setHfpActiveDevice(device); 274 break; 275 } 276 break; 277 } 278 if (prevState == BluetoothProfile.STATE_CONNECTED) { 279 // Device disconnected 280 if (DBG) { 281 Log.d(TAG, 282 "handleMessage(MESSAGE_HFP_ACTION_CONNECTION_STATE_CHANGED): " 283 + "device " + device + " disconnected"); 284 } 285 mHfpConnectedDevices.remove(device); 286 if (Objects.equals(mHfpActiveDevice, device)) { 287 setHfpActiveDevice(null); 288 } 289 } 290 } 291 break; 292 293 case MESSAGE_HFP_ACTION_ACTIVE_DEVICE_CHANGED: { 294 Intent intent = (Intent) msg.obj; 295 BluetoothDevice device = 296 intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 297 if (DBG) { 298 Log.d(TAG, "handleMessage(MESSAGE_HFP_ACTION_ACTIVE_DEVICE_CHANGED): " 299 + "device= " + device); 300 } 301 if (device != null && !Objects.equals(mHfpActiveDevice, device)) { 302 setHearingAidActiveDevice(null); 303 } 304 // Just assign locally the new value 305 mHfpActiveDevice = device; 306 } 307 break; 308 309 case MESSAGE_HEARING_AID_ACTION_ACTIVE_DEVICE_CHANGED: { 310 Intent intent = (Intent) msg.obj; 311 BluetoothDevice device = 312 intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 313 if (DBG) { 314 Log.d(TAG, "handleMessage(MESSAGE_HA_ACTION_ACTIVE_DEVICE_CHANGED): " 315 + "device= " + device); 316 } 317 // Just assign locally the new value 318 mHearingAidActiveDevice = device; 319 if (device != null) { 320 setA2dpActiveDevice(null); 321 setHfpActiveDevice(null); 322 } 323 } 324 break; 325 } 326 } 327 } 328 329 /** Notifications of audio device connection and disconnection events. */ 330 @SuppressLint("AndroidFrameworkRequiresPermission") 331 private class AudioManagerAudioDeviceCallback extends AudioDeviceCallback { isWiredAudioHeadset(AudioDeviceInfo deviceInfo)332 private boolean isWiredAudioHeadset(AudioDeviceInfo deviceInfo) { 333 switch (deviceInfo.getType()) { 334 case AudioDeviceInfo.TYPE_WIRED_HEADSET: 335 case AudioDeviceInfo.TYPE_WIRED_HEADPHONES: 336 case AudioDeviceInfo.TYPE_USB_HEADSET: 337 return true; 338 default: 339 break; 340 } 341 return false; 342 } 343 344 @Override onAudioDevicesAdded(AudioDeviceInfo[] addedDevices)345 public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { 346 if (DBG) { 347 Log.d(TAG, "onAudioDevicesAdded"); 348 } 349 boolean hasAddedWiredDevice = false; 350 for (AudioDeviceInfo deviceInfo : addedDevices) { 351 if (DBG) { 352 Log.d(TAG, "Audio device added: " + deviceInfo.getProductName() + " type: " 353 + deviceInfo.getType()); 354 } 355 if (isWiredAudioHeadset(deviceInfo)) { 356 hasAddedWiredDevice = true; 357 break; 358 } 359 } 360 if (hasAddedWiredDevice) { 361 wiredAudioDeviceConnected(); 362 } 363 } 364 365 @Override onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices)366 public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) { 367 } 368 } 369 ActiveDeviceManager(AdapterService service, ServiceFactory factory)370 ActiveDeviceManager(AdapterService service, ServiceFactory factory) { 371 mAdapterService = service; 372 mFactory = factory; 373 mAudioManager = (AudioManager) service.getSystemService(Context.AUDIO_SERVICE); 374 mAudioManagerAudioDeviceCallback = new AudioManagerAudioDeviceCallback(); 375 } 376 start()377 void start() { 378 if (DBG) { 379 Log.d(TAG, "start()"); 380 } 381 382 mHandlerThread = new HandlerThread("BluetoothActiveDeviceManager"); 383 mHandlerThread.start(); 384 mHandler = new ActiveDeviceManagerHandler(mHandlerThread.getLooper()); 385 386 IntentFilter filter = new IntentFilter(); 387 filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); 388 filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED); 389 filter.addAction(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED); 390 filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); 391 filter.addAction(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED); 392 filter.addAction(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED); 393 mAdapterService.registerReceiver(mReceiver, filter); 394 395 mAudioManager.registerAudioDeviceCallback(mAudioManagerAudioDeviceCallback, mHandler); 396 } 397 cleanup()398 void cleanup() { 399 if (DBG) { 400 Log.d(TAG, "cleanup()"); 401 } 402 403 mAudioManager.unregisterAudioDeviceCallback(mAudioManagerAudioDeviceCallback); 404 mAdapterService.unregisterReceiver(mReceiver); 405 if (mHandlerThread != null) { 406 mHandlerThread.quit(); 407 mHandlerThread = null; 408 } 409 resetState(); 410 } 411 412 /** 413 * Get the {@link Looper} for the handler thread. This is used in testing and helper 414 * objects 415 * 416 * @return {@link Looper} for the handler thread 417 */ 418 @VisibleForTesting getHandlerLooper()419 public Looper getHandlerLooper() { 420 if (mHandlerThread == null) { 421 return null; 422 } 423 return mHandlerThread.getLooper(); 424 } 425 setA2dpActiveDevice(BluetoothDevice device)426 private void setA2dpActiveDevice(BluetoothDevice device) { 427 if (DBG) { 428 Log.d(TAG, "setA2dpActiveDevice(" + device + ")"); 429 } 430 final A2dpService a2dpService = mFactory.getA2dpService(); 431 if (a2dpService == null) { 432 return; 433 } 434 if (!a2dpService.setActiveDevice(device)) { 435 return; 436 } 437 mA2dpActiveDevice = device; 438 } 439 440 @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) setHfpActiveDevice(BluetoothDevice device)441 private void setHfpActiveDevice(BluetoothDevice device) { 442 if (DBG) { 443 Log.d(TAG, "setHfpActiveDevice(" + device + ")"); 444 } 445 final HeadsetService headsetService = mFactory.getHeadsetService(); 446 if (headsetService == null) { 447 return; 448 } 449 if (!headsetService.setActiveDevice(device)) { 450 return; 451 } 452 mHfpActiveDevice = device; 453 } 454 setHearingAidActiveDevice(BluetoothDevice device)455 private void setHearingAidActiveDevice(BluetoothDevice device) { 456 if (DBG) { 457 Log.d(TAG, "setHearingAidActiveDevice(" + device + ")"); 458 } 459 final HearingAidService hearingAidService = mFactory.getHearingAidService(); 460 if (hearingAidService == null) { 461 return; 462 } 463 if (!hearingAidService.setActiveDevice(device)) { 464 return; 465 } 466 mHearingAidActiveDevice = device; 467 } 468 resetState()469 private void resetState() { 470 mA2dpConnectedDevices.clear(); 471 mA2dpActiveDevice = null; 472 473 mHfpConnectedDevices.clear(); 474 mHfpActiveDevice = null; 475 476 mHearingAidActiveDevice = null; 477 } 478 479 @VisibleForTesting getBroadcastReceiver()480 BroadcastReceiver getBroadcastReceiver() { 481 return mReceiver; 482 } 483 484 @VisibleForTesting getA2dpActiveDevice()485 BluetoothDevice getA2dpActiveDevice() { 486 return mA2dpActiveDevice; 487 } 488 489 @VisibleForTesting getHfpActiveDevice()490 BluetoothDevice getHfpActiveDevice() { 491 return mHfpActiveDevice; 492 } 493 494 @VisibleForTesting getHearingAidActiveDevice()495 BluetoothDevice getHearingAidActiveDevice() { 496 return mHearingAidActiveDevice; 497 } 498 499 /** 500 * Called when a wired audio device is connected. 501 * It might be called multiple times each time a wired audio device is connected. 502 */ 503 @VisibleForTesting 504 @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) wiredAudioDeviceConnected()505 void wiredAudioDeviceConnected() { 506 if (DBG) { 507 Log.d(TAG, "wiredAudioDeviceConnected"); 508 } 509 setA2dpActiveDevice(null); 510 setHfpActiveDevice(null); 511 setHearingAidActiveDevice(null); 512 } 513 } 514