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