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