• 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.BluetoothDevice;
21 import android.bluetooth.BluetoothHeadset;
22 import android.bluetooth.BluetoothHearingAid;
23 import android.bluetooth.BluetoothProfile;
24 import android.bluetooth.BluetoothLeAudio;
25 import android.content.Context;
26 import android.os.Message;
27 import android.telecom.Log;
28 import android.telecom.Logging.Session;
29 import android.util.SparseArray;
30 
31 import com.android.internal.annotations.VisibleForTesting;
32 import com.android.internal.os.SomeArgs;
33 import com.android.internal.util.IState;
34 import com.android.internal.util.State;
35 import com.android.internal.util.StateMachine;
36 import com.android.server.telecom.TelecomSystem;
37 import com.android.server.telecom.Timeouts;
38 
39 import java.util.ArrayList;
40 import java.util.Collection;
41 import java.util.HashMap;
42 import java.util.HashSet;
43 import java.util.LinkedHashSet;
44 import java.util.List;
45 import java.util.Map;
46 import java.util.Objects;
47 import java.util.Optional;
48 import java.util.Set;
49 import java.util.concurrent.BlockingQueue;
50 import java.util.concurrent.LinkedBlockingQueue;
51 import java.util.concurrent.TimeUnit;
52 
53 public class BluetoothRouteManager extends StateMachine {
54     private static final String LOG_TAG = BluetoothRouteManager.class.getSimpleName();
55 
56     private static final SparseArray<String> MESSAGE_CODE_TO_NAME = new SparseArray<String>() {{
57          put(NEW_DEVICE_CONNECTED, "NEW_DEVICE_CONNECTED");
58          put(LOST_DEVICE, "LOST_DEVICE");
59          put(CONNECT_BT, "CONNECT_BT");
60          put(DISCONNECT_BT, "DISCONNECT_BT");
61          put(RETRY_BT_CONNECTION, "RETRY_BT_CONNECTION");
62          put(BT_AUDIO_IS_ON, "BT_AUDIO_IS_ON");
63          put(BT_AUDIO_LOST, "BT_AUDIO_LOST");
64          put(CONNECTION_TIMEOUT, "CONNECTION_TIMEOUT");
65          put(GET_CURRENT_STATE, "GET_CURRENT_STATE");
66          put(RUN_RUNNABLE, "RUN_RUNNABLE");
67     }};
68 
69     public static final String AUDIO_OFF_STATE_NAME = "AudioOff";
70     public static final String AUDIO_CONNECTING_STATE_NAME_PREFIX = "Connecting";
71     public static final String AUDIO_CONNECTED_STATE_NAME_PREFIX = "Connected";
72 
73     // Timeout for querying the current state from the state machine handler.
74     private static final int GET_STATE_TIMEOUT = 1000;
75 
76     public interface BluetoothStateListener {
onBluetoothDeviceListChanged()77         void onBluetoothDeviceListChanged();
onBluetoothActiveDevicePresent()78         void onBluetoothActiveDevicePresent();
onBluetoothActiveDeviceGone()79         void onBluetoothActiveDeviceGone();
onBluetoothAudioConnected()80         void onBluetoothAudioConnected();
onBluetoothAudioConnecting()81         void onBluetoothAudioConnecting();
onBluetoothAudioDisconnected()82         void onBluetoothAudioDisconnected();
83         /**
84          * This gets called when we get an unexpected state change from Bluetooth. Their stack does
85          * weird things sometimes, so this is really a signal for the listener to refresh their
86          * internal state and make sure it matches up with what the BT stack is doing.
87          */
onUnexpectedBluetoothStateChange()88         void onUnexpectedBluetoothStateChange();
89     }
90 
91     /**
92      * Constants representing messages sent to the state machine.
93      * Messages are expected to be sent with {@link SomeArgs} as the obj.
94      * In all cases, arg1 will be the log session.
95      */
96     // arg2: Address of the new device
97     public static final int NEW_DEVICE_CONNECTED = 1;
98     // arg2: Address of the lost device
99     public static final int LOST_DEVICE = 2;
100 
101     // arg2 (optional): the address of the specific device to connect to.
102     public static final int CONNECT_BT = 100;
103     // No args.
104     public static final int DISCONNECT_BT = 101;
105     // arg2: the address of the device to connect to.
106     public static final int RETRY_BT_CONNECTION = 102;
107 
108     // arg2: the address of the device that is on
109     public static final int BT_AUDIO_IS_ON = 200;
110     // arg2: the address of the device that lost BT audio
111     public static final int BT_AUDIO_LOST = 201;
112 
113     // No args; only used internally
114     public static final int CONNECTION_TIMEOUT = 300;
115 
116     // Get the current state and send it through the BlockingQueue<IState> provided as the object
117     // arg.
118     public static final int GET_CURRENT_STATE = 400;
119 
120     // arg2: Runnable
121     public static final int RUN_RUNNABLE = 9001;
122 
123     private static final int MAX_CONNECTION_RETRIES = 2;
124 
125     // States
126     private final class AudioOffState extends State {
127         @Override
getName()128         public String getName() {
129             return AUDIO_OFF_STATE_NAME;
130         }
131 
132         @Override
enter()133         public void enter() {
134             BluetoothDevice erroneouslyConnectedDevice = getBluetoothAudioConnectedDevice();
135             if (erroneouslyConnectedDevice != null) {
136                 Log.w(LOG_TAG, "Entering AudioOff state but device %s appears to be connected. " +
137                         "Switching to audio-on state for that device.", erroneouslyConnectedDevice);
138                 // change this to just transition to the new audio on state
139                 transitionToActualState();
140             }
141             cleanupStatesForDisconnectedDevices();
142             if (mListener != null) {
143                 mListener.onBluetoothAudioDisconnected();
144             }
145         }
146 
147         @Override
processMessage(Message msg)148         public boolean processMessage(Message msg) {
149             if (msg.what == RUN_RUNNABLE) {
150                 ((Runnable) msg.obj).run();
151                 return HANDLED;
152             }
153 
154             SomeArgs args = (SomeArgs) msg.obj;
155             try {
156                 switch (msg.what) {
157                     case NEW_DEVICE_CONNECTED:
158                         addDevice((String) args.arg2);
159                         break;
160                     case LOST_DEVICE:
161                         removeDevice((String) args.arg2);
162                         break;
163                     case CONNECT_BT:
164                         String actualAddress = connectBtAudio((String) args.arg2,
165                             false /* switchingBtDevices*/);
166 
167                         if (actualAddress != null) {
168                             transitionTo(getConnectingStateForAddress(actualAddress,
169                                     "AudioOff/CONNECT_BT"));
170                         } else {
171                             Log.w(LOG_TAG, "Tried to connect to %s but failed to connect to" +
172                                     " any BT device.", (String) args.arg2);
173                         }
174                         break;
175                     case DISCONNECT_BT:
176                         // Ignore.
177                         break;
178                     case RETRY_BT_CONNECTION:
179                         Log.i(LOG_TAG, "Retrying BT connection to %s", (String) args.arg2);
180                         String retryAddress = connectBtAudio((String) args.arg2, args.argi1,
181                             false /* switchingBtDevices*/);
182 
183                         if (retryAddress != null) {
184                             transitionTo(getConnectingStateForAddress(retryAddress,
185                                     "AudioOff/RETRY_BT_CONNECTION"));
186                         } else {
187                             Log.i(LOG_TAG, "Retry failed.");
188                         }
189                         break;
190                     case CONNECTION_TIMEOUT:
191                         // Ignore.
192                         break;
193                     case BT_AUDIO_IS_ON:
194                         String address = (String) args.arg2;
195                         Log.w(LOG_TAG, "BT audio unexpectedly turned on from device %s", address);
196                         transitionTo(getConnectedStateForAddress(address,
197                                 "AudioOff/BT_AUDIO_IS_ON"));
198                         break;
199                     case BT_AUDIO_LOST:
200                         Log.i(LOG_TAG, "Received BT off for device %s while BT off.",
201                                 (String) args.arg2);
202                         mListener.onUnexpectedBluetoothStateChange();
203                         break;
204                     case GET_CURRENT_STATE:
205                         BlockingQueue<IState> sink = (BlockingQueue<IState>) args.arg3;
206                         sink.offer(this);
207                         break;
208                 }
209             } finally {
210                 args.recycle();
211             }
212             return HANDLED;
213         }
214     }
215 
216     private final class AudioConnectingState extends State {
217         private final String mDeviceAddress;
218 
AudioConnectingState(String address)219         AudioConnectingState(String address) {
220             mDeviceAddress = address;
221         }
222 
223         @Override
getName()224         public String getName() {
225             return AUDIO_CONNECTING_STATE_NAME_PREFIX + ":" + mDeviceAddress;
226         }
227 
228         @Override
enter()229         public void enter() {
230             SomeArgs args = SomeArgs.obtain();
231             args.arg1 = Log.createSubsession();
232             sendMessageDelayed(CONNECTION_TIMEOUT, args,
233                     mTimeoutsAdapter.getBluetoothPendingTimeoutMillis(
234                             mContext.getContentResolver()));
235             mListener.onBluetoothAudioConnecting();
236         }
237 
238         @Override
exit()239         public void exit() {
240             removeMessages(CONNECTION_TIMEOUT);
241         }
242 
243         @Override
processMessage(Message msg)244         public boolean processMessage(Message msg) {
245             if (msg.what == RUN_RUNNABLE) {
246                 ((Runnable) msg.obj).run();
247                 return HANDLED;
248             }
249 
250             SomeArgs args = (SomeArgs) msg.obj;
251             String address = (String) args.arg2;
252             boolean switchingBtDevices = !Objects.equals(mDeviceAddress, address);
253             try {
254                 switch (msg.what) {
255                     case NEW_DEVICE_CONNECTED:
256                         // If the device isn't new, don't bother passing it up.
257                         addDevice(address);
258                         break;
259                     case LOST_DEVICE:
260                         removeDevice((String) args.arg2);
261                         if (Objects.equals(address, mDeviceAddress)) {
262                             transitionToActualState();
263                         }
264                         break;
265                     case CONNECT_BT:
266                         if (!switchingBtDevices) {
267                             // Ignore repeated connection attempts to the same device
268                             break;
269                         }
270 
271                         String actualAddress = connectBtAudio(address,
272                             true /* switchingBtDevices*/);
273                         if (actualAddress != null) {
274                             transitionTo(getConnectingStateForAddress(actualAddress,
275                                     "AudioConnecting/CONNECT_BT"));
276                         } else {
277                             Log.w(LOG_TAG, "Tried to connect to %s but failed" +
278                                     " to connect to any BT device.", (String) args.arg2);
279                         }
280                         break;
281                     case DISCONNECT_BT:
282                         mDeviceManager.disconnectAudio();
283                         break;
284                     case RETRY_BT_CONNECTION:
285                         if (!switchingBtDevices) {
286                             Log.d(LOG_TAG, "Retry message came through while connecting.");
287                             break;
288                         }
289 
290                         String retryAddress = connectBtAudio(address, args.argi1,
291                             true /* switchingBtDevices*/);
292                         if (retryAddress != null) {
293                             transitionTo(getConnectingStateForAddress(retryAddress,
294                                     "AudioConnecting/RETRY_BT_CONNECTION"));
295                         } else {
296                             Log.i(LOG_TAG, "Retry failed.");
297                         }
298                         break;
299                     case CONNECTION_TIMEOUT:
300                         Log.i(LOG_TAG, "Connection with device %s timed out.",
301                                 mDeviceAddress);
302                         transitionToActualState();
303                         break;
304                     case BT_AUDIO_IS_ON:
305                         if (Objects.equals(mDeviceAddress, address)) {
306                             Log.i(LOG_TAG, "BT connection success for device %s.", mDeviceAddress);
307                             transitionTo(mAudioConnectedStates.get(mDeviceAddress));
308                         } else {
309                             Log.w(LOG_TAG, "In connecting state for device %s but %s" +
310                                     " is now connected", mDeviceAddress, address);
311                             transitionTo(getConnectedStateForAddress(address,
312                                     "AudioConnecting/BT_AUDIO_IS_ON"));
313                         }
314                         break;
315                     case BT_AUDIO_LOST:
316                         if (Objects.equals(mDeviceAddress, address) || address == null) {
317                             Log.i(LOG_TAG, "Connection with device %s failed.",
318                                     mDeviceAddress);
319                             transitionToActualState();
320                         } else {
321                             Log.w(LOG_TAG, "Got BT lost message for device %s while" +
322                                     " connecting to %s.", address, mDeviceAddress);
323                             mListener.onUnexpectedBluetoothStateChange();
324                         }
325                         break;
326                     case GET_CURRENT_STATE:
327                         BlockingQueue<IState> sink = (BlockingQueue<IState>) args.arg3;
328                         sink.offer(this);
329                         break;
330                 }
331             } finally {
332                 args.recycle();
333             }
334             return HANDLED;
335         }
336     }
337 
338     private final class AudioConnectedState extends State {
339         private final String mDeviceAddress;
340 
AudioConnectedState(String address)341         AudioConnectedState(String address) {
342             mDeviceAddress = address;
343         }
344 
345         @Override
getName()346         public String getName() {
347             return AUDIO_CONNECTED_STATE_NAME_PREFIX + ":" + mDeviceAddress;
348         }
349 
350         @Override
enter()351         public void enter() {
352             // Remove any of the retries that are still in the queue once any device becomes
353             // connected.
354             removeMessages(RETRY_BT_CONNECTION);
355             // Remove and add to ensure that the device is at the top.
356             mMostRecentlyUsedDevices.remove(mDeviceAddress);
357             mMostRecentlyUsedDevices.add(mDeviceAddress);
358             mListener.onBluetoothAudioConnected();
359         }
360 
361         @Override
processMessage(Message msg)362         public boolean processMessage(Message msg) {
363             if (msg.what == RUN_RUNNABLE) {
364                 ((Runnable) msg.obj).run();
365                 return HANDLED;
366             }
367 
368             SomeArgs args = (SomeArgs) msg.obj;
369             String address = (String) args.arg2;
370             boolean switchingBtDevices = !Objects.equals(mDeviceAddress, address);
371             try {
372                 switch (msg.what) {
373                     case NEW_DEVICE_CONNECTED:
374                         addDevice(address);
375                         break;
376                     case LOST_DEVICE:
377                         removeDevice((String) args.arg2);
378                         if (Objects.equals(address, mDeviceAddress)) {
379                             transitionToActualState();
380                         }
381                         break;
382                     case CONNECT_BT:
383                         if (!switchingBtDevices) {
384                             // Ignore connection to already connected device but still notify
385                             // CallAudioRouteStateMachine since this might be a switch from other
386                             // to this already connected BT audio
387                             mListener.onBluetoothAudioConnected();
388                             break;
389                         }
390 
391                         String actualAddress = connectBtAudio(address,
392                             true /* switchingBtDevices*/);
393                         if (actualAddress != null) {
394                             transitionTo(getConnectingStateForAddress(address,
395                                     "AudioConnected/CONNECT_BT"));
396                         } else {
397                             Log.w(LOG_TAG, "Tried to connect to %s but failed" +
398                                     " to connect to any BT device.", (String) args.arg2);
399                         }
400                         break;
401                     case DISCONNECT_BT:
402                         mDeviceManager.disconnectAudio();
403                         break;
404                     case RETRY_BT_CONNECTION:
405                         if (!switchingBtDevices) {
406                             Log.d(LOG_TAG, "Retry message came through while connected.");
407                             break;
408                         }
409 
410                         String retryAddress = connectBtAudio(address, args.argi1,
411                             true /* switchingBtDevices*/);
412                         if (retryAddress != null) {
413                             transitionTo(getConnectingStateForAddress(retryAddress,
414                                     "AudioConnected/RETRY_BT_CONNECTION"));
415                         } else {
416                             Log.i(LOG_TAG, "Retry failed.");
417                         }
418                         break;
419                     case CONNECTION_TIMEOUT:
420                         Log.w(LOG_TAG, "Received CONNECTION_TIMEOUT while connected.");
421                         break;
422                     case BT_AUDIO_IS_ON:
423                         if (Objects.equals(mDeviceAddress, address)) {
424                             Log.i(LOG_TAG,
425                                     "Received redundant BT_AUDIO_IS_ON for %s", mDeviceAddress);
426                         } else {
427                             Log.w(LOG_TAG, "In connected state for device %s but %s" +
428                                     " is now connected", mDeviceAddress, address);
429                             transitionTo(getConnectedStateForAddress(address,
430                                     "AudioConnected/BT_AUDIO_IS_ON"));
431                         }
432                         break;
433                     case BT_AUDIO_LOST:
434                         if (Objects.equals(mDeviceAddress, address) || address == null) {
435                             Log.i(LOG_TAG, "BT connection with device %s lost.", mDeviceAddress);
436                             transitionToActualState();
437                         } else {
438                             Log.w(LOG_TAG, "Got BT lost message for device %s while" +
439                                     " connected to %s.", address, mDeviceAddress);
440                             mListener.onUnexpectedBluetoothStateChange();
441                         }
442                         break;
443                     case GET_CURRENT_STATE:
444                         BlockingQueue<IState> sink = (BlockingQueue<IState>) args.arg3;
445                         sink.offer(this);
446                         break;
447                 }
448             } finally {
449                 args.recycle();
450             }
451             return HANDLED;
452         }
453     }
454 
455     private final State mAudioOffState;
456     private final Map<String, AudioConnectingState> mAudioConnectingStates = new HashMap<>();
457     private final Map<String, AudioConnectedState> mAudioConnectedStates = new HashMap<>();
458     private final Set<State> statesToCleanUp = new HashSet<>();
459     private final LinkedHashSet<String> mMostRecentlyUsedDevices = new LinkedHashSet<>();
460 
461     private final TelecomSystem.SyncRoot mLock;
462     private final Context mContext;
463     private final Timeouts.Adapter mTimeoutsAdapter;
464 
465     private BluetoothStateListener mListener;
466     private BluetoothDeviceManager mDeviceManager;
467     // Tracks the active devices in the BT stack (HFP or hearing aid or le audio).
468     private BluetoothDevice mHfpActiveDeviceCache = null;
469     private BluetoothDevice mHearingAidActiveDeviceCache = null;
470     private BluetoothDevice mLeAudioActiveDeviceCache = null;
471     private BluetoothDevice mMostRecentlyReportedActiveDevice = null;
472 
BluetoothRouteManager(Context context, TelecomSystem.SyncRoot lock, BluetoothDeviceManager deviceManager, Timeouts.Adapter timeoutsAdapter)473     public BluetoothRouteManager(Context context, TelecomSystem.SyncRoot lock,
474             BluetoothDeviceManager deviceManager, Timeouts.Adapter timeoutsAdapter) {
475         super(BluetoothRouteManager.class.getSimpleName());
476         mContext = context;
477         mLock = lock;
478         mDeviceManager = deviceManager;
479         mDeviceManager.setBluetoothRouteManager(this);
480         mTimeoutsAdapter = timeoutsAdapter;
481 
482         mAudioOffState = new AudioOffState();
483         addState(mAudioOffState);
484         setInitialState(mAudioOffState);
485         start();
486     }
487 
488     @Override
onPreHandleMessage(Message msg)489     protected void onPreHandleMessage(Message msg) {
490         if (msg.obj != null && msg.obj instanceof SomeArgs) {
491             SomeArgs args = (SomeArgs) msg.obj;
492 
493             Log.continueSession(((Session) args.arg1), "BRM.pM_" + msg.what);
494             Log.i(LOG_TAG, "%s received message: %s.", this,
495                     MESSAGE_CODE_TO_NAME.get(msg.what));
496         } else if (msg.what == RUN_RUNNABLE && msg.obj instanceof Runnable) {
497             Log.i(LOG_TAG, "Running runnable for testing");
498         } else {
499             Log.w(LOG_TAG, "Message sent must be of type nonnull SomeArgs, but got " +
500                     (msg.obj == null ? "null" : msg.obj.getClass().getSimpleName()));
501             Log.w(LOG_TAG, "The message was of code %d = %s",
502                     msg.what, MESSAGE_CODE_TO_NAME.get(msg.what));
503         }
504     }
505 
506     @Override
onPostHandleMessage(Message msg)507     protected void onPostHandleMessage(Message msg) {
508         Log.endSession();
509     }
510 
511     /**
512      * Returns whether there is a BT device available to route audio to.
513      * @return true if there is a device, false otherwise.
514      */
isBluetoothAvailable()515     public boolean isBluetoothAvailable() {
516         return mDeviceManager.getNumConnectedDevices() > 0;
517     }
518 
519     /**
520      * This method needs be synchronized with the local looper because getCurrentState() depends
521      * on the internal state of the state machine being consistent. Therefore, there may be a
522      * delay when calling this method.
523      * @return
524      */
isBluetoothAudioConnectedOrPending()525     public boolean isBluetoothAudioConnectedOrPending() {
526         SomeArgs args = SomeArgs.obtain();
527         args.arg1 = Log.createSubsession();
528         BlockingQueue<IState> stateQueue = new LinkedBlockingQueue<>();
529         // Use arg3 because arg2 is reserved for the device address
530         args.arg3 = stateQueue;
531         sendMessage(GET_CURRENT_STATE, args);
532 
533         try {
534             IState currentState = stateQueue.poll(GET_STATE_TIMEOUT, TimeUnit.MILLISECONDS);
535             if (currentState == null) {
536                 Log.w(LOG_TAG, "Failed to get a state from the state machine in time -- Handler " +
537                         "stuck?");
538                 return false;
539             }
540             return currentState != mAudioOffState;
541         } catch (InterruptedException e) {
542             Log.w(LOG_TAG, "isBluetoothAudioConnectedOrPending -- interrupted getting state");
543             return false;
544         }
545     }
546 
547     /**
548      * Attempts to connect to Bluetooth audio. If the first connection attempt synchronously
549      * fails, schedules a retry at a later time.
550      * @param address The MAC address of the bluetooth device to connect to. If null, the most
551      *                recently used device will be used.
552      */
connectBluetoothAudio(String address)553     public void connectBluetoothAudio(String address) {
554         SomeArgs args = SomeArgs.obtain();
555         args.arg1 = Log.createSubsession();
556         args.arg2 = address;
557         sendMessage(CONNECT_BT, args);
558     }
559 
560     /**
561      * Disconnects Bluetooth audio.
562      */
disconnectBluetoothAudio()563     public void disconnectBluetoothAudio() {
564         SomeArgs args = SomeArgs.obtain();
565         args.arg1 = Log.createSubsession();
566         sendMessage(DISCONNECT_BT, args);
567     }
568 
disconnectAudio()569     public void disconnectAudio() {
570         mDeviceManager.disconnectAudio();
571     }
572 
cacheHearingAidDevice()573     public void cacheHearingAidDevice() {
574         mDeviceManager.cacheHearingAidDevice();
575     }
576 
restoreHearingAidDevice()577     public void restoreHearingAidDevice() {
578         mDeviceManager.restoreHearingAidDevice();
579     }
580 
setListener(BluetoothStateListener listener)581     public void setListener(BluetoothStateListener listener) {
582         mListener = listener;
583     }
584 
onDeviceAdded(String newDeviceAddress)585     public void onDeviceAdded(String newDeviceAddress) {
586         SomeArgs args = SomeArgs.obtain();
587         args.arg1 = Log.createSubsession();
588         args.arg2 = newDeviceAddress;
589         sendMessage(NEW_DEVICE_CONNECTED, args);
590 
591         mListener.onBluetoothDeviceListChanged();
592     }
593 
onDeviceLost(String lostDeviceAddress)594     public void onDeviceLost(String lostDeviceAddress) {
595         SomeArgs args = SomeArgs.obtain();
596         args.arg1 = Log.createSubsession();
597         args.arg2 = lostDeviceAddress;
598         sendMessage(LOST_DEVICE, args);
599 
600         mListener.onBluetoothDeviceListChanged();
601     }
602 
onAudioOn(String address)603     public void onAudioOn(String address) {
604         Session session = Log.createSubsession();
605         SomeArgs args = SomeArgs.obtain();
606         args.arg1 = session;
607         args.arg2 = address;
608         sendMessage(BT_AUDIO_IS_ON, args);
609     }
610 
onAudioLost(String address)611     public void onAudioLost(String address) {
612         Session session = Log.createSubsession();
613         SomeArgs args = SomeArgs.obtain();
614         args.arg1 = session;
615         args.arg2 = address;
616         sendMessage(BT_AUDIO_LOST, args);
617     }
618 
onActiveDeviceChanged(BluetoothDevice device, int deviceType)619     public void onActiveDeviceChanged(BluetoothDevice device, int deviceType) {
620         boolean wasActiveDevicePresent = hasBtActiveDevice();
621         if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
622             mLeAudioActiveDeviceCache = device;
623             if (device == null) {
624                 mDeviceManager.clearLeAudioCommunicationDevice();
625             }
626         } else if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID) {
627             mHearingAidActiveDeviceCache = device;
628             if (device == null) {
629                 mDeviceManager.clearHearingAidCommunicationDevice();
630             }
631         } else if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEADSET) {
632             mHfpActiveDeviceCache = device;
633         } else {
634             return;
635         }
636 
637         if (device != null) mMostRecentlyReportedActiveDevice = device;
638 
639         boolean isActiveDevicePresent = hasBtActiveDevice();
640 
641         if (wasActiveDevicePresent && !isActiveDevicePresent) {
642             mListener.onBluetoothActiveDeviceGone();
643         } else if (!wasActiveDevicePresent && isActiveDevicePresent) {
644             mListener.onBluetoothActiveDevicePresent();
645         }
646     }
647 
hasBtActiveDevice()648     public boolean hasBtActiveDevice() {
649         return mLeAudioActiveDeviceCache != null ||
650                 mHearingAidActiveDeviceCache != null ||
651                 mHfpActiveDeviceCache != null;
652     }
653 
isCachedLeAudioDevice(BluetoothDevice device)654     public boolean isCachedLeAudioDevice(BluetoothDevice device) {
655         return mLeAudioActiveDeviceCache != null && mLeAudioActiveDeviceCache.equals(device);
656     }
657 
isCachedHearingAidDevice(BluetoothDevice device)658     public boolean isCachedHearingAidDevice(BluetoothDevice device) {
659         return mHearingAidActiveDeviceCache != null && mHearingAidActiveDeviceCache.equals(device);
660     }
661 
getConnectedDevices()662     public Collection<BluetoothDevice> getConnectedDevices() {
663         return mDeviceManager.getUniqueConnectedDevices();
664     }
665 
connectBtAudio(String address, boolean switchingBtDevices)666     private String connectBtAudio(String address, boolean switchingBtDevices) {
667         return connectBtAudio(address, 0, switchingBtDevices);
668     }
669 
670     /**
671      * Initiates a connection to the BT address specified.
672      * Note: This method is not synchronized on the Telecom lock, so don't try and call back into
673      * Telecom from within it.
674      * @param address The address that should be tried first. May be null.
675      * @param retryCount The number of times this connection attempt has been retried.
676      * @param switchingBtDevices Used when there is existing audio connection to other Bt device.
677      * @return The address of the device that's actually being connected to, or null if no
678      * connection was successful.
679      */
connectBtAudio(String address, int retryCount, boolean switchingBtDevices)680     private String connectBtAudio(String address, int retryCount, boolean switchingBtDevices) {
681         Collection<BluetoothDevice> deviceList = mDeviceManager.getConnectedDevices();
682         Optional<BluetoothDevice> matchingDevice = deviceList.stream()
683                 .filter(d -> Objects.equals(d.getAddress(), address))
684                 .findAny();
685 
686         if (switchingBtDevices) {
687             /* When new Bluetooth connects audio, make sure previous one has disconnected audio. */
688             mDeviceManager.disconnectAudio();
689         }
690 
691         String actualAddress = matchingDevice.isPresent()
692                 ? address : getActiveDeviceAddress();
693         if (actualAddress == null) {
694             Log.i(this, "No device specified and BT stack has no active device."
695                     + " Using arbitrary device");
696             if (deviceList.size() > 0) {
697                 actualAddress = deviceList.iterator().next().getAddress();
698             } else {
699                 Log.i(this, "No devices available at all. Not connecting.");
700                 return null;
701             }
702         }
703         if (!matchingDevice.isPresent()) {
704             Log.i(this, "No device with address %s available. Using %s instead.",
705                     address, actualAddress);
706         }
707 
708         BluetoothDevice alreadyConnectedDevice = getBluetoothAudioConnectedDevice();
709         if (alreadyConnectedDevice != null && alreadyConnectedDevice.getAddress().equals(
710                 actualAddress)) {
711             Log.i(this, "trying to connect to already connected device -- skipping connection"
712                     + " and going into the actual connected state.");
713             transitionToActualState();
714             return null;
715         }
716 
717         if (!mDeviceManager.connectAudio(actualAddress, switchingBtDevices)) {
718             boolean shouldRetry = retryCount < MAX_CONNECTION_RETRIES;
719             Log.w(LOG_TAG, "Could not connect to %s. Will %s", actualAddress,
720                     shouldRetry ? "retry" : "not retry");
721             if (shouldRetry) {
722                 SomeArgs args = SomeArgs.obtain();
723                 args.arg1 = Log.createSubsession();
724                 args.arg2 = actualAddress;
725                 args.argi1 = retryCount + 1;
726                 sendMessageDelayed(RETRY_BT_CONNECTION, args,
727                         mTimeoutsAdapter.getRetryBluetoothConnectAudioBackoffMillis(
728                                 mContext.getContentResolver()));
729             }
730             return null;
731         }
732 
733         return actualAddress;
734     }
735 
736     private String getActiveDeviceAddress() {
737         if (mHfpActiveDeviceCache != null) {
738             return mHfpActiveDeviceCache.getAddress();
739         }
740         if (mHearingAidActiveDeviceCache != null) {
741             return mHearingAidActiveDeviceCache.getAddress();
742         }
743         if (mLeAudioActiveDeviceCache != null) {
744             return mLeAudioActiveDeviceCache.getAddress();
745         }
746         return null;
747     }
748 
749     private void transitionToActualState() {
750         BluetoothDevice possiblyAlreadyConnectedDevice = getBluetoothAudioConnectedDevice();
751         if (possiblyAlreadyConnectedDevice != null) {
752             Log.i(LOG_TAG, "Device %s is already connected; going to AudioConnected.",
753                     possiblyAlreadyConnectedDevice);
754             transitionTo(getConnectedStateForAddress(
755                     possiblyAlreadyConnectedDevice.getAddress(), "transitionToActualState"));
756         } else {
757             transitionTo(mAudioOffState);
758         }
759     }
760 
761     /**
762      * @return The BluetoothDevice that is connected to BT audio, null if none are connected.
763      */
764     @VisibleForTesting
765     public BluetoothDevice getBluetoothAudioConnectedDevice() {
766         BluetoothAdapter bluetoothAdapter = mDeviceManager.getBluetoothAdapter();
767         BluetoothHeadset bluetoothHeadset = mDeviceManager.getBluetoothHeadset();
768         BluetoothHearingAid bluetoothHearingAid = mDeviceManager.getBluetoothHearingAid();
769         BluetoothLeAudio bluetoothLeAudio = mDeviceManager.getLeAudioService();
770 
771         BluetoothDevice hfpAudioOnDevice = null;
772         BluetoothDevice hearingAidActiveDevice = null;
773         BluetoothDevice leAudioActiveDevice = null;
774 
775         if (bluetoothAdapter == null) {
776             Log.i(this, "getBluetoothAudioConnectedDevice: no adapter available.");
777             return null;
778         }
779         if (bluetoothHeadset == null && bluetoothHearingAid == null && bluetoothLeAudio == null) {
780             Log.i(this, "getBluetoothAudioConnectedDevice: no service available.");
781             return null;
782         }
783 
784         int activeDevices = 0;
785         if (bluetoothHeadset != null) {
786             for (BluetoothDevice device : bluetoothAdapter.getActiveDevices(
787                         BluetoothProfile.HEADSET)) {
788                 hfpAudioOnDevice = device;
789                 break;
790             }
791 
792             if (hfpAudioOnDevice != null && bluetoothHeadset.getAudioState(hfpAudioOnDevice)
793                     == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
794                 hfpAudioOnDevice = null;
795             } else {
796                 activeDevices++;
797             }
798         }
799 
800         if (bluetoothHearingAid != null) {
801             if (mDeviceManager.isHearingAidSetAsCommunicationDevice()) {
802                 for (BluetoothDevice device : bluetoothAdapter.getActiveDevices(
803                         BluetoothProfile.HEARING_AID)) {
804                     if (device != null) {
805                         hearingAidActiveDevice = device;
806                         activeDevices++;
807                         break;
808                     }
809                 }
810             }
811         }
812 
813         if (bluetoothLeAudio != null) {
814             if (mDeviceManager.isLeAudioCommunicationDevice()) {
815                 for (BluetoothDevice device : bluetoothAdapter.getActiveDevices(
816                         BluetoothProfile.LE_AUDIO)) {
817                     if (device != null) {
818                         leAudioActiveDevice = device;
819                         activeDevices++;
820                         break;
821                     }
822                 }
823             }
824         }
825 
826         // Return the active device reported by either HFP, hearing aid or le audio. If more than
827         // one is reporting active devices, go with the most recent one as reported by the receiver.
828         if (activeDevices > 1) {
829             Log.i(this, "More than one profile reporting active devices. Going with the most"
830                     + " recently reported active device: %s", mMostRecentlyReportedActiveDevice);
831             return mMostRecentlyReportedActiveDevice;
832         }
833 
834         if (leAudioActiveDevice != null) {
835             return leAudioActiveDevice;
836         }
837 
838         if (hearingAidActiveDevice != null) {
839             return hearingAidActiveDevice;
840         }
841 
842         return hfpAudioOnDevice;
843     }
844 
845     /**
846      * Check if in-band ringing is currently enabled. In-band ringing could be disabled during an
847      * active connection.
848      *
849      * @return true if in-band ringing is enabled, false if in-band ringing is disabled
850      */
851     @VisibleForTesting
isInbandRingingEnabled()852     public boolean isInbandRingingEnabled() {
853         return mDeviceManager.isInbandRingingEnabled();
854     }
855 
addDevice(String address)856     private boolean addDevice(String address) {
857         if (mAudioConnectingStates.containsKey(address)) {
858             Log.i(this, "Attempting to add device %s twice.", address);
859             return false;
860         }
861         AudioConnectedState audioConnectedState = new AudioConnectedState(address);
862         AudioConnectingState audioConnectingState = new AudioConnectingState(address);
863         mAudioConnectingStates.put(address, audioConnectingState);
864         mAudioConnectedStates.put(address, audioConnectedState);
865         addState(audioConnectedState);
866         addState(audioConnectingState);
867         return true;
868     }
869 
removeDevice(String address)870     private boolean removeDevice(String address) {
871         if (!mAudioConnectingStates.containsKey(address)) {
872             Log.i(this, "Attempting to remove already-removed device %s", address);
873             return false;
874         }
875         statesToCleanUp.add(mAudioConnectingStates.remove(address));
876         statesToCleanUp.add(mAudioConnectedStates.remove(address));
877         mMostRecentlyUsedDevices.remove(address);
878         return true;
879     }
880 
getConnectingStateForAddress(String address, String error)881     private AudioConnectingState getConnectingStateForAddress(String address, String error) {
882         if (!mAudioConnectingStates.containsKey(address)) {
883             Log.w(LOG_TAG, "Device being connected to does not have a corresponding state: %s",
884                     error);
885             addDevice(address);
886         }
887         return mAudioConnectingStates.get(address);
888     }
889 
getConnectedStateForAddress(String address, String error)890     private AudioConnectedState getConnectedStateForAddress(String address, String error) {
891         if (!mAudioConnectedStates.containsKey(address)) {
892             Log.w(LOG_TAG, "Device already connected to does" +
893                     " not have a corresponding state: %s", error);
894             addDevice(address);
895         }
896         return mAudioConnectedStates.get(address);
897     }
898 
899     /**
900      * Removes the states for disconnected devices from the state machine. Called when entering
901      * AudioOff so that none of the states-to-be-removed are active.
902      */
cleanupStatesForDisconnectedDevices()903     private void cleanupStatesForDisconnectedDevices() {
904         for (State state : statesToCleanUp) {
905             if (state != null) {
906                 removeState(state);
907             }
908         }
909         statesToCleanUp.clear();
910     }
911 
912     @VisibleForTesting
setInitialStateForTesting(String stateName, BluetoothDevice device)913     public void setInitialStateForTesting(String stateName, BluetoothDevice device) {
914         sendMessage(RUN_RUNNABLE, (Runnable) () -> {
915             switch (stateName) {
916                 case AUDIO_OFF_STATE_NAME:
917                     transitionTo(mAudioOffState);
918                     break;
919                 case AUDIO_CONNECTING_STATE_NAME_PREFIX:
920                     transitionTo(getConnectingStateForAddress(device.getAddress(),
921                             "setInitialStateForTesting"));
922                     break;
923                 case AUDIO_CONNECTED_STATE_NAME_PREFIX:
924                     transitionTo(getConnectedStateForAddress(device.getAddress(),
925                             "setInitialStateForTesting"));
926                     break;
927             }
928             Log.i(LOG_TAG, "transition for testing done: %s", stateName);
929         });
930     }
931 
932     @VisibleForTesting
setActiveDeviceCacheForTesting(BluetoothDevice device, int deviceType)933     public void setActiveDeviceCacheForTesting(BluetoothDevice device, int deviceType) {
934         if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
935           mLeAudioActiveDeviceCache = device;
936         } else if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID) {
937             mHearingAidActiveDeviceCache = device;
938         } else if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEADSET) {
939             mHfpActiveDeviceCache = device;
940         }
941     }
942 }
943