blob: 298129a487ff9209d62d676626d9a373534c7db3 [file] [log] [blame]
Hall Liu8502d6e2016-10-31 14:07:52 -07001/*
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
17package com.android.server.telecom.bluetooth;
18
19import android.bluetooth.BluetoothDevice;
Hall Liuea8d15d2016-10-31 14:16:03 -070020import android.content.Context;
Hall Liuea8d15d2016-10-31 14:16:03 -070021import android.os.Message;
22import android.telecom.Log;
23import android.telecom.Logging.Session;
24import android.util.SparseArray;
Hall Liu8502d6e2016-10-31 14:07:52 -070025
Hall Liuea8d15d2016-10-31 14:16:03 -070026import com.android.internal.annotations.VisibleForTesting;
27import com.android.internal.os.SomeArgs;
Hall Liub4d0b912017-04-11 18:05:16 -070028import com.android.internal.util.IState;
Hall Liuea8d15d2016-10-31 14:16:03 -070029import com.android.internal.util.State;
30import com.android.internal.util.StateMachine;
31import com.android.server.telecom.BluetoothHeadsetProxy;
32import com.android.server.telecom.TelecomSystem;
33import com.android.server.telecom.Timeouts;
Hall Liu8502d6e2016-10-31 14:07:52 -070034
Hall Liue79cec02018-05-02 14:15:12 -070035import java.util.ArrayList;
Hall Liu132a5572017-11-02 18:44:57 -070036import java.util.Collection;
Hall Liue79cec02018-05-02 14:15:12 -070037import java.util.Collections;
Hall Liuea8d15d2016-10-31 14:16:03 -070038import java.util.HashMap;
39import java.util.HashSet;
40import java.util.LinkedHashSet;
41import java.util.List;
42import java.util.Map;
43import java.util.Objects;
44import java.util.Optional;
45import java.util.Set;
Hall Liu132a5572017-11-02 18:44:57 -070046import java.util.concurrent.BlockingQueue;
47import java.util.concurrent.LinkedBlockingQueue;
Hall Liub4d0b912017-04-11 18:05:16 -070048import java.util.concurrent.TimeUnit;
Hall Liuea8d15d2016-10-31 14:16:03 -070049
50public 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");
Hall Liu9086fb12017-11-07 18:01:53 -080062 put(GET_CURRENT_STATE, "GET_CURRENT_STATE");
Hall Liuea8d15d2016-10-31 14:16:03 -070063 put(RUN_RUNNABLE, "RUN_RUNNABLE");
64 }};
65
Hall Liuea8d15d2016-10-31 14:16:03 -070066 public static final String AUDIO_OFF_STATE_NAME = "AudioOff";
67 public static final String AUDIO_CONNECTING_STATE_NAME_PREFIX = "Connecting";
68 public static final String AUDIO_CONNECTED_STATE_NAME_PREFIX = "Connected";
69
Hall Liu132a5572017-11-02 18:44:57 -070070 // Timeout for querying the current state from the state machine handler.
71 private static final int GET_STATE_TIMEOUT = 1000;
72
Hall Liuea8d15d2016-10-31 14:16:03 -070073 public interface BluetoothStateListener {
Hall Liu132a5572017-11-02 18:44:57 -070074 void onBluetoothDeviceListChanged();
Hall Liucc742152018-05-09 17:48:40 -070075 void onBluetoothActiveDevicePresent();
76 void onBluetoothActiveDeviceGone();
Hall Liu132a5572017-11-02 18:44:57 -070077 void onBluetoothAudioConnected();
78 void onBluetoothAudioDisconnected();
Hall Liu8502d6e2016-10-31 14:07:52 -070079 }
80
Hall Liuea8d15d2016-10-31 14:16:03 -070081 /**
82 * Constants representing messages sent to the state machine.
83 * Messages are expected to be sent with {@link SomeArgs} as the obj.
84 * In all cases, arg1 will be the log session.
85 */
86 // arg2: Address of the new device
87 public static final int NEW_DEVICE_CONNECTED = 1;
88 // arg2: Address of the lost device
89 public static final int LOST_DEVICE = 2;
90
91 // arg2 (optional): the address of the specific device to connect to.
92 public static final int CONNECT_HFP = 100;
93 // No args.
94 public static final int DISCONNECT_HFP = 101;
95 // arg2: the address of the device to connect to.
96 public static final int RETRY_HFP_CONNECTION = 102;
97
98 // arg2: the address of the device that is on
99 public static final int HFP_IS_ON = 200;
100 // arg2: the address of the device that lost HFP
101 public static final int HFP_LOST = 201;
102
103 // No args; only used internally
104 public static final int CONNECTION_TIMEOUT = 300;
105
Hall Liu132a5572017-11-02 18:44:57 -0700106 // Get the current state and send it through the BlockingQueue<IState> provided as the object
107 // arg.
108 public static final int GET_CURRENT_STATE = 400;
109
Hall Liuea8d15d2016-10-31 14:16:03 -0700110 // arg2: Runnable
111 public static final int RUN_RUNNABLE = 9001;
112
Hall Liuf789bc02017-07-17 15:59:54 -0700113 private static final int MAX_CONNECTION_RETRIES = 2;
114
Hall Liuea8d15d2016-10-31 14:16:03 -0700115 // States
116 private final class AudioOffState extends State {
117 @Override
118 public String getName() {
119 return AUDIO_OFF_STATE_NAME;
120 }
121
122 @Override
123 public void enter() {
124 BluetoothDevice erroneouslyConnectedDevice = getBluetoothAudioConnectedDevice();
125 if (erroneouslyConnectedDevice != null) {
126 Log.w(LOG_TAG, "Entering AudioOff state but device %s appears to be connected. " +
127 "Disconnecting.", erroneouslyConnectedDevice);
128 disconnectAudio();
129 }
130 cleanupStatesForDisconnectedDevices();
Hall Liu132a5572017-11-02 18:44:57 -0700131 if (mListener != null) {
132 mListener.onBluetoothAudioDisconnected();
133 }
Hall Liuea8d15d2016-10-31 14:16:03 -0700134 }
135
136 @Override
137 public boolean processMessage(Message msg) {
138 if (msg.what == RUN_RUNNABLE) {
139 ((Runnable) msg.obj).run();
140 return HANDLED;
141 }
142
143 SomeArgs args = (SomeArgs) msg.obj;
144 try {
145 switch (msg.what) {
146 case NEW_DEVICE_CONNECTED:
Hall Liu132a5572017-11-02 18:44:57 -0700147 addDevice((String) args.arg2);
Hall Liuea8d15d2016-10-31 14:16:03 -0700148 break;
149 case LOST_DEVICE:
Hall Liu132a5572017-11-02 18:44:57 -0700150 removeDevice((String) args.arg2);
Hall Liuea8d15d2016-10-31 14:16:03 -0700151 break;
152 case CONNECT_HFP:
153 String actualAddress = connectHfpAudio((String) args.arg2);
154
155 if (actualAddress != null) {
Hall Liuea8d15d2016-10-31 14:16:03 -0700156 transitionTo(getConnectingStateForAddress(actualAddress,
157 "AudioOff/CONNECT_HFP"));
158 } else {
159 Log.w(LOG_TAG, "Tried to connect to %s but failed to connect to" +
160 " any HFP device.", (String) args.arg2);
161 }
162 break;
163 case DISCONNECT_HFP:
164 // Ignore.
165 break;
166 case RETRY_HFP_CONNECTION:
167 Log.i(LOG_TAG, "Retrying HFP connection to %s", (String) args.arg2);
Hall Liuf789bc02017-07-17 15:59:54 -0700168 String retryAddress = connectHfpAudio((String) args.arg2, args.argi1);
Hall Liuea8d15d2016-10-31 14:16:03 -0700169
170 if (retryAddress != null) {
Hall Liuea8d15d2016-10-31 14:16:03 -0700171 transitionTo(getConnectingStateForAddress(retryAddress,
172 "AudioOff/RETRY_HFP_CONNECTION"));
173 } else {
174 Log.i(LOG_TAG, "Retry failed.");
175 }
176 break;
177 case CONNECTION_TIMEOUT:
178 // Ignore.
179 break;
180 case HFP_IS_ON:
181 String address = (String) args.arg2;
182 Log.w(LOG_TAG, "HFP audio unexpectedly turned on from device %s", address);
Hall Liuea8d15d2016-10-31 14:16:03 -0700183 transitionTo(getConnectedStateForAddress(address, "AudioOff/HFP_IS_ON"));
184 break;
185 case HFP_LOST:
186 Log.i(LOG_TAG, "Received HFP off for device %s while HFP off.",
187 (String) args.arg2);
188 break;
Hall Liu132a5572017-11-02 18:44:57 -0700189 case GET_CURRENT_STATE:
190 BlockingQueue<IState> sink = (BlockingQueue<IState>) args.arg3;
191 sink.offer(this);
192 break;
Hall Liuea8d15d2016-10-31 14:16:03 -0700193 }
194 } finally {
195 args.recycle();
196 }
197 return HANDLED;
198 }
199 }
200
201 private final class AudioConnectingState extends State {
202 private final String mDeviceAddress;
203
204 AudioConnectingState(String address) {
205 mDeviceAddress = address;
206 }
207
208 @Override
209 public String getName() {
210 return AUDIO_CONNECTING_STATE_NAME_PREFIX + ":" + mDeviceAddress;
211 }
212
213 @Override
214 public void enter() {
215 SomeArgs args = SomeArgs.obtain();
216 args.arg1 = Log.createSubsession();
217 sendMessageDelayed(CONNECTION_TIMEOUT, args,
218 mTimeoutsAdapter.getBluetoothPendingTimeoutMillis(
219 mContext.getContentResolver()));
Hall Liu132a5572017-11-02 18:44:57 -0700220 // Pretend like audio is connected when communicating w/ CARSM.
221 mListener.onBluetoothAudioConnected();
Hall Liuea8d15d2016-10-31 14:16:03 -0700222 }
223
224 @Override
225 public void exit() {
226 removeMessages(CONNECTION_TIMEOUT);
227 }
228
229 @Override
230 public boolean processMessage(Message msg) {
231 if (msg.what == RUN_RUNNABLE) {
232 ((Runnable) msg.obj).run();
233 return HANDLED;
234 }
235
236 SomeArgs args = (SomeArgs) msg.obj;
237 String address = (String) args.arg2;
238 try {
239 switch (msg.what) {
240 case NEW_DEVICE_CONNECTED:
241 // If the device isn't new, don't bother passing it up.
Hall Liu132a5572017-11-02 18:44:57 -0700242 addDevice(address);
Hall Liuea8d15d2016-10-31 14:16:03 -0700243 break;
244 case LOST_DEVICE:
245 removeDevice((String) args.arg2);
Hall Liuea8d15d2016-10-31 14:16:03 -0700246 if (Objects.equals(address, mDeviceAddress)) {
Hall Liucc742152018-05-09 17:48:40 -0700247 transitionToActualState();
Hall Liuea8d15d2016-10-31 14:16:03 -0700248 }
249 break;
250 case CONNECT_HFP:
251 if (Objects.equals(mDeviceAddress, address)) {
252 // Ignore repeated connection attempts to the same device
253 break;
254 }
255 String actualAddress = connectHfpAudio(address);
256
257 if (actualAddress != null) {
Hall Liuea8d15d2016-10-31 14:16:03 -0700258 transitionTo(getConnectingStateForAddress(actualAddress,
259 "AudioConnecting/CONNECT_HFP"));
260 } else {
261 Log.w(LOG_TAG, "Tried to connect to %s but failed" +
262 " to connect to any HFP device.", (String) args.arg2);
263 }
264 break;
265 case DISCONNECT_HFP:
266 disconnectAudio();
Hall Liuea8d15d2016-10-31 14:16:03 -0700267 transitionTo(mAudioOffState);
268 break;
269 case RETRY_HFP_CONNECTION:
270 if (Objects.equals(address, mDeviceAddress)) {
271 Log.d(LOG_TAG, "Retry message came through while connecting.");
272 } else {
Hall Liuf789bc02017-07-17 15:59:54 -0700273 String retryAddress = connectHfpAudio(address, args.argi1);
Hall Liuea8d15d2016-10-31 14:16:03 -0700274 if (retryAddress != null) {
275 transitionTo(getConnectingStateForAddress(retryAddress,
276 "AudioConnecting/RETRY_HFP_CONNECTION"));
277 } else {
278 Log.i(LOG_TAG, "Retry failed.");
279 }
280 }
281 break;
282 case CONNECTION_TIMEOUT:
283 Log.i(LOG_TAG, "Connection with device %s timed out.",
284 mDeviceAddress);
Hall Liu132a5572017-11-02 18:44:57 -0700285 transitionToActualState();
Hall Liuea8d15d2016-10-31 14:16:03 -0700286 break;
287 case HFP_IS_ON:
288 if (Objects.equals(mDeviceAddress, address)) {
289 Log.i(LOG_TAG, "HFP connection success for device %s.", mDeviceAddress);
290 transitionTo(mAudioConnectedStates.get(mDeviceAddress));
291 } else {
292 Log.w(LOG_TAG, "In connecting state for device %s but %s" +
293 " is now connected", mDeviceAddress, address);
294 transitionTo(getConnectedStateForAddress(address,
295 "AudioConnecting/HFP_IS_ON"));
296 }
Hall Liuea8d15d2016-10-31 14:16:03 -0700297 break;
298 case HFP_LOST:
299 if (Objects.equals(mDeviceAddress, address)) {
300 Log.i(LOG_TAG, "Connection with device %s failed.",
301 mDeviceAddress);
Hall Liu132a5572017-11-02 18:44:57 -0700302 transitionToActualState();
Hall Liuea8d15d2016-10-31 14:16:03 -0700303 } else {
304 Log.w(LOG_TAG, "Got HFP lost message for device %s while" +
305 " connecting to %s.", address, mDeviceAddress);
306 }
307 break;
Hall Liu132a5572017-11-02 18:44:57 -0700308 case GET_CURRENT_STATE:
309 BlockingQueue<IState> sink = (BlockingQueue<IState>) args.arg3;
310 sink.offer(this);
311 break;
Hall Liuea8d15d2016-10-31 14:16:03 -0700312 }
313 } finally {
314 args.recycle();
315 }
316 return HANDLED;
317 }
318 }
319
320 private final class AudioConnectedState extends State {
321 private final String mDeviceAddress;
322
323 AudioConnectedState(String address) {
324 mDeviceAddress = address;
325 }
326
327 @Override
328 public String getName() {
329 return AUDIO_CONNECTED_STATE_NAME_PREFIX + ":" + mDeviceAddress;
330 }
331
332 @Override
333 public void enter() {
334 // Remove any of the retries that are still in the queue once any device becomes
335 // connected.
336 removeMessages(RETRY_HFP_CONNECTION);
337 // Remove and add to ensure that the device is at the top.
338 mMostRecentlyUsedDevices.remove(mDeviceAddress);
339 mMostRecentlyUsedDevices.add(mDeviceAddress);
Hall Liu132a5572017-11-02 18:44:57 -0700340 mListener.onBluetoothAudioConnected();
Hall Liuea8d15d2016-10-31 14:16:03 -0700341 }
342
343 @Override
344 public boolean processMessage(Message msg) {
345 if (msg.what == RUN_RUNNABLE) {
346 ((Runnable) msg.obj).run();
347 return HANDLED;
348 }
349
350 SomeArgs args = (SomeArgs) msg.obj;
351 String address = (String) args.arg2;
352 try {
353 switch (msg.what) {
354 case NEW_DEVICE_CONNECTED:
Hall Liu132a5572017-11-02 18:44:57 -0700355 addDevice(address);
Hall Liuea8d15d2016-10-31 14:16:03 -0700356 break;
357 case LOST_DEVICE:
358 removeDevice((String) args.arg2);
Hall Liuea8d15d2016-10-31 14:16:03 -0700359 if (Objects.equals(address, mDeviceAddress)) {
Hall Liucc742152018-05-09 17:48:40 -0700360 transitionToActualState();
Hall Liuea8d15d2016-10-31 14:16:03 -0700361 }
362 break;
363 case CONNECT_HFP:
364 if (Objects.equals(mDeviceAddress, address)) {
365 // Ignore connection to already connected device.
366 break;
367 }
368 String actualAddress = connectHfpAudio(address);
369
370 if (actualAddress != null) {
Hall Liuea8d15d2016-10-31 14:16:03 -0700371 transitionTo(getConnectingStateForAddress(address,
372 "AudioConnected/CONNECT_HFP"));
373 } else {
374 Log.w(LOG_TAG, "Tried to connect to %s but failed" +
375 " to connect to any HFP device.", (String) args.arg2);
376 }
377 break;
378 case DISCONNECT_HFP:
379 disconnectAudio();
Hall Liuea8d15d2016-10-31 14:16:03 -0700380 transitionTo(mAudioOffState);
381 break;
382 case RETRY_HFP_CONNECTION:
383 if (Objects.equals(address, mDeviceAddress)) {
384 Log.d(LOG_TAG, "Retry message came through while connected.");
385 } else {
Hall Liuf789bc02017-07-17 15:59:54 -0700386 String retryAddress = connectHfpAudio(address, args.argi1);
Hall Liuea8d15d2016-10-31 14:16:03 -0700387 if (retryAddress != null) {
Hall Liuea8d15d2016-10-31 14:16:03 -0700388 transitionTo(getConnectingStateForAddress(retryAddress,
389 "AudioConnected/RETRY_HFP_CONNECTION"));
390 } else {
391 Log.i(LOG_TAG, "Retry failed.");
392 }
393 }
394 break;
395 case CONNECTION_TIMEOUT:
396 Log.w(LOG_TAG, "Received CONNECTION_TIMEOUT while connected.");
397 break;
398 case HFP_IS_ON:
399 if (Objects.equals(mDeviceAddress, address)) {
400 Log.i(LOG_TAG, "Received redundant HFP_IS_ON for %s", mDeviceAddress);
401 } else {
402 Log.w(LOG_TAG, "In connected state for device %s but %s" +
403 " is now connected", mDeviceAddress, address);
404 transitionTo(getConnectedStateForAddress(address,
405 "AudioConnected/HFP_IS_ON"));
406 }
407 break;
408 case HFP_LOST:
409 if (Objects.equals(mDeviceAddress, address)) {
410 Log.i(LOG_TAG, "HFP connection with device %s lost.", mDeviceAddress);
Hall Liucc742152018-05-09 17:48:40 -0700411 transitionToActualState();
Hall Liuea8d15d2016-10-31 14:16:03 -0700412 } else {
413 Log.w(LOG_TAG, "Got HFP lost message for device %s while" +
414 " connected to %s.", address, mDeviceAddress);
415 }
416 break;
Hall Liu132a5572017-11-02 18:44:57 -0700417 case GET_CURRENT_STATE:
418 BlockingQueue<IState> sink = (BlockingQueue<IState>) args.arg3;
419 sink.offer(this);
420 break;
Hall Liuea8d15d2016-10-31 14:16:03 -0700421 }
422 } finally {
423 args.recycle();
424 }
425 return HANDLED;
426 }
427 }
428
429 private final State mAudioOffState;
430 private final Map<String, AudioConnectingState> mAudioConnectingStates = new HashMap<>();
431 private final Map<String, AudioConnectedState> mAudioConnectedStates = new HashMap<>();
432 private final Set<State> statesToCleanUp = new HashSet<>();
433 private final LinkedHashSet<String> mMostRecentlyUsedDevices = new LinkedHashSet<>();
434
435 private final TelecomSystem.SyncRoot mLock;
436 private final Context mContext;
437 private final Timeouts.Adapter mTimeoutsAdapter;
438
439 private BluetoothStateListener mListener;
440 private BluetoothDeviceManager mDeviceManager;
Hall Liucc742152018-05-09 17:48:40 -0700441 // Tracks the active device in the BT stack.
442 private BluetoothDevice mActiveDeviceCache = null;
Hall Liuea8d15d2016-10-31 14:16:03 -0700443
444 public BluetoothRouteManager(Context context, TelecomSystem.SyncRoot lock,
445 BluetoothDeviceManager deviceManager, Timeouts.Adapter timeoutsAdapter) {
446 super(BluetoothRouteManager.class.getSimpleName());
447 mContext = context;
448 mLock = lock;
449 mDeviceManager = deviceManager;
450 mDeviceManager.setBluetoothRouteManager(this);
451 mTimeoutsAdapter = timeoutsAdapter;
452
Hall Liuea8d15d2016-10-31 14:16:03 -0700453 mAudioOffState = new AudioOffState();
454 addState(mAudioOffState);
455 setInitialState(mAudioOffState);
456 start();
457 }
458
459 @Override
460 protected void onPreHandleMessage(Message msg) {
461 if (msg.obj != null && msg.obj instanceof SomeArgs) {
462 SomeArgs args = (SomeArgs) msg.obj;
463
464 Log.continueSession(((Session) args.arg1), "BRM.pM_" + msg.what);
465 Log.i(LOG_TAG, "Message received: %s.", MESSAGE_CODE_TO_NAME.get(msg.what));
466 } else if (msg.what == RUN_RUNNABLE && msg.obj instanceof Runnable) {
467 Log.i(LOG_TAG, "Running runnable for testing");
468 } else {
469 Log.w(LOG_TAG, "Message sent must be of type nonnull SomeArgs, but got " +
470 (msg.obj == null ? "null" : msg.obj.getClass().getSimpleName()));
471 Log.w(LOG_TAG, "The message was of code %d = %s",
472 msg.what, MESSAGE_CODE_TO_NAME.get(msg.what));
473 }
474 }
475
476 @Override
477 protected void onPostHandleMessage(Message msg) {
478 Log.endSession();
479 }
480
481 /**
482 * Returns whether there is a HFP device available to route audio to.
483 * @return true if there is a device, false otherwise.
484 */
485 public boolean isBluetoothAvailable() {
486 return mDeviceManager.getNumConnectedDevices() > 0;
487 }
488
Hall Liub4d0b912017-04-11 18:05:16 -0700489 /**
490 * This method needs be synchronized with the local looper because getCurrentState() depends
491 * on the internal state of the state machine being consistent. Therefore, there may be a
492 * delay when calling this method.
493 * @return
494 */
Hall Liuea8d15d2016-10-31 14:16:03 -0700495 public boolean isBluetoothAudioConnectedOrPending() {
Hall Liu132a5572017-11-02 18:44:57 -0700496 SomeArgs args = SomeArgs.obtain();
497 args.arg1 = Log.createSubsession();
498 BlockingQueue<IState> stateQueue = new LinkedBlockingQueue<>();
499 // Use arg3 because arg2 is reserved for the device address
500 args.arg3 = stateQueue;
501 sendMessage(GET_CURRENT_STATE, args);
502
Hall Liub4d0b912017-04-11 18:05:16 -0700503 try {
Hall Liu132a5572017-11-02 18:44:57 -0700504 IState currentState = stateQueue.poll(GET_STATE_TIMEOUT, TimeUnit.MILLISECONDS);
505 if (currentState == null) {
506 Log.w(LOG_TAG, "Failed to get a state from the state machine in time -- Handler " +
507 "stuck?");
508 return false;
509 }
510 return currentState != mAudioOffState;
Hall Liub4d0b912017-04-11 18:05:16 -0700511 } catch (InterruptedException e) {
512 Log.w(LOG_TAG, "isBluetoothAudioConnectedOrPending -- interrupted getting state");
513 return false;
514 }
Hall Liuea8d15d2016-10-31 14:16:03 -0700515 }
516
517 /**
518 * Attempts to connect to Bluetooth audio. If the first connection attempt synchronously
519 * fails, schedules a retry at a later time.
520 * @param address The MAC address of the bluetooth device to connect to. If null, the most
521 * recently used device will be used.
522 */
523 public void connectBluetoothAudio(String address) {
524 SomeArgs args = SomeArgs.obtain();
525 args.arg1 = Log.createSubsession();
526 args.arg2 = address;
527 sendMessage(CONNECT_HFP, args);
528 }
529
530 /**
531 * Disconnects Bluetooth HFP audio.
532 */
533 public void disconnectBluetoothAudio() {
534 SomeArgs args = SomeArgs.obtain();
535 args.arg1 = Log.createSubsession();
536 sendMessage(DISCONNECT_HFP, args);
537 }
538
539 public void setListener(BluetoothStateListener listener) {
540 mListener = listener;
541 }
542
Hall Liu132a5572017-11-02 18:44:57 -0700543 public void onDeviceAdded(String newDeviceAddress) {
Hall Liuea8d15d2016-10-31 14:16:03 -0700544 SomeArgs args = SomeArgs.obtain();
545 args.arg1 = Log.createSubsession();
Hall Liu132a5572017-11-02 18:44:57 -0700546 args.arg2 = newDeviceAddress;
Hall Liuea8d15d2016-10-31 14:16:03 -0700547 sendMessage(NEW_DEVICE_CONNECTED, args);
Hall Liu132a5572017-11-02 18:44:57 -0700548
549 mListener.onBluetoothDeviceListChanged();
Hall Liuea8d15d2016-10-31 14:16:03 -0700550 }
551
Hall Liu132a5572017-11-02 18:44:57 -0700552 public void onDeviceLost(String lostDeviceAddress) {
Hall Liuea8d15d2016-10-31 14:16:03 -0700553 SomeArgs args = SomeArgs.obtain();
554 args.arg1 = Log.createSubsession();
Hall Liu132a5572017-11-02 18:44:57 -0700555 args.arg2 = lostDeviceAddress;
Hall Liuea8d15d2016-10-31 14:16:03 -0700556 sendMessage(LOST_DEVICE, args);
Hall Liu132a5572017-11-02 18:44:57 -0700557
558 mListener.onBluetoothDeviceListChanged();
Hall Liucc742152018-05-09 17:48:40 -0700559 }
560
561 public void onActiveDeviceChanged(BluetoothDevice device) {
562 BluetoothDevice oldActiveDevice = mActiveDeviceCache;
563 mActiveDeviceCache = device;
564 if ((oldActiveDevice == null) ^ (device == null)) {
565 if (device == null) {
566 mListener.onBluetoothActiveDeviceGone();
567 } else {
568 mListener.onBluetoothActiveDevicePresent();
569 }
Hall Liu132a5572017-11-02 18:44:57 -0700570 }
571 }
572
573 public Collection<BluetoothDevice> getConnectedDevices() {
Hall Liue79cec02018-05-02 14:15:12 -0700574 return Collections.unmodifiableCollection(
575 new ArrayList<>(mDeviceManager.getConnectedDevices()));
Hall Liuea8d15d2016-10-31 14:16:03 -0700576 }
577
578 private String connectHfpAudio(String address) {
Hall Liucc742152018-05-09 17:48:40 -0700579 return connectHfpAudio(address, 0);
Hall Liuea8d15d2016-10-31 14:16:03 -0700580 }
581
582 /**
583 * Initiates a HFP connection to the BT address specified.
584 * Note: This method is not synchronized on the Telecom lock, so don't try and call back into
585 * Telecom from within it.
586 * @param address The address that should be tried first. May be null.
Hall Liuf789bc02017-07-17 15:59:54 -0700587 * @param retryCount The number of times this connection attempt has been retried.
Hall Liuea8d15d2016-10-31 14:16:03 -0700588 * @return The address of the device that's actually being connected to, or null if no
589 * connection was successful.
590 */
Hall Liucc742152018-05-09 17:48:40 -0700591 private String connectHfpAudio(String address, int retryCount) {
Hall Liu132a5572017-11-02 18:44:57 -0700592 Collection<BluetoothDevice> deviceList = getConnectedDevices();
Hall Liuea8d15d2016-10-31 14:16:03 -0700593 Optional<BluetoothDevice> matchingDevice = deviceList.stream()
594 .filter(d -> Objects.equals(d.getAddress(), address))
595 .findAny();
596
Hall Liucc742152018-05-09 17:48:40 -0700597 String actualAddress = matchingDevice.isPresent()
598 ? address : getActiveDeviceAddress();
Hall Liuea8d15d2016-10-31 14:16:03 -0700599 if (!matchingDevice.isPresent()) {
600 Log.i(this, "No device with address %s available. Using %s instead.",
601 address, actualAddress);
602 }
Hall Liucc742152018-05-09 17:48:40 -0700603 if (actualAddress == null) {
604 Log.i(this, "No device specified and BT stack has no active device. Not connecting.");
605 return null;
606 }
607 if (!connectAudio(actualAddress)) {
Hall Liuf789bc02017-07-17 15:59:54 -0700608 boolean shouldRetry = retryCount < MAX_CONNECTION_RETRIES;
609 Log.w(LOG_TAG, "Could not connect to %s. Will %s", actualAddress,
610 shouldRetry ? "retry" : "not retry");
Hall Liuea8d15d2016-10-31 14:16:03 -0700611 if (shouldRetry) {
612 SomeArgs args = SomeArgs.obtain();
613 args.arg1 = Log.createSubsession();
614 args.arg2 = actualAddress;
Hall Liuf789bc02017-07-17 15:59:54 -0700615 args.argi1 = retryCount + 1;
Hall Liuea8d15d2016-10-31 14:16:03 -0700616 sendMessageDelayed(RETRY_HFP_CONNECTION, args,
617 mTimeoutsAdapter.getRetryBluetoothConnectAudioBackoffMillis(
618 mContext.getContentResolver()));
619 }
620 return null;
621 }
622
623 return actualAddress;
624 }
625
Hall Liucc742152018-05-09 17:48:40 -0700626 private String getActiveDeviceAddress() {
627 return mActiveDeviceCache == null ? null : mActiveDeviceCache.getAddress();
Hall Liuea8d15d2016-10-31 14:16:03 -0700628 }
629
Hall Liu132a5572017-11-02 18:44:57 -0700630 private void transitionToActualState() {
Hall Liuea8d15d2016-10-31 14:16:03 -0700631 BluetoothDevice possiblyAlreadyConnectedDevice = getBluetoothAudioConnectedDevice();
632 if (possiblyAlreadyConnectedDevice != null) {
633 Log.i(LOG_TAG, "Device %s is already connected; going to AudioConnected.",
634 possiblyAlreadyConnectedDevice);
635 transitionTo(getConnectedStateForAddress(
636 possiblyAlreadyConnectedDevice.getAddress(), "transitionToActualState"));
Hall Liuea8d15d2016-10-31 14:16:03 -0700637 } else {
638 transitionTo(mAudioOffState);
Hall Liuea8d15d2016-10-31 14:16:03 -0700639 }
640 }
641
642 /**
643 * @return The BluetoothDevice that is connected to BT audio, null if none are connected.
644 */
645 @VisibleForTesting
646 public BluetoothDevice getBluetoothAudioConnectedDevice() {
647 BluetoothHeadsetProxy bluetoothHeadset = mDeviceManager.getHeadsetService();
648 if (bluetoothHeadset == null) {
649 Log.i(this, "getBluetoothAudioConnectedDevice: no headset service available.");
650 return null;
651 }
652 List<BluetoothDevice> deviceList = bluetoothHeadset.getConnectedDevices();
653
654 for (int i = 0; i < deviceList.size(); i++) {
655 BluetoothDevice device = deviceList.get(i);
656 boolean isAudioOn = bluetoothHeadset.isAudioConnected(device);
657 Log.v(this, "isBluetoothAudioConnected: ==> isAudioOn = " + isAudioOn
658 + "for headset: " + device);
659 if (isAudioOn) {
660 return device;
661 }
662 }
663 return null;
664 }
665
Jack Hef8c23ee2018-01-05 17:14:09 -0800666 /**
667 * Check if in-band ringing is currently enabled. In-band ringing could be disabled during an
668 * active connection.
669 *
670 * @return true if in-band ringing is enabled, false if in-band ringing is disabled
671 */
672 @VisibleForTesting
673 public boolean isInbandRingingEnabled() {
674 BluetoothHeadsetProxy bluetoothHeadset = mDeviceManager.getHeadsetService();
675 if (bluetoothHeadset == null) {
676 Log.i(this, "isInbandRingingEnabled: no headset service available.");
677 return false;
678 }
679 return bluetoothHeadset.isInbandRingingEnabled();
680 }
681
Hall Liuea8d15d2016-10-31 14:16:03 -0700682 private boolean connectAudio(String address) {
683 BluetoothHeadsetProxy bluetoothHeadset = mDeviceManager.getHeadsetService();
684 if (bluetoothHeadset == null) {
685 Log.w(this, "Trying to connect audio but no headset service exists.");
686 return false;
687 }
Hall Liu67eb3b02018-04-27 17:54:46 -0700688 BluetoothDevice device = mDeviceManager.getDeviceFromAddress(address);
689 if (device == null) {
690 Log.w(this, "Attempting to turn on audio for a disconnected device");
691 return false;
692 }
693 boolean success = bluetoothHeadset.setActiveDevice(device);
694 if (!success) {
695 Log.w(LOG_TAG, "Couldn't set active device to %s", address);
696 return false;
697 }
698 if (!bluetoothHeadset.isAudioOn()) {
699 return bluetoothHeadset.connectAudio();
700 }
701 return true;
Hall Liuea8d15d2016-10-31 14:16:03 -0700702 }
703
704 private void disconnectAudio() {
705 BluetoothHeadsetProxy bluetoothHeadset = mDeviceManager.getHeadsetService();
706 if (bluetoothHeadset == null) {
707 Log.w(this, "Trying to disconnect audio but no headset service exists.");
708 } else {
709 bluetoothHeadset.disconnectAudio();
710 }
711 }
712
713 private boolean addDevice(String address) {
714 if (mAudioConnectingStates.containsKey(address)) {
715 Log.i(this, "Attempting to add device %s twice.", address);
716 return false;
717 }
718 AudioConnectedState audioConnectedState = new AudioConnectedState(address);
719 AudioConnectingState audioConnectingState = new AudioConnectingState(address);
720 mAudioConnectingStates.put(address, audioConnectingState);
721 mAudioConnectedStates.put(address, audioConnectedState);
722 addState(audioConnectedState);
723 addState(audioConnectingState);
724 return true;
725 }
726
727 private boolean removeDevice(String address) {
728 if (!mAudioConnectingStates.containsKey(address)) {
729 Log.i(this, "Attempting to remove already-removed device %s", address);
730 return false;
731 }
732 statesToCleanUp.add(mAudioConnectingStates.remove(address));
733 statesToCleanUp.add(mAudioConnectedStates.remove(address));
734 mMostRecentlyUsedDevices.remove(address);
735 return true;
736 }
737
738 private AudioConnectingState getConnectingStateForAddress(String address, String error) {
739 if (!mAudioConnectingStates.containsKey(address)) {
740 Log.w(LOG_TAG, "Device being connected to does not have a corresponding state: %s",
741 error);
742 addDevice(address);
743 }
744 return mAudioConnectingStates.get(address);
745 }
746
747 private AudioConnectedState getConnectedStateForAddress(String address, String error) {
748 if (!mAudioConnectedStates.containsKey(address)) {
749 Log.w(LOG_TAG, "Device already connected to does" +
750 " not have a corresponding state: %s", error);
751 addDevice(address);
752 }
753 return mAudioConnectedStates.get(address);
754 }
755
756 /**
757 * Removes the states for disconnected devices from the state machine. Called when entering
758 * AudioOff so that none of the states-to-be-removed are active.
759 */
760 private void cleanupStatesForDisconnectedDevices() {
761 for (State state : statesToCleanUp) {
762 if (state != null) {
763 removeState(state);
764 }
765 }
766 statesToCleanUp.clear();
767 }
768
769 @VisibleForTesting
770 public void setInitialStateForTesting(String stateName, BluetoothDevice device) {
771 switch (stateName) {
772 case AUDIO_OFF_STATE_NAME:
773 transitionTo(mAudioOffState);
774 break;
775 case AUDIO_CONNECTING_STATE_NAME_PREFIX:
776 transitionTo(getConnectingStateForAddress(device.getAddress(),
777 "setInitialStateForTesting"));
778 break;
779 case AUDIO_CONNECTED_STATE_NAME_PREFIX:
780 transitionTo(getConnectedStateForAddress(device.getAddress(),
781 "setInitialStateForTesting"));
782 break;
783 }
Hall Liu8502d6e2016-10-31 14:07:52 -0700784 }
Hall Liucc742152018-05-09 17:48:40 -0700785
786 @VisibleForTesting
787 public void setActiveDeviceCacheForTesting(BluetoothDevice device) {
788 mActiveDeviceCache = device;
789 }
Hall Liu8502d6e2016-10-31 14:07:52 -0700790}