blob: aa52e730541868ded87e647897ae5116845811b2 [file] [log] [blame]
/*
* Copyright (C) 2017 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_AUTOCONNECT_MESSAGING_DEVICES;
import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_AUTOCONNECT_MUSIC_DEVICES;
import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_AUTOCONNECT_NETWORK_DEVICES;
import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_AUTOCONNECT_PHONE_DEVICES;
import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_PROFILES_INHIBITED;
import android.annotation.Nullable;
import android.app.ActivityManager;
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.Car;
import android.car.CarBluetoothManager;
import android.car.CarNotConnectedException;
import android.car.ICarBluetoothUserService;
import android.car.ICarUserService;
import android.car.drivingstate.CarUxRestrictions;
import android.car.drivingstate.ICarUxRestrictionsChangeListener;
import android.car.hardware.CarPropertyValue;
import android.car.hardware.power.CarPowerManager;
import android.car.hardware.power.CarPowerManager.CarPowerStateListener;
import android.car.hardware.property.CarPropertyEvent;
import android.car.hardware.property.ICarPropertyEventListener;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.hardware.automotive.vehicle.V2_0.VehicleIgnitionState;
import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.ParcelUuid;
import android.os.Parcelable;
import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
/**
* A Bluetooth Device Connection policy that is specific to the use cases of a Car. A car's
* bluetooth capabilities in terms of the profiles it supports and its use cases are unique.
* Hence the CarService manages the policy that drives when and what to connect to.
*
* When to connect:
* The policy can be configured to listen to various vehicle events that are appropriate to
* trigger a connection attempt. Signals like door unlock/open, ignition state changes indicate
* user entry and there by attempt to connect to their devices. This removes the need for the user
* to manually connect his device everytime they get in a car.
*
* Which device to connect:
* The policy also keeps track of the {Profile : DevicesThatCanConnectOnTheProfile} and when
* it is time to connect, picks the device that is appropriate and available.
* For every profile, the policy attempts to connect to the last connected device first. The policy
* maintains a list of connect-able devices for every profile, in the order of how recently they
* connected. The device that successfully connects on a profile is moved to the top of the list
* of devices for that profile, so the next time a connection attempt is made, the policy starts
* with the last connected device first.
*/
public class BluetoothDeviceConnectionPolicy {
private static final String TAG = "BTDevConnectionPolicy";
private static final String SETTINGS_DELIMITER = ",";
private static final boolean DBG = Utils.DBG;
private static final Binder RESTORED_PROFILE_INHIBIT_TOKEN = new Binder();
private static final long RESTORE_BACKOFF_MILLIS = 1000L;
private final Context mContext;
private boolean mInitialized = false;
private boolean mUserSpecificInfoInitialized = false;
private final Object mSetupLock = new Object();
// The main data structure that holds on to the {profile:list of known and connectible devices}
HashMap<Integer, BluetoothDevicesInfo> mProfileToConnectableDevicesMap;
// Keep a map of the maximum number of connections allowed for any profile we plan to support.
private static final Map<Integer, Integer> sNumSupportedActiveConnections = new HashMap<>();
static {
sNumSupportedActiveConnections.put(BluetoothProfile.HEADSET_CLIENT, 4);
sNumSupportedActiveConnections.put(BluetoothProfile.PBAP_CLIENT, 4);
sNumSupportedActiveConnections.put(BluetoothProfile.A2DP_SINK, 1);
sNumSupportedActiveConnections.put(BluetoothProfile.MAP_CLIENT, 4);
sNumSupportedActiveConnections.put(BluetoothProfile.PAN, 1);
}
private BluetoothAutoConnectStateMachine mBluetoothAutoConnectStateMachine;
private final BluetoothAdapter mBluetoothAdapter;
private BroadcastReceiver mBluetoothBroadcastReceiver;
private ICarUserService mCarUserService;
private PerUserCarServiceHelper mUserServiceHelper;
private ICarBluetoothUserService mCarBluetoothUserService;
private ReentrantLock mCarUserServiceAccessLock;
// Events that are listened to for triggering an auto-connect:
// Door unlock and ignition switch ON come from Car Property Service
private final CarPropertyService mCarPropertyService;
private final CarPropertyListener mPropertyEventListener;
// Car service binder to setup listening for power manager updates
private final Car mCar;
private CarPowerManager mCarPowerManager;
private final CarPowerStateListener mCarPowerStateListener = new CarPowerStateListener() {
@Override
public void onStateChanged(int state, CompletableFuture<Void> future) {
if (DBG) Log.d(TAG, "Car power state has changed to " + state);
// ON is the state when user turned on the car (it can be either ignition or
// door unlock) the policy for ON is defined by OEMs and we can rely on that.
if (state == CarPowerManager.CarPowerStateListener.ON) {
Log.i(TAG, "Car is powering on. Enable Bluetooth and auto-connect to devices.");
if (isBluetoothPersistedOn()) {
enabledBluetooth();
}
initiateConnection();
return;
}
// Since we're appearing to be off after shutdown prepare, but may stay on in idle mode,
// we'll turn off Bluetooth to disconnect devices and better the "off" illusion
if (state == CarPowerManager.CarPowerStateListener.SHUTDOWN_PREPARE) {
Log.i(TAG, "Car is preparing for shutdown. Disable bluetooth adapter.");
disableBluetooth();
// Let CPMS know we're ready to shutdown. Otherwise, CPMS will get stuck for
// up to an hour.
if (future != null) {
future.complete(null);
}
return;
}
}
};
private final ServiceConnection mCarServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.i(TAG, "Car is now connected, getting CarPowerManager service");
try {
mCarPowerManager = (CarPowerManager) mCar.getCarManager(Car.POWER_SERVICE);
mCarPowerManager.setListener(mCarPowerStateListener);
} catch (CarNotConnectedException e) {
Log.e(TAG, "Failed to get CarPowerManager instance", e);
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.i(TAG, "Car is now disconnected");
if (mCarPowerManager != null) {
mCarPowerManager.clearListener();
}
}
};
// PerUserCarService related listeners
private final UserServiceConnectionCallback mServiceCallback;
// Car Bluetooth Priority Settings Manager
private final CarBluetoothService mCarBluetoothService;
// Car UX Restrictions Manager Service to know when user restrictions are in place
private final CarUxRestrictionsManagerService mUxRService;
private final CarUxRServiceListener mUxRListener;
// Fast Pair Provider to allow discovery of new phones
private final FastPairProvider mFastPairProvider;
// The Bluetooth profiles that the CarService will try to auto-connect on.
final List<Integer> mProfilesToConnect;
private final List<Integer> mPrioritiesSupported;
private static final int MAX_CONNECT_RETRIES = 1;
private static final int PROFILE_NOT_AVAILABLE = -1;
private int mUserId;
// Device & Profile currently being connected on
private ConnectionParams mConnectionInFlight;
// Allow write to Settings.Secure
private boolean mAllowReadWriteToSettings = true;
// Maintain a list of Paired devices which haven't connected on any profiles yet.
private Set<BluetoothDevice> mPairedButUnconnectedDevices = new HashSet<>();
// State for profile inhibits.
@GuardedBy("this")
private final SetMultimap<ConnectionParams, InhibitRecord> mProfileInhibits =
new SetMultimap<>();
@GuardedBy("this")
private final HashSet<InhibitRecord> mRestoredInhibits = new HashSet<>();
@GuardedBy("this")
private final HashSet<ConnectionParams> mAlreadyDisabledProfiles = new HashSet<>();
private final Handler mHandler = new Handler(Looper.getMainLooper());
public static BluetoothDeviceConnectionPolicy create(Context context,
CarPropertyService carPropertyService, PerUserCarServiceHelper userServiceHelper,
CarUxRestrictionsManagerService uxrService, CarBluetoothService bluetoothService) {
return new BluetoothDeviceConnectionPolicy(context, carPropertyService, userServiceHelper,
uxrService, bluetoothService);
}
private BluetoothDeviceConnectionPolicy(Context context, CarPropertyService carPropertyService,
PerUserCarServiceHelper userServiceHelper, CarUxRestrictionsManagerService uxrService,
CarBluetoothService bluetoothService) {
mContext = context;
mCarPropertyService = carPropertyService;
mUserServiceHelper = userServiceHelper;
mUxRService = uxrService;
mCarBluetoothService = bluetoothService;
mCarUserServiceAccessLock = new ReentrantLock();
mProfilesToConnect = Arrays.asList(
BluetoothProfile.HEADSET_CLIENT, BluetoothProfile.A2DP_SINK,
BluetoothProfile.PBAP_CLIENT, BluetoothProfile.MAP_CLIENT, BluetoothProfile.PAN);
mPrioritiesSupported = Arrays.asList(
CarBluetoothManager.BLUETOOTH_DEVICE_CONNECTION_PRIORITY_0,
CarBluetoothManager.BLUETOOTH_DEVICE_CONNECTION_PRIORITY_1
);
// Listen to events for triggering auto connect
mPropertyEventListener = new CarPropertyListener();
// Listen to UX Restrictions to know when to enable fast-pairing
mUxRListener = new CarUxRServiceListener();
// Listen to User switching to connect to per User device.
mServiceCallback = new UserServiceConnectionCallback();
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter == null) {
Log.w(TAG, "No Bluetooth Adapter Available");
}
mFastPairProvider = new FastPairProvider(mContext);
// Connect to car
mCar = Car.createCar(context, mCarServiceConnection);
mCar.connect();
}
/**
* ConnectionParams - parameters/objects relevant to the bluetooth connection calls.
* This encapsulates the information that is passed around across different methods in the
* policy. Contains the bluetooth device {@link BluetoothDevice} and the list of profiles that
* we want that device to connect on.
* Used as the currency that methods use to talk to each other in the policy.
*/
public static class ConnectionParams {
// Examples:
// 01:23:45:67:89:AB/9
// null/0
// null/null
private static final String FLATTENED_PATTERN =
"^(([0-9A-F]{2}:){5}[0-9A-F]{2}|null)/([0-9]+|null)$";
@Nullable private final BluetoothDevice mBluetoothDevice;
@Nullable private final Integer mBluetoothProfile;
public ConnectionParams() {
this(null, null);
}
public ConnectionParams(@Nullable Integer profile) {
this(profile, null);
}
public ConnectionParams(@Nullable Integer profile, @Nullable BluetoothDevice device) {
mBluetoothProfile = profile;
mBluetoothDevice = device;
}
@Nullable
public BluetoothDevice getBluetoothDevice() {
return mBluetoothDevice;
}
@Nullable
public Integer getBluetoothProfile() {
return mBluetoothProfile;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof ConnectionParams)) {
return false;
}
ConnectionParams otherParams = (ConnectionParams) other;
return Objects.equals(mBluetoothDevice, otherParams.mBluetoothDevice)
&& Objects.equals(mBluetoothProfile, otherParams.mBluetoothProfile);
}
@Override
public int hashCode() {
return Objects.hash(mBluetoothDevice, mBluetoothProfile);
}
@Override
public String toString() {
return flattenToString();
}
/** Converts these {@link ConnectionParams} to a parseable string representation. */
public String flattenToString() {
return mBluetoothDevice + "/" + mBluetoothProfile;
}
/**
* Creates a {@link ConnectionParams} from a previous output of {@link #flattenToString()}.
*
* @param flattenedParams A flattened string representation of a {@link ConnectionParams}.
* @param adapter A {@link BluetoothAdapter} used to convert Bluetooth addresses into
* {@link BluetoothDevice} objects.
*/
public static ConnectionParams parse(String flattenedParams, BluetoothAdapter adapter) {
if (!flattenedParams.matches(FLATTENED_PATTERN)) {
throw new IllegalArgumentException("Bad format for flattened ConnectionParams");
}
String[] parts = flattenedParams.split("/");
BluetoothDevice device;
if (!"null".equals(parts[0])) {
device = adapter.getRemoteDevice(parts[0]);
} else {
device = null;
}
Integer profile;
if (!"null".equals(parts[1])) {
profile = Integer.valueOf(parts[1]);
} else {
profile = null;
}
return new ConnectionParams(profile, device);
}
}
private class InhibitRecord implements IBinder.DeathRecipient {
private final ConnectionParams mParams;
private final IBinder mToken;
private boolean mRemoved = false;
InhibitRecord(ConnectionParams params, IBinder token) {
this.mParams = params;
this.mToken = token;
}
public ConnectionParams getParams() {
return mParams;
}
public IBinder getToken() {
return mToken;
}
public boolean removeSelf() {
synchronized (BluetoothDeviceConnectionPolicy.this) {
if (mRemoved) {
return true;
}
if (removeInhibitRecord(this)) {
mRemoved = true;
return true;
} else {
return false;
}
}
}
@Override
public void binderDied() {
if (DBG) {
Log.d(TAG, "Releasing inhibit request on profile "
+ Utils.getProfileName(mParams.getBluetoothProfile())
+ " for device " + mParams.getBluetoothDevice()
+ ": requesting process died");
}
removeSelf();
}
}
/**
* BluetoothBroadcastReceiver receives the bluetooth related intents that are relevant to
* connection
* and bonding state changes. Reports the information to the {@link
* BluetoothDeviceConnectionPolicy}
* for it update its status.
*/
public class BluetoothBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (DBG) {
if (device != null) {
Log.d(TAG, "Received Intent for device: " + device + " " + action);
} else {
Log.d(TAG, "Received Intent no device: " + action);
}
}
ConnectionParams connectParams;
if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) {
int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
BluetoothDevice.ERROR);
updateBondState(device, bondState);
} else if (BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
connectParams = new ConnectionParams(BluetoothProfile.A2DP_SINK, device);
int currState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE,
BluetoothProfile.STATE_DISCONNECTED);
notifyConnectionStatus(connectParams, currState);
} else if (BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
connectParams = new ConnectionParams(BluetoothProfile.HEADSET_CLIENT, device);
int currState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE,
BluetoothProfile.STATE_DISCONNECTED);
notifyConnectionStatus(connectParams, currState);
} else if (BluetoothPan.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
connectParams = new ConnectionParams(BluetoothProfile.PAN, device);
int currState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE,
BluetoothProfile.STATE_DISCONNECTED);
notifyConnectionStatus(connectParams, currState);
} else if (BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
connectParams = new ConnectionParams(BluetoothProfile.PBAP_CLIENT, device);
int currState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE,
BluetoothProfile.STATE_DISCONNECTED);
notifyConnectionStatus(connectParams, currState);
} else if (BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
connectParams = new ConnectionParams(BluetoothProfile.MAP_CLIENT, device);
int currState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE,
BluetoothProfile.STATE_DISCONNECTED);
notifyConnectionStatus(connectParams, currState);
} else if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
int currState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
if (DBG) {
Log.d(TAG, "Bluetooth Adapter State: " + currState);
}
if (currState == BluetoothAdapter.STATE_ON) {
// Read from Settings which devices to connect to and populate
// mProfileToConnectableDevicesMap
readAndRebuildDeviceMapFromSettings();
initiateConnection();
} else if (currState == BluetoothAdapter.STATE_OFF) {
// Write currently connected device snapshot to file.
mBluetoothAutoConnectStateMachine.sendMessage(
BluetoothAutoConnectStateMachine.ADAPTER_OFF);
writeDeviceInfoToSettings();
resetBluetoothDevicesConnectionInfo();
}
} else if (BluetoothDevice.ACTION_UUID.equals(action)) {
// Received during pairing with the UUIDs of the Bluetooth profiles supported by
// the remote device.
if (DBG) {
Log.d(TAG, "Received UUID intent for device " + device);
}
Parcelable[] uuids = intent.getParcelableArrayExtra(BluetoothDevice.EXTRA_UUID);
if (uuids != null) {
ParcelUuid[] uuidsToSend = new ParcelUuid[uuids.length];
for (int i = 0; i < uuidsToSend.length; i++) {
uuidsToSend[i] = (ParcelUuid) uuids[i];
}
setProfilePriorities(device, uuidsToSend, BluetoothProfile.PRIORITY_ON);
}
}
}
}
/**
* Set priority for the Bluetooth profiles.
*
* The Bluetooth service stores the priority of a Bluetooth profile per device as a key value
* pair - BluetoothProfile_device:<Priority>.
* When we pair a device from the Settings App, the expected behavior is for the app to connect
* on all appropriate profiles after successful pairing automatically, without the user having
* to explicitly issue a connect. The settings app checks for the priority of the device from
* the above key-value pair and if the priority is set to PRIORITY_OFF or PRIORITY_UNDEFINED,
* the settings app will stop with just pairing and not connect.
* This scenario will happen when we pair a device, then unpair it and then pair it again. When
* the device is unpaired, the BT stack sets the priority for that device to PRIORITY_UNDEFINED
* ( as a way of resetting). So, the next time the same device is paired, the Settings app will
* stop with just pairing and not connect as explained above. Here, we register to receive the
* ACTION_UUID intent, which will broadcast the UUIDs corresponding to the profiles supported by
* the remote device which is successfully paired and we turn on the priority so when the
* Settings app tries to check before connecting, the priority is set to the expected value.
*
* @param device - Remote Bluetooth device
* @param uuids - UUIDs of the Bluetooth Profiles supported by the remote device
* @param priority - priority to set
*/
private void setProfilePriorities(BluetoothDevice device, ParcelUuid[] uuids, int priority) {
// need the BluetoothProfile proxy to be able to call the setPriority API
if (mCarBluetoothUserService == null) {
mCarBluetoothUserService = setupBluetoothUserService();
}
if (mCarBluetoothUserService != null) {
for (Integer profile : mProfilesToConnect) {
synchronized (this) {
ConnectionParams params = new ConnectionParams(profile, device);
// If this profile is inhibited, don't try to change its priority until the
// inhibit is released. Instead, if the profile is being enabled, take it off of
// the "previously disabled profiles" list, so it will be restored when all
// inhibits are removed.
if (mProfileInhibits.keySet().contains(params)) {
if (DBG) {
Log.i(TAG, "Not setting profile " + profile + " priority of "
+ device.getAddress() + " to " + priority + ": inhibited");
}
if (priority == BluetoothProfile.PRIORITY_ON) {
mAlreadyDisabledProfiles.remove(params);
}
continue;
}
}
setBluetoothProfilePriorityIfUuidFound(uuids, profile, device, priority);
}
}
}
private void setBluetoothProfilePriorityIfUuidFound(ParcelUuid[] uuids, int profile,
BluetoothDevice device, int priority) {
if (mCarBluetoothUserService == null || device == null) {
return;
}
// Build a list of UUIDs that represent a profile.
List<ParcelUuid> uuidsToCheck = new ArrayList<>();
switch (profile) {
case BluetoothProfile.A2DP_SINK:
uuidsToCheck.add(BluetoothUuid.AudioSource);
break;
case BluetoothProfile.HEADSET_CLIENT:
uuidsToCheck.add(BluetoothUuid.Handsfree_AG);
uuidsToCheck.add(BluetoothUuid.HSP_AG);
break;
case BluetoothProfile.PBAP_CLIENT:
uuidsToCheck.add(BluetoothUuid.PBAP_PSE);
break;
case BluetoothProfile.MAP_CLIENT:
uuidsToCheck.add(BluetoothUuid.MAS);
break;
case BluetoothProfile.PAN:
uuidsToCheck.add(BluetoothUuid.PANU);
break;
}
for (ParcelUuid uuid : uuidsToCheck) {
if (BluetoothUuid.isUuidPresent(uuids, uuid)) {
try {
mCarBluetoothUserService.setProfilePriority(profile, device, priority);
} catch (RemoteException e) {
Log.e(TAG, "RemoteException calling setProfilePriority");
}
// if any one of the uuid in uuidsTocheck is present, set the priority and break
break;
}
}
}
/**
* Cleanup state and reinitialize whenever we connect to the PerUserCarService.
* This happens in init() and whenever the PerUserCarService is restarted on User Switch Events
*/
@VisibleForTesting
class UserServiceConnectionCallback implements PerUserCarServiceHelper.ServiceCallback {
@Override
public void onServiceConnected(ICarUserService carUserService) {
if (mCarUserServiceAccessLock != null) {
mCarUserServiceAccessLock.lock();
try {
mCarUserService = carUserService;
} finally {
mCarUserServiceAccessLock.unlock();
}
}
if (DBG) {
Log.d(TAG, "Connected to PerUserCarService");
}
// Get the BluetoothUserService and also setup the Bluetooth Connection Proxy for
// all profiles.
mCarBluetoothUserService = setupBluetoothUserService();
// re-initialize for current user.
initializeUserSpecificInfo();
// Restore profile inhibits, if any, that were saved from last run...
restoreProfileInhibitsFromSettings();
// ... and start trying to remove them.
removeRestoredProfileInhibits();
}
@Override
public void onPreUnbind() {
if (DBG) {
Log.d(TAG, "Before Unbinding from UserService");
}
// Try to release profile inhibits now, before CarBluetoothUserService goes away.
// This also stops any active attempts to remove restored inhibits.
//
// If any can't be released, they'll persist in settings and will be cleaned up
// next time this user starts. This can happen if the Bluetooth profile proxies in
// CarBluetoothUserService unbind before we get the chance to make calls on them.
releaseAllInhibitsBeforeUnbind();
try {
if (mCarBluetoothUserService != null) {
mCarBluetoothUserService.closeBluetoothConnectionProxy();
}
} catch (RemoteException e) {
Log.e(TAG,
"Remote Exception during closeBluetoothConnectionProxy(): "
+ e.getMessage());
}
// Clean up information related to user who went background.
cleanupUserSpecificInfo();
}
@Override
public void onServiceDisconnected() {
if (DBG) {
Log.d(TAG, "Disconnected from PerUserCarService");
}
if (mCarUserServiceAccessLock != null) {
mCarUserServiceAccessLock.lock();
try {
mCarBluetoothUserService = null;
mCarUserService = null;
} finally {
mCarUserServiceAccessLock.unlock();
}
}
}
}
/**
* Gets the Per User Car Bluetooth Service (ICarBluetoothService) from the PerUserCarService
* which acts as a top level Service running in the current user context.
* Also sets up the connection proxy objects required to communicate with the Bluetooth
* Profile Services.
*
* @return ICarBluetoothUserService running in current user
*/
private ICarBluetoothUserService setupBluetoothUserService() {
ICarBluetoothUserService carBluetoothUserService = null;
if (mCarUserService != null) {
try {
carBluetoothUserService = mCarUserService.getBluetoothUserService();
if (carBluetoothUserService != null) {
if (DBG) {
Log.d(TAG, "Got CarBTUsrSvc");
}
carBluetoothUserService.setupBluetoothConnectionProxy();
}
} catch (RemoteException e) {
Log.e(TAG, "Remote Service Exception on ServiceConnection Callback: "
+ e.getMessage());
}
} else {
if (DBG) {
Log.d(TAG, "PerUserCarService not connected");
}
}
return carBluetoothUserService;
}
/**
* Setup the Bluetooth profile service connections and Vehicle Event listeners.
* and start the state machine -{@link BluetoothAutoConnectStateMachine}
*/
public synchronized void init() {
if (DBG) {
Log.d(TAG, "init()");
}
// Initialize information specific to current user.
initializeUserSpecificInfo();
// Listen to various events coming from the vehicle.
setupEventListenersLocked();
mInitialized = true;
}
/**
* Setup and initialize information that is specific per User account, which involves:
* 1. Reading the list of devices to connect for current user and initialize the deviceMap
* with that information.
* 2. Register a BroadcastReceiver for bluetooth related events for the current user.
* 3. Start and bind to {@link PerUserCarService} as current user.
* 4. Start the {@link BluetoothAutoConnectStateMachine}
*/
private void initializeUserSpecificInfo() {
synchronized (mSetupLock) {
if (DBG) {
Log.d(TAG, "initializeUserSpecificInfo()");
}
if (mUserSpecificInfoInitialized) {
if (DBG) {
Log.d(TAG, "Already Initialized");
}
return;
}
mUserId = ActivityManager.getCurrentUser();
mBluetoothAutoConnectStateMachine = BluetoothAutoConnectStateMachine.make(this);
readAndRebuildDeviceMapFromSettings();
setupBluetoothEventsIntentFilterLocked();
mConnectionInFlight = new ConnectionParams();
mUserSpecificInfoInitialized = true;
}
}
/**
* Setting up the Intent filter for Bluetooth related broadcasts
* This includes knowing when the
* 1. Bluetooth Adapter turned on/off
* 2. Bonding State of a device changes
* 3. A specific profile's connection state changes.
*/
private void setupBluetoothEventsIntentFilterLocked() {
mBluetoothBroadcastReceiver = new BluetoothBroadcastReceiver();
IntentFilter profileFilter = new IntentFilter();
profileFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
profileFilter.addAction(BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED);
profileFilter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
profileFilter.addAction(BluetoothPan.ACTION_CONNECTION_STATE_CHANGED);
profileFilter.addAction(BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED);
profileFilter.addAction(BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED);
profileFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
profileFilter.addAction(BluetoothDevice.ACTION_UUID);
if (mContext != null) {
mContext.registerReceiverAsUser(mBluetoothBroadcastReceiver, UserHandle.CURRENT,
profileFilter, null, null);
}
}
/**
* Initialize the {@link #mProfileToConnectableDevicesMap}.
* {@link #mProfileToConnectableDevicesMap} stores the profile:DeviceList information. This
* method retrieves it from persistent memory.
*/
private synchronized void initDeviceMap() {
if (mProfileToConnectableDevicesMap == null) {
mProfileToConnectableDevicesMap = new HashMap<>();
for (Integer profile : mProfilesToConnect) {
// Build the BluetoothDevicesInfo for this profile.
BluetoothDevicesInfo devicesInfo = new BluetoothDevicesInfo(profile,
sNumSupportedActiveConnections.get(profile));
mProfileToConnectableDevicesMap.put(profile, devicesInfo);
}
if (DBG) {
Log.d(TAG, "Created a new empty Device Map");
}
}
}
/**
* Setting up Listeners to the various events we are interested in listening to for initiating
* Bluetooth connection attempts.
*/
private void setupEventListenersLocked() {
// Setting up a listener for events from CarPropertyService
// For now, we listen to door unlock signal and Ignition state START coming from
// {@link CarPropertyService}
mCarPropertyService.registerListener(VehicleProperty.DOOR_LOCK, 0, mPropertyEventListener);
mCarPropertyService.registerListener(VehicleProperty.IGNITION_STATE, 0,
mPropertyEventListener);
// Get Current restrictions and handle them
handleUxRestrictionsChanged(mUxRService.getCurrentUxRestrictions());
// Register for future changes to the DrivingStateRestrictions
mUxRService.registerUxRestrictionsChangeListener(mUxRListener);
mUserServiceHelper.registerServiceCallback(mServiceCallback);
}
/**
* Handles events coming in from the {@link CarPropertyService}
* The events that can trigger Bluetooth Scanning from CarPropertyService are Door Unlock and
* Igntion START. Upon an event of interest, initiate a connection attempt by calling
* the policy {@link BluetoothDeviceConnectionPolicy}
*/
@VisibleForTesting
class CarPropertyListener extends ICarPropertyEventListener.Stub {
@Override
public void onEvent(List<CarPropertyEvent> events) throws RemoteException {
for (CarPropertyEvent event : events) {
if (DBG) {
Log.d(TAG, "Cabin change Event : " + event.getEventType());
}
CarPropertyValue value = event.getCarPropertyValue();
Object o = value.getValue();
switch (value.getPropertyId()) {
case VehicleProperty.DOOR_LOCK:
if (o instanceof Boolean) {
Boolean locked = (Boolean) o;
if (DBG) {
Log.d(TAG, "Door Lock: " + locked);
}
// Attempting a connection only on a door unlock
if (!locked) {
initiateConnection();
}
}
break;
case VehicleProperty.IGNITION_STATE:
if (o instanceof Integer) {
Integer state = (Integer) o;
if (DBG) {
Log.d(TAG, "Sensor value : " + state);
}
// Attempting a connection only on IgntionState START
if (state == VehicleIgnitionState.START) {
initiateConnection();
}
}
break;
}
}
}
}
private class CarUxRServiceListener extends ICarUxRestrictionsChangeListener.Stub {
@Override
public void onUxRestrictionsChanged(CarUxRestrictions restrictions) {
handleUxRestrictionsChanged(restrictions);
}
}
private void handleUxRestrictionsChanged(CarUxRestrictions restrictions) {
if (restrictions == null) {
return;
}
if ((restrictions.getActiveRestrictions() & CarUxRestrictions.UX_RESTRICTIONS_NO_SETUP)
== CarUxRestrictions.UX_RESTRICTIONS_NO_SETUP) {
mFastPairProvider.stopAdvertising();
} else {
mFastPairProvider.startAdvertising();
}
}
/**
* Clean up slate. Close the Bluetooth profile service connections and quit the state machine -
* {@link BluetoothAutoConnectStateMachine}
*/
public synchronized void release() {
if (DBG) {
Log.d(TAG, "release()");
}
mInitialized = false;
writeDeviceInfoToSettings();
cleanupUserSpecificInfo();
closeEventListeners();
mCar.disconnect();
}
/**
* Clean up information related to user who went background.
*/
private void cleanupUserSpecificInfo() {
synchronized (mSetupLock) {
if (DBG) {
Log.d(TAG, "cleanupUserSpecificInfo()");
}
if (!mUserSpecificInfoInitialized) {
if (DBG) {
Log.d(TAG, "User specific Info Not initialized..Not cleaning up");
}
return;
}
mUserSpecificInfoInitialized = false;
// quit the state machine
mBluetoothAutoConnectStateMachine.doQuit();
mProfileToConnectableDevicesMap = null;
mConnectionInFlight = null;
if (mBluetoothBroadcastReceiver != null) {
if (mContext != null) {
mContext.unregisterReceiver(mBluetoothBroadcastReceiver);
}
mBluetoothBroadcastReceiver = null;
}
}
}
/**
* Unregister the listeners to the various Vehicle events coming from other parts of the
* CarService
*/
private void closeEventListeners() {
if (DBG) {
Log.d(TAG, "closeEventListeners()");
}
mCarPropertyService.unregisterListener(VehicleProperty.DOOR_LOCK, mPropertyEventListener);
mCarPropertyService.unregisterListener(VehicleProperty.IGNITION_STATE,
mPropertyEventListener);
mUserServiceHelper.unregisterServiceCallback(mServiceCallback);
}
/**
* Resets the {@link BluetoothDevicesInfo#mConnectionInfo} of all the profiles to start from
* a clean slate. The ConnectionInfo has all the book keeping information regarding the state
* of connection attempts - like which device in the device list for the profile is the next
* to try connecting etc.
* This method does not clear the {@link BluetoothDevicesInfo#mDeviceInfoList} like the {@link
* #resetProfileToConnectableDevicesMap()} method does.
*/
private synchronized void resetBluetoothDevicesConnectionInfo() {
if (DBG) {
Log.d(TAG, "Resetting ConnectionInfo for all profiles");
}
for (BluetoothDevicesInfo devInfo : mProfileToConnectableDevicesMap.values()) {
devInfo.resetConnectionInfoLocked();
}
}
@VisibleForTesting
BroadcastReceiver getBluetoothBroadcastReceiver() {
return mBluetoothBroadcastReceiver;
}
@VisibleForTesting
UserServiceConnectionCallback getServiceCallback() {
return mServiceCallback;
}
@VisibleForTesting
CarPropertyListener getCarPropertyListener() {
return mPropertyEventListener;
}
@VisibleForTesting
synchronized void setAllowReadWriteToSettings(boolean allowWrite) {
mAllowReadWriteToSettings = allowWrite;
}
@VisibleForTesting
BluetoothDevicesInfo getBluetoothDevicesInfo(int profile) {
return mProfileToConnectableDevicesMap.get(profile);
}
@VisibleForTesting
String toDebugString() {
StringBuilder buf = new StringBuilder();
for (Integer profile : mProfileToConnectableDevicesMap.keySet()) {
BluetoothDevicesInfo info = mProfileToConnectableDevicesMap.get(profile);
buf.append(" \n**** profile = " + profile);
buf.append(", " + info.toDebugString());
}
return buf.toString();
}
/**
* Resets the {@link #mProfileToConnectableDevicesMap} to a clean and empty slate.
*/
public synchronized void resetProfileToConnectableDevicesMap() {
if (DBG) {
Log.d(TAG, "Resetting the mProfilesToConnectableDevicesMap");
}
for (BluetoothDevicesInfo devInfo : mProfileToConnectableDevicesMap.values()) {
devInfo.resetDeviceListLocked();
}
}
/**
* Returns the list of profiles that the Autoconnection policy attempts to connect on
*
* @return profile list.
*/
public synchronized List<Integer> getProfilesToConnect() {
return mProfilesToConnect;
}
/**
* Add a new Profile to the list of To Be Connected profiles.
*
* @param profile - ProfileInfo of the new profile to be added.
*/
public synchronized void addProfile(Integer profile) {
mProfilesToConnect.add(profile);
}
/**
* Request to disconnect the given profile on the given device, and prevent it from reconnecting
* until either the request is released, or the process owning the given token dies.
* @return True if the profile was successfully inhibited, false if an error occurred.
*/
boolean requestProfileInhibit(BluetoothDevice device, int profile, IBinder token) {
if (DBG) {
Log.d(TAG, "Request profile inhibit: profile " + Utils.getProfileName(profile)
+ ", device " + device.getAddress());
}
ConnectionParams params = new ConnectionParams(profile, device);
InhibitRecord record = new InhibitRecord(params, token);
return addInhibitRecord(record);
}
/**
* Undo a previous call to {@link #requestProfileInhibit} with the same parameters,
* and reconnect the profile if no other requests are active.
*
* @return True if the request was released, false if an error occurred.
*/
boolean releaseProfileInhibit(BluetoothDevice device, int profile, IBinder token) {
if (DBG) {
Log.d(TAG, "Release profile inhibit: profile " + Utils.getProfileName(profile)
+ ", device " + device.getAddress());
}
ConnectionParams params = new ConnectionParams(profile, device);
InhibitRecord record;
synchronized (this) {
record = findInhibitRecordLocked(params, token);
}
if (record == null) {
Log.e(TAG, "Record not found");
return false;
}
return record.removeSelf();
}
/** Add a profile inhibit record, disabling the profile if necessary. */
private synchronized boolean addInhibitRecord(InhibitRecord record) {
ConnectionParams params = record.getParams();
if (!isProxyAvailable(params.getBluetoothProfile())) {
return false;
}
Set<InhibitRecord> previousRecords = mProfileInhibits.get(params);
if (findInhibitRecordLocked(params, record.getToken()) != null) {
Log.e(TAG, "Inhibit request already registered - skipping duplicate");
return false;
}
try {
record.getToken().linkToDeath(record, 0);
} catch (RemoteException e) {
Log.e(TAG, "Could not link to death on inhibit token (already dead?)", e);
return false;
}
boolean isNewlyAdded = previousRecords.isEmpty();
mProfileInhibits.put(params, record);
if (isNewlyAdded) {
try {
int priority =
mCarBluetoothUserService.getProfilePriority(
params.getBluetoothProfile(),
params.getBluetoothDevice());
if (priority == BluetoothProfile.PRIORITY_OFF) {
// This profile was already disabled (and not as the result of an inhibit).
// Add it to the already-disabled list, and do nothing else.
mAlreadyDisabledProfiles.add(params);
if (DBG) {
Log.d(TAG, "Profile " + Utils.getProfileName(params.getBluetoothProfile())
+ " already disabled for device " + params.getBluetoothDevice()
+ " - suppressing re-enable");
}
} else {
mCarBluetoothUserService.setProfilePriority(
params.getBluetoothProfile(),
params.getBluetoothDevice(),
BluetoothProfile.PRIORITY_OFF);
mCarBluetoothUserService.bluetoothDisconnectFromProfile(
params.getBluetoothProfile(),
params.getBluetoothDevice());
if (DBG) {
Log.d(TAG, "Disabled profile "
+ Utils.getProfileName(params.getBluetoothProfile())
+ " for device " + params.getBluetoothDevice());
}
}
} catch (RemoteException e) {
Log.e(TAG, "Could not disable profile", e);
record.getToken().unlinkToDeath(record, 0);
mProfileInhibits.remove(params, record);
return false;
}
}
saveProfileInhibitsToSettingsLocked();
return true;
}
/** Remove a given profile inhibit record, reconnecting if necessary. */
private synchronized boolean removeInhibitRecord(InhibitRecord record) {
ConnectionParams params = record.getParams();
if (!isProxyAvailable(params.getBluetoothProfile())) {
return false;
}
if (!mProfileInhibits.containsEntry(params, record)) {
Log.e(TAG, "Record already removed");
// Removing something a second time vacuously succeeds.
return true;
}
// Re-enable profile before unlinking and removing the record, in case of error.
// The profile should be re-enabled if this record is the only one left for that
// device and profile combination.
if (mProfileInhibits.get(params).size() == 1) {
if (!restoreProfilePriority(params)) {
return false;
}
}
record.getToken().unlinkToDeath(record, 0);
mProfileInhibits.remove(params, record);
saveProfileInhibitsToSettingsLocked();
return true;
}
/** Find the inhibit record, if any, corresponding to the given parameters and token. */
@Nullable
private InhibitRecord findInhibitRecordLocked(ConnectionParams params, IBinder token) {
return mProfileInhibits.get(params)
.stream()
.filter(r -> r.getToken() == token)
.findAny()
.orElse(null);
}
/** Re-enable and reconnect a given profile for a device. */
private boolean restoreProfilePriority(ConnectionParams params) {
if (!isProxyAvailable(params.getBluetoothProfile())) {
return false;
}
if (mAlreadyDisabledProfiles.remove(params)) {
// The profile does not need any state changes, since it was disabled
// before it was inhibited. Leave it disabled.
if (DBG) {
Log.d(TAG, "Not restoring profile "
+ Utils.getProfileName(params.getBluetoothProfile()) + " for device "
+ params.getBluetoothDevice() + " - was manually disabled");
}
return true;
}
try {
mCarBluetoothUserService.setProfilePriority(
params.getBluetoothProfile(),
params.getBluetoothDevice(),
BluetoothProfile.PRIORITY_ON);
mCarBluetoothUserService.bluetoothConnectToProfile(
params.getBluetoothProfile(),
params.getBluetoothDevice());
if (DBG) {
Log.d(TAG, "Restored profile " + Utils.getProfileName(params.getBluetoothProfile())
+ " for device " + params.getBluetoothDevice());
}
return true;
} catch (RemoteException e) {
Log.e(TAG, "Could not enable profile", e);
return false;
}
}
/** Dump all currently-active profile inhibits to {@link Settings.Secure}. */
private void saveProfileInhibitsToSettingsLocked() {
Set<ConnectionParams> inhibitedProfiles = new HashSet<>(mProfileInhibits.keySet());
// Don't write out profiles that were disabled before a request was made, since
// restoring those profiles is a no-op.
inhibitedProfiles.removeAll(mAlreadyDisabledProfiles);
String savedDisconnects =
inhibitedProfiles
.stream()
.map(ConnectionParams::flattenToString)
.collect(Collectors.joining(SETTINGS_DELIMITER));
if (DBG) {
Log.d(TAG, "Saving inhibits to settings for u" + mUserId + ": " + savedDisconnects);
}
Settings.Secure.putStringForUser(
mContext.getContentResolver(), KEY_BLUETOOTH_PROFILES_INHIBITED,
savedDisconnects, mUserId);
}
/** Create {@link InhibitRecord}s for all profile inhibits written to settings. */
private synchronized void restoreProfileInhibitsFromSettings() {
if (mBluetoothAdapter == null) {
Log.e(TAG, "Cannot restore inhibit records - Bluetooth not available");
return;
}
String savedConnectionParams = Settings.Secure.getStringForUser(
mContext.getContentResolver(),
KEY_BLUETOOTH_PROFILES_INHIBITED,
mUserId);
if (TextUtils.isEmpty(savedConnectionParams)) {
return;
}
if (DBG) {
Log.d(TAG, "Restoring profile inhibits: " + savedConnectionParams);
}
for (String paramsStr : savedConnectionParams.split(SETTINGS_DELIMITER)) {
try {
ConnectionParams params = ConnectionParams.parse(paramsStr, mBluetoothAdapter);
InhibitRecord record =
new InhibitRecord(params, RESTORED_PROFILE_INHIBIT_TOKEN);
mProfileInhibits.put(params, record);
mRestoredInhibits.add(record);
if (DBG) {
Log.d(TAG, "Restored profile inhibits for " + params);
}
} catch (IllegalArgumentException e) {
Log.e(TAG, "Bad format for saved profile inhibit: " + paramsStr, e);
// We won't ever be able to fix a bad parse, so skip it and move on.
}
}
}
/**
* Try once to remove all restored profile inhibits.
*
* If the CarBluetoothUserService is not yet available, or it hasn't yet bound its profile
* proxies, the removal will fail, and will need to be retried later.
*/
private void tryRemoveRestoredProfileInhibitsLocked() {
HashSet<InhibitRecord> successfullyRemoved = new HashSet<>();
for (InhibitRecord record : mRestoredInhibits) {
if (removeInhibitRecord(record)) {
successfullyRemoved.add(record);
}
}
mRestoredInhibits.removeAll(successfullyRemoved);
}
/**
* Keep trying to remove all profile inhibits that were restored from settings
* until all such inhibits have been removed.
*/
private synchronized void removeRestoredProfileInhibits() {
tryRemoveRestoredProfileInhibitsLocked();
if (!mRestoredInhibits.isEmpty()) {
if (DBG) {
Log.d(TAG, "Could not remove all restored profile inhibits - "
+ "trying again in " + RESTORE_BACKOFF_MILLIS + "ms");
}
mHandler.postDelayed(
this::removeRestoredProfileInhibits,
RESTORED_PROFILE_INHIBIT_TOKEN,
RESTORE_BACKOFF_MILLIS);
}
}
/** Release all active inhibit records prior to user switch or shutdown. */
private synchronized void releaseAllInhibitsBeforeUnbind() {
if (DBG) {
Log.d(TAG, "Unbinding CarBluetoothUserService - releasing all profile inhibits");
}
for (ConnectionParams params : mProfileInhibits.keySet()) {
for (InhibitRecord record : mProfileInhibits.get(params)) {
record.removeSelf();
}
}
// Some inhibits might be hanging around because they couldn't be cleaned up.
// Make sure they get persisted...
saveProfileInhibitsToSettingsLocked();
// ...then clear them from the map.
mProfileInhibits.clear();
// We don't need to maintain previously-disabled profiles any more - they were already
// skipped in saveProfileInhibitsToSettingsLocked() above, and they don't need any
// further handling when the user resumes.
mAlreadyDisabledProfiles.clear();
// Clean up bookkeeping for restored inhibits. (If any are still around, they'll be
// restored again when this user restarts.)
mHandler.removeCallbacksAndMessages(RESTORED_PROFILE_INHIBIT_TOKEN);
mRestoredInhibits.clear();
}
/**
* Add or remove a device based on the bonding state change.
*
* @param device - device to add/remove
* @param bondState - current bonding state
*/
private void updateBondState(BluetoothDevice device, int bondState) {
if (device == null) {
Log.e(TAG, "updateBondState: device Null");
return;
}
if (DBG) {
Log.d(TAG, "BondState :" + bondState + " Device: " + device);
}
// Bonded devices are added to a profile's device list after the device CONNECTS on the
// profile. When unpaired, we remove the device from all of the profiles' device list.
if (bondState == BluetoothDevice.BOND_NONE) {
for (Integer profile : mProfilesToConnect) {
if (DBG) {
Log.d(TAG, "Removing " + device + " from profile: " + profile);
}
removeDeviceFromProfile(device, profile);
}
} else if (bondState == BluetoothDevice.BOND_BONDED) {
// A device just paired. When it connects on HFP profile, then immediately initiate
// connections on PBAP and MAP. for now, maintain this fact in a data structure
// to be looked up when HFP profile connects.
mPairedButUnconnectedDevices.add(device);
}
}
/**
* Add a new device to the list of devices connect-able on the given profile
*
* @param device - Bluetooth device to be added
* @param profile - profile to add the device to.
*/
private synchronized void addDeviceToProfile(BluetoothDevice device, Integer profile) {
BluetoothDevicesInfo devInfo = mProfileToConnectableDevicesMap.get(profile);
if (devInfo == null) {
if (DBG) {
Log.d(TAG, "Creating devInfo for profile: " + profile);
}
devInfo = new BluetoothDevicesInfo(profile);
mProfileToConnectableDevicesMap.put(profile, devInfo);
}
devInfo.addDeviceLocked(device);
}
/**
* Remove the device from the list of devices connect-able on the gievn profile.
*
* @param device - Bluetooth device to be removed
* @param profile - profile to remove the device from
*/
private synchronized void removeDeviceFromProfile(BluetoothDevice device, Integer profile) {
BluetoothDevicesInfo devInfo = mProfileToConnectableDevicesMap.get(profile);
if (devInfo != null) {
devInfo.removeDeviceLocked(device);
}
}
/**
* Initiate a bluetooth connection.
*/
private void initiateConnection() {
// Make sure the bluetooth adapter is available & enabled.
if (mBluetoothAdapter == null) {
Log.w(TAG, "Bluetooth Adapter null");
return;
}
if (mBluetoothAdapter.isEnabled()) {
if (isDeviceMapEmpty()) {
if (DBG) {
Log.d(TAG, "Device Map is empty. Nothing to connect to");
}
return;
}
resetDeviceAvailableToConnect();
if (DBG) {
Log.d(TAG, "initiateConnection() Reset Device Availability");
}
mBluetoothAutoConnectStateMachine.sendMessage(BluetoothAutoConnectStateMachine
.CONNECT);
} else {
if (DBG) {
Log.d(TAG, "Bluetooth Adapter not enabled.");
}
}
}
/**
* Find an unconnected profile and find a device to connect on it.
* Finds the appropriate device for the profile from the information available in
* {@link #mProfileToConnectableDevicesMap}
*
* @return true - if we found a device to connect on for any of the {@link #mProfilesToConnect}
* false - if we cannot find a device to connect to or if we are not ready to connect yet.
*/
public synchronized boolean findDeviceToConnect() {
if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()
|| mProfileToConnectableDevicesMap == null || !mInitialized) {
if (DBG) {
if (mProfileToConnectableDevicesMap == null) {
Log.d(TAG, "findDeviceToConnect(): Device Map null");
} else {
Log.d(TAG, "findDeviceToConnect(): BT Adapter not enabled");
}
}
return false;
}
boolean connectingToADevice = false;
// Get the first unconnected profile that we can try to make a connection
Integer nextProfile = getNextProfileToConnectLocked();
// Keep going through the profiles until we find a device that we can connect to
while (nextProfile != PROFILE_NOT_AVAILABLE) {
if (DBG) {
Log.d(TAG, "connectToProfile(): " + nextProfile);
}
// find a device that is next in line for a connection attempt for that profile
// and try connecting to it.
connectingToADevice = connectToNextDeviceInQueueLocked(nextProfile);
// If we found a device to connect, break out of the loop
if (connectingToADevice) {
if (DBG) {
Log.d(TAG, "Found device to connect to");
}
// set up a time out
mBluetoothAutoConnectStateMachine.sendMessageDelayed(
BluetoothAutoConnectStateMachine.CONNECT_TIMEOUT, mConnectionInFlight,
BluetoothAutoConnectStateMachine.CONNECTION_TIMEOUT_MS);
break;
} else {
// result will be false, if there are no more devices to connect
// or if the ProfileProxy objects are null (ServiceConnection
// not yet established for this profile)
if (DBG) {
Log.d(TAG, "No more device to connect on Profile: " + nextProfile);
}
nextProfile = getNextProfileToConnectLocked();
}
}
return connectingToADevice;
}
/**
* Get the first unconnected profile.
*
* @return profile to connect.
* Special return value 0 if
* 1. all profiles have been connected on.
* 2. no profile connected but no nearby known device that can be connected to
*/
private Integer getNextProfileToConnectLocked() {
for (Integer profile : mProfilesToConnect) {
BluetoothDevicesInfo devInfo = mProfileToConnectableDevicesMap.get(profile);
if (devInfo != null) {
if (devInfo.isProfileConnectableLocked()) {
return profile;
}
} else {
Log.e(TAG, "Unexpected: devInfo null for profile: " + profile);
}
}
// Reaching here denotes all profiles are connected or No devices available for any profile
if (DBG) {
Log.d(TAG, "No disconnected profiles");
}
return PROFILE_NOT_AVAILABLE;
}
/**
* Checks if the Bluetooth profile service's proxy object is available.
* Proxy obj should be available before we can attempt to connect on that profile.
*
* @param profile The profile to check the presence of the proxy object
* @return True if the proxy obj exists. False otherwise.
*/
private boolean isProxyAvailable(Integer profile) {
if (mCarBluetoothUserService == null) {
mCarBluetoothUserService = setupBluetoothUserService();
if (mCarBluetoothUserService == null) {
Log.d(TAG, "CarBluetoothUserSvc null. Car service not bound to PerUserCarSvc.");
return false;
}
}
try {
if (!mCarBluetoothUserService.isBluetoothConnectionProxyAvailable(profile)) {
// proxy unavailable.
if (DBG) {
Log.d(TAG, "Proxy for Bluetooth Profile Service Unavailable: " + profile);
}
return false;
}
} catch (RemoteException e) {
Log.e(TAG, "Car BT Service Remote Exception.");
return false;
}
return true;
}
/**
* Creates a device connection for the given profile.
*
* @param profile The profile on which the connection is being setup
* @param device The device for which the connection is to be made.
* @return true if the connection is setup successfully. False otherwise.
*/
boolean connectToDeviceOnProfile(Integer profile, BluetoothDevice device) {
if (DBG) {
Log.d(TAG, "in connectToDeviceOnProfile for Profile:" + profile +
", device: " + Utils.getDeviceDebugInfo(device));
}
if (!isProxyAvailable(profile)) {
if (DBG) {
Log.d(TAG, "No proxy available for Profile: " + profile);
}
return false;
}
BluetoothDevicesInfo devInfo = mProfileToConnectableDevicesMap.get(profile);
if (devInfo == null) {
if (DBG) {
Log.d(TAG, "devInfo NULL on Profile:" + profile);
}
return false;
}
int state = devInfo.getCurrentConnectionStateLocked(device);
if (state == BluetoothProfile.STATE_CONNECTED ||
state == BluetoothProfile.STATE_CONNECTING) {
if (DBG) {
Log.d(TAG, "device " + Utils.getDeviceDebugInfo(device) +
" is already connected/connecting on Profile:" + profile);
}
return true;
}
if (!devInfo.isProfileConnectableLocked()) {
if (DBG) {
Log.d(TAG, "isProfileConnectableLocked FALSE on Profile:" + profile +
" this means number of connections on this profile max'ed out or " +
" no more devices available to connect on this profile");
}
return false;
}
try {
if (mCarBluetoothUserService != null) {
mCarBluetoothUserService.bluetoothConnectToProfile((int) profile, device);
devInfo.setConnectionStateLocked(device, BluetoothProfile.STATE_CONNECTING);
// Increment the retry count & cache what is being connected to
// This method is already called from a synchronized context.
mConnectionInFlight = new ConnectionParams(profile, device);
devInfo.incrementRetryCountLocked();
if (DBG) {
Log.d(TAG, "Increment Retry to: " + devInfo.getRetryCountLocked() +
", for Profile: " + profile + ", on device: " +
Utils.getDeviceDebugInfo(device));
}
return true;
} else {
Log.e(TAG, "CarBluetoothUserSvc null");
}
} catch (RemoteException e) {
Log.e(TAG, "Remote User Service stopped responding: " + e.getMessage());
}
return false;
}
/**
* Helper method to reset info in {@link #mConnectionInFlight} in the given
* {@link BluetoothDevicesInfo} object.
*
* @param devInfo the {@link BluetoothDevicesInfo} where the info is to be reset.
*/
private void setProfileOnDeviceToUnavailable(BluetoothDevicesInfo devInfo) {
mConnectionInFlight = new ConnectionParams(0, null);
devInfo.setDeviceAvailableToConnectLocked(false);
}
boolean doesDeviceExistForProfile(Integer profile, BluetoothDevice device) {
BluetoothDevicesInfo devInfo = mProfileToConnectableDevicesMap.get(profile);
if (devInfo == null) {
if (DBG) {
Log.d(TAG, "devInfo NULL on Profile:" + profile);
}
return false;
}
boolean b = devInfo.checkDeviceInListLocked(device);
if (DBG) {
Log.d(TAG, "Is device " + Utils.getDeviceDebugInfo(device) +
" listed for profile " + profile + ": " + b);
}
return b;
}
/**
* Try to connect to the next device in the device list for the given profile.
*
* @param profile - profile to connect on
* @return - true if we found a device to connect on for this profile
* false - if we cannot find a device to connect to.
*/
private boolean connectToNextDeviceInQueueLocked(Integer profile) {
// Get the Device Information for the given profile and find the next device to connect on
BluetoothDevice devToConnect = null;
BluetoothDevicesInfo devInfo = mProfileToConnectableDevicesMap.get(profile);
if (devInfo == null) {
Log.e(TAG, "Unexpected: No device Queue for this profile: " + profile);
return false;
}
if (isProxyAvailable(profile)) {
// Get the next device in the device list for this profile.
devToConnect = devInfo.getNextDeviceInQueueLocked();
if (devToConnect != null) {
// deviceAvailable && proxyAvailable
if (connectToDeviceOnProfile(profile, devToConnect)) return true;
} else {
// device unavailable
if (DBG) {
Log.d(TAG, "No paired nearby device to connect to for profile: " + profile);
}
}
}
// reset the mConnectionInFlight
setProfileOnDeviceToUnavailable(devInfo);
return false;
}
/**
* Update the device connection status for a profile and also notify the state machine.
* This gets called from {@link BluetoothBroadcastReceiver} when it receives a Profile's
* CONNECTION_STATE_CHANGED intent.
*
* @param params - {@link ConnectionParams} device and profile list info
* @param currentState - connection result to update
*/
private void notifyConnectionStatus(ConnectionParams params, int currentState) {
// Update the profile's BluetoothDevicesInfo.
boolean isConnected;
switch (currentState) {
case BluetoothProfile.STATE_DISCONNECTED: {
isConnected = false;
break;
}
case BluetoothProfile.STATE_CONNECTED: {
isConnected = true;
break;
}
default: {
if (DBG) {
Log.d(TAG, "notifyConnectionStatus() Ignoring state: " + currentState);
}
return;
}
}
boolean updateSuccessful = updateDeviceConnectionStatus(params, isConnected);
if (updateSuccessful) {
if (isConnected) {
mBluetoothAutoConnectStateMachine.sendMessage(
BluetoothAutoConnectStateMachine.DEVICE_CONNECTED,
params);
} else {
mBluetoothAutoConnectStateMachine.sendMessage(
BluetoothAutoConnectStateMachine.DEVICE_DISCONNECTED,
params);
}
}
}
/**
* Update the profile's {@link BluetoothDevicesInfo} with the result of the connection
* attempt. This gets called from the {@link BluetoothAutoConnectStateMachine} when the
* connection attempt times out or from {@link BluetoothBroadcastReceiver} when it receives
* a Profile's CONNECTION_STATE_CHANGED intent.
*
* @param params - {@link ConnectionParams} device and profile list info
* @param didConnect - connection result to update
*/
public synchronized boolean updateDeviceConnectionStatus(ConnectionParams params,
boolean didConnect) {
if (params == null || params.getBluetoothDevice() == null) {
Log.e(TAG, "updateDeviceConnectionStatus: null params");
return false;
}
// Get the profile to update
Integer profileToUpdate = params.getBluetoothProfile();
BluetoothDevice deviceThatConnected = params.getBluetoothDevice();
if (DBG) {
Log.d(TAG, "Profile: " + profileToUpdate + " Connected: " + didConnect + " on "
+ deviceThatConnected);
}
// If the connection update is on a different profile or device (a very rare possibility),
// it is handled automatically. Just logging it here.
if (DBG) {
if (mConnectionInFlight != null && mConnectionInFlight.getBluetoothProfile() != null) {
if (profileToUpdate.equals(mConnectionInFlight.getBluetoothProfile()) == false) {
Log.d(TAG, "Updating profile " + profileToUpdate
+ " different from connection in flight "
+ mConnectionInFlight.getBluetoothProfile());
}
}
if (mConnectionInFlight != null && mConnectionInFlight.getBluetoothDevice() != null) {
if (deviceThatConnected.equals(mConnectionInFlight.getBluetoothDevice()) == false) {
Log.d(TAG, "Updating device: " + deviceThatConnected
+ " different from connection in flight: "
+ mConnectionInFlight.getBluetoothDevice());
}
}
}
BluetoothDevicesInfo devInfo = null;
devInfo = mProfileToConnectableDevicesMap.get(profileToUpdate);
if (devInfo == null) {
Log.e(TAG, "Unexpected: devInfo null for profile: " + profileToUpdate);
return false;
}
boolean retry = canRetryConnection(profileToUpdate);
// Update the status and also if a retry attempt can be made if the
// connection timed out in the previous attempt.
if (DBG) {
Log.d(TAG, "Retry? : " + retry);
}
devInfo.updateConnectionStatusLocked(deviceThatConnected, didConnect, retry);
// Write to persistent memory to have the latest snapshot available
writeDeviceInfoToSettings(params);
return true;
}
/**
* Returns if we can retry connection attempt on the given profile for the device that is
* currently in the head of the queue.
*
* @param profile - Profile to check
*/
private synchronized boolean canRetryConnection(Integer profile) {
BluetoothDevicesInfo devInfo = mProfileToConnectableDevicesMap.get(profile);
if (devInfo == null) {
Log.e(TAG, "Unexpected: No device Queue for this profile: " + profile);
return false;
}
if (devInfo.getRetryCountLocked() < MAX_CONNECT_RETRIES) {
return true;
} else {
return false;
}
}
/**
* Helper method to see if there are any connect-able devices on any of the
* profiles.
*
* @return true - if {@link #mProfileToConnectableDevicesMap} does not have any devices for any
* profiles.
* false - if {@link #mProfileToConnectableDevicesMap} has a device for at least one profile.
*/
private synchronized boolean isDeviceMapEmpty() {
boolean empty = true;
for (Integer profile : mProfilesToConnect) {
BluetoothDevicesInfo devInfo = mProfileToConnectableDevicesMap.get(profile);
if (devInfo != null) {
if (devInfo.getNumberOfPairedDevicesLocked() != 0) {
if (DBG) {
Log.d(TAG, "Device map not empty. Profile: " + profile + " has "
+ devInfo.getNumberOfPairedDevicesLocked() + " paired devices");
}
empty = false;
break;
}
}
}
return empty;
}
/**
* Reset the Device Available to Connect information for all profiles to Available.
* If in a previous connection attempt, we failed to connect on all devices for a profile,
* we would update deviceAvailableToConnect for that profile to false. That information
* is used to deduce if we should move to the next profile. If marked false, we will not
* try to connect on that profile anymore as part of that connection attempt.
* However, when we get another connection trigger from the vehicle, we need to reset the
* deviceAvailableToConnect information so we can start the connection attempts all over
* again.
*/
private synchronized void resetDeviceAvailableToConnect() {
for (BluetoothDevicesInfo devInfo : mProfileToConnectableDevicesMap.values()) {
devInfo.setDeviceAvailableToConnectLocked(true);
devInfo.resetDeviceIndex();
}
}
/**
* Utility function - Prints the Profile: list of devices information to log
* Caller should wrap a DBG around this, since this is for debugging purpose.
*
* @param writer - PrintWriter
*/
private synchronized void printDeviceMap(PrintWriter writer) {
if (mProfileToConnectableDevicesMap == null) {
return;
}
for (Integer profile : mProfileToConnectableDevicesMap.keySet()) {
writer.print("Profile: " + Utils.getProfileName(profile) + "\t");
writer.print("\n" + mProfileToConnectableDevicesMap.get(profile).toDebugString());
writer.println();
}
}
/**
* Get the persisted Bluetooth state from Settings
*/
private boolean isBluetoothPersistedOn() {
return (Settings.Global.getInt(
mContext.getContentResolver(), Settings.Global.BLUETOOTH_ON, -1) != 0);
}
/**
* Turn on the Bluetooth Adapter.
*/
private void enabledBluetooth() {
if (DBG) Log.d(TAG, "Enable bluetooth adapter");
if (mBluetoothAdapter == null) {
Log.e(TAG, "Cannot enable Bluetooth adapter. The object is null.");
return;
}
mBluetoothAdapter.enable();
}
/**
* Turn off the Bluetooth Adapter.
*
* Tells BluetoothAdapter to shut down _without_ persisting the off state as the desired state
* of the Bluetooth adapter for next start up.
*/
private void disableBluetooth() {
if (DBG) Log.d(TAG, "Disable bluetooth, do not persist state across reboot");
if (mBluetoothAdapter == null) {
Log.e(TAG, "Cannot disable Bluetooth adapter. The object is null.");
return;
}
mBluetoothAdapter.disable(false);
}
/**
* Write the device list for all bluetooth profiles that connected.
*
* @return true if the write was successful, false otherwise
*/
private synchronized boolean writeDeviceInfoToSettings() {
ConnectionParams params;
boolean writeResult;
for (Integer profile : mProfilesToConnect) {
params = new ConnectionParams(profile);
writeResult = writeDeviceInfoToSettings(params);
if (!writeResult) {
Log.e(TAG, "Error writing Device Info for profile:" + profile);
return writeResult;
}
}
return true;
}
/**
* Write information about which devices connected on which profile to Settings.Secure.
* Essentially the list of devices that a profile can connect on the next auto-connect
* attempt.
*
* @param params - ConnectionParams indicating which bluetooth profile to write this
* information
* for.
* @return true if the write was successful, false otherwise
*/
public synchronized boolean writeDeviceInfoToSettings(ConnectionParams params) {
if (!mAllowReadWriteToSettings) {
return false;
}
boolean writeSuccess = true;
Integer profileToUpdate = params.getBluetoothProfile();
if (mProfileToConnectableDevicesMap == null) {
writeSuccess = false;
} else {
List<String> deviceNames = new ArrayList<>();
BluetoothDevicesInfo devicesInfo = mProfileToConnectableDevicesMap.get(profileToUpdate);
StringBuilder sb = new StringBuilder();
String delimiter = ""; // start off with no delimiter.
// Iterate through the List<BluetoothDevice> and build a String that is
// names of all devices to be connected for this profile joined together and
// delimited by a delimiter (its a ',' here)
if (devicesInfo != null && devicesInfo.getDeviceList() != null) {
for (BluetoothDevice device : devicesInfo.getDeviceList()) {
sb.append(delimiter);
sb.append(device.getAddress());
delimiter = SETTINGS_DELIMITER;
}
}
// joinedDeviceNames has something like "22:22:33:44:55:AB,22:23:xx:xx:xx:xx"
// mac addresses of connectable devices separated by a delimiter
String joinedDeviceNames = sb.toString();
if (DBG) {
Log.d(TAG, "Profile: " + profileToUpdate + " Writing: " + joinedDeviceNames);
}
switch (profileToUpdate) {
case BluetoothProfile.A2DP_SINK:
Settings.Secure.putStringForUser(mContext.getContentResolver(),
KEY_BLUETOOTH_AUTOCONNECT_MUSIC_DEVICES,
joinedDeviceNames, mUserId);
break;
case BluetoothProfile.HEADSET_CLIENT:
Settings.Secure.putStringForUser(mContext.getContentResolver(),
KEY_BLUETOOTH_AUTOCONNECT_PHONE_DEVICES,
joinedDeviceNames, mUserId);
break;
case BluetoothProfile.PBAP_CLIENT:
// use the phone
break;
case BluetoothProfile.MAP_CLIENT:
Settings.Secure.putStringForUser(mContext.getContentResolver(),
KEY_BLUETOOTH_AUTOCONNECT_MESSAGING_DEVICES,
joinedDeviceNames, mUserId);
break;
case BluetoothProfile.PAN:
Settings.Secure.putStringForUser(mContext.getContentResolver(),
KEY_BLUETOOTH_AUTOCONNECT_NETWORK_DEVICES,
joinedDeviceNames, mUserId);
break;
}
}
return writeSuccess;
}
/**
* Read the device information from Settings.Secure and populate the
* {@link #mProfileToConnectableDevicesMap}
*
* Device MAC addresses are written to Settings.Secure delimited by a ','.
* Ex: android.car.BLUETOOTH_AUTOCONNECT_PHONE_DEVICES: xx:xx:xx:xx:xx:xx,yy:yy:yy:yy:yy
* denotes that two devices with addresses xx:xx:xx:xx:xx:xx & yy:yy:yy:yy:yy:yy were connected
* as phones (in HFP and PBAP profiles) the last time this user was logged in.
*
* @return - true if the read was successful, false if 1. BT Adapter not enabled 2. No prior
* bonded devices 3. No information stored in Settings for this user.
*/
public synchronized boolean readAndRebuildDeviceMapFromSettings() {
List<String> deviceList;
String devices = null;
// Create and initialize mProfileToConnectableDevicesMap if needed.
initDeviceMap();
if (mBluetoothAdapter != null) {
if (DBG) {
Log.d(TAG,
"Number of Bonded devices:" + mBluetoothAdapter.getBondedDevices().size());
}
if (mBluetoothAdapter.getBondedDevices().isEmpty()) {
if (DBG) {
Log.d(TAG, "No Bonded Devices available. Quit rebuilding");
}
return false;
}
}
if (!mAllowReadWriteToSettings) {
return false;
}
// Read from Settings.Secure for the current user. There are 3 keys 1 each for Phone
// (HFP & PBAP), 1 for Music (A2DP) and 1 for Messaging device (MAP)
for (Integer profile : mProfilesToConnect) {
switch (profile) {
case BluetoothProfile.A2DP_SINK:
devices = Settings.Secure.getStringForUser(mContext.getContentResolver(),
KEY_BLUETOOTH_AUTOCONNECT_MUSIC_DEVICES, mUserId);
break;
case BluetoothProfile.PBAP_CLIENT:
// fall through
case BluetoothProfile.HEADSET_CLIENT:
devices = Settings.Secure.getStringForUser(mContext.getContentResolver(),
KEY_BLUETOOTH_AUTOCONNECT_PHONE_DEVICES, mUserId);
break;
case BluetoothProfile.MAP_CLIENT:
devices = Settings.Secure.getStringForUser(mContext.getContentResolver(),
KEY_BLUETOOTH_AUTOCONNECT_MESSAGING_DEVICES, mUserId);
break;
case BluetoothProfile.PAN:
devices = Settings.Secure.getStringForUser(mContext.getContentResolver(),
KEY_BLUETOOTH_AUTOCONNECT_NETWORK_DEVICES, mUserId);
default:
Log.e(TAG, "Unexpected profile");
break;
}
if (devices == null) {
if (DBG) {
Log.d(TAG, "No device information stored in Settings");
}
return false;
}
if (DBG) {
Log.d(TAG, "Devices in Settings: " + devices);
}
// Get a list of Device Mac Addresses from the value
deviceList = Arrays.asList(devices.split(SETTINGS_DELIMITER));
if (deviceList == null) {
return false;
}
BluetoothDevicesInfo devicesInfo = mProfileToConnectableDevicesMap.get(profile);
// Do we have a bonded device with this name? If so, get it and populate the device
// map.
for (String address : deviceList) {
BluetoothDevice deviceToAdd = getBondedDeviceWithGivenName(address);
if (deviceToAdd != null) {
devicesInfo.addDeviceLocked(deviceToAdd);
} else {
if (DBG) {
Log.d(TAG, "No device with name " + address + " found in bonded devices");
}
}
}
mProfileToConnectableDevicesMap.put(profile, devicesInfo);
// Check to see if there are any primary or secondary devices for this profile and
// update BluetoothDevicesInfo with the priority information.
for (int priority : mPrioritiesSupported) {
readAndTagDeviceWithPriorityFromSettings(profile, priority);
}
}
return true;
}
/**
* Read from Secure Settings if there are primary or secondary devices marked for this
* Bluetooth profile. If there are tagged devices, update the BluetoothDevicesInfo so the
* policy can prioritize those devices when making connection attempts.
*
* @param profile - Bluetooth Profile to check
* @param priority - Priority to check
*/
private void readAndTagDeviceWithPriorityFromSettings(int profile, int priority) {
BluetoothDevicesInfo devicesInfo = mProfileToConnectableDevicesMap.get(profile);
if (devicesInfo == null) {
return;
}
if (!mCarBluetoothService.isPriorityDevicePresent(profile, priority)) {
// There is no device for this priority - either it hasn't been set or has been removed.
// So check if the policy has a device associated with this priority and remove it.
BluetoothDevice deviceToClear = devicesInfo.getBluetoothDeviceForPriorityLocked(
priority);
if (deviceToClear != null) {
if (DBG) {
Log.d(TAG, "Clearing priority for: " +
Utils.getDeviceDebugInfo(deviceToClear));
}
devicesInfo.removeBluetoothDevicePriorityLocked(deviceToClear);
}
} else {
// There is a device with the given priority for the given profile. Update the
// policy's records.
String deviceName = mCarBluetoothService.getDeviceNameWithPriority(profile,
priority);
if (deviceName != null) {
BluetoothDevice bluetoothDevice = getBondedDeviceWithGivenName(deviceName);
if (bluetoothDevice != null) {
if (DBG) {
Log.d(TAG, "Setting priority: " + priority + " for " + deviceName);
}
tagDeviceWithPriority(bluetoothDevice, profile, priority);
}
}
}
}
/**
* Tag a Bluetooth device with priority - Primary or Secondary. This only updates the policy's
* record (BluetoothDevicesInfo) of the priority information.
*
* @param device - BluetoothDevice to tag
* @param profile - BluetoothProfile to tag
* @param priority - Priority to tag with
*/
@VisibleForTesting
void tagDeviceWithPriority(BluetoothDevice device, int profile, int priority) {
BluetoothDevicesInfo devicesInfo = mProfileToConnectableDevicesMap.get(profile);
if (device != null) {
if (DBG) {
Log.d(TAG, "Profile: " + profile + " : " + device + " Priority: " + priority);
}
devicesInfo.setBluetoothDevicePriorityLocked(device, priority);
}
}
/**
* Given the device name, find the corresponding {@link BluetoothDevice} from the list of
* Bonded devices.
*
* @param name Bluetooth Device name
*/
@Nullable
private BluetoothDevice getBondedDeviceWithGivenName(String name) {
if (mBluetoothAdapter == null) {
if (DBG) {
Log.d(TAG, "Bluetooth Adapter Null");
}
return null;
}
if (name == null) {
Log.w(TAG, "getBondedDeviceWithGivenName() Passing in a null name");
return null;
}
if (DBG) {
Log.d(TAG, "Looking for bonded device: " + name);
}
BluetoothDevice btDevice = null;
Set<BluetoothDevice> bondedDevices = mBluetoothAdapter.getBondedDevices();
for (BluetoothDevice bd : bondedDevices) {
if (name.equals(bd.getAddress())) {
btDevice = bd;
break;
}
}
return btDevice;
}
public void dump(PrintWriter writer) {
writer.println("*BluetoothDeviceConnectionPolicy*");
printDeviceMap(writer);
mBluetoothAutoConnectStateMachine.dump(writer);
String inhibits;
synchronized (this) {
inhibits = mProfileInhibits.keySet().toString();
}
writer.println("Inhibited profiles: " + inhibits);
}
}