• 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.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 {
onBluetoothStateChange(int oldState, int newState)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
getName()153         public String getName() {
154             return AUDIO_OFF_STATE_NAME;
155         }
156 
157         @Override
enter()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
processMessage(Message msg)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 
AudioConnectingState(String address)252         AudioConnectingState(String address) {
253             mDeviceAddress = address;
254         }
255 
256         @Override
getName()257         public String getName() {
258             return AUDIO_CONNECTING_STATE_NAME_PREFIX + ":" + mDeviceAddress;
259         }
260 
261         @Override
enter()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
exit()271         public void exit() {
272             removeMessages(CONNECTION_TIMEOUT);
273         }
274 
275         @Override
processMessage(Message msg)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 
AudioConnectedState(String address)390         AudioConnectedState(String address) {
391             mDeviceAddress = address;
392         }
393 
394         @Override
getName()395         public String getName() {
396             return AUDIO_CONNECTED_STATE_NAME_PREFIX + ":" + mDeviceAddress;
397         }
398 
399         @Override
enter()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
processMessage(Message msg)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 
BluetoothRouteManager(Context context, TelecomSystem.SyncRoot lock, BluetoothDeviceManager deviceManager, Timeouts.Adapter timeoutsAdapter)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
onPreHandleMessage(Message msg)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
onPostHandleMessage(Message msg)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      */
isBluetoothAvailable()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      */
isBluetoothAudioConnectedOrPending()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      */
connectBluetoothAudio(String address)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      */
disconnectBluetoothAudio()626     public void disconnectBluetoothAudio() {
627         SomeArgs args = SomeArgs.obtain();
628         args.arg1 = Log.createSubsession();
629         sendMessage(DISCONNECT_HFP, args);
630     }
631 
setListener(BluetoothStateListener listener)632     public void setListener(BluetoothStateListener listener) {
633         mListener = listener;
634     }
635 
onDeviceAdded(BluetoothDevice newDevice)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 
onDeviceLost(BluetoothDevice lostDevice)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 
connectHfpAudio(String address)650     private String connectHfpAudio(String address) {
651         return connectHfpAudio(address, true, null);
652     }
653 
connectHfpAudio(String address, boolean shouldRetry)654     private String connectHfpAudio(String address, boolean shouldRetry) {
655         return connectHfpAudio(address, shouldRetry, null);
656     }
657 
connectHfpAudio(String address, String excludeAddress)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      */
connectHfpAudio(String address, boolean shouldRetry, String excludeAddress)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 
getPreferredDevice(String excludeAddress)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 
transitionToActualState(int currentBtState)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
getBluetoothAudioConnectedDevice()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 
connectAudio(String address)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 
disconnectAudio()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 
addDevice(String address)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 
removeDevice(String address)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 
getConnectingStateForAddress(String address, String error)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 
getConnectedStateForAddress(String address, String error)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      */
cleanupStatesForDisconnectedDevices()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
setInitialStateForTesting(String stateName, BluetoothDevice device)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