1 /*
2  * Copyright (C) 2016 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 android.bluetooth.BluetoothAdapter;
20 import android.bluetooth.BluetoothClass;
21 import android.bluetooth.BluetoothDevice;
22 import android.bluetooth.BluetoothHeadset;
23 import android.bluetooth.BluetoothHearingAid;
24 import android.bluetooth.BluetoothProfile;
25 import android.bluetooth.BluetoothLeAudio;
26 import android.content.Context;
27 import android.media.AudioDeviceInfo;
28 import android.os.Message;
29 import android.telecom.Log;
30 import android.telecom.Logging.Session;
31 import android.util.Pair;
32 import android.util.SparseArray;
33 
34 import com.android.internal.annotations.VisibleForTesting;
35 import com.android.internal.os.SomeArgs;
36 import com.android.internal.util.IState;
37 import com.android.internal.util.State;
38 import com.android.internal.util.StateMachine;
39 import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
40 import com.android.server.telecom.TelecomSystem;
41 import com.android.server.telecom.Timeouts;
42 import com.android.server.telecom.flags.FeatureFlags;
43 
44 import java.util.Collection;
45 import java.util.HashMap;
46 import java.util.HashSet;
47 import java.util.LinkedHashSet;
48 import java.util.List;
49 import java.util.Map;
50 import java.util.Objects;
51 import java.util.Optional;
52 import java.util.Set;
53 import java.util.concurrent.BlockingQueue;
54 import java.util.concurrent.LinkedBlockingQueue;
55 import java.util.concurrent.TimeUnit;
56 
57 public class BluetoothRouteManager extends StateMachine {
58     private static final String LOG_TAG = BluetoothRouteManager.class.getSimpleName();
59 
60     private static final SparseArray<String> MESSAGE_CODE_TO_NAME = new SparseArray<String>() {{
61          put(NEW_DEVICE_CONNECTED, "NEW_DEVICE_CONNECTED");
62          put(LOST_DEVICE, "LOST_DEVICE");
63          put(CONNECT_BT, "CONNECT_BT");
64          put(DISCONNECT_BT, "DISCONNECT_BT");
65          put(RETRY_BT_CONNECTION, "RETRY_BT_CONNECTION");
66          put(BT_AUDIO_IS_ON, "BT_AUDIO_IS_ON");
67          put(BT_AUDIO_LOST, "BT_AUDIO_LOST");
68          put(CONNECTION_TIMEOUT, "CONNECTION_TIMEOUT");
69          put(GET_CURRENT_STATE, "GET_CURRENT_STATE");
70          put(RUN_RUNNABLE, "RUN_RUNNABLE");
71     }};
72 
73     public static final String AUDIO_OFF_STATE_NAME = "AudioOff";
74     public static final String AUDIO_CONNECTING_STATE_NAME_PREFIX = "Connecting";
75     public static final String AUDIO_CONNECTED_STATE_NAME_PREFIX = "Connected";
76 
77     // Timeout for querying the current state from the state machine handler.
78     private static final int GET_STATE_TIMEOUT = 1000;
79 
80     public interface BluetoothStateListener {
onBluetoothDeviceListChanged()81         void onBluetoothDeviceListChanged();
onBluetoothActiveDevicePresent()82         void onBluetoothActiveDevicePresent();
onBluetoothActiveDeviceGone()83         void onBluetoothActiveDeviceGone();
onBluetoothAudioConnected()84         void onBluetoothAudioConnected();
onBluetoothAudioConnecting()85         void onBluetoothAudioConnecting();
onBluetoothAudioDisconnected()86         void onBluetoothAudioDisconnected();
87         /**
88          * This gets called when we get an unexpected state change from Bluetooth. Their stack does
89          * weird things sometimes, so this is really a signal for the listener to refresh their
90          * internal state and make sure it matches up with what the BT stack is doing.
91          */
onUnexpectedBluetoothStateChange()92         void onUnexpectedBluetoothStateChange();
93     }
94 
95     /**
96      * Constants representing messages sent to the state machine.
97      * Messages are expected to be sent with {@link SomeArgs} as the obj.
98      * In all cases, arg1 will be the log session.
99      */
100     // arg2: Address of the new device
101     public static final int NEW_DEVICE_CONNECTED = 1;
102     // arg2: Address of the lost device
103     public static final int LOST_DEVICE = 2;
104 
105     // arg2 (optional): the address of the specific device to connect to.
106     public static final int CONNECT_BT = 100;
107     // No args.
108     public static final int DISCONNECT_BT = 101;
109     // arg2: the address of the device to connect to.
110     public static final int RETRY_BT_CONNECTION = 102;
111 
112     // arg2: the address of the device that is on
113     public static final int BT_AUDIO_IS_ON = 200;
114     // arg2: the address of the device that lost BT audio
115     public static final int BT_AUDIO_LOST = 201;
116 
117     // No args; only used internally
118     public static final int CONNECTION_TIMEOUT = 300;
119 
120     // Get the current state and send it through the BlockingQueue<IState> provided as the object
121     // arg.
122     public static final int GET_CURRENT_STATE = 400;
123 
124     // arg2: Runnable
125     public static final int RUN_RUNNABLE = 9001;
126 
127     private static final int MAX_CONNECTION_RETRIES = 2;
128 
129     // States
130     private final class AudioOffState extends State {
131         @Override
getName()132         public String getName() {
133             return AUDIO_OFF_STATE_NAME;
134         }
135 
136         @Override
enter()137         public void enter() {
138             BluetoothDevice erroneouslyConnectedDevice = getBluetoothAudioConnectedDevice();
139             if (erroneouslyConnectedDevice != null &&
140                 !erroneouslyConnectedDevice.equals(mHearingAidActiveDeviceCache)) {
141                 Log.w(LOG_TAG, "Entering AudioOff state but device %s appears to be connected. " +
142                         "Switching to audio-on state for that device.", erroneouslyConnectedDevice);
143                 // change this to just transition to the new audio on state
144                 transitionToActualState();
145             }
146             cleanupStatesForDisconnectedDevices();
147             if (mListener != null) {
148                 mListener.onBluetoothAudioDisconnected();
149             }
150         }
151 
152         @Override
processMessage(Message msg)153         public boolean processMessage(Message msg) {
154             if (msg.what == RUN_RUNNABLE) {
155                 ((Runnable) msg.obj).run();
156                 return HANDLED;
157             }
158 
159             SomeArgs args = (SomeArgs) msg.obj;
160             try {
161                 switch (msg.what) {
162                     case NEW_DEVICE_CONNECTED:
163                         addDevice((String) args.arg2);
164                         break;
165                     case LOST_DEVICE:
166                         removeDevice((String) args.arg2);
167                         break;
168                     case CONNECT_BT:
169                         String actualAddress;
170                         boolean connected;
171                         if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
172                             Pair<String, Boolean> addressInfo = computeAddressToConnectTo(
173                                     (String) args.arg2, false, null);
174                             // See if we need to transition route if the device is already
175                             // connected. If connected, another connection will not occur.
176                             addressInfo = handleDeviceAlreadyConnected(addressInfo);
177                             actualAddress = addressInfo.first;
178                             connected = connectBtAudio(actualAddress, 0,
179                                     false /* switchingBtDevices*/);
180                         } else {
181                             actualAddress = connectBtAudioLegacy((String) args.arg2, false);
182                             connected = actualAddress != null;
183                         }
184 
185                         if (connected) {
186                             transitionTo(getConnectingStateForAddress(actualAddress,
187                                     "AudioOff/CONNECT_BT"));
188                         } else {
189                             Log.w(LOG_TAG, "Tried to connect to %s but failed to connect to" +
190                                     " any BT device.", (String) args.arg2);
191                         }
192                         break;
193                     case DISCONNECT_BT:
194                         // Ignore.
195                         break;
196                     case RETRY_BT_CONNECTION:
197                         Log.i(LOG_TAG, "Retrying BT connection to %s", (String) args.arg2);
198                         String retryAddress;
199                         boolean retrySuccessful;
200                         if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
201                             Pair<String, Boolean> retryAddressInfo = computeAddressToConnectTo(
202                                     (String) args.arg2, false, null);
203                             // See if we need to transition route if the device is already
204                             // connected. If connected, another connection will not occur.
205                             retryAddressInfo = handleDeviceAlreadyConnected(retryAddressInfo);
206                             retryAddress = retryAddressInfo.first;
207                             retrySuccessful = connectBtAudio(retryAddress, args.argi1,
208                                     false /* switchingBtDevices*/);
209                         } else {
210                             retryAddress = connectBtAudioLegacy((String) args.arg2, args.argi1,
211                                     false /* switchingBtDevices*/);
212                             retrySuccessful = retryAddress != null;
213                         }
214 
215                         if (retrySuccessful) {
216                             transitionTo(getConnectingStateForAddress(retryAddress,
217                                     "AudioOff/RETRY_BT_CONNECTION"));
218                         } else {
219                             Log.i(LOG_TAG, "Retry failed.");
220                         }
221                         break;
222                     case CONNECTION_TIMEOUT:
223                         // Ignore.
224                         break;
225                     case BT_AUDIO_IS_ON:
226                         String address = (String) args.arg2;
227                         Log.w(LOG_TAG, "BT audio unexpectedly turned on from device %s", address);
228                         transitionTo(getConnectedStateForAddress(address,
229                                 "AudioOff/BT_AUDIO_IS_ON"));
230                         break;
231                     case BT_AUDIO_LOST:
232                         Log.i(LOG_TAG, "Received BT off for device %s while BT off.",
233                                 (String) args.arg2);
234                         mListener.onUnexpectedBluetoothStateChange();
235                         break;
236                     case GET_CURRENT_STATE:
237                         BlockingQueue<IState> sink = (BlockingQueue<IState>) args.arg3;
238                         sink.offer(this);
239                         break;
240                 }
241             } finally {
242                 args.recycle();
243             }
244             return HANDLED;
245         }
246     }
247 
248     private final class AudioConnectingState extends State {
249         private final String mDeviceAddress;
250 
AudioConnectingState(String address)251         AudioConnectingState(String address) {
252             mDeviceAddress = address;
253         }
254 
255         @Override
getName()256         public String getName() {
257             return AUDIO_CONNECTING_STATE_NAME_PREFIX + ":" + mDeviceAddress;
258         }
259 
260         @Override
enter()261         public void enter() {
262             SomeArgs args = SomeArgs.obtain();
263             args.arg1 = Log.createSubsession();
264             sendMessageDelayed(CONNECTION_TIMEOUT, args,
265                     mTimeoutsAdapter.getBluetoothPendingTimeoutMillis(
266                             mContext.getContentResolver()));
267             mListener.onBluetoothAudioConnecting();
268         }
269 
270         @Override
exit()271         public void exit() {
272             removeMessages(CONNECTION_TIMEOUT);
273         }
274 
275         @Override
processMessage(Message msg)276         public boolean processMessage(Message msg) {
277             if (msg.what == RUN_RUNNABLE) {
278                 ((Runnable) msg.obj).run();
279                 return HANDLED;
280             }
281 
282             SomeArgs args = (SomeArgs) msg.obj;
283             String address = (String) args.arg2;
284             boolean switchingBtDevices = !Objects.equals(mDeviceAddress, address);
285 
286             if (switchingBtDevices) { // check if it is an hearing aid pair
287                 BluetoothAdapter bluetoothAdapter = mDeviceManager.getBluetoothAdapter();
288                 if (bluetoothAdapter != null) {
289                     List<BluetoothDevice> activeHearingAids =
290                       bluetoothAdapter.getActiveDevices(BluetoothProfile.HEARING_AID);
291                     for (BluetoothDevice hearingAid : activeHearingAids) {
292                         if (hearingAid != null) {
293                             String hearingAidAddress = hearingAid.getAddress();
294                             if (hearingAidAddress != null) {
295                                 if (hearingAidAddress.equals(address) ||
296                                     hearingAidAddress.equals(mDeviceAddress)) {
297                                     switchingBtDevices = false;
298                                     break;
299                                 }
300                             }
301                         }
302                     }
303                 }
304                 if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
305                     switchingBtDevices &= (mDeviceAddress != null);
306                 }
307             }
308             try {
309                 switch (msg.what) {
310                     case NEW_DEVICE_CONNECTED:
311                         // If the device isn't new, don't bother passing it up.
312                         addDevice(address);
313                         break;
314                     case LOST_DEVICE:
315                         removeDevice((String) args.arg2);
316                         if (Objects.equals(address, mDeviceAddress)) {
317                             transitionToActualState();
318                         }
319                         break;
320                     case CONNECT_BT:
321                         String actualAddress = null;
322                         if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
323                             Pair<String, Boolean> addressInfo = computeAddressToConnectTo(address,
324                                     switchingBtDevices, mDeviceAddress);
325                             // See if we need to transition route if the device is already
326                             // connected. If connected, another connection will not occur.
327                             addressInfo = handleDeviceAlreadyConnected(addressInfo);
328                             actualAddress = addressInfo.first;
329                             switchingBtDevices = addressInfo.second;
330                         }
331 
332                         if (!switchingBtDevices) {
333                             // Ignore repeated connection attempts to the same device
334                             break;
335                         }
336 
337                         if (!mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
338                             actualAddress = connectBtAudioLegacy(address,
339                                     true /* switchingBtDevices*/);
340                         }
341                         boolean connected = mFeatureFlags.resolveSwitchingBtDevicesComputation()
342                                 ? connectBtAudio(actualAddress, 0, true /* switchingBtDevices*/)
343                                 : actualAddress != null;
344                         if (connected) {
345                             transitionTo(getConnectingStateForAddress(actualAddress,
346                                     "AudioConnecting/CONNECT_BT"));
347                         } else {
348                             Log.w(LOG_TAG, "Tried to connect to %s but failed" +
349                                     " to connect to any BT device.", (String) args.arg2);
350                         }
351                         break;
352                     case DISCONNECT_BT:
353                         mDeviceManager.disconnectAudio();
354                         break;
355                     case RETRY_BT_CONNECTION:
356                         String retryAddress = null;
357                         if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
358                             Pair<String, Boolean> retryAddressInfo = computeAddressToConnectTo(
359                                     address, switchingBtDevices, mDeviceAddress);
360                             // See if we need to transition route if the device is already
361                             // connected. If connected, another connection will not occur.
362                             retryAddressInfo = handleDeviceAlreadyConnected(retryAddressInfo);
363                             retryAddress = retryAddressInfo.first;
364                             switchingBtDevices = retryAddressInfo.second;
365                         }
366 
367                         if (!switchingBtDevices) {
368                             Log.d(LOG_TAG, "Retry message came through while connecting.");
369                             break;
370                         }
371 
372                         if (!mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
373                             retryAddress = connectBtAudioLegacy(address, args.argi1,
374                                     true /* switchingBtDevices*/);
375                         }
376                         boolean retrySuccessful = mFeatureFlags
377                                 .resolveSwitchingBtDevicesComputation()
378                                 ? connectBtAudio(retryAddress, args.argi1,
379                                         true /* switchingBtDevices*/)
380                                 : retryAddress != null;
381                         if (retrySuccessful) {
382                             transitionTo(getConnectingStateForAddress(retryAddress,
383                                     "AudioConnecting/RETRY_BT_CONNECTION"));
384                         } else {
385                             Log.i(LOG_TAG, "Retry failed.");
386                         }
387                         break;
388                     case CONNECTION_TIMEOUT:
389                         Log.i(LOG_TAG, "Connection with device %s timed out.",
390                                 mDeviceAddress);
391                         transitionToActualState();
392                         break;
393                     case BT_AUDIO_IS_ON:
394                         if (Objects.equals(mDeviceAddress, address)) {
395                             Log.i(LOG_TAG, "BT connection success for device %s.", mDeviceAddress);
396                             transitionTo(mAudioConnectedStates.get(mDeviceAddress));
397                         } else {
398                             Log.w(LOG_TAG, "In connecting state for device %s but %s" +
399                                     " is now connected", mDeviceAddress, address);
400                             transitionTo(getConnectedStateForAddress(address,
401                                     "AudioConnecting/BT_AUDIO_IS_ON"));
402                         }
403                         break;
404                     case BT_AUDIO_LOST:
405                         if (Objects.equals(mDeviceAddress, address) || address == null) {
406                             Log.i(LOG_TAG, "Connection with device %s failed.",
407                                     mDeviceAddress);
408                             transitionToActualState();
409                         } else {
410                             Log.w(LOG_TAG, "Got BT lost message for device %s while" +
411                                     " connecting to %s.", address, mDeviceAddress);
412                             mListener.onUnexpectedBluetoothStateChange();
413                         }
414                         break;
415                     case GET_CURRENT_STATE:
416                         BlockingQueue<IState> sink = (BlockingQueue<IState>) args.arg3;
417                         sink.offer(this);
418                         break;
419                 }
420             } finally {
421                 args.recycle();
422             }
423             return HANDLED;
424         }
425     }
426 
427     private final class AudioConnectedState extends State {
428         private final String mDeviceAddress;
429 
AudioConnectedState(String address)430         AudioConnectedState(String address) {
431             mDeviceAddress = address;
432         }
433 
434         @Override
getName()435         public String getName() {
436             return AUDIO_CONNECTED_STATE_NAME_PREFIX + ":" + mDeviceAddress;
437         }
438 
439         @Override
enter()440         public void enter() {
441             // Remove any of the retries that are still in the queue once any device becomes
442             // connected.
443             removeMessages(RETRY_BT_CONNECTION);
444             // Remove and add to ensure that the device is at the top.
445             mMostRecentlyUsedDevices.remove(mDeviceAddress);
446             mMostRecentlyUsedDevices.add(mDeviceAddress);
447             mListener.onBluetoothAudioConnected();
448         }
449 
450         @Override
processMessage(Message msg)451         public boolean processMessage(Message msg) {
452             if (msg.what == RUN_RUNNABLE) {
453                 ((Runnable) msg.obj).run();
454                 return HANDLED;
455             }
456 
457             SomeArgs args = (SomeArgs) msg.obj;
458             String address = (String) args.arg2;
459             boolean switchingBtDevices = !Objects.equals(mDeviceAddress, address);
460             if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
461                 switchingBtDevices &= (mDeviceAddress != null);
462             }
463 
464             try {
465                 switch (msg.what) {
466                     case NEW_DEVICE_CONNECTED:
467                         addDevice(address);
468                         break;
469                     case LOST_DEVICE:
470                         removeDevice((String) args.arg2);
471                         if (Objects.equals(address, mDeviceAddress)) {
472                             transitionToActualState();
473                         }
474                         break;
475                     case CONNECT_BT:
476                         String actualAddress = null;
477                         if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
478                             Pair<String, Boolean> addressInfo = computeAddressToConnectTo(address,
479                                     switchingBtDevices, mDeviceAddress);
480                             // See if we need to transition route if the device is already
481                             // connected. If connected, another connection will not occur.
482                             addressInfo = handleDeviceAlreadyConnected(addressInfo);
483                             actualAddress = addressInfo.first;
484                             switchingBtDevices = addressInfo.second;
485                         }
486 
487                         if (!switchingBtDevices) {
488                             // Ignore connection to already connected device but still notify
489                             // CallAudioRouteStateMachine since this might be a switch from other
490                             // to this already connected BT audio
491                             mListener.onBluetoothAudioConnected();
492                             break;
493                         }
494 
495                         if (!mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
496                             actualAddress = connectBtAudioLegacy(address,
497                                     true /* switchingBtDevices*/);
498                         }
499                         boolean connected = mFeatureFlags.resolveSwitchingBtDevicesComputation()
500                                 ? connectBtAudio(actualAddress, 0, true /* switchingBtDevices*/)
501                                 : actualAddress != null;
502                         if (connected) {
503                             if (mFeatureFlags.useActualAddressToEnterConnectingState()) {
504                                 transitionTo(getConnectingStateForAddress(actualAddress,
505                                         "AudioConnected/CONNECT_BT"));
506                             } else {
507                                 transitionTo(getConnectingStateForAddress(address,
508                                         "AudioConnected/CONNECT_BT"));
509                             }
510                         } else {
511                             Log.w(LOG_TAG, "Tried to connect to %s but failed" +
512                                     " to connect to any BT device.", (String) args.arg2);
513                         }
514                         break;
515                     case DISCONNECT_BT:
516                         mDeviceManager.disconnectAudio();
517                         break;
518                     case RETRY_BT_CONNECTION:
519                         String retryAddress = null;
520                         if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
521                             Pair<String, Boolean> retryAddressInfo = computeAddressToConnectTo(
522                                     address, switchingBtDevices, mDeviceAddress);
523                             // See if we need to transition route if the device is already
524                             // connected. If connected, another connection will not occur.
525                             retryAddressInfo = handleDeviceAlreadyConnected(retryAddressInfo);
526                             retryAddress = retryAddressInfo.first;
527                             switchingBtDevices = retryAddressInfo.second;
528                         }
529 
530                         if (!switchingBtDevices) {
531                             Log.d(LOG_TAG, "Retry message came through while connected.");
532                             break;
533                         }
534 
535                         if (!mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
536                             retryAddress = connectBtAudioLegacy(address, args.argi1,
537                                     true /* switchingBtDevices*/);
538                         }
539                         boolean retrySuccessful = mFeatureFlags
540                                 .resolveSwitchingBtDevicesComputation()
541                                 ? connectBtAudio(retryAddress, args.argi1,
542                                         true /* switchingBtDevices*/)
543                                 : retryAddress != null;
544                         if (retrySuccessful) {
545                             transitionTo(getConnectingStateForAddress(retryAddress,
546                                     "AudioConnected/RETRY_BT_CONNECTION"));
547                         } else {
548                             Log.i(LOG_TAG, "Retry failed.");
549                         }
550                         break;
551                     case CONNECTION_TIMEOUT:
552                         Log.w(LOG_TAG, "Received CONNECTION_TIMEOUT while connected.");
553                         break;
554                     case BT_AUDIO_IS_ON:
555                         if (Objects.equals(mDeviceAddress, address)) {
556                             Log.i(LOG_TAG,
557                                     "Received redundant BT_AUDIO_IS_ON for %s", mDeviceAddress);
558                         } else {
559                             Log.w(LOG_TAG, "In connected state for device %s but %s" +
560                                     " is now connected", mDeviceAddress, address);
561                             transitionTo(getConnectedStateForAddress(address,
562                                     "AudioConnected/BT_AUDIO_IS_ON"));
563                         }
564                         break;
565                     case BT_AUDIO_LOST:
566                         if (Objects.equals(mDeviceAddress, address) || address == null) {
567                             Log.i(LOG_TAG, "BT connection with device %s lost.", mDeviceAddress);
568                             transitionToActualState();
569                         } else {
570                             Log.w(LOG_TAG, "Got BT lost message for device %s while" +
571                                     " connected to %s.", address, mDeviceAddress);
572                             mListener.onUnexpectedBluetoothStateChange();
573                         }
574                         break;
575                     case GET_CURRENT_STATE:
576                         BlockingQueue<IState> sink = (BlockingQueue<IState>) args.arg3;
577                         sink.offer(this);
578                         break;
579                 }
580             } finally {
581                 args.recycle();
582             }
583             return HANDLED;
584         }
585     }
586 
587     private final State mAudioOffState;
588     private final Map<String, AudioConnectingState> mAudioConnectingStates = new HashMap<>();
589     private final Map<String, AudioConnectedState> mAudioConnectedStates = new HashMap<>();
590     private final Set<State> statesToCleanUp = new HashSet<>();
591     private final LinkedHashSet<String> mMostRecentlyUsedDevices = new LinkedHashSet<>();
592 
593     private final TelecomSystem.SyncRoot mLock;
594     private final Context mContext;
595     private final Timeouts.Adapter mTimeoutsAdapter;
596 
597     private BluetoothStateListener mListener;
598     private BluetoothDeviceManager mDeviceManager;
599     // Tracks the active devices in the BT stack (HFP or hearing aid or le audio).
600     private BluetoothDevice mHfpActiveDeviceCache = null;
601     private BluetoothDevice mHearingAidActiveDeviceCache = null;
602     private BluetoothDevice mLeAudioActiveDeviceCache = null;
603     private BluetoothDevice mMostRecentlyReportedActiveDevice = null;
604     private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
605     private FeatureFlags mFeatureFlags;
606 
BluetoothRouteManager(Context context, TelecomSystem.SyncRoot lock, BluetoothDeviceManager deviceManager, Timeouts.Adapter timeoutsAdapter, CallAudioCommunicationDeviceTracker communicationDeviceTracker, FeatureFlags featureFlags)607     public BluetoothRouteManager(Context context, TelecomSystem.SyncRoot lock,
608             BluetoothDeviceManager deviceManager, Timeouts.Adapter timeoutsAdapter,
609             CallAudioCommunicationDeviceTracker communicationDeviceTracker,
610             FeatureFlags featureFlags) {
611         super(BluetoothRouteManager.class.getSimpleName());
612         mContext = context;
613         mLock = lock;
614         mDeviceManager = deviceManager;
615         mDeviceManager.setBluetoothRouteManager(this);
616         mTimeoutsAdapter = timeoutsAdapter;
617         mCommunicationDeviceTracker = communicationDeviceTracker;
618         mFeatureFlags = featureFlags;
619 
620         mAudioOffState = new AudioOffState();
621         addState(mAudioOffState);
622         setInitialState(mAudioOffState);
623         start();
624     }
625 
626     @Override
onPreHandleMessage(Message msg)627     protected void onPreHandleMessage(Message msg) {
628         if (msg.obj != null && msg.obj instanceof SomeArgs) {
629             SomeArgs args = (SomeArgs) msg.obj;
630 
631             Log.continueSession(((Session) args.arg1), "BRM.pM_" + msg.what);
632             Log.i(LOG_TAG, "%s received message: %s.", this,
633                     MESSAGE_CODE_TO_NAME.get(msg.what));
634         } else if (msg.what == RUN_RUNNABLE && msg.obj instanceof Runnable) {
635             Log.i(LOG_TAG, "Running runnable for testing");
636         } else {
637             Log.w(LOG_TAG, "Message sent must be of type nonnull SomeArgs, but got " +
638                     (msg.obj == null ? "null" : msg.obj.getClass().getSimpleName()));
639             Log.w(LOG_TAG, "The message was of code %d = %s",
640                     msg.what, MESSAGE_CODE_TO_NAME.get(msg.what));
641         }
642     }
643 
644     @Override
onPostHandleMessage(Message msg)645     protected void onPostHandleMessage(Message msg) {
646         Log.endSession();
647     }
648 
649     /**
650      * Returns whether there is a BT device available to route audio to.
651      * @return true if there is a device, false otherwise.
652      */
isBluetoothAvailable()653     public boolean isBluetoothAvailable() {
654         return mDeviceManager.getNumConnectedDevices() > 0;
655     }
656 
657     /**
658      * This method needs be synchronized with the local looper because getCurrentState() depends
659      * on the internal state of the state machine being consistent. Therefore, there may be a
660      * delay when calling this method.
661      * @return
662      */
isBluetoothAudioConnectedOrPending()663     public boolean isBluetoothAudioConnectedOrPending() {
664         SomeArgs args = SomeArgs.obtain();
665         args.arg1 = Log.createSubsession();
666         BlockingQueue<IState> stateQueue = new LinkedBlockingQueue<>();
667         // Use arg3 because arg2 is reserved for the device address
668         args.arg3 = stateQueue;
669         sendMessage(GET_CURRENT_STATE, args);
670 
671         try {
672             IState currentState = stateQueue.poll(GET_STATE_TIMEOUT, TimeUnit.MILLISECONDS);
673             if (currentState == null) {
674                 Log.w(LOG_TAG, "Failed to get a state from the state machine in time -- Handler " +
675                         "stuck?");
676                 return false;
677             }
678             return currentState != mAudioOffState;
679         } catch (InterruptedException e) {
680             Log.w(LOG_TAG, "isBluetoothAudioConnectedOrPending -- interrupted getting state");
681             return false;
682         }
683     }
684 
685     /**
686      * Attempts to connect to Bluetooth audio. If the first connection attempt synchronously
687      * fails, schedules a retry at a later time.
688      * @param address The MAC address of the bluetooth device to connect to. If null, the most
689      *                recently used device will be used.
690      */
connectBluetoothAudio(String address)691     public void connectBluetoothAudio(String address) {
692         SomeArgs args = SomeArgs.obtain();
693         args.arg1 = Log.createSubsession();
694         args.arg2 = address;
695         sendMessage(CONNECT_BT, args);
696     }
697 
698     /**
699      * Disconnects Bluetooth audio.
700      */
disconnectBluetoothAudio()701     public void disconnectBluetoothAudio() {
702         SomeArgs args = SomeArgs.obtain();
703         args.arg1 = Log.createSubsession();
704         sendMessage(DISCONNECT_BT, args);
705     }
706 
disconnectAudio()707     public void disconnectAudio() {
708         mDeviceManager.disconnectAudio();
709     }
710 
cacheHearingAidDevice()711     public void cacheHearingAidDevice() {
712         mDeviceManager.cacheHearingAidDevice();
713     }
714 
restoreHearingAidDevice()715     public void restoreHearingAidDevice() {
716         mDeviceManager.restoreHearingAidDevice();
717     }
718 
setListener(BluetoothStateListener listener)719     public void setListener(BluetoothStateListener listener) {
720         mListener = listener;
721     }
722 
onDeviceAdded(String newDeviceAddress)723     public void onDeviceAdded(String newDeviceAddress) {
724         SomeArgs args = SomeArgs.obtain();
725         args.arg1 = Log.createSubsession();
726         args.arg2 = newDeviceAddress;
727         sendMessage(NEW_DEVICE_CONNECTED, args);
728 
729         mListener.onBluetoothDeviceListChanged();
730     }
731 
onDeviceLost(String lostDeviceAddress)732     public void onDeviceLost(String lostDeviceAddress) {
733         SomeArgs args = SomeArgs.obtain();
734         args.arg1 = Log.createSubsession();
735         args.arg2 = lostDeviceAddress;
736         sendMessage(LOST_DEVICE, args);
737 
738         mListener.onBluetoothDeviceListChanged();
739     }
740 
onAudioOn(String address)741     public void onAudioOn(String address) {
742         Session session = Log.createSubsession();
743         SomeArgs args = SomeArgs.obtain();
744         args.arg1 = session;
745         args.arg2 = address;
746         sendMessage(BT_AUDIO_IS_ON, args);
747     }
748 
onAudioLost(String address)749     public void onAudioLost(String address) {
750         Session session = Log.createSubsession();
751         SomeArgs args = SomeArgs.obtain();
752         args.arg1 = session;
753         args.arg2 = address;
754         sendMessage(BT_AUDIO_LOST, args);
755     }
756 
onActiveDeviceChanged(BluetoothDevice device, int deviceType)757     public void onActiveDeviceChanged(BluetoothDevice device, int deviceType) {
758         boolean wasActiveDevicePresent = hasBtActiveDevice();
759         if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
760             mLeAudioActiveDeviceCache = device;
761             if (device == null) {
762                 if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
763                     mCommunicationDeviceTracker.clearCommunicationDevice(
764                             AudioDeviceInfo.TYPE_BLE_HEADSET);
765                 } else {
766                     mDeviceManager.clearLeAudioCommunicationDevice();
767                 }
768             }
769         } else if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID) {
770             mHearingAidActiveDeviceCache = device;
771             if (device == null) {
772                 if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) {
773                     mCommunicationDeviceTracker.clearCommunicationDevice(
774                             AudioDeviceInfo.TYPE_HEARING_AID);
775                 } else {
776                     mDeviceManager.clearHearingAidCommunicationDevice();
777                 }
778             }
779         } else if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEADSET) {
780             mHfpActiveDeviceCache = device;
781         } else {
782             return;
783         }
784 
785         if (device != null) mMostRecentlyReportedActiveDevice = device;
786 
787         boolean isActiveDevicePresent = hasBtActiveDevice();
788 
789         if (wasActiveDevicePresent && !isActiveDevicePresent) {
790             mListener.onBluetoothActiveDeviceGone();
791         } else if (!wasActiveDevicePresent && isActiveDevicePresent) {
792             mListener.onBluetoothActiveDevicePresent();
793         }
794     }
795 
getMostRecentlyReportedActiveDevice()796     public BluetoothDevice getMostRecentlyReportedActiveDevice() {
797         return mMostRecentlyReportedActiveDevice;
798     }
799 
hasBtActiveDevice()800     public boolean hasBtActiveDevice() {
801         return mLeAudioActiveDeviceCache != null ||
802                 mHearingAidActiveDeviceCache != null ||
803                 mHfpActiveDeviceCache != null;
804     }
805 
isCachedLeAudioDevice(BluetoothDevice device)806     public boolean isCachedLeAudioDevice(BluetoothDevice device) {
807         return mLeAudioActiveDeviceCache != null && mLeAudioActiveDeviceCache.equals(device);
808     }
809 
isCachedHearingAidDevice(BluetoothDevice device)810     public boolean isCachedHearingAidDevice(BluetoothDevice device) {
811         return mHearingAidActiveDeviceCache != null && mHearingAidActiveDeviceCache.equals(device);
812     }
813 
getConnectedDevices()814     public Collection<BluetoothDevice> getConnectedDevices() {
815         return mDeviceManager.getUniqueConnectedDevices();
816     }
817 
isWatch(BluetoothDevice device)818     public boolean isWatch(BluetoothDevice device) {
819         if (device == null) {
820             Log.i(this, "isWatch: device is null. Returning false");
821             return false;
822         }
823 
824         BluetoothClass deviceClass = device.getBluetoothClass();
825         if (deviceClass != null && deviceClass.getDeviceClass()
826                 == BluetoothClass.Device.WEARABLE_WRIST_WATCH) {
827             Log.i(this, "isWatch: bluetooth class component is a WEARABLE_WRIST_WATCH.");
828             return true;
829         }
830 
831         // Check metadata
832         byte[] deviceType = device.getMetadata(BluetoothDevice.METADATA_DEVICE_TYPE);
833         if (deviceType == null) {
834             return false;
835         }
836         String deviceTypeStr = new String(deviceType);
837         if (deviceTypeStr.equals(BluetoothDevice.DEVICE_TYPE_WATCH)) {
838             Log.i(this, "isWatch: bluetooth device type is DEVICE_TYPE_WATCH.");
839             return true;
840         }
841 
842         return false;
843     }
844 
845     /**
846      * Determines the address that should be used for the connection attempt. In the case that the
847      * specified address to be used is null, Telecom will try to find an arbitrary address to
848      * connect instead.
849      *
850      * @param address The address that should be prioritized for the connection attempt
851      * @param switchingBtDevices Used when there is existing audio connection to other Bt device.
852      * @param stateAddress The address stored in the state that indicates the connecting/connected
853      *                     device.
854      * @return {@link Pair} containing the address to connect to and whether an existing BT audio
855      *                      connection for a different device exists.
856      */
computeAddressToConnectTo( String address, boolean switchingBtDevices, String stateAddress)857     private Pair<String, Boolean> computeAddressToConnectTo(
858             String address, boolean switchingBtDevices, String stateAddress) {
859         Collection<BluetoothDevice> deviceList = mDeviceManager.getConnectedDevices();
860         Optional<BluetoothDevice> matchingDevice = deviceList.stream()
861                 .filter(d -> Objects.equals(d.getAddress(), address))
862                 .findAny();
863 
864         String actualAddress = matchingDevice.isPresent()
865                 ? address : getActiveDeviceAddress();
866         if (actualAddress == null) {
867             Log.i(this, "No device specified and BT stack has no active device."
868                     + " Using arbitrary device - except watch");
869             if (deviceList.size() > 0) {
870                 for (BluetoothDevice device : deviceList) {
871                     if (mFeatureFlags.ignoreAutoRouteToWatchDevice() && isWatch(device)) {
872                         Log.i(this, "Skipping a watch device: " + device);
873                         continue;
874                     }
875                     actualAddress = device.getAddress();
876                     break;
877                 }
878             }
879 
880             if (actualAddress == null) {
881                 Log.i(this, "No devices available at all. Not connecting.");
882                 return new Pair<>(null, false);
883             }
884             if (switchingBtDevices && actualAddress.equals(stateAddress)) {
885                 switchingBtDevices = false;
886             }
887         }
888         if (!matchingDevice.isPresent()) {
889             Log.i(this, "No device with address %s available. Using %s instead.",
890                     address, actualAddress);
891         }
892         return new Pair<>(actualAddress, switchingBtDevices);
893     }
894 
895     /**
896      * Handles route switching to the connected state for a device. This currently handles the case
897      * for hearing aids when the route manager reports AudioOff since Telecom doesn't treat HA as
898      * the active device outside of a call.
899      *
900      * @param addressInfo A {@link Pair} containing the BT address to connect to as well as if we're
901      *                    handling a switch of BT devices.
902      * @return {@link Pair} indicating the address to connect to as well as if we're handling a
903      *                      switch of BT devices. If the device is already connected, then the
904      *                      return value will be {null, false} to indicate that a connection attempt
905      *                      is not required.
906      */
handleDeviceAlreadyConnected(Pair<String, Boolean> addressInfo)907     private Pair<String, Boolean> handleDeviceAlreadyConnected(Pair<String, Boolean> addressInfo) {
908         String address = addressInfo.first;
909         BluetoothDevice alreadyConnectedDevice = getBluetoothAudioConnectedDevice();
910         if (alreadyConnectedDevice != null && alreadyConnectedDevice.getAddress().equals(
911                 address)) {
912             Log.i(this, "trying to connect to already connected device -- skipping connection"
913                     + " and going into the actual connected state.");
914             transitionToActualState();
915             return new Pair<>(null, false);
916         }
917         return addressInfo;
918     }
919 
920     /**
921      * Initiates a connection to the BT address specified.
922      * Note: This method is not synchronized on the Telecom lock, so don't try and call back into
923      * Telecom from within it.
924      * @param address The address that should be tried first. May be null.
925      * @param retryCount The number of times this connection attempt has been retried.
926      * @param switchingBtDevices Used when there is existing audio connection to other Bt device.
927      * @return {@code true} if the connection to the address was successful, otherwise {@code false}
928      *          if the connection fails.
929      *
930      * Note: This should only be used in par with the resolveSwitchingBtDevicesComputation flag.
931      */
connectBtAudio(String address, int retryCount, boolean switchingBtDevices)932     private boolean connectBtAudio(String address, int retryCount, boolean switchingBtDevices) {
933         if (address == null) {
934             return false;
935         }
936 
937         if (switchingBtDevices) {
938             /* When new Bluetooth connects audio, make sure previous one has disconnected audio. */
939             mDeviceManager.disconnectAudio();
940         }
941 
942         if (!mDeviceManager.connectAudio(address, switchingBtDevices)) {
943             boolean shouldRetry = retryCount < MAX_CONNECTION_RETRIES;
944             Log.w(LOG_TAG, "Could not connect to %s. Will %s", address,
945                     shouldRetry ? "retry" : "not retry");
946             if (shouldRetry) {
947                 SomeArgs args = SomeArgs.obtain();
948                 args.arg1 = Log.createSubsession();
949                 args.arg2 = address;
950                 args.argi1 = retryCount + 1;
951                 sendMessageDelayed(RETRY_BT_CONNECTION, args,
952                         mTimeoutsAdapter.getRetryBluetoothConnectAudioBackoffMillis(
953                                 mContext.getContentResolver()));
954             }
955             return false;
956         }
957 
958         return true;
959     }
960 
961     private String connectBtAudioLegacy(String address, boolean switchingBtDevices) {
962         return connectBtAudioLegacy(address, 0, switchingBtDevices);
963     }
964 
965     /**
966      * Initiates a connection to the BT address specified.
967      * Note: This method is not synchronized on the Telecom lock, so don't try and call back into
968      * Telecom from within it.
969      * @param address The address that should be tried first. May be null.
970      * @param retryCount The number of times this connection attempt has been retried.
971      * @param switchingBtDevices Used when there is existing audio connection to other Bt device.
972      * @return The address of the device that's actually being connected to, or null if no
973      * connection was successful.
974      */
975     private String connectBtAudioLegacy(String address, int retryCount,
976             boolean switchingBtDevices) {
977         Collection<BluetoothDevice> deviceList = mDeviceManager.getConnectedDevices();
978         Optional<BluetoothDevice> matchingDevice = deviceList.stream()
979                 .filter(d -> Objects.equals(d.getAddress(), address))
980                 .findAny();
981 
982         if (switchingBtDevices) {
983             /* When new Bluetooth connects audio, make sure previous one has disconnected audio. */
984             mDeviceManager.disconnectAudio();
985         }
986 
987         String actualAddress = matchingDevice.isPresent()
988                 ? address : getActiveDeviceAddress();
989         if (actualAddress == null) {
990             Log.i(this, "No device specified and BT stack has no active device."
991                     + " Using arbitrary device - except watch");
992             if (deviceList.size() > 0) {
993                 for (BluetoothDevice device : deviceList) {
994                     if (mFeatureFlags.ignoreAutoRouteToWatchDevice() && isWatch(device)) {
995                         Log.i(this, "Skipping a watch device: " + device);
996                         continue;
997                     }
998                     actualAddress = device.getAddress();
999                     break;
1000                 }
1001             }
1002 
1003             if (actualAddress == null) {
1004                 Log.i(this, "No devices available at all. Not connecting.");
1005                 return null;
1006             }
1007         }
1008         if (!matchingDevice.isPresent()) {
1009             Log.i(this, "No device with address %s available. Using %s instead.",
1010                     address, actualAddress);
1011         }
1012 
1013         BluetoothDevice alreadyConnectedDevice = getBluetoothAudioConnectedDevice();
1014         if (alreadyConnectedDevice != null && alreadyConnectedDevice.getAddress().equals(
1015                 actualAddress)) {
1016             Log.i(this, "trying to connect to already connected device -- skipping connection"
1017                     + " and going into the actual connected state.");
1018             transitionToActualState();
1019             return null;
1020         }
1021 
1022         if (!mDeviceManager.connectAudio(actualAddress, switchingBtDevices)) {
1023             boolean shouldRetry = retryCount < MAX_CONNECTION_RETRIES;
1024             Log.w(LOG_TAG, "Could not connect to %s. Will %s", actualAddress,
1025                     shouldRetry ? "retry" : "not retry");
1026             if (shouldRetry) {
1027                 SomeArgs args = SomeArgs.obtain();
1028                 args.arg1 = Log.createSubsession();
1029                 args.arg2 = actualAddress;
1030                 args.argi1 = retryCount + 1;
1031                 sendMessageDelayed(RETRY_BT_CONNECTION, args,
1032                         mTimeoutsAdapter.getRetryBluetoothConnectAudioBackoffMillis(
1033                                 mContext.getContentResolver()));
1034             }
1035             return null;
1036         }
1037 
1038         return actualAddress;
1039     }
1040 
1041     private String getActiveDeviceAddress() {
1042         if (mHfpActiveDeviceCache != null) {
1043             return mHfpActiveDeviceCache.getAddress();
1044         }
1045         if (mHearingAidActiveDeviceCache != null) {
1046             return mHearingAidActiveDeviceCache.getAddress();
1047         }
1048         if (mLeAudioActiveDeviceCache != null) {
1049             return mLeAudioActiveDeviceCache.getAddress();
1050         }
1051         return null;
1052     }
1053 
1054     private void transitionToActualState() {
1055         BluetoothDevice possiblyAlreadyConnectedDevice = getBluetoothAudioConnectedDevice();
1056         if (possiblyAlreadyConnectedDevice != null) {
1057             Log.i(LOG_TAG, "Device %s is already connected; going to AudioConnected.",
1058                     possiblyAlreadyConnectedDevice);
1059             transitionTo(getConnectedStateForAddress(
1060                     possiblyAlreadyConnectedDevice.getAddress(), "transitionToActualState"));
1061         } else {
1062             transitionTo(mAudioOffState);
1063         }
1064     }
1065 
1066     /**
1067      * @return The BluetoothDevice that is connected to BT audio, null if none are connected.
1068      */
1069     @VisibleForTesting
1070     public BluetoothDevice getBluetoothAudioConnectedDevice() {
1071         BluetoothAdapter bluetoothAdapter = mDeviceManager.getBluetoothAdapter();
1072         BluetoothHeadset bluetoothHeadset = mDeviceManager.getBluetoothHeadset();
1073         BluetoothHearingAid bluetoothHearingAid = mDeviceManager.getBluetoothHearingAid();
1074         BluetoothLeAudio bluetoothLeAudio = mDeviceManager.getLeAudioService();
1075 
1076         BluetoothDevice hfpAudioOnDevice = null;
1077         BluetoothDevice hearingAidActiveDevice = null;
1078         BluetoothDevice leAudioActiveDevice = null;
1079 
1080         if (bluetoothAdapter == null) {
1081             Log.i(this, "getBluetoothAudioConnectedDevice: no adapter available.");
1082             return null;
1083         }
1084         if (bluetoothHeadset == null && bluetoothHearingAid == null && bluetoothLeAudio == null) {
1085             Log.i(this, "getBluetoothAudioConnectedDevice: no service available.");
1086             return null;
1087         }
1088 
1089         int activeDevices = 0;
1090         if (bluetoothHeadset != null) {
1091             for (BluetoothDevice device : bluetoothAdapter.getActiveDevices(
1092                         BluetoothProfile.HEADSET)) {
1093                 hfpAudioOnDevice = device;
1094                 break;
1095             }
1096 
1097             if (hfpAudioOnDevice != null && bluetoothHeadset.getAudioState(hfpAudioOnDevice)
1098                     == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
1099                 hfpAudioOnDevice = null;
1100             } else {
1101                 activeDevices++;
1102             }
1103         }
1104 
1105         boolean isHearingAidSetForCommunication =
1106                 mFeatureFlags.callAudioCommunicationDeviceRefactor()
1107                 ? mCommunicationDeviceTracker.isAudioDeviceSetForType(
1108                         AudioDeviceInfo.TYPE_HEARING_AID)
1109                 : mDeviceManager.isHearingAidSetAsCommunicationDevice();
1110         if (bluetoothHearingAid != null) {
1111             if (isHearingAidSetForCommunication) {
1112                 List<BluetoothDevice> hearingAidsActiveDevices = bluetoothAdapter.getActiveDevices(
1113                         BluetoothProfile.HEARING_AID);
1114                 if (hearingAidsActiveDevices.contains(mHearingAidActiveDeviceCache)) {
1115                     hearingAidActiveDevice = mHearingAidActiveDeviceCache;
1116                     activeDevices++;
1117                 } else {
1118                     for (BluetoothDevice device : hearingAidsActiveDevices) {
1119                         if (device != null) {
1120                             hearingAidActiveDevice = device;
1121                             activeDevices++;
1122                             break;
1123                         }
1124                     }
1125                 }
1126             }
1127         }
1128 
1129         boolean isLeAudioSetForCommunication =
1130                 mFeatureFlags.callAudioCommunicationDeviceRefactor()
1131                         ? mCommunicationDeviceTracker.isAudioDeviceSetForType(
1132                         AudioDeviceInfo.TYPE_BLE_HEADSET)
1133                         : mDeviceManager.isLeAudioCommunicationDevice();
1134         if (bluetoothLeAudio != null) {
1135             if (isLeAudioSetForCommunication) {
1136                 for (BluetoothDevice device : bluetoothAdapter.getActiveDevices(
1137                         BluetoothProfile.LE_AUDIO)) {
1138                     if (device != null) {
1139                         leAudioActiveDevice = device;
1140                         activeDevices++;
1141                         break;
1142                     }
1143                 }
1144             }
1145         }
1146 
1147         // Return the active device reported by either HFP, hearing aid or le audio. If more than
1148         // one is reporting active devices, go with the most recent one as reported by the receiver.
1149         if (activeDevices > 1) {
1150             Log.i(this, "More than one profile reporting active devices. Going with the most"
1151                     + " recently reported active device: %s", mMostRecentlyReportedActiveDevice);
1152             return mMostRecentlyReportedActiveDevice;
1153         }
1154 
1155         if (leAudioActiveDevice != null) {
1156             return leAudioActiveDevice;
1157         }
1158 
1159         if (hearingAidActiveDevice != null) {
1160             return hearingAidActiveDevice;
1161         }
1162 
1163         return hfpAudioOnDevice;
1164     }
1165 
1166     /**
1167      * Check if in-band ringing is currently enabled. In-band ringing could be disabled during an
1168      * active connection.
1169      *
1170      * @return true if in-band ringing is enabled, false if in-band ringing is disabled
1171      */
1172     @VisibleForTesting
isInbandRingingEnabled()1173     public boolean isInbandRingingEnabled() {
1174         return mDeviceManager.isInbandRingingEnabled();
1175     }
1176 
1177     @VisibleForTesting
isInbandRingEnabled(BluetoothDevice bluetoothDevice)1178     public boolean isInbandRingEnabled(BluetoothDevice bluetoothDevice) {
1179         return mDeviceManager.isInbandRingEnabled(bluetoothDevice);
1180     }
1181 
addDevice(String address)1182     private boolean addDevice(String address) {
1183         if (mAudioConnectingStates.containsKey(address)) {
1184             Log.i(this, "Attempting to add device %s twice.", address);
1185             return false;
1186         }
1187         AudioConnectedState audioConnectedState = new AudioConnectedState(address);
1188         AudioConnectingState audioConnectingState = new AudioConnectingState(address);
1189         mAudioConnectingStates.put(address, audioConnectingState);
1190         mAudioConnectedStates.put(address, audioConnectedState);
1191         addState(audioConnectedState);
1192         addState(audioConnectingState);
1193         return true;
1194     }
1195 
removeDevice(String address)1196     private boolean removeDevice(String address) {
1197         if (!mAudioConnectingStates.containsKey(address)) {
1198             Log.i(this, "Attempting to remove already-removed device %s", address);
1199             return false;
1200         }
1201         statesToCleanUp.add(mAudioConnectingStates.remove(address));
1202         statesToCleanUp.add(mAudioConnectedStates.remove(address));
1203         mMostRecentlyUsedDevices.remove(address);
1204         return true;
1205     }
1206 
getConnectingStateForAddress(String address, String error)1207     private AudioConnectingState getConnectingStateForAddress(String address, String error) {
1208         if (!mAudioConnectingStates.containsKey(address)) {
1209             Log.w(LOG_TAG, "Device being connected to does not have a corresponding state: %s",
1210                     error);
1211             addDevice(address);
1212         }
1213         return mAudioConnectingStates.get(address);
1214     }
1215 
getConnectedStateForAddress(String address, String error)1216     private AudioConnectedState getConnectedStateForAddress(String address, String error) {
1217         if (!mAudioConnectedStates.containsKey(address)) {
1218             Log.w(LOG_TAG, "Device already connected to does" +
1219                     " not have a corresponding state: %s", error);
1220             addDevice(address);
1221         }
1222         return mAudioConnectedStates.get(address);
1223     }
1224 
1225     /**
1226      * Removes the states for disconnected devices from the state machine. Called when entering
1227      * AudioOff so that none of the states-to-be-removed are active.
1228      */
cleanupStatesForDisconnectedDevices()1229     private void cleanupStatesForDisconnectedDevices() {
1230         for (State state : statesToCleanUp) {
1231             if (state != null) {
1232                 removeState(state);
1233             }
1234         }
1235         statesToCleanUp.clear();
1236     }
1237 
1238     @VisibleForTesting
setInitialStateForTesting(String stateName, BluetoothDevice device)1239     public void setInitialStateForTesting(String stateName, BluetoothDevice device) {
1240         sendMessage(RUN_RUNNABLE, (Runnable) () -> {
1241             switch (stateName) {
1242                 case AUDIO_OFF_STATE_NAME:
1243                     transitionTo(mAudioOffState);
1244                     break;
1245                 case AUDIO_CONNECTING_STATE_NAME_PREFIX:
1246                     transitionTo(getConnectingStateForAddress(device.getAddress(),
1247                             "setInitialStateForTesting"));
1248                     break;
1249                 case AUDIO_CONNECTED_STATE_NAME_PREFIX:
1250                     transitionTo(getConnectedStateForAddress(device.getAddress(),
1251                             "setInitialStateForTesting"));
1252                     break;
1253             }
1254             Log.i(LOG_TAG, "transition for testing done: %s", stateName);
1255         });
1256     }
1257 
1258     @VisibleForTesting
setActiveDeviceCacheForTesting(BluetoothDevice device, int deviceType)1259     public void setActiveDeviceCacheForTesting(BluetoothDevice device, int deviceType) {
1260         if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
1261           mLeAudioActiveDeviceCache = device;
1262         } else if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID) {
1263             mHearingAidActiveDeviceCache = device;
1264         } else if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEADSET) {
1265             mHfpActiveDeviceCache = device;
1266         }
1267     }
1268 
getDeviceManager()1269     public BluetoothDeviceManager getDeviceManager() {
1270         return mDeviceManager;
1271     }
1272 }
1273