blob: 5ab5af750ccb6a105a7e97250cee8cd3ccba0551 [file] [log] [blame]
/*
* Copyright (C) 2019 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.car;
import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_A2DP_SINK_DEVICES;
import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_HFP_CLIENT_DEVICES;
import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_MAP_CLIENT_DEVICES;
import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_PAN_DEVICES;
import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_PBAP_CLIENT_DEVICES;
import android.bluetooth.BluetoothA2dpSink;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadsetClient;
import android.bluetooth.BluetoothMapClient;
import android.bluetooth.BluetoothPan;
import android.bluetooth.BluetoothPbapClient;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.car.ICarBluetoothUserService;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import android.os.ParcelUuid;
import android.os.Parcelable;
import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* BluetoothProfileDeviceManager - Manages a list of devices, sorted by connection attempt priority.
* Provides a means for other applications to request connection events and adjust the device
* connection priorities. Access to these functions is provided through CarBluetoothManager.
*/
public class BluetoothProfileDeviceManager {
private static final String TAG = CarLog.tagFor(BluetoothProfileDeviceManager.class);
private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
private final Context mContext;
private final int mUserId;
private Set<String> mBondingDevices = new HashSet<>();
private static final String SETTINGS_DELIMITER = ",";
private static final int AUTO_CONNECT_TIMEOUT_MS = 8000;
private static final Object AUTO_CONNECT_TOKEN = new Object();
private static class BluetoothProfileInfo {
final String mSettingsKey;
final String mConnectionAction;
final ParcelUuid[] mUuids;
final int[] mProfileTriggers;
private BluetoothProfileInfo(String action, String settingsKey, ParcelUuid[] uuids,
int[] profileTriggers) {
mSettingsKey = settingsKey;
mConnectionAction = action;
mUuids = uuids;
mProfileTriggers = profileTriggers;
}
}
private static final SparseArray<BluetoothProfileInfo> sProfileActions = new SparseArray();
static {
sProfileActions.put(BluetoothProfile.A2DP_SINK,
new BluetoothProfileInfo(BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED,
KEY_BLUETOOTH_A2DP_SINK_DEVICES, new ParcelUuid[] {
BluetoothUuid.A2DP_SOURCE
}, new int[] {}));
sProfileActions.put(BluetoothProfile.HEADSET_CLIENT,
new BluetoothProfileInfo(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED,
KEY_BLUETOOTH_HFP_CLIENT_DEVICES, new ParcelUuid[] {
BluetoothUuid.HFP_AG,
BluetoothUuid.HSP_AG
}, new int[] {BluetoothProfile.MAP_CLIENT, BluetoothProfile.PBAP_CLIENT}));
sProfileActions.put(BluetoothProfile.MAP_CLIENT,
new BluetoothProfileInfo(BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED,
KEY_BLUETOOTH_MAP_CLIENT_DEVICES, new ParcelUuid[] {
BluetoothUuid.MAS
}, new int[] {}));
sProfileActions.put(BluetoothProfile.PAN,
new BluetoothProfileInfo(BluetoothPan.ACTION_CONNECTION_STATE_CHANGED,
KEY_BLUETOOTH_PAN_DEVICES, new ParcelUuid[] {
BluetoothUuid.PANU
}, new int[] {}));
sProfileActions.put(BluetoothProfile.PBAP_CLIENT,
new BluetoothProfileInfo(BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED,
KEY_BLUETOOTH_PBAP_CLIENT_DEVICES, new ParcelUuid[] {
BluetoothUuid.PBAP_PSE
}, new int[] {}));
}
// Fixed per-profile information for the profile this object manages
private final int mProfileId;
private final String mSettingsKey;
private final String mProfileConnectionAction;
private final ParcelUuid[] mProfileUuids;
private final int[] mProfileTriggers;
// Central priority list of devices
private final Object mPrioritizedDevicesLock = new Object();
@GuardedBy("mPrioritizedDevicesLock")
private ArrayList<BluetoothDevice> mPrioritizedDevices;
// Auto connection process state
private final Object mAutoConnectLock = new Object();
@GuardedBy("mAutoConnectLock")
private boolean mConnecting = false;
@GuardedBy("mAutoConnectLock")
private int mAutoConnectPriority;
@GuardedBy("mAutoConnectLock")
private ArrayList<BluetoothDevice> mAutoConnectingDevices;
private final BluetoothAdapter mBluetoothAdapter;
private final BluetoothBroadcastReceiver mBluetoothBroadcastReceiver;
private final ICarBluetoothUserService mBluetoothUserProxies;
private final Handler mHandler = new Handler(
CarServiceUtils.getHandlerThread(CarBluetoothService.THREAD_NAME).getLooper());
/**
* A BroadcastReceiver that listens specifically for actions related to the profile we're
* tracking and uses them to update the status.
*/
private class BluetoothBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (mProfileConnectionAction.equals(action)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE,
BluetoothProfile.STATE_DISCONNECTED);
handleDeviceConnectionStateChange(device, state);
} else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
BluetoothDevice.ERROR);
handleDeviceBondStateChange(device, state);
} else if (BluetoothDevice.ACTION_UUID.equals(action)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
Parcelable[] uuids = intent.getParcelableArrayExtra(BluetoothDevice.EXTRA_UUID);
handleDeviceUuidEvent(device, uuids);
} else if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
handleAdapterStateChange(state);
}
}
}
/**
* Handles an incoming Profile-Device connection event.
*
* On <BluetoothProfile>.ACTION_CONNECTION_STATE_CHANGED coming from the BroadcastReceiver:
* On connected, if we're auto connecting and this is the current device we're managing, then
* see if we can move on to the next device in the list. Otherwise, If the device connected
* then add it to our priority list if it's not on their already.
*
* On disconnected, if the device that disconnected also has had its profile priority set to
* PRIORITY_OFF, then remove it from our list.
*
* @param device - The Bluetooth device the state change is for
* @param state - The new profile connection state of the device
*/
private void handleDeviceConnectionStateChange(BluetoothDevice device, int state) {
logd("Connection state changed [device: " + device + ", state: "
+ Utils.getConnectionStateName(state) + "]");
if (state == BluetoothProfile.STATE_CONNECTED) {
if (isAutoConnecting() && isAutoConnectingDevice(device)) {
continueAutoConnecting();
} else {
if (getProfilePriority(device) >= BluetoothProfile.PRIORITY_ON) {
addDevice(device); // No-op if device is in the list.
}
triggerConnections(device);
}
}
// NOTE: We wanted check on disconnect if a device is priority off and use that as an
// indicator to remove a device from the list, but priority reporting can be flaky and
// was leading to us removing devices when we didn't want to.
}
/**
* Handles an incoming device bond status event.
*
* On BluetoothDevice.ACTION_BOND_STATE_CHANGED:
* - If a device becomes unbonded, remove it from our list if it's there.
* - If it's bonded, then add it to our list if the UUID set says it supports us.
*
* @param device - The Bluetooth device the state change is for
* @param state - The new bond state of the device
*/
private void handleDeviceBondStateChange(BluetoothDevice device, int state) {
logd("Bond state has changed [device: " + device + ", state: "
+ Utils.getBondStateName(state) + "]");
if (state == BluetoothDevice.BOND_NONE) {
mBondingDevices.remove(device.getAddress());
// Note: We have seen cases of unbonding events being sent without actually
// unbonding the device.
removeDevice(device);
} else if (state == BluetoothDevice.BOND_BONDING) {
mBondingDevices.add(device.getAddress());
} else if (state == BluetoothDevice.BOND_BONDED) {
addBondedDeviceIfSupported(device);
mBondingDevices.remove(device.getAddress());
}
}
/**
* Handles an incoming device UUID set update event for bonding devices.
*
* On BluetoothDevice.ACTION_UUID:
* If the UUID is one this profile cares about, set the profile priority for the device that
* the UUID was found on to PRIORITY_ON if its not PRIORITY_OFF already (meaning inhibited or
* disabled by the user through settings).
*
* @param device - The Bluetooth device the UUID event is for
* @param uuids - The incoming set of supported UUIDs for the device
*/
private void handleDeviceUuidEvent(BluetoothDevice device, Parcelable[] uuids) {
logd("UUIDs found, device: " + device);
if (!mBondingDevices.remove(device.getAddress())) return;
if (uuids != null) {
ParcelUuid[] uuidsToSend = new ParcelUuid[uuids.length];
for (int i = 0; i < uuidsToSend.length; i++) {
uuidsToSend[i] = (ParcelUuid) uuids[i];
}
provisionDeviceIfSupported(device, uuidsToSend);
}
}
/**
* Handle an adapter state change event.
*
* On BluetoothAdapter.ACTION_STATE_CHANGED:
* If the adapter is going into the OFF state, then cancel any auto connecting, commit our
* priority list and go idle.
*
* @param state - The new state of the Bluetooth adapter
*/
private void handleAdapterStateChange(int state) {
logd("Bluetooth Adapter state changed: " + Utils.getAdapterStateName(state));
// Crashes of the BT stack mean we're not promised to see all the state changes we
// might want to see. In order to be a bit more robust to crashes, we'll treat any
// non-ON state as a time to cancel auto-connect. This gives us a better chance of
// seeing a cancel state before a crash, as well as makes sure we're "cancelled"
// before we see an ON.
if (state != BluetoothAdapter.STATE_ON) {
cancelAutoConnecting();
}
// To reduce how many times we're committing the list, we'll only write back on off
if (state == BluetoothAdapter.STATE_OFF) {
commit();
}
}
/**
* Creates an instance of BluetoothProfileDeviceManager that will manage devices
* for the given profile ID.
*
* @param context - context of calling code
* @param userId - ID of user we want to manage devices for
* @param bluetoothUserProxies - Set of per-user bluetooth proxies for calling into the
* bluetooth stack as the current user.
* @param profileId - BluetoothProfile integer that represents the profile we're managing
* @return A new instance of a BluetoothProfileDeviceManager, or null on any error
*/
public static BluetoothProfileDeviceManager create(Context context, int userId,
ICarBluetoothUserService bluetoothUserProxies, int profileId) {
try {
return new BluetoothProfileDeviceManager(context, userId, bluetoothUserProxies,
profileId);
} catch (NullPointerException | IllegalArgumentException e) {
return null;
}
}
/**
* Creates an instance of BluetoothProfileDeviceManager that will manage devices
* for the given profile ID.
*
* @param context - context of calling code
* @param userId - ID of user we want to manage devices for
* @param bluetoothUserProxies - Set of per-user bluetooth proxies for calling into the
* bluetooth stack as the current user.
* @param profileId - BluetoothProfile integer that represents the profile we're managing
* @return A new instance of a BluetoothProfileDeviceManager
*/
private BluetoothProfileDeviceManager(Context context, int userId,
ICarBluetoothUserService bluetoothUserProxies, int profileId) {
mContext = Objects.requireNonNull(context);
mUserId = userId;
mBluetoothUserProxies = bluetoothUserProxies;
mPrioritizedDevices = new ArrayList<>();
BluetoothProfileInfo bpi = sProfileActions.get(profileId);
if (bpi == null) {
throw new IllegalArgumentException("Provided profile " + Utils.getProfileName(profileId)
+ " is unrecognized");
}
mProfileId = profileId;
mSettingsKey = bpi.mSettingsKey;
mProfileConnectionAction = bpi.mConnectionAction;
mProfileUuids = bpi.mUuids;
mProfileTriggers = bpi.mProfileTriggers;
mBluetoothBroadcastReceiver = new BluetoothBroadcastReceiver();
mBluetoothAdapter = Objects.requireNonNull(BluetoothAdapter.getDefaultAdapter());
}
/**
* Begin managing devices for this profile. Sets the start state from persistent memory.
*/
public void start() {
logd("Starting device management");
load();
synchronized (mAutoConnectLock) {
mConnecting = false;
mAutoConnectPriority = -1;
mAutoConnectingDevices = null;
}
IntentFilter profileFilter = new IntentFilter();
profileFilter.addAction(mProfileConnectionAction);
profileFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
profileFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
profileFilter.addAction(BluetoothDevice.ACTION_UUID);
mContext.registerReceiverAsUser(mBluetoothBroadcastReceiver, UserHandle.CURRENT,
profileFilter, null, null);
}
/**
* Stop managing devices for this profile. Commits the final priority list to persistent memory
* and cleans up local resources.
*/
public void stop() {
logd("Stopping device management");
if (mBluetoothBroadcastReceiver != null) {
if (mContext != null) {
mContext.unregisterReceiver(mBluetoothBroadcastReceiver);
}
}
cancelAutoConnecting();
commit();
return;
}
/**
* Loads the current device priority list from persistent memory in {@link Settings.Secure}.
*
* This will overwrite the contents of the local priority list. It does not attempt to take the
* union of the file and existing set. As such, you likely do not want to load after starting.
* Failed attempts to load leave the prioritized device list unchanged.
*
* @return true on success, false otherwise
*/
private boolean load() {
logd("Loading device priority list snapshot using key '" + mSettingsKey + "'");
// Read from Settings.Secure for our profile, as the current user.
String devicesStr = Settings.Secure.getStringForUser(mContext.getContentResolver(),
mSettingsKey, mUserId);
logd("Found Device String: '" + devicesStr + "'");
if (devicesStr == null || "".equals(devicesStr)) {
return false;
}
// Split string into list of device MAC addresses
List<String> deviceList = Arrays.asList(devicesStr.split(SETTINGS_DELIMITER));
if (deviceList == null) {
return false;
}
// Turn the strings into full blown Bluetooth devices
ArrayList<BluetoothDevice> devices = new ArrayList<>();
for (String address : deviceList) {
try {
BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
devices.add(device);
} catch (IllegalArgumentException e) {
logw("Unable to parse address '" + address + "' to a device");
continue;
}
}
synchronized (mPrioritizedDevicesLock) {
mPrioritizedDevices = devices;
}
logd("Loaded Priority list: " + devices);
return true;
}
/**
* Commits the current device priority list to persistent memory in {@link Settings.Secure}.
*
* @return true on success, false otherwise
*/
private boolean commit() {
StringBuilder sb = new StringBuilder();
String delimiter = "";
synchronized (mPrioritizedDevicesLock) {
for (BluetoothDevice device : mPrioritizedDevices) {
sb.append(delimiter);
sb.append(device.getAddress());
delimiter = SETTINGS_DELIMITER;
}
}
String devicesStr = sb.toString();
Settings.Secure.putStringForUser(mContext.getContentResolver(), mSettingsKey, devicesStr,
mUserId);
logd("Committed key: " + mSettingsKey + ", value: '" + devicesStr + "'");
return true;
}
/**
* Syncs the current priority list against the list of bonded devices from the adapter so that
* we can make sure things haven't changed on us between the last two times we've ran.
*/
private void sync() {
logd("Syncing the priority list with the adapter's list of bonded devices");
Set<BluetoothDevice> bondedDevices = mBluetoothAdapter.getBondedDevices();
for (BluetoothDevice device : bondedDevices) {
addDevice(device); // No-op if device is already in the priority list
}
synchronized (mPrioritizedDevicesLock) {
ArrayList<BluetoothDevice> devices = getDeviceListSnapshot();
for (BluetoothDevice device : devices) {
if (!bondedDevices.contains(device)) {
removeDevice(device);
}
}
}
}
/**
* Makes a clone of the current prioritized device list in a synchronized fashion
*
* @return A clone of the most up to date prioritized device list
*/
public ArrayList<BluetoothDevice> getDeviceListSnapshot() {
ArrayList<BluetoothDevice> devices = new ArrayList<>();
synchronized (mPrioritizedDevicesLock) {
devices = (ArrayList) mPrioritizedDevices.clone();
}
return devices;
}
/**
* Adds a device to the end of the priority list.
*
* @param device - The device you wish to add
*/
public void addDevice(BluetoothDevice device) {
if (device == null) return;
synchronized (mPrioritizedDevicesLock) {
if (mPrioritizedDevices.contains(device)) return;
logd("Add device " + device);
mPrioritizedDevices.add(device);
commit();
}
}
/**
* Removes a device from the priority list.
*
* @param device - The device you wish to remove
*/
public void removeDevice(BluetoothDevice device) {
if (device == null) return;
synchronized (mPrioritizedDevicesLock) {
if (!mPrioritizedDevices.contains(device)) return;
logd("Remove device " + device);
mPrioritizedDevices.remove(device);
commit();
}
}
/**
* Get the connection priority of a device.
*
* @param device - The device you want the priority of
* @return The priority of the device, or -1 if the device is not in the list
*/
public int getDeviceConnectionPriority(BluetoothDevice device) {
if (device == null) return -1;
logd("Get connection priority of " + device);
synchronized (mPrioritizedDevicesLock) {
return mPrioritizedDevices.indexOf(device);
}
}
/**
* Set the connection priority of a device.
*
* If the devide does not exist, it will be added. If the priority is less than zero,
* no priority will be set. If the priority exceeds the bounds of the list, no priority will be
* set.
*
* @param device - The device you want to set the priority of
* @param priority - The priority you want to the device to have
*/
public void setDeviceConnectionPriority(BluetoothDevice device, int priority) {
synchronized (mPrioritizedDevicesLock) {
if (device == null || priority < 0 || priority > mPrioritizedDevices.size()
|| getDeviceConnectionPriority(device) == priority) return;
if (mPrioritizedDevices.contains(device)) {
mPrioritizedDevices.remove(device);
if (priority > mPrioritizedDevices.size()) priority = mPrioritizedDevices.size();
}
logd("Set connection priority of " + device + " to " + priority);
mPrioritizedDevices.add(priority, device);
commit();
}
}
/**
* Connect a specific device on this profile.
*
* @param device - The device to connect
* @return true on success, false otherwise
*/
private boolean connect(BluetoothDevice device) {
logd("Connecting " + device);
try {
return mBluetoothUserProxies.bluetoothConnectToProfile(mProfileId, device);
} catch (RemoteException e) {
logw("Failed to connect " + device + ", Reason: " + e);
}
return false;
}
/**
* Disconnect a specific device from this profile.
*
* @param device - The device to disconnect
* @return true on success, false otherwise
*/
private boolean disconnect(BluetoothDevice device) {
logd("Disconnecting " + device);
try {
return mBluetoothUserProxies.bluetoothDisconnectFromProfile(mProfileId, device);
} catch (RemoteException e) {
logw("Failed to disconnect " + device + ", Reason: " + e);
}
return false;
}
/**
* Gets the Bluetooth stack priority on this profile for a specific device.
*
* @param device - The device to get the Bluetooth stack priority of
* @return The Bluetooth stack priority on this profile for the given device
*/
private int getProfilePriority(BluetoothDevice device) {
try {
return mBluetoothUserProxies.getProfilePriority(mProfileId, device);
} catch (RemoteException e) {
logw("Failed to get bluetooth stack priority for " + device + ", Reason: " + e);
}
return BluetoothProfile.PRIORITY_UNDEFINED;
}
/**
* Gets the Bluetooth stack priority on this profile for a specific device.
*
* @param device - The device to set the Bluetooth stack priority of
* @return true on success, false otherwise
*/
private boolean setProfilePriority(BluetoothDevice device, int priority) {
logd("Set " + device + " stack priority to " + Utils.getProfilePriorityName(priority));
try {
mBluetoothUserProxies.setProfilePriority(mProfileId, device, priority);
} catch (RemoteException e) {
logw("Failed to set bluetooth stack priority for " + device + ", Reason: " + e);
return false;
}
return true;
}
/**
* Begins the process of connecting to devices, one by one, in the order that the priority
* list currently specifies.
*
* If we are already connecting, or no devices are present, then no work is done.
*/
public void beginAutoConnecting() {
logd("Request to begin auto connection process");
synchronized (mAutoConnectLock) {
if (isAutoConnecting()) {
logd("Auto connect requested while we are already auto connecting.");
return;
}
if (mBluetoothAdapter.getState() != BluetoothAdapter.STATE_ON) {
logd("Bluetooth Adapter is not on, cannot connect devices");
return;
}
mAutoConnectingDevices = getDeviceListSnapshot();
if (mAutoConnectingDevices.size() == 0) {
logd("No saved devices to auto-connect to.");
cancelAutoConnecting();
return;
}
mConnecting = true;
mAutoConnectPriority = 0;
}
autoConnectWithTimeout();
}
/**
* Connects the current priority device and sets a timeout timer to indicate when to give up and
* move on to the next one.
*/
private void autoConnectWithTimeout() {
synchronized (mAutoConnectLock) {
if (!isAutoConnecting()) {
logd("Autoconnect process was cancelled, skipping connecting next device.");
return;
}
if (mAutoConnectPriority < 0 || mAutoConnectPriority >= mAutoConnectingDevices.size()) {
return;
}
BluetoothDevice device = mAutoConnectingDevices.get(mAutoConnectPriority);
logd("Auto connecting (" + mAutoConnectPriority + ") device: " + device);
mHandler.post(() -> {
boolean connectStatus = connect(device);
if (!connectStatus) {
logw("Connection attempt immediately failed, moving to the next device");
continueAutoConnecting();
}
});
mHandler.postDelayed(() -> {
logw("Auto connect process has timed out connecting to " + device);
continueAutoConnecting();
}, AUTO_CONNECT_TOKEN, AUTO_CONNECT_TIMEOUT_MS);
}
}
/**
* Will forcibly move the auto connect process to the next device, or finish it if no more
* devices are available.
*/
private void continueAutoConnecting() {
logd("Continue auto-connect process on next device");
synchronized (mAutoConnectLock) {
if (!isAutoConnecting()) {
logd("Autoconnect process was cancelled, no need to continue.");
return;
}
mHandler.removeCallbacksAndMessages(AUTO_CONNECT_TOKEN);
mAutoConnectPriority++;
if (mAutoConnectPriority >= mAutoConnectingDevices.size()) {
logd("No more devices to connect to");
cancelAutoConnecting();
return;
}
}
autoConnectWithTimeout();
}
/**
* Cancels the auto-connection process. Any in-flight connection attempts will still be tried.
*
* Canceling is defined as deleting the snapshot of devices, resetting the device to connect
* index, setting the connecting boolean to null, and removing any pending timeouts if they
* exist.
*
* If there are no auto-connects in process this will do nothing.
*/
private void cancelAutoConnecting() {
logd("Cleaning up any auto-connect process");
synchronized (mAutoConnectLock) {
if (!isAutoConnecting()) return;
mHandler.removeCallbacksAndMessages(AUTO_CONNECT_TOKEN);
mConnecting = false;
mAutoConnectPriority = -1;
mAutoConnectingDevices = null;
}
}
/**
* Get the auto-connect status of thie profile device manager
*
* @return true on success, false otherwise
*/
public boolean isAutoConnecting() {
synchronized (mAutoConnectLock) {
return mConnecting;
}
}
/**
* Determine if a device is the currently auto-connecting device
*
* @param device - A BluetoothDevice object to compare against any know auto connecting device
* @return true if the input device is the device we're currently connecting, false otherwise
*/
private boolean isAutoConnectingDevice(BluetoothDevice device) {
synchronized (mAutoConnectLock) {
if (mAutoConnectingDevices == null) return false;
return mAutoConnectingDevices.get(mAutoConnectPriority).equals(device);
}
}
/**
* Given a device, will check the cached UUID set and see if it supports this profile. If it
* does then we will add it to the end of our prioritized set and attempt a connection if and
* only if the Bluetooth device priority allows a connection.
*
* Will do nothing if the device isn't bonded.
*/
private void addBondedDeviceIfSupported(BluetoothDevice device) {
logd("Add device " + device + " if it is supported");
if (device.getBondState() != BluetoothDevice.BOND_BONDED) return;
if (BluetoothUuid.containsAnyUuid(device.getUuids(), mProfileUuids)
&& getProfilePriority(device) >= BluetoothProfile.PRIORITY_ON) {
addDevice(device);
}
}
/**
* Checks the reported UUIDs for a device to see if the device supports this profile. If it does
* then it will update the underlying Bluetooth stack with PRIORITY_ON so long as the device
* doesn't have a PRIORITY_OFF value set.
*
* @param device - The device that may support our profile
* @param uuids - The set of UUIDs for the device, which may include our profile
*/
private void provisionDeviceIfSupported(BluetoothDevice device, ParcelUuid[] uuids) {
logd("Checking UUIDs for device: " + device);
if (BluetoothUuid.containsAnyUuid(uuids, mProfileUuids)) {
int devicePriority = getProfilePriority(device);
logd("Device " + device + " supports this profile. Priority: "
+ Utils.getProfilePriorityName(devicePriority));
// Transition from PRIORITY_OFF to any other Bluetooth stack priority value is supposed
// to be a user choice, enabled through the Settings applications. That's why we don't
// do it here for them.
if (devicePriority == BluetoothProfile.PRIORITY_UNDEFINED) {
// As a note, UUID updates happen during pairing, as well as each time the adapter
// turns on. Initiating connections to bonded device following UUID verification
// would defeat the purpose of the priority list. They don't arrive in a predictable
// order either. Since we call this function on UUID discovery, don't connect here!
setProfilePriority(device, BluetoothProfile.PRIORITY_ON);
return;
}
}
logd("Provisioning of " + device + " has ended without priority being set");
}
/**
* Trigger connections of related Bluetooth profiles on a device
*
* @param device - The Bluetooth device you would like to connect to
*/
private void triggerConnections(BluetoothDevice device) {
for (int profile : mProfileTriggers) {
logd("Trigger connection to " + Utils.getProfileName(profile) + "on " + device);
try {
mBluetoothUserProxies.bluetoothConnectToProfile(profile, device);
} catch (RemoteException e) {
logw("Failed to connect " + device + ", Reason: " + e);
}
}
}
/**
* Writes the verbose current state of the object to the PrintWriter
*
* @param writer PrintWriter object to write lines to
*/
public void dump(PrintWriter writer, String indent) {
writer.println(indent + "BluetoothProfileDeviceManager [" + Utils.getProfileName(mProfileId)
+ "]");
writer.println(indent + "\tUser: " + mUserId);
writer.println(indent + "\tSettings Location: " + mSettingsKey);
writer.println(indent + "\tUser Proxies Exist: "
+ (mBluetoothUserProxies != null ? "Yes" : "No"));
writer.println(indent + "\tAuto-Connecting: " + (isAutoConnecting() ? "Yes" : "No"));
writer.println(indent + "\tPriority List:");
ArrayList<BluetoothDevice> devices = getDeviceListSnapshot();
for (BluetoothDevice device : devices) {
writer.println(indent + "\t\t" + device.getAddress() + " - " + device.getName());
}
}
/**
* Log a message to DEBUG
*/
private void logd(String msg) {
if (DBG) {
Slog.d(TAG, "[" + Utils.getProfileName(mProfileId) + " - User: " + mUserId + "] "
+ msg);
}
}
/**
* Log a message to WARN
*/
private void logw(String msg) {
Slog.w(TAG, "[" + Utils.getProfileName(mProfileId) + " - User: " + mUserId + "] " + msg);
}
}