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.server.telecom.bluetooth; 18 19 import static com.android.server.telecom.CallAudioRouteAdapter.BT_ACTIVE_DEVICE_GONE; 20 import static com.android.server.telecom.CallAudioRouteAdapter.BT_ACTIVE_DEVICE_PRESENT; 21 import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_CONNECTED; 22 import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_DISCONNECTED; 23 import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_ADDED; 24 import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_REMOVED; 25 import static com.android.server.telecom.CallAudioRouteAdapter.PENDING_ROUTE_FAILED; 26 import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BASELINE_ROUTE; 27 import static com.android.server.telecom.CallAudioRouteController.INCLUDE_BLUETOOTH_IN_BASELINE; 28 import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_IS_ON; 29 import static com.android.server.telecom.bluetooth.BluetoothRouteManager.BT_AUDIO_LOST; 30 31 import android.bluetooth.BluetoothAdapter; 32 import android.bluetooth.BluetoothDevice; 33 import android.bluetooth.BluetoothHeadset; 34 import android.bluetooth.BluetoothHearingAid; 35 import android.bluetooth.BluetoothLeAudio; 36 import android.bluetooth.BluetoothProfile; 37 import android.content.BroadcastReceiver; 38 import android.content.Context; 39 import android.content.Intent; 40 import android.content.IntentFilter; 41 import android.media.AudioDeviceInfo; 42 import android.os.Bundle; 43 import android.sysprop.BluetoothProperties; 44 import android.telecom.Log; 45 import android.telecom.Logging.Session; 46 import android.util.Pair; 47 48 import com.android.internal.os.SomeArgs; 49 import com.android.server.telecom.AudioRoute; 50 import com.android.server.telecom.CallAudioCommunicationDeviceTracker; 51 import com.android.server.telecom.CallAudioRouteAdapter; 52 import com.android.server.telecom.CallAudioRouteController; 53 import com.android.server.telecom.flags.FeatureFlags; 54 55 import java.util.Objects; 56 57 public class BluetoothStateReceiver extends BroadcastReceiver { 58 private static final String LOG_TAG = BluetoothStateReceiver.class.getSimpleName(); 59 public static final IntentFilter INTENT_FILTER; 60 static { 61 INTENT_FILTER = new IntentFilter(); 62 INTENT_FILTER.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); 63 INTENT_FILTER.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED); 64 INTENT_FILTER.addAction(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED); 65 INTENT_FILTER.addAction(BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED); 66 INTENT_FILTER.addAction(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED); 67 INTENT_FILTER.addAction(BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED); 68 INTENT_FILTER.addAction(BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED); 69 INTENT_FILTER.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); 70 } 71 72 // If not in a call, BSR won't listen to the Bluetooth stack's HFP on/off messages, since 73 // other apps could be turning it on and off. We don't want to interfere. 74 private boolean mIsInCall = false; 75 private final BluetoothRouteManager mBluetoothRouteManager; 76 private final BluetoothDeviceManager mBluetoothDeviceManager; 77 private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker; 78 private FeatureFlags mFeatureFlags; 79 private boolean mIsScoManagedByAudio; 80 private CallAudioRouteAdapter mCallAudioRouteAdapter; 81 onReceive(Context context, Intent intent)82 public void onReceive(Context context, Intent intent) { 83 Log.startSession("BSR.oR"); 84 try { 85 String action = intent.getAction(); 86 switch (action) { 87 case BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED: 88 handleAudioStateChanged(intent); 89 break; 90 case BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED: 91 case BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED: 92 case BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED: 93 handleConnectionStateChanged(intent); 94 break; 95 case BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED: 96 case BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED: 97 case BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED: 98 handleActiveDeviceChanged(intent); 99 break; 100 } 101 } finally { 102 Log.endSession(); 103 } 104 } 105 handleAudioStateChanged(Intent intent)106 private void handleAudioStateChanged(Intent intent) { 107 int bluetoothHeadsetAudioState = 108 intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, 109 BluetoothHeadset.STATE_AUDIO_DISCONNECTED); 110 BluetoothDevice device = 111 intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class); 112 if (device == null) { 113 Log.w(LOG_TAG, "Got null device from broadcast. " + 114 "Ignoring."); 115 return; 116 } 117 118 Log.i(LOG_TAG, "Device %s transitioned to audio state %d", 119 device.getAddress(), bluetoothHeadsetAudioState); 120 Session session = Log.createSubsession(); 121 SomeArgs args = SomeArgs.obtain(); 122 args.arg1 = session; 123 args.arg2 = device.getAddress(); 124 switch (bluetoothHeadsetAudioState) { 125 case BluetoothHeadset.STATE_AUDIO_CONNECTED: 126 if (mFeatureFlags.useRefactoredAudioRouteSwitching()) { 127 CallAudioRouteController audioRouteController = 128 (CallAudioRouteController) mCallAudioRouteAdapter; 129 audioRouteController.setScoAudioConnectedDevice(device); 130 AudioRoute btRoute = audioRouteController.getBluetoothRoute( 131 AudioRoute.TYPE_BLUETOOTH_SCO, device.getAddress()); 132 if (audioRouteController.isPending() && Objects.equals(audioRouteController 133 .getPendingAudioRoute().getDestRoute(), btRoute)) { 134 mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, 135 device); 136 } else { 137 // It's possible that the initial BT connection fails but BT_AUDIO_CONNECTED 138 // is sent later, indicating that SCO audio is on. We should route 139 // appropriately in order for the UI to reflect this state. 140 if (btRoute != null) { 141 audioRouteController.getPendingAudioRoute().overrideDestRoute(btRoute); 142 audioRouteController.overrideIsPending(true); 143 audioRouteController.getPendingAudioRoute() 144 .setCommunicationDeviceType(AudioRoute.TYPE_BLUETOOTH_SCO); 145 mCallAudioRouteAdapter.sendMessageWithSessionInfo( 146 CallAudioRouteAdapter.EXIT_PENDING_ROUTE); 147 } 148 } 149 } else { 150 if (!mIsInCall) { 151 Log.i(LOG_TAG, "Ignoring BT audio on since we're not in a call"); 152 return; 153 } 154 mBluetoothRouteManager.sendMessage(BT_AUDIO_IS_ON, args); 155 } 156 break; 157 case BluetoothHeadset.STATE_AUDIO_DISCONNECTED: 158 if (mFeatureFlags.useRefactoredAudioRouteSwitching()) { 159 CallAudioRouteController audioRouteController = 160 (CallAudioRouteController) mCallAudioRouteAdapter; 161 audioRouteController.setScoAudioConnectedDevice(null); 162 if (audioRouteController.isPending()) { 163 mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0, 164 device); 165 } else { 166 // Handle case where BT stack signals SCO disconnected but Telecom isn't 167 // processing any pending routes. This explicitly addresses cf instances 168 // where a remote device disconnects SCO. Telecom should ensure that audio 169 // is properly routed in the UI. 170 audioRouteController.getPendingAudioRoute() 171 .setCommunicationDeviceType(AudioRoute.TYPE_INVALID); 172 mCallAudioRouteAdapter.sendMessageWithSessionInfo(SWITCH_BASELINE_ROUTE, 173 INCLUDE_BLUETOOTH_IN_BASELINE, device.getAddress()); 174 } 175 } else { 176 mBluetoothRouteManager.sendMessage(BT_AUDIO_LOST, args); 177 } 178 break; 179 } 180 } 181 handleConnectionStateChanged(Intent intent)182 private void handleConnectionStateChanged(Intent intent) { 183 int bluetoothHeadsetState = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, 184 BluetoothHeadset.STATE_DISCONNECTED); 185 BluetoothDevice device = 186 intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class); 187 188 if (device == null) { 189 Log.w(LOG_TAG, "Got null device from broadcast. " + 190 "Ignoring."); 191 return; 192 } 193 194 int deviceType; 195 @AudioRoute.AudioRouteType int audioRouteType; 196 if (BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED.equals(intent.getAction())) { 197 deviceType = BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO; 198 audioRouteType = AudioRoute.TYPE_BLUETOOTH_LE; 199 } else if (BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) { 200 deviceType = BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID; 201 audioRouteType = AudioRoute.TYPE_BLUETOOTH_HA; 202 } else if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) { 203 deviceType = BluetoothDeviceManager.DEVICE_TYPE_HEADSET; 204 audioRouteType = AudioRoute.TYPE_BLUETOOTH_SCO; 205 } else { 206 Log.w(LOG_TAG, "handleConnectionStateChanged: %s invalid device type", device); 207 return; 208 } 209 210 Log.i(LOG_TAG, "%s device %s changed state to %d", 211 BluetoothDeviceManager.getDeviceTypeString(deviceType), 212 device.getAddress(), bluetoothHeadsetState); 213 214 if (bluetoothHeadsetState == BluetoothProfile.STATE_CONNECTED) { 215 if (mFeatureFlags.useRefactoredAudioRouteSwitching()) { 216 mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_DEVICE_ADDED, 217 audioRouteType, device); 218 if (mFeatureFlags.keepBluetoothDevicesCacheUpdated()) { 219 mBluetoothDeviceManager.onDeviceConnected(device, deviceType); 220 } 221 } else { 222 mBluetoothDeviceManager.onDeviceConnected(device, deviceType); 223 } 224 } else if (bluetoothHeadsetState == BluetoothProfile.STATE_DISCONNECTED 225 || bluetoothHeadsetState == BluetoothProfile.STATE_DISCONNECTING) { 226 if (mFeatureFlags.useRefactoredAudioRouteSwitching()) { 227 mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_DEVICE_REMOVED, 228 audioRouteType, device); 229 if (mFeatureFlags.keepBluetoothDevicesCacheUpdated()) { 230 mBluetoothDeviceManager.onDeviceDisconnected(device, deviceType); 231 } 232 } else { 233 mBluetoothDeviceManager.onDeviceDisconnected(device, deviceType); 234 } 235 } 236 } 237 handleActiveDeviceChanged(Intent intent)238 private void handleActiveDeviceChanged(Intent intent) { 239 BluetoothDevice device = 240 intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class); 241 242 int deviceType; 243 @AudioRoute.AudioRouteType int audioRouteType; 244 if (BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED.equals(intent.getAction())) { 245 deviceType = BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO; 246 audioRouteType = AudioRoute.TYPE_BLUETOOTH_LE; 247 } else if (BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED.equals(intent.getAction())) { 248 deviceType = BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID; 249 audioRouteType = AudioRoute.TYPE_BLUETOOTH_HA; 250 } else if (BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED.equals(intent.getAction())) { 251 deviceType = BluetoothDeviceManager.DEVICE_TYPE_HEADSET; 252 audioRouteType = AudioRoute.TYPE_BLUETOOTH_SCO; 253 } else { 254 Log.w(LOG_TAG, "handleActiveDeviceChanged: %s invalid device type", device); 255 return; 256 } 257 258 Log.i(LOG_TAG, "Device %s is now the preferred BT device for %s", device, 259 BluetoothDeviceManager.getDeviceTypeString(deviceType)); 260 261 if (mFeatureFlags.useRefactoredAudioRouteSwitching()) { 262 CallAudioRouteController audioRouteController = (CallAudioRouteController) 263 mCallAudioRouteAdapter; 264 if (device == null) { 265 // Update the active device cache immediately. 266 audioRouteController.updateActiveBluetoothDevice(new Pair(audioRouteType, null)); 267 mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_GONE, 268 audioRouteType); 269 } else { 270 // Update the active device cache immediately. 271 audioRouteController.updateActiveBluetoothDevice( 272 new Pair(audioRouteType, device.getAddress())); 273 mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT, 274 audioRouteType, device.getAddress()); 275 if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID 276 || deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO 277 || mIsScoManagedByAudio) { 278 if (!mIsInCall) { 279 Log.i(LOG_TAG, "Ignoring audio on since we're not in a call"); 280 return; 281 } 282 if (!mBluetoothDeviceManager.setCommunicationDeviceForAddress( 283 device.getAddress())) { 284 Log.i(this, "handleActiveDeviceChanged: Failed to set " 285 + "communication device for %s.", device); 286 if (!mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()) { 287 Log.i(this, "Sending PENDING_ROUTE_FAILED " 288 + "to pending audio route."); 289 mCallAudioRouteAdapter.getPendingAudioRoute() 290 .onMessageReceived(new Pair<>(PENDING_ROUTE_FAILED, 291 device.getAddress()), device.getAddress()); 292 } else { 293 Log.i(this, "Refrain from sending PENDING_ROUTE_FAILED" 294 + " to pending audio route."); 295 } 296 } else { 297 // Track the currently set communication device. 298 mCallAudioRouteAdapter.getPendingAudioRoute() 299 .setCommunicationDeviceType(audioRouteType); 300 if (audioRouteType == AudioRoute.TYPE_BLUETOOTH_SCO) { 301 mCallAudioRouteAdapter.getPendingAudioRoute() 302 .addMessage(BT_AUDIO_CONNECTED, device.getAddress()); 303 } 304 } 305 } 306 } 307 } else { 308 mBluetoothRouteManager.onActiveDeviceChanged(device, deviceType); 309 if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID || 310 deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) { 311 Session session = Log.createSubsession(); 312 SomeArgs args = SomeArgs.obtain(); 313 args.arg1 = session; 314 if (device == null) { 315 mBluetoothRouteManager.sendMessage(BT_AUDIO_LOST, args); 316 } else { 317 if (!mIsInCall) { 318 Log.i(LOG_TAG, "Ignoring audio on since we're not in a call"); 319 return; 320 } 321 args.arg2 = device.getAddress(); 322 323 boolean usePreferredAudioProfile = false; 324 BluetoothAdapter bluetoothAdapter = mBluetoothDeviceManager 325 .getBluetoothAdapter(); 326 int preferredDuplexProfile = BluetoothProfile.LE_AUDIO; 327 if (bluetoothAdapter != null) { 328 Bundle preferredAudioProfiles = bluetoothAdapter.getPreferredAudioProfiles( 329 device); 330 if (preferredAudioProfiles != null && !preferredAudioProfiles.isEmpty() 331 && preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX) 332 != 0) { 333 Log.i(this, "Preferred duplex profile for device=" + device + " is " 334 + preferredAudioProfiles.getInt( 335 BluetoothAdapter.AUDIO_MODE_DUPLEX)); 336 usePreferredAudioProfile = true; 337 preferredDuplexProfile = 338 preferredAudioProfiles.getInt( 339 BluetoothAdapter.AUDIO_MODE_DUPLEX); 340 } 341 } 342 343 if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) { 344 /* In Le Audio case, once device got Active, the Telecom needs to make sure 345 * it is set as communication device before we can say that BT_AUDIO_IS_ON 346 */ 347 boolean isLeAudioSetForCommunication = 348 mFeatureFlags.callAudioCommunicationDeviceRefactor() 349 ? mCommunicationDeviceTracker.setCommunicationDevice( 350 AudioDeviceInfo.TYPE_BLE_HEADSET, device) 351 : mBluetoothDeviceManager.setLeAudioCommunicationDevice(); 352 if ((!usePreferredAudioProfile 353 || preferredDuplexProfile == BluetoothProfile.LE_AUDIO) 354 && !isLeAudioSetForCommunication) { 355 Log.w(LOG_TAG, 356 "Device %s cannot be use as LE audio communication device.", 357 device); 358 } 359 } else { 360 boolean isHearingAidSetForCommunication = 361 mFeatureFlags.callAudioCommunicationDeviceRefactor() 362 ? mCommunicationDeviceTracker.setCommunicationDevice( 363 AudioDeviceInfo.TYPE_HEARING_AID, null) 364 : mBluetoothDeviceManager 365 .setHearingAidCommunicationDevice(); 366 /* deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID */ 367 if (!isHearingAidSetForCommunication) { 368 Log.w(LOG_TAG, 369 "Device %s cannot be use as hearing aid communication device.", 370 device); 371 } else { 372 mBluetoothRouteManager.sendMessage(BT_AUDIO_IS_ON, args); 373 } 374 } 375 } 376 } 377 } 378 } 379 getBluetoothDeviceManager()380 public BluetoothDeviceManager getBluetoothDeviceManager() { 381 return mBluetoothDeviceManager; 382 } 383 BluetoothStateReceiver(BluetoothDeviceManager deviceManager, BluetoothRouteManager routeManager, CallAudioCommunicationDeviceTracker communicationDeviceTracker, FeatureFlags featureFlags)384 public BluetoothStateReceiver(BluetoothDeviceManager deviceManager, 385 BluetoothRouteManager routeManager, 386 CallAudioCommunicationDeviceTracker communicationDeviceTracker, 387 FeatureFlags featureFlags) { 388 mBluetoothDeviceManager = deviceManager; 389 mBluetoothRouteManager = routeManager; 390 mCommunicationDeviceTracker = communicationDeviceTracker; 391 mFeatureFlags = featureFlags; 392 // Indication that SCO is managed by audio (i.e. supports setCommunicationDevice). 393 mIsScoManagedByAudio = android.media.audio.Flags.scoManagedByAudio() 394 && BluetoothProperties.isScoManagedByAudioEnabled().orElse(false); 395 } 396 setIsInCall(boolean isInCall)397 public void setIsInCall(boolean isInCall) { 398 mIsInCall = isInCall; 399 } 400 setCallAudioRouteAdapter(CallAudioRouteAdapter adapter)401 public void setCallAudioRouteAdapter(CallAudioRouteAdapter adapter) { 402 mCallAudioRouteAdapter = adapter; 403 } 404 } 405