blob: 494089b2bcd6fd3437a8f4af1abf875d0be0d51c [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.server.telecom.bluetooth;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Message;
import android.telecom.Log;
import android.telecom.Logging.Session;
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
import com.android.server.telecom.BluetoothHeadsetProxy;
import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.Timeouts;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
public class BluetoothRouteManager extends StateMachine {
private static final String LOG_TAG = BluetoothRouteManager.class.getSimpleName();
private static final SparseArray<String> MESSAGE_CODE_TO_NAME = new SparseArray<String>() {{
put(NEW_DEVICE_CONNECTED, "NEW_DEVICE_CONNECTED");
put(LOST_DEVICE, "LOST_DEVICE");
put(CONNECT_HFP, "CONNECT_HFP");
put(DISCONNECT_HFP, "DISCONNECT_HFP");
put(RETRY_HFP_CONNECTION, "RETRY_HFP_CONNECTION");
put(HFP_IS_ON, "HFP_IS_ON");
put(HFP_LOST, "HFP_LOST");
put(CONNECTION_TIMEOUT, "CONNECTION_TIMEOUT");
put(RUN_RUNNABLE, "RUN_RUNNABLE");
}};
// Constants for compatiblity with current CARSM/CARPA
// TODO: delete and replace with new direct interface to CARPA.
public static final int BLUETOOTH_UNINITIALIZED = 0;
public static final int BLUETOOTH_DISCONNECTED = 1;
public static final int BLUETOOTH_DEVICE_CONNECTED = 2;
public static final int BLUETOOTH_AUDIO_PENDING = 3;
public static final int BLUETOOTH_AUDIO_CONNECTED = 4;
public static final String AUDIO_OFF_STATE_NAME = "AudioOff";
public static final String AUDIO_CONNECTING_STATE_NAME_PREFIX = "Connecting";
public static final String AUDIO_CONNECTED_STATE_NAME_PREFIX = "Connected";
public interface BluetoothStateListener {
void onBluetoothStateChange(int oldState, int newState);
}
// Broadcast receiver to receive audio state change broadcasts from the BT stack
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.startSession("BRM.oR");
try {
String action = intent.getAction();
if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
int bluetoothHeadsetAudioState =
intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
BluetoothDevice device =
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (device == null) {
Log.w(BluetoothRouteManager.this, "Got null device from broadcast. " +
"Ignoring.");
return;
}
Log.i(BluetoothRouteManager.this, "Device %s transitioned to audio state %d",
device.getAddress(), bluetoothHeadsetAudioState);
Session session = Log.createSubsession();
SomeArgs args = SomeArgs.obtain();
args.arg1 = session;
args.arg2 = device.getAddress();
switch (bluetoothHeadsetAudioState) {
case BluetoothHeadset.STATE_AUDIO_CONNECTED:
sendMessage(HFP_IS_ON, args);
break;
case BluetoothHeadset.STATE_AUDIO_DISCONNECTED:
sendMessage(HFP_LOST, args);
break;
}
}
} finally {
Log.endSession();
}
}
};
/**
* Constants representing messages sent to the state machine.
* Messages are expected to be sent with {@link SomeArgs} as the obj.
* In all cases, arg1 will be the log session.
*/
// arg2: Address of the new device
public static final int NEW_DEVICE_CONNECTED = 1;
// arg2: Address of the lost device
public static final int LOST_DEVICE = 2;
// arg2 (optional): the address of the specific device to connect to.
public static final int CONNECT_HFP = 100;
// No args.
public static final int DISCONNECT_HFP = 101;
// arg2: the address of the device to connect to.
public static final int RETRY_HFP_CONNECTION = 102;
// arg2: the address of the device that is on
public static final int HFP_IS_ON = 200;
// arg2: the address of the device that lost HFP
public static final int HFP_LOST = 201;
// No args; only used internally
public static final int CONNECTION_TIMEOUT = 300;
// arg2: Runnable
public static final int RUN_RUNNABLE = 9001;
// States
private final class AudioOffState extends State {
@Override
public String getName() {
return AUDIO_OFF_STATE_NAME;
}
@Override
public void enter() {
BluetoothDevice erroneouslyConnectedDevice = getBluetoothAudioConnectedDevice();
if (erroneouslyConnectedDevice != null) {
Log.w(LOG_TAG, "Entering AudioOff state but device %s appears to be connected. " +
"Disconnecting.", erroneouslyConnectedDevice);
disconnectAudio();
}
cleanupStatesForDisconnectedDevices();
}
@Override
public boolean processMessage(Message msg) {
if (msg.what == RUN_RUNNABLE) {
((Runnable) msg.obj).run();
return HANDLED;
}
SomeArgs args = (SomeArgs) msg.obj;
try {
switch (msg.what) {
case NEW_DEVICE_CONNECTED:
// If the device isn't new, don't bother passing it up.
if (addDevice((String) args.arg2)) {
// TODO: replace with new interface
if (mDeviceManager.getNumConnectedDevices() == 1) {
mListener.onBluetoothStateChange(
BLUETOOTH_DISCONNECTED, BLUETOOTH_DEVICE_CONNECTED);
}
}
break;
case LOST_DEVICE:
// If the device has already been removed, don't bother passing it up.
if (removeDevice((String) args.arg2)) {
// TODO: replace with new interface
if (mDeviceManager.getNumConnectedDevices() == 0) {
mListener.onBluetoothStateChange(
BLUETOOTH_DEVICE_CONNECTED, BLUETOOTH_DISCONNECTED);
}
}
break;
case CONNECT_HFP:
String actualAddress = connectHfpAudio((String) args.arg2);
if (actualAddress != null) {
mListener.onBluetoothStateChange(BLUETOOTH_DEVICE_CONNECTED,
BLUETOOTH_AUDIO_PENDING);
transitionTo(getConnectingStateForAddress(actualAddress,
"AudioOff/CONNECT_HFP"));
} else {
Log.w(LOG_TAG, "Tried to connect to %s but failed to connect to" +
" any HFP device.", (String) args.arg2);
}
break;
case DISCONNECT_HFP:
// Ignore.
break;
case RETRY_HFP_CONNECTION:
Log.i(LOG_TAG, "Retrying HFP connection to %s", (String) args.arg2);
String retryAddress = connectHfpAudio((String) args.arg2, false);
if (retryAddress != null) {
mListener.onBluetoothStateChange(BLUETOOTH_DEVICE_CONNECTED,
BLUETOOTH_AUDIO_PENDING);
transitionTo(getConnectingStateForAddress(retryAddress,
"AudioOff/RETRY_HFP_CONNECTION"));
} else {
Log.i(LOG_TAG, "Retry failed.");
}
break;
case CONNECTION_TIMEOUT:
// Ignore.
break;
case HFP_IS_ON:
String address = (String) args.arg2;
Log.w(LOG_TAG, "HFP audio unexpectedly turned on from device %s", address);
mListener.onBluetoothStateChange(BLUETOOTH_DEVICE_CONNECTED,
BLUETOOTH_AUDIO_CONNECTED);
transitionTo(getConnectedStateForAddress(address, "AudioOff/HFP_IS_ON"));
break;
case HFP_LOST:
Log.i(LOG_TAG, "Received HFP off for device %s while HFP off.",
(String) args.arg2);
break;
}
} finally {
args.recycle();
}
return HANDLED;
}
}
private final class AudioConnectingState extends State {
private final String mDeviceAddress;
AudioConnectingState(String address) {
mDeviceAddress = address;
}
@Override
public String getName() {
return AUDIO_CONNECTING_STATE_NAME_PREFIX + ":" + mDeviceAddress;
}
@Override
public void enter() {
SomeArgs args = SomeArgs.obtain();
args.arg1 = Log.createSubsession();
sendMessageDelayed(CONNECTION_TIMEOUT, args,
mTimeoutsAdapter.getBluetoothPendingTimeoutMillis(
mContext.getContentResolver()));
}
@Override
public void exit() {
removeMessages(CONNECTION_TIMEOUT);
}
@Override
public boolean processMessage(Message msg) {
if (msg.what == RUN_RUNNABLE) {
((Runnable) msg.obj).run();
return HANDLED;
}
SomeArgs args = (SomeArgs) msg.obj;
String address = (String) args.arg2;
try {
switch (msg.what) {
case NEW_DEVICE_CONNECTED:
// If the device isn't new, don't bother passing it up.
if (addDevice(address)) {
// TODO: replace with new interface
if (mDeviceManager.getNumConnectedDevices() == 1) {
Log.w(LOG_TAG, "Newly connected device is only device" +
" while audio pending.");
}
}
break;
case LOST_DEVICE:
removeDevice((String) args.arg2);
if (Objects.equals(address, mDeviceAddress)) {
String newAddress = connectHfpAudio(null);
if (newAddress != null) {
mListener.onBluetoothStateChange(BLUETOOTH_AUDIO_PENDING,
BLUETOOTH_AUDIO_PENDING);
transitionTo(getConnectingStateForAddress(newAddress,
"AudioConnecting/LOST_DEVICE"));
} else {
int numConnectedDevices = mDeviceManager.getNumConnectedDevices();
mListener.onBluetoothStateChange(BLUETOOTH_AUDIO_PENDING,
numConnectedDevices == 0 ? BLUETOOTH_DISCONNECTED :
BLUETOOTH_DEVICE_CONNECTED);
transitionTo(mAudioOffState);
}
}
break;
case CONNECT_HFP:
if (Objects.equals(mDeviceAddress, address)) {
// Ignore repeated connection attempts to the same device
break;
}
String actualAddress = connectHfpAudio(address);
if (actualAddress != null) {
mListener.onBluetoothStateChange(BLUETOOTH_AUDIO_PENDING,
BLUETOOTH_AUDIO_PENDING);
transitionTo(getConnectingStateForAddress(actualAddress,
"AudioConnecting/CONNECT_HFP"));
} else {
Log.w(LOG_TAG, "Tried to connect to %s but failed" +
" to connect to any HFP device.", (String) args.arg2);
}
break;
case DISCONNECT_HFP:
disconnectAudio();
mListener.onBluetoothStateChange(BLUETOOTH_AUDIO_PENDING,
BLUETOOTH_DEVICE_CONNECTED);
transitionTo(mAudioOffState);
break;
case RETRY_HFP_CONNECTION:
if (Objects.equals(address, mDeviceAddress)) {
Log.d(LOG_TAG, "Retry message came through while connecting.");
} else {
String retryAddress = connectHfpAudio(address, false);
if (retryAddress != null) {
transitionTo(getConnectingStateForAddress(retryAddress,
"AudioConnecting/RETRY_HFP_CONNECTION"));
} else {
Log.i(LOG_TAG, "Retry failed.");
}
}
break;
case CONNECTION_TIMEOUT:
Log.i(LOG_TAG, "Connection with device %s timed out.",
mDeviceAddress);
transitionToActualState(BLUETOOTH_AUDIO_PENDING);
break;
case HFP_IS_ON:
if (Objects.equals(mDeviceAddress, address)) {
Log.i(LOG_TAG, "HFP connection success for device %s.", mDeviceAddress);
transitionTo(mAudioConnectedStates.get(mDeviceAddress));
} else {
Log.w(LOG_TAG, "In connecting state for device %s but %s" +
" is now connected", mDeviceAddress, address);
transitionTo(getConnectedStateForAddress(address,
"AudioConnecting/HFP_IS_ON"));
}
mListener.onBluetoothStateChange(BLUETOOTH_AUDIO_PENDING,
BLUETOOTH_AUDIO_CONNECTED);
break;
case HFP_LOST:
if (Objects.equals(mDeviceAddress, address)) {
Log.i(LOG_TAG, "Connection with device %s failed.",
mDeviceAddress);
transitionToActualState(BLUETOOTH_AUDIO_PENDING);
} else {
Log.w(LOG_TAG, "Got HFP lost message for device %s while" +
" connecting to %s.", address, mDeviceAddress);
}
break;
}
} finally {
args.recycle();
}
return HANDLED;
}
}
private final class AudioConnectedState extends State {
private final String mDeviceAddress;
AudioConnectedState(String address) {
mDeviceAddress = address;
}
@Override
public String getName() {
return AUDIO_CONNECTED_STATE_NAME_PREFIX + ":" + mDeviceAddress;
}
@Override
public void enter() {
// Remove any of the retries that are still in the queue once any device becomes
// connected.
removeMessages(RETRY_HFP_CONNECTION);
// Remove and add to ensure that the device is at the top.
mMostRecentlyUsedDevices.remove(mDeviceAddress);
mMostRecentlyUsedDevices.add(mDeviceAddress);
}
@Override
public boolean processMessage(Message msg) {
if (msg.what == RUN_RUNNABLE) {
((Runnable) msg.obj).run();
return HANDLED;
}
SomeArgs args = (SomeArgs) msg.obj;
String address = (String) args.arg2;
try {
switch (msg.what) {
case NEW_DEVICE_CONNECTED:
// If the device isn't new, don't bother passing it up.
if (addDevice(address)) {
// TODO: Replace with new interface
if (mDeviceManager.getNumConnectedDevices() == 1) {
Log.w(LOG_TAG, "Newly connected device is only" +
" device while audio connected.");
}
}
break;
case LOST_DEVICE:
removeDevice((String) args.arg2);
if (Objects.equals(address, mDeviceAddress)) {
String newAddress = connectHfpAudio(null);
if (newAddress != null) {
mListener.onBluetoothStateChange(BLUETOOTH_AUDIO_CONNECTED,
BLUETOOTH_AUDIO_PENDING);
transitionTo(getConnectingStateForAddress(newAddress,
"AudioConnected/LOST_DEVICE"));
} else {
int numConnectedDevices = mDeviceManager.getNumConnectedDevices();
mListener.onBluetoothStateChange(BLUETOOTH_AUDIO_CONNECTED,
numConnectedDevices == 0 ? BLUETOOTH_DISCONNECTED :
BLUETOOTH_DEVICE_CONNECTED);
transitionTo(mAudioOffState);
}
}
break;
case CONNECT_HFP:
if (Objects.equals(mDeviceAddress, address)) {
// Ignore connection to already connected device.
break;
}
String actualAddress = connectHfpAudio(address);
if (actualAddress != null) {
mListener.onBluetoothStateChange(BLUETOOTH_AUDIO_CONNECTED,
BLUETOOTH_AUDIO_PENDING);
transitionTo(getConnectingStateForAddress(address,
"AudioConnected/CONNECT_HFP"));
} else {
Log.w(LOG_TAG, "Tried to connect to %s but failed" +
" to connect to any HFP device.", (String) args.arg2);
}
break;
case DISCONNECT_HFP:
disconnectAudio();
mListener.onBluetoothStateChange(BLUETOOTH_AUDIO_CONNECTED,
BLUETOOTH_DEVICE_CONNECTED);
transitionTo(mAudioOffState);
break;
case RETRY_HFP_CONNECTION:
if (Objects.equals(address, mDeviceAddress)) {
Log.d(LOG_TAG, "Retry message came through while connected.");
} else {
String retryAddress = connectHfpAudio(address, false);
if (retryAddress != null) {
mListener.onBluetoothStateChange(BLUETOOTH_AUDIO_CONNECTED,
BLUETOOTH_AUDIO_PENDING);
transitionTo(getConnectingStateForAddress(retryAddress,
"AudioConnected/RETRY_HFP_CONNECTION"));
} else {
Log.i(LOG_TAG, "Retry failed.");
}
}
break;
case CONNECTION_TIMEOUT:
Log.w(LOG_TAG, "Received CONNECTION_TIMEOUT while connected.");
break;
case HFP_IS_ON:
if (Objects.equals(mDeviceAddress, address)) {
Log.i(LOG_TAG, "Received redundant HFP_IS_ON for %s", mDeviceAddress);
} else {
Log.w(LOG_TAG, "In connected state for device %s but %s" +
" is now connected", mDeviceAddress, address);
transitionTo(getConnectedStateForAddress(address,
"AudioConnected/HFP_IS_ON"));
}
break;
case HFP_LOST:
if (Objects.equals(mDeviceAddress, address)) {
Log.i(LOG_TAG, "HFP connection with device %s lost.", mDeviceAddress);
String nextAddress = connectHfpAudio(null, mDeviceAddress);
if (nextAddress == null) {
Log.i(LOG_TAG, "No suitable fallback device. Going to AUDIO_OFF.");
transitionToActualState(BLUETOOTH_AUDIO_CONNECTED);
} else {
mListener.onBluetoothStateChange(BLUETOOTH_AUDIO_CONNECTED,
BLUETOOTH_AUDIO_PENDING);
transitionTo(getConnectingStateForAddress(nextAddress,
"AudioConnected/HFP_LOST"));
}
} else {
Log.w(LOG_TAG, "Got HFP lost message for device %s while" +
" connected to %s.", address, mDeviceAddress);
}
break;
}
} finally {
args.recycle();
}
return HANDLED;
}
}
private final State mAudioOffState;
private final Map<String, AudioConnectingState> mAudioConnectingStates = new HashMap<>();
private final Map<String, AudioConnectedState> mAudioConnectedStates = new HashMap<>();
private final Set<State> statesToCleanUp = new HashSet<>();
private final LinkedHashSet<String> mMostRecentlyUsedDevices = new LinkedHashSet<>();
private final TelecomSystem.SyncRoot mLock;
private final Context mContext;
private final Timeouts.Adapter mTimeoutsAdapter;
private BluetoothStateListener mListener;
private BluetoothDeviceManager mDeviceManager;
public BluetoothRouteManager(Context context, TelecomSystem.SyncRoot lock,
BluetoothDeviceManager deviceManager, Timeouts.Adapter timeoutsAdapter) {
super(BluetoothRouteManager.class.getSimpleName());
mContext = context;
mLock = lock;
mDeviceManager = deviceManager;
mDeviceManager.setBluetoothRouteManager(this);
mTimeoutsAdapter = timeoutsAdapter;
IntentFilter intentFilter = new IntentFilter(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
context.registerReceiver(mReceiver, intentFilter);
mAudioOffState = new AudioOffState();
addState(mAudioOffState);
setInitialState(mAudioOffState);
start();
}
@Override
protected void onPreHandleMessage(Message msg) {
if (msg.obj != null && msg.obj instanceof SomeArgs) {
SomeArgs args = (SomeArgs) msg.obj;
Log.continueSession(((Session) args.arg1), "BRM.pM_" + msg.what);
Log.i(LOG_TAG, "Message received: %s.", MESSAGE_CODE_TO_NAME.get(msg.what));
} else if (msg.what == RUN_RUNNABLE && msg.obj instanceof Runnable) {
Log.i(LOG_TAG, "Running runnable for testing");
} else {
Log.w(LOG_TAG, "Message sent must be of type nonnull SomeArgs, but got " +
(msg.obj == null ? "null" : msg.obj.getClass().getSimpleName()));
Log.w(LOG_TAG, "The message was of code %d = %s",
msg.what, MESSAGE_CODE_TO_NAME.get(msg.what));
}
}
@Override
protected void onPostHandleMessage(Message msg) {
Log.endSession();
}
/**
* Returns whether there is a HFP device available to route audio to.
* @return true if there is a device, false otherwise.
*/
public boolean isBluetoothAvailable() {
return mDeviceManager.getNumConnectedDevices() > 0;
}
public boolean isBluetoothAudioConnectedOrPending() {
return getCurrentState() != mAudioOffState;
}
/**
* Attempts to connect to Bluetooth audio. If the first connection attempt synchronously
* fails, schedules a retry at a later time.
* @param address The MAC address of the bluetooth device to connect to. If null, the most
* recently used device will be used.
*/
public void connectBluetoothAudio(String address) {
SomeArgs args = SomeArgs.obtain();
args.arg1 = Log.createSubsession();
args.arg2 = address;
sendMessage(CONNECT_HFP, args);
}
/**
* Disconnects Bluetooth HFP audio.
*/
public void disconnectBluetoothAudio() {
SomeArgs args = SomeArgs.obtain();
args.arg1 = Log.createSubsession();
sendMessage(DISCONNECT_HFP, args);
}
public void setListener(BluetoothStateListener listener) {
mListener = listener;
}
public void onDeviceAdded(BluetoothDevice newDevice) {
SomeArgs args = SomeArgs.obtain();
args.arg1 = Log.createSubsession();
args.arg2 = newDevice.getAddress();
sendMessage(NEW_DEVICE_CONNECTED, args);
}
public void onDeviceLost(BluetoothDevice lostDevice) {
SomeArgs args = SomeArgs.obtain();
args.arg1 = Log.createSubsession();
args.arg2 = lostDevice.getAddress();
sendMessage(LOST_DEVICE, args);
}
private String connectHfpAudio(String address) {
return connectHfpAudio(address, true, null);
}
private String connectHfpAudio(String address, boolean shouldRetry) {
return connectHfpAudio(address, shouldRetry, null);
}
private String connectHfpAudio(String address, String excludeAddress) {
return connectHfpAudio(address, true, excludeAddress);
}
/**
* Initiates a HFP connection to the BT address specified.
* Note: This method is not synchronized on the Telecom lock, so don't try and call back into
* Telecom from within it.
* @param address The address that should be tried first. May be null.
* @param shouldRetry true if there should be a retry-with-backoff if connection is
* immediately unsuccessful, false otherwise.
* @param excludeAddress Don't connect to this address.
* @return The address of the device that's actually being connected to, or null if no
* connection was successful.
*/
private String connectHfpAudio(String address, boolean shouldRetry, String excludeAddress) {
BluetoothHeadsetProxy bluetoothHeadset = mDeviceManager.getHeadsetService();
if (bluetoothHeadset == null) {
Log.i(this, "connectHfpAudio: no headset service available.");
return null;
}
List<BluetoothDevice> deviceList = bluetoothHeadset.getConnectedDevices();
Optional<BluetoothDevice> matchingDevice = deviceList.stream()
.filter(d -> Objects.equals(d.getAddress(), address))
.findAny();
String actualAddress = matchingDevice.isPresent() ?
address : getPreferredDevice(excludeAddress);
if (!matchingDevice.isPresent()) {
Log.i(this, "No device with address %s available. Using %s instead.",
address, actualAddress);
}
if (actualAddress != null && !connectAudio(actualAddress)) {
Log.w(LOG_TAG, "Could not connect to %s. Will %s", shouldRetry ? "retry" : "not retry");
if (shouldRetry) {
SomeArgs args = SomeArgs.obtain();
args.arg1 = Log.createSubsession();
args.arg2 = actualAddress;
sendMessageDelayed(RETRY_HFP_CONNECTION, args,
mTimeoutsAdapter.getRetryBluetoothConnectAudioBackoffMillis(
mContext.getContentResolver()));
}
return null;
}
return actualAddress;
}
private String getPreferredDevice(String excludeAddress) {
String preferredDevice = null;
for (String address : mMostRecentlyUsedDevices) {
if (!Objects.equals(excludeAddress, address)) {
preferredDevice = address;
}
}
if (preferredDevice == null) {
return mDeviceManager.getMostRecentlyConnectedDevice(excludeAddress);
}
return preferredDevice;
}
private void transitionToActualState(int currentBtState) {
BluetoothDevice possiblyAlreadyConnectedDevice = getBluetoothAudioConnectedDevice();
if (possiblyAlreadyConnectedDevice != null) {
Log.i(LOG_TAG, "Device %s is already connected; going to AudioConnected.",
possiblyAlreadyConnectedDevice);
transitionTo(getConnectedStateForAddress(
possiblyAlreadyConnectedDevice.getAddress(), "transitionToActualState"));
// TODO: replace with new interface
mListener.onBluetoothStateChange(currentBtState, BLUETOOTH_AUDIO_CONNECTED);
} else {
transitionTo(mAudioOffState);
mListener.onBluetoothStateChange(currentBtState,
mDeviceManager.getNumConnectedDevices() > 0 ?
BLUETOOTH_DEVICE_CONNECTED : BLUETOOTH_DISCONNECTED);
}
}
/**
* @return The BluetoothDevice that is connected to BT audio, null if none are connected.
*/
@VisibleForTesting
public BluetoothDevice getBluetoothAudioConnectedDevice() {
BluetoothHeadsetProxy bluetoothHeadset = mDeviceManager.getHeadsetService();
if (bluetoothHeadset == null) {
Log.i(this, "getBluetoothAudioConnectedDevice: no headset service available.");
return null;
}
List<BluetoothDevice> deviceList = bluetoothHeadset.getConnectedDevices();
for (int i = 0; i < deviceList.size(); i++) {
BluetoothDevice device = deviceList.get(i);
boolean isAudioOn = bluetoothHeadset.isAudioConnected(device);
Log.v(this, "isBluetoothAudioConnected: ==> isAudioOn = " + isAudioOn
+ "for headset: " + device);
if (isAudioOn) {
return device;
}
}
return null;
}
private boolean connectAudio(String address) {
BluetoothHeadsetProxy bluetoothHeadset = mDeviceManager.getHeadsetService();
if (bluetoothHeadset == null) {
Log.w(this, "Trying to connect audio but no headset service exists.");
return false;
}
// TODO: update once connectAudio supports passing in a device.
return bluetoothHeadset.connectAudio();
}
private void disconnectAudio() {
BluetoothHeadsetProxy bluetoothHeadset = mDeviceManager.getHeadsetService();
if (bluetoothHeadset == null) {
Log.w(this, "Trying to disconnect audio but no headset service exists.");
} else {
bluetoothHeadset.disconnectAudio();
}
}
private boolean addDevice(String address) {
if (mAudioConnectingStates.containsKey(address)) {
Log.i(this, "Attempting to add device %s twice.", address);
return false;
}
AudioConnectedState audioConnectedState = new AudioConnectedState(address);
AudioConnectingState audioConnectingState = new AudioConnectingState(address);
mAudioConnectingStates.put(address, audioConnectingState);
mAudioConnectedStates.put(address, audioConnectedState);
addState(audioConnectedState);
addState(audioConnectingState);
return true;
}
private boolean removeDevice(String address) {
if (!mAudioConnectingStates.containsKey(address)) {
Log.i(this, "Attempting to remove already-removed device %s", address);
return false;
}
statesToCleanUp.add(mAudioConnectingStates.remove(address));
statesToCleanUp.add(mAudioConnectedStates.remove(address));
mMostRecentlyUsedDevices.remove(address);
return true;
}
private AudioConnectingState getConnectingStateForAddress(String address, String error) {
if (!mAudioConnectingStates.containsKey(address)) {
Log.w(LOG_TAG, "Device being connected to does not have a corresponding state: %s",
error);
addDevice(address);
}
return mAudioConnectingStates.get(address);
}
private AudioConnectedState getConnectedStateForAddress(String address, String error) {
if (!mAudioConnectedStates.containsKey(address)) {
Log.w(LOG_TAG, "Device already connected to does" +
" not have a corresponding state: %s", error);
addDevice(address);
}
return mAudioConnectedStates.get(address);
}
/**
* Removes the states for disconnected devices from the state machine. Called when entering
* AudioOff so that none of the states-to-be-removed are active.
*/
private void cleanupStatesForDisconnectedDevices() {
for (State state : statesToCleanUp) {
if (state != null) {
removeState(state);
}
}
statesToCleanUp.clear();
}
@VisibleForTesting
public void setInitialStateForTesting(String stateName, BluetoothDevice device) {
switch (stateName) {
case AUDIO_OFF_STATE_NAME:
transitionTo(mAudioOffState);
break;
case AUDIO_CONNECTING_STATE_NAME_PREFIX:
transitionTo(getConnectingStateForAddress(device.getAddress(),
"setInitialStateForTesting"));
break;
case AUDIO_CONNECTED_STATE_NAME_PREFIX:
transitionTo(getConnectedStateForAddress(device.getAddress(),
"setInitialStateForTesting"));
break;
}
}
}