• 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();
onBluetoothAudioDisconnected()81         void onBluetoothAudioDisconnected();
82         /**
83          * This gets called when we get an unexpected state change from Bluetooth. Their stack does
84          * weird things sometimes, so this is really a signal for the listener to refresh their
85          * internal state and make sure it matches up with what the BT stack is doing.
86          */
onUnexpectedBluetoothStateChange()87         void onUnexpectedBluetoothStateChange();
88     }
89 
90     /**
91      * Constants representing messages sent to the state machine.
92      * Messages are expected to be sent with {@link SomeArgs} as the obj.
93      * In all cases, arg1 will be the log session.
94      */
95     // arg2: Address of the new device
96     public static final int NEW_DEVICE_CONNECTED = 1;
97     // arg2: Address of the lost device
98     public static final int LOST_DEVICE = 2;
99 
100     // arg2 (optional): the address of the specific device to connect to.
101     public static final int CONNECT_BT = 100;
102     // No args.
103     public static final int DISCONNECT_BT = 101;
104     // arg2: the address of the device to connect to.
105     public static final int RETRY_BT_CONNECTION = 102;
106 
107     // arg2: the address of the device that is on
108     public static final int BT_AUDIO_IS_ON = 200;
109     // arg2: the address of the device that lost BT audio
110     public static final int BT_AUDIO_LOST = 201;
111 
112     // No args; only used internally
113     public static final int CONNECTION_TIMEOUT = 300;
114 
115     // Get the current state and send it through the BlockingQueue<IState> provided as the object
116     // arg.
117     public static final int GET_CURRENT_STATE = 400;
118 
119     // arg2: Runnable
120     public static final int RUN_RUNNABLE = 9001;
121 
122     private static final int MAX_CONNECTION_RETRIES = 2;
123 
124     // States
125     private final class AudioOffState extends State {
126         @Override
getName()127         public String getName() {
128             return AUDIO_OFF_STATE_NAME;
129         }
130 
131         @Override
enter()132         public void enter() {
133             BluetoothDevice erroneouslyConnectedDevice = getBluetoothAudioConnectedDevice();
134             if (erroneouslyConnectedDevice != null) {
135                 Log.w(LOG_TAG, "Entering AudioOff state but device %s appears to be connected. " +
136                         "Switching to audio-on state for that device.", erroneouslyConnectedDevice);
137                 // change this to just transition to the new audio on state
138                 transitionToActualState();
139             }
140             cleanupStatesForDisconnectedDevices();
141             if (mListener != null) {
142                 mListener.onBluetoothAudioDisconnected();
143             }
144         }
145 
146         @Override
processMessage(Message msg)147         public boolean processMessage(Message msg) {
148             if (msg.what == RUN_RUNNABLE) {
149                 ((Runnable) msg.obj).run();
150                 return HANDLED;
151             }
152 
153             SomeArgs args = (SomeArgs) msg.obj;
154             try {
155                 switch (msg.what) {
156                     case NEW_DEVICE_CONNECTED:
157                         addDevice((String) args.arg2);
158                         break;
159                     case LOST_DEVICE:
160                         removeDevice((String) args.arg2);
161                         break;
162                     case CONNECT_BT:
163                         String actualAddress = connectBtAudio((String) args.arg2,
164                             false /* switchingBtDevices*/);
165 
166                         if (actualAddress != null) {
167                             transitionTo(getConnectingStateForAddress(actualAddress,
168                                     "AudioOff/CONNECT_BT"));
169                         } else {
170                             Log.w(LOG_TAG, "Tried to connect to %s but failed to connect to" +
171                                     " any BT device.", (String) args.arg2);
172                         }
173                         break;
174                     case DISCONNECT_BT:
175                         // Ignore.
176                         break;
177                     case RETRY_BT_CONNECTION:
178                         Log.i(LOG_TAG, "Retrying BT connection to %s", (String) args.arg2);
179                         String retryAddress = connectBtAudio((String) args.arg2, args.argi1,
180                             false /* switchingBtDevices*/);
181 
182                         if (retryAddress != null) {
183                             transitionTo(getConnectingStateForAddress(retryAddress,
184                                     "AudioOff/RETRY_BT_CONNECTION"));
185                         } else {
186                             Log.i(LOG_TAG, "Retry failed.");
187                         }
188                         break;
189                     case CONNECTION_TIMEOUT:
190                         // Ignore.
191                         break;
192                     case BT_AUDIO_IS_ON:
193                         String address = (String) args.arg2;
194                         Log.w(LOG_TAG, "BT audio unexpectedly turned on from device %s", address);
195                         transitionTo(getConnectedStateForAddress(address,
196                                 "AudioOff/BT_AUDIO_IS_ON"));
197                         break;
198                     case BT_AUDIO_LOST:
199                         Log.i(LOG_TAG, "Received BT off for device %s while BT off.",
200                                 (String) args.arg2);
201                         mListener.onUnexpectedBluetoothStateChange();
202                         break;
203                     case GET_CURRENT_STATE:
204                         BlockingQueue<IState> sink = (BlockingQueue<IState>) args.arg3;
205                         sink.offer(this);
206                         break;
207                 }
208             } finally {
209                 args.recycle();
210             }
211             return HANDLED;
212         }
213     }
214 
215     private final class AudioConnectingState extends State {
216         private final String mDeviceAddress;
217 
AudioConnectingState(String address)218         AudioConnectingState(String address) {
219             mDeviceAddress = address;
220         }
221 
222         @Override
getName()223         public String getName() {
224             return AUDIO_CONNECTING_STATE_NAME_PREFIX + ":" + mDeviceAddress;
225         }
226 
227         @Override
enter()228         public void enter() {
229             SomeArgs args = SomeArgs.obtain();
230             args.arg1 = Log.createSubsession();
231             sendMessageDelayed(CONNECTION_TIMEOUT, args,
232                     mTimeoutsAdapter.getBluetoothPendingTimeoutMillis(
233                             mContext.getContentResolver()));
234             // Pretend like audio is connected when communicating w/ CARSM.
235             mListener.onBluetoothAudioConnected();
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         BluetoothHeadset bluetoothHeadset = mDeviceManager.getBluetoothHeadset();
854         if (bluetoothHeadset == null) {
855             Log.i(this, "isInbandRingingEnabled: no headset service available.");
856             return false;
857         }
858         return bluetoothHeadset.isInbandRingingEnabled();
859     }
860 
addDevice(String address)861     private boolean addDevice(String address) {
862         if (mAudioConnectingStates.containsKey(address)) {
863             Log.i(this, "Attempting to add device %s twice.", address);
864             return false;
865         }
866         AudioConnectedState audioConnectedState = new AudioConnectedState(address);
867         AudioConnectingState audioConnectingState = new AudioConnectingState(address);
868         mAudioConnectingStates.put(address, audioConnectingState);
869         mAudioConnectedStates.put(address, audioConnectedState);
870         addState(audioConnectedState);
871         addState(audioConnectingState);
872         return true;
873     }
874 
removeDevice(String address)875     private boolean removeDevice(String address) {
876         if (!mAudioConnectingStates.containsKey(address)) {
877             Log.i(this, "Attempting to remove already-removed device %s", address);
878             return false;
879         }
880         statesToCleanUp.add(mAudioConnectingStates.remove(address));
881         statesToCleanUp.add(mAudioConnectedStates.remove(address));
882         mMostRecentlyUsedDevices.remove(address);
883         return true;
884     }
885 
getConnectingStateForAddress(String address, String error)886     private AudioConnectingState getConnectingStateForAddress(String address, String error) {
887         if (!mAudioConnectingStates.containsKey(address)) {
888             Log.w(LOG_TAG, "Device being connected to does not have a corresponding state: %s",
889                     error);
890             addDevice(address);
891         }
892         return mAudioConnectingStates.get(address);
893     }
894 
getConnectedStateForAddress(String address, String error)895     private AudioConnectedState getConnectedStateForAddress(String address, String error) {
896         if (!mAudioConnectedStates.containsKey(address)) {
897             Log.w(LOG_TAG, "Device already connected to does" +
898                     " not have a corresponding state: %s", error);
899             addDevice(address);
900         }
901         return mAudioConnectedStates.get(address);
902     }
903 
904     /**
905      * Removes the states for disconnected devices from the state machine. Called when entering
906      * AudioOff so that none of the states-to-be-removed are active.
907      */
cleanupStatesForDisconnectedDevices()908     private void cleanupStatesForDisconnectedDevices() {
909         for (State state : statesToCleanUp) {
910             if (state != null) {
911                 removeState(state);
912             }
913         }
914         statesToCleanUp.clear();
915     }
916 
917     @VisibleForTesting
setInitialStateForTesting(String stateName, BluetoothDevice device)918     public void setInitialStateForTesting(String stateName, BluetoothDevice device) {
919         sendMessage(RUN_RUNNABLE, (Runnable) () -> {
920             switch (stateName) {
921                 case AUDIO_OFF_STATE_NAME:
922                     transitionTo(mAudioOffState);
923                     break;
924                 case AUDIO_CONNECTING_STATE_NAME_PREFIX:
925                     transitionTo(getConnectingStateForAddress(device.getAddress(),
926                             "setInitialStateForTesting"));
927                     break;
928                 case AUDIO_CONNECTED_STATE_NAME_PREFIX:
929                     transitionTo(getConnectedStateForAddress(device.getAddress(),
930                             "setInitialStateForTesting"));
931                     break;
932             }
933             Log.i(LOG_TAG, "transition for testing done: %s", stateName);
934         });
935     }
936 
937     @VisibleForTesting
setActiveDeviceCacheForTesting(BluetoothDevice device, int deviceType)938     public void setActiveDeviceCacheForTesting(BluetoothDevice device, int deviceType) {
939         if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO) {
940           mLeAudioActiveDeviceCache = device;
941         } else if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID) {
942             mHearingAidActiveDeviceCache = device;
943         } else if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEADSET) {
944             mHfpActiveDeviceCache = device;
945         }
946     }
947 }
948