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