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