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