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