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