ActiveDeviceManager.java revision 4bc7daecf842b2cfbd0741668df798d531f7dcda
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.os.Handler;
30import android.os.HandlerThread;
31import android.os.Looper;
32import android.os.Message;
33import android.util.Log;
34
35import com.android.bluetooth.a2dp.A2dpService;
36import com.android.bluetooth.hearingaid.HearingAidService;
37import com.android.bluetooth.hfp.HeadsetService;
38
39import java.util.LinkedList;
40import java.util.List;
41import java.util.Objects;
42
43/**
44 * The active device manager is responsible for keeping track of the
45 * connected A2DP/HFP/AVRCP devices and select which device is
46 * active (for each profile).
47 *
48 * Current policy (subject to change):
49 * 1) If the maximum number of connected devices is one, the manager doesn't
50 *    do anything. Each profile is responsible for automatically selecting
51 *    the connected device as active. Only if the maximum number of connected
52 *    devices is more than one, the rules below will apply.
53 * 2) The selected A2DP active device is the one used for AVRCP as well.
54 * 3) The HFP active device might be different from the A2DP active device.
55 * 4) The Active Device Manager always listens for
56 *    ACTION_ACTIVE_DEVICE_CHANGED broadcasts for each profile:
57 *    - BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED for A2DP
58 *    - BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED for HFP
59 *    If such broadcast is received (e.g., triggered indirectly by user
60 *    action on the UI), the device in the received broacast is marked
61 *    as the current active device for that profile.
62 * 5) If there are no connected devices (e.g., during startup, or after all
63 *    devices have been disconnected, the active device per profile
64 *    (either A2DP or HFP) is selected as follows:
65 * 5.1) The first connected device (for either A2DP or HFP) is immediately
66 *      selected as active for that profile. Assume the first connected device
67 *      is for A2DP.
68 * 5.2) A timer is started: if the same device is connected for the other
69 *      profile as well (HFP in this example) while the timer is running,
70 *      and there is no active HFP device yet, that device is selected as
71 *      active for HFP as well. The purpose is to select by default the same
72 *      device as active for both profiles.
73 * 5.3) While the timer is running, all other HFP connected devices are
74 *      listed locally, but none of those devices is selected as active.
75 * 5.4) While the timer is running, if ACTION_ACTIVE_DEVICE_CHANGED broadcast
76 *      is received for HFP, the device contained in the broadcast is
77 *      marked as active.
78 * 5.5) If the timer expires and no HFP device has been selected as active,
79 *      the first HFP connected device is selected as active.
80 * 6) 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 * 7) 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 */
90class ActiveDeviceManager {
91    private static final boolean DBG = true;
92    private static final String TAG = "BluetoothActiveDeviceManager";
93
94    // Message types for the handler
95    private static final int MESSAGE_ADAPTER_ACTION_STATE_CHANGED = 1;
96    private static final int MESSAGE_SELECT_ACTICE_DEVICE_TIMEOUT = 2;
97    private static final int MESSAGE_A2DP_ACTION_CONNECTION_STATE_CHANGED = 3;
98    private static final int MESSAGE_A2DP_ACTION_ACTIVE_DEVICE_CHANGED = 4;
99    private static final int MESSAGE_HFP_ACTION_CONNECTION_STATE_CHANGED = 5;
100    private static final int MESSAGE_HFP_ACTION_ACTIVE_DEVICE_CHANGED = 6;
101    private static final int MESSAGE_HEARING_AID_ACTION_ACTIVE_DEVICE_CHANGED = 7;
102
103    // Timeouts
104    private static final int SELECT_ACTIVE_DEVICE_TIMEOUT_MS = 6000; // 6s
105
106    private final AdapterService mAdapterService;
107    private final ServiceFactory mFactory;
108    private HandlerThread mHandlerThread = null;
109    private Handler mHandler = null;
110
111    private final List<BluetoothDevice> mA2dpConnectedDevices = new LinkedList<>();
112    private final List<BluetoothDevice> mHfpConnectedDevices = new LinkedList<>();
113    private BluetoothDevice mA2dpActiveDevice = null;
114    private BluetoothDevice mHfpActiveDevice = null;
115    private BluetoothDevice mHearingAidActiveDevice = null;
116
117    // Broadcast receiver for all changes
118    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
119        @Override
120        public void onReceive(Context context, Intent intent) {
121            String action = intent.getAction();
122            if (action == null) {
123                Log.e(TAG, "Received intent with null action");
124                return;
125            }
126            switch (action) {
127                case BluetoothAdapter.ACTION_STATE_CHANGED:
128                    mHandler.obtainMessage(MESSAGE_ADAPTER_ACTION_STATE_CHANGED,
129                                           intent).sendToTarget();
130                    break;
131                case BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED:
132                    mHandler.obtainMessage(MESSAGE_A2DP_ACTION_CONNECTION_STATE_CHANGED,
133                                           intent).sendToTarget();
134                    break;
135                case BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED:
136                    mHandler.obtainMessage(MESSAGE_A2DP_ACTION_ACTIVE_DEVICE_CHANGED,
137                                           intent).sendToTarget();
138                    break;
139                case BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED:
140                    mHandler.obtainMessage(MESSAGE_HFP_ACTION_CONNECTION_STATE_CHANGED,
141                                           intent).sendToTarget();
142                    break;
143                case BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED:
144                    mHandler.obtainMessage(MESSAGE_HFP_ACTION_ACTIVE_DEVICE_CHANGED,
145                        intent).sendToTarget();
146                    break;
147                case BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED:
148                    mHandler.obtainMessage(MESSAGE_HEARING_AID_ACTION_ACTIVE_DEVICE_CHANGED,
149                            intent).sendToTarget();
150                    break;
151                default:
152                    Log.e(TAG, "Received unexpected intent, action=" + action);
153                    break;
154            }
155        }
156    };
157
158    class ActivePoliceManagerHandler extends Handler {
159        ActivePoliceManagerHandler(Looper looper) {
160            super(looper);
161        }
162
163        @Override
164        public void handleMessage(Message msg) {
165            switch (msg.what) {
166                case MESSAGE_ADAPTER_ACTION_STATE_CHANGED: {
167                    Intent intent = (Intent) msg.obj;
168                    int newState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
169                    if (DBG) {
170                        Log.d(TAG, "handleMessage(MESSAGE_ADAPTER_ACTION_STATE_CHANGED): newState="
171                                + newState);
172                    }
173                    if (newState == BluetoothAdapter.STATE_ON) {
174                        resetState();
175                    }
176                }
177                break;
178
179                case MESSAGE_SELECT_ACTICE_DEVICE_TIMEOUT: {
180                    if (DBG) {
181                        Log.d(TAG, "handleMessage(MESSAGE_SELECT_ACTICE_DEVICE_TIMEOUT)");
182                    }
183                    // Set the first connected device as active
184                    if ((mA2dpActiveDevice == null) && !mA2dpConnectedDevices.isEmpty()
185                            && mHearingAidActiveDevice == null) {
186                        setA2dpActiveDevice(mA2dpConnectedDevices.get(0));
187                    }
188                    if ((mHfpActiveDevice == null) && !mHfpConnectedDevices.isEmpty()
189                            && mHearingAidActiveDevice == null) {
190                        setHfpActiveDevice(mHfpConnectedDevices.get(0));
191                    }
192                }
193                break;
194
195                case MESSAGE_A2DP_ACTION_CONNECTION_STATE_CHANGED: {
196                    Intent intent = (Intent) msg.obj;
197                    BluetoothDevice device =
198                            intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
199                    int prevState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
200                    int nextState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
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;
210                        }
211                        if (!hasConnectedClassicDevices() && mHearingAidActiveDevice == null) {
212                            // First connected device: select it as active and start the timer
213                            mA2dpConnectedDevices.add(device);
214                            Message m = obtainMessage(MESSAGE_SELECT_ACTICE_DEVICE_TIMEOUT);
215                            sendMessageDelayed(m, SELECT_ACTIVE_DEVICE_TIMEOUT_MS);
216                            setA2dpActiveDevice(device);
217                            break;
218                        }
219                        mA2dpConnectedDevices.add(device);
220                        // Check whether the active device for the other profile is same
221                        if ((mA2dpActiveDevice == null) && matchesActiveDevice(device)
222                                && mHearingAidActiveDevice == null) {
223                            setA2dpActiveDevice(device);
224                            break;
225                        }
226                        // Check whether the active device selection timer is not running
227                        if ((mA2dpActiveDevice == null)
228                                && !hasMessages(MESSAGE_SELECT_ACTICE_DEVICE_TIMEOUT)
229                                && mHearingAidActiveDevice == null) {
230                            setA2dpActiveDevice(mA2dpConnectedDevices.get(0));
231                            break;
232                        }
233                        break;
234                    }
235                    if ((prevState == BluetoothProfile.STATE_CONNECTED)
236                            && (nextState != prevState)) {
237                        // Device disconnected
238                        if (DBG) {
239                            Log.d(TAG,
240                                    "handleMessage(MESSAGE_A2DP_ACTION_CONNECTION_STATE_CHANGED): "
241                                    + "device " + device + " disconnected");
242                        }
243                        mA2dpConnectedDevices.remove(device);
244                        if (Objects.equals(mA2dpActiveDevice, device)) {
245                            setA2dpActiveDevice(null);
246                        }
247                    }
248                }
249                break;
250
251                case MESSAGE_A2DP_ACTION_ACTIVE_DEVICE_CHANGED: {
252                    Intent intent = (Intent) msg.obj;
253                    BluetoothDevice device =
254                            intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
255                    if (DBG) {
256                        Log.d(TAG, "handleMessage(MESSAGE_A2DP_ACTION_ACTIVE_DEVICE_CHANGED): "
257                                + "device= " + device);
258                    }
259                    removeMessages(MESSAGE_SELECT_ACTICE_DEVICE_TIMEOUT);
260                    if (device != null && !Objects.equals(mA2dpActiveDevice, device)) {
261                        setHearingAidActiveDevice(null);
262                    }
263                    // Just assign locally the new value
264                    mA2dpActiveDevice = device;
265                }
266                break;
267
268                case MESSAGE_HFP_ACTION_CONNECTION_STATE_CHANGED: {
269                    Intent intent = (Intent) msg.obj;
270                    BluetoothDevice device =
271                            intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
272                    int prevState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
273                    int nextState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
274                    // TODO: Copy the corresponding logic from the processing of
275                    // message MESSAGE_A2DP_ACTION_CONNECTION_STATE_CHANGED
276                }
277                break;
278
279                case MESSAGE_HFP_ACTION_ACTIVE_DEVICE_CHANGED: {
280                    Intent intent = (Intent) msg.obj;
281                    BluetoothDevice device =
282                            intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
283                    if (DBG) {
284                        Log.d(TAG, "handleMessage(MESSAGE_HFP_ACTION_ACTIVE_DEVICE_CHANGED): "
285                                + "device= " + device);
286                    }
287                    removeMessages(MESSAGE_SELECT_ACTICE_DEVICE_TIMEOUT);
288                    if (device != null && !Objects.equals(mHfpActiveDevice, device)) {
289                        setHearingAidActiveDevice(null);
290                    }
291                    // Just assign locally the new value
292                    mHfpActiveDevice = device;
293                }
294                break;
295
296                case MESSAGE_HEARING_AID_ACTION_ACTIVE_DEVICE_CHANGED: {
297                    Intent intent = (Intent) msg.obj;
298                    BluetoothDevice device =
299                            intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
300                    if (DBG) {
301                        Log.d(TAG, "handleMessage(MESSAGE_HA_ACTION_ACTIVE_DEVICE_CHANGED): "
302                                + "device= " + device);
303                    }
304                    removeMessages(MESSAGE_SELECT_ACTICE_DEVICE_TIMEOUT);
305                    // Just assign locally the new value
306                    mHearingAidActiveDevice = device;
307                    if (device != null) {
308                        setA2dpActiveDevice(null);
309                        setHfpActiveDevice(null);
310                    }
311                }
312                break;
313            }
314        }
315    }
316
317    ActiveDeviceManager(AdapterService service, ServiceFactory factory) {
318        mAdapterService = service;
319        mFactory = factory;
320    }
321
322    void start() {
323        if (DBG) {
324            Log.d(TAG, "start()");
325        }
326
327        mHandlerThread = new HandlerThread("BluetoothActiveDeviceManager");
328        mHandlerThread.start();
329        mHandler = new ActivePoliceManagerHandler(mHandlerThread.getLooper());
330
331        IntentFilter filter = new IntentFilter();
332        filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
333        filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
334        filter.addAction(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED);
335        filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
336        filter.addAction(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED);
337        filter.addAction(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED);
338        mAdapterService.registerReceiver(mReceiver, filter);
339    }
340
341    void cleanup() {
342        if (DBG) {
343            Log.d(TAG, "cleanup()");
344        }
345
346        mAdapterService.unregisterReceiver(mReceiver);
347        if (mHandlerThread != null) {
348            mHandlerThread.quit();
349            mHandlerThread = null;
350        }
351        resetState();
352    }
353
354    private void setA2dpActiveDevice(BluetoothDevice device) {
355        if (DBG) {
356            Log.d(TAG, "setA2dpActiveDevice(" + device + ")");
357        }
358        final A2dpService a2dpService = mFactory.getA2dpService();
359        if (a2dpService == null) {
360            return;
361        }
362        if (!a2dpService.setActiveDevice(device)) {
363            return;
364        }
365        mA2dpActiveDevice = device;
366    }
367
368    private void setHfpActiveDevice(BluetoothDevice device) {
369        if (DBG) {
370            Log.d(TAG, "setHfpActiveDevice(" + device + ")");
371        }
372        final HeadsetService headsetService = mFactory.getHeadsetService();
373        if (headsetService == null) {
374            return;
375        }
376        if (!headsetService.setActiveDevice(device)) {
377            return;
378        }
379        mHfpActiveDevice = device;
380    }
381
382    private void setHearingAidActiveDevice(BluetoothDevice device) {
383        if (DBG) {
384            Log.d(TAG, "setHearingAidActiveDevice(" + device + ")");
385        }
386        final HearingAidService hearingAidService = mFactory.getHearingAidService();
387        if (hearingAidService == null) {
388            return;
389        }
390        if (!hearingAidService.setActiveDevice(device)) {
391            return;
392        }
393        mHearingAidActiveDevice = device;
394    }
395
396    private boolean hasConnectedClassicDevices() {
397        return (!mA2dpConnectedDevices.isEmpty() || !mHfpConnectedDevices.isEmpty());
398    }
399
400    private boolean matchesActiveDevice(BluetoothDevice device) {
401        return (Objects.equals(mA2dpActiveDevice, device)
402                || Objects.equals(mHfpActiveDevice, device));
403    }
404
405    private void resetState() {
406        if (mHandler != null) {
407            mHandler.removeMessages(MESSAGE_SELECT_ACTICE_DEVICE_TIMEOUT);
408        }
409        mA2dpConnectedDevices.clear();
410        mA2dpActiveDevice = null;
411
412        mHfpConnectedDevices.clear();
413        mHfpActiveDevice = null;
414    }
415}
416