• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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