ActiveDeviceManager.java revision bd161c11c4bee853ed833d150a9bb19ee410d6d0
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
17package com.android.bluetooth.btservice;
18
19import android.bluetooth.BluetoothA2dp;
20import android.bluetooth.BluetoothAdapter;
21import android.bluetooth.BluetoothDevice;
22import android.bluetooth.BluetoothHeadset;
23import android.bluetooth.BluetoothHearingAid;
24import android.bluetooth.BluetoothProfile;
25import android.content.BroadcastReceiver;
26import android.content.Context;
27import android.content.Intent;
28import android.content.IntentFilter;
29import android.media.AudioDeviceCallback;
30import android.media.AudioDeviceInfo;
31import android.media.AudioManager;
32import android.os.Handler;
33import android.os.HandlerThread;
34import android.os.Looper;
35import android.os.Message;
36import android.support.annotation.VisibleForTesting;
37import android.util.Log;
38
39import com.android.bluetooth.a2dp.A2dpService;
40import com.android.bluetooth.hearingaid.HearingAidService;
41import com.android.bluetooth.hfp.HeadsetService;
42
43import java.util.LinkedList;
44import java.util.List;
45import 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 */
102class 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 {
169        ActiveDeviceManagerHandler(Looper looper) {
170            super(looper);
171        }
172
173        @Override
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 {
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
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
363        public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
364        }
365    }
366
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
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
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
416    public Looper getHandlerLooper() {
417        if (mHandlerThread == null) {
418            return null;
419        }
420        return mHandlerThread.getLooper();
421    }
422
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
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
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
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
476    BroadcastReceiver getBroadcastReceiver() {
477        return mReceiver;
478    }
479
480    @VisibleForTesting
481    BluetoothDevice getA2dpActiveDevice() {
482        return mA2dpActiveDevice;
483    }
484
485    @VisibleForTesting
486    BluetoothDevice getHfpActiveDevice() {
487        return mHfpActiveDevice;
488    }
489
490    @VisibleForTesting
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
500    void wiredAudioDeviceConnected() {
501        if (DBG) {
502            Log.d(TAG, "wiredAudioDeviceConnected");
503        }
504        setA2dpActiveDevice(null);
505        setHfpActiveDevice(null);
506        setHearingAidActiveDevice(null);
507    }
508}
509