Home | History | Annotate | Download | only in bluetooth
      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 {
     75         void onBluetoothDeviceListChanged();
     76         void onBluetoothActiveDevicePresent();
     77         void onBluetoothActiveDeviceGone();
     78         void onBluetoothAudioConnected();
     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
    119         public String getName() {
    120             return AUDIO_OFF_STATE_NAME;
    121         }
    122 
    123         @Override
    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
    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 
    205         AudioConnectingState(String address) {
    206             mDeviceAddress = address;
    207         }
    208 
    209         @Override
    210         public String getName() {
    211             return AUDIO_CONNECTING_STATE_NAME_PREFIX + ":" + mDeviceAddress;
    212         }
    213 
    214         @Override
    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
    226         public void exit() {
    227             removeMessages(CONNECTION_TIMEOUT);
    228         }
    229 
    230         @Override
    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 
    324         AudioConnectedState(String address) {
    325             mDeviceAddress = address;
    326         }
    327 
    328         @Override
    329         public String getName() {
    330             return AUDIO_CONNECTED_STATE_NAME_PREFIX + ":" + mDeviceAddress;
    331         }
    332 
    333         @Override
    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
    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 
    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
    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
    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      */
    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      */
    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      */
    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      */
    534     public void disconnectBluetoothAudio() {
    535         SomeArgs args = SomeArgs.obtain();
    536         args.arg1 = Log.createSubsession();
    537         sendMessage(DISCONNECT_HFP, args);
    538     }
    539 
    540     public void setListener(BluetoothStateListener listener) {
    541         mListener = listener;
    542     }
    543 
    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 
    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 
    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 
    574     public Collection<BluetoothDevice> getConnectedDevices() {
    575         return Collections.unmodifiableCollection(
    576                 new ArrayList<>(mDeviceManager.getConnectedDevices()));
    577     }
    578 
    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      */
    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