blob: 13beb9b5d913dc8b76c23d2be993a8dfdb993fbd [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 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.car.ICarBluetoothUserService;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Slog;
import android.util.SparseBooleanArray;
import com.android.car.bluetooth.FastPairProvider;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class CarBluetoothUserService extends ICarBluetoothUserService.Stub {
private static final String TAG = CarLog.tagFor(CarBluetoothUserService.class);
private static final int PROXY_OPERATION_TIMEOUT_MS = 8_000;
// Profiles we support
private static final List<Integer> sProfilesToConnect = Arrays.asList(
BluetoothProfile.HEADSET_CLIENT,
BluetoothProfile.PBAP_CLIENT,
BluetoothProfile.A2DP_SINK,
BluetoothProfile.MAP_CLIENT,
BluetoothProfile.PAN
);
private final PerUserCarService mService;
private final BluetoothAdapter mBluetoothAdapter;
// Profile Proxies Objects to pair with above list. Access to these proxy objects will all be
// guarded by the below mBluetoothProxyLock
private BluetoothA2dpSink mBluetoothA2dpSink;
private BluetoothHeadsetClient mBluetoothHeadsetClient;
private BluetoothPbapClient mBluetoothPbapClient;
private BluetoothMapClient mBluetoothMapClient;
private BluetoothPan mBluetoothPan;
// Concurrency variables for waitForProxies. Used so we can best effort block with a timeout
// while waiting for services to be bound to the proxy objects.
private final ReentrantLock mBluetoothProxyLock;
private final Condition mConditionAllProxiesConnected;
private final FastPairProvider mFastPairProvider;
private SparseBooleanArray mBluetoothProfileStatus;
private int mConnectedProfiles;
/**
* Create a CarBluetoothUserService instance.
*
* @param service - A reference to a PerUserCarService, so we can use its context to receive
* updates as a particular user.
*/
public CarBluetoothUserService(PerUserCarService service) {
mService = service;
mConnectedProfiles = 0;
mBluetoothProfileStatus = new SparseBooleanArray();
for (int profile : sProfilesToConnect) {
mBluetoothProfileStatus.put(profile, false);
}
mBluetoothProxyLock = new ReentrantLock();
mConditionAllProxiesConnected = mBluetoothProxyLock.newCondition();
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
Objects.requireNonNull(mBluetoothAdapter, "Bluetooth adapter cannot be null");
mFastPairProvider = new FastPairProvider(service);
}
/**
* Setup connections to the profile proxy objects that talk to the Bluetooth profile services.
*
* Proxy references are held by the Bluetooth Framework on our behalf. We will be notified each
* time the underlying service connects for each proxy we create. Notifications stop when we
* close the proxy. As such, each time this is called we clean up any existing proxies before
* creating new ones.
*/
@Override
public void setupBluetoothConnectionProxies() {
logd("Initiate connections to profile proxies");
// Clear existing proxy objects
closeBluetoothConnectionProxies();
// Create proxy for each supported profile. Objects arrive later in the profile listener.
// Operations on the proxies expect them to be connected. Functions below should call
// waitForProxies() to best effort wait for them to be up if Bluetooth is enabled.
for (int profile : sProfilesToConnect) {
logd("Creating proxy for %s", Utils.getProfileName(profile));
mBluetoothAdapter.getProfileProxy(mService.getApplicationContext(),
mProfileListener, profile);
}
mFastPairProvider.start();
}
/**
* Close connections to the profile proxy objects
*/
@Override
public void closeBluetoothConnectionProxies() {
logd("Clean up profile proxy objects");
mBluetoothProxyLock.lock();
try {
mBluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP_SINK, mBluetoothA2dpSink);
mBluetoothA2dpSink = null;
mBluetoothProfileStatus.put(BluetoothProfile.A2DP_SINK, false);
mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET_CLIENT,
mBluetoothHeadsetClient);
mBluetoothHeadsetClient = null;
mBluetoothProfileStatus.put(BluetoothProfile.HEADSET_CLIENT, false);
mBluetoothAdapter.closeProfileProxy(BluetoothProfile.PBAP_CLIENT, mBluetoothPbapClient);
mBluetoothPbapClient = null;
mBluetoothProfileStatus.put(BluetoothProfile.PBAP_CLIENT, false);
mBluetoothAdapter.closeProfileProxy(BluetoothProfile.MAP_CLIENT, mBluetoothMapClient);
mBluetoothMapClient = null;
mBluetoothProfileStatus.put(BluetoothProfile.MAP_CLIENT, false);
mBluetoothAdapter.closeProfileProxy(BluetoothProfile.PAN, mBluetoothPan);
mBluetoothPan = null;
mBluetoothProfileStatus.put(BluetoothProfile.PAN, false);
mConnectedProfiles = 0;
} finally {
mBluetoothProxyLock.unlock();
}
mFastPairProvider.stop();
}
/**
* Listen for and collect Bluetooth profile proxy connections and disconnections.
*/
private BluetoothProfile.ServiceListener mProfileListener =
new BluetoothProfile.ServiceListener() {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
logd("onServiceConnected profile: %s", Utils.getProfileName(profile));
// Grab the profile proxy object and update the status book keeping in one step so the
// book keeping and proxy objects never disagree
mBluetoothProxyLock.lock();
try {
switch (profile) {
case BluetoothProfile.A2DP_SINK:
mBluetoothA2dpSink = (BluetoothA2dpSink) proxy;
break;
case BluetoothProfile.HEADSET_CLIENT:
mBluetoothHeadsetClient = (BluetoothHeadsetClient) proxy;
break;
case BluetoothProfile.PBAP_CLIENT:
mBluetoothPbapClient = (BluetoothPbapClient) proxy;
break;
case BluetoothProfile.MAP_CLIENT:
mBluetoothMapClient = (BluetoothMapClient) proxy;
break;
case BluetoothProfile.PAN:
mBluetoothPan = (BluetoothPan) proxy;
break;
default:
logd("Unhandled profile connected: %s", Utils.getProfileName(profile));
break;
}
if (!mBluetoothProfileStatus.get(profile, false)) {
mBluetoothProfileStatus.put(profile, true);
mConnectedProfiles++;
if (mConnectedProfiles == sProfilesToConnect.size()) {
logd("All profiles have connected");
mConditionAllProxiesConnected.signal();
}
} else {
Slog.w(TAG, "Received duplicate service connection event for: "
+ Utils.getProfileName(profile));
}
} finally {
mBluetoothProxyLock.unlock();
}
}
public void onServiceDisconnected(int profile) {
logd("onServiceDisconnected profile: %s", Utils.getProfileName(profile));
mBluetoothProxyLock.lock();
try {
if (mBluetoothProfileStatus.get(profile, false)) {
mBluetoothProfileStatus.put(profile, false);
mConnectedProfiles--;
} else {
Slog.w(TAG, "Received duplicate service disconnection event for: "
+ Utils.getProfileName(profile));
}
} finally {
mBluetoothProxyLock.unlock();
}
}
};
/**
* Check if a proxy is available for the given profile to talk to the Profile's bluetooth
* service.
*
* @param profile - Bluetooth profile to check for
* @return - true if proxy available, false if not.
*/
@Override
public boolean isBluetoothConnectionProxyAvailable(int profile) {
if (!mBluetoothAdapter.isEnabled()) return false;
boolean proxyConnected = false;
mBluetoothProxyLock.lock();
try {
proxyConnected = mBluetoothProfileStatus.get(profile, false);
} finally {
mBluetoothProxyLock.unlock();
}
return proxyConnected;
}
/**
* Wait for the proxy objects to be up for all profiles, with a timeout.
*
* @param timeout Amount of time in milliseconds to wait for giving up on the wait operation
* @return True if the condition was satisfied within the timeout, False otherwise
*/
private boolean waitForProxies(int timeout /* ms */) {
logd("waitForProxies()");
// If bluetooth isn't on then the operation waiting on proxies was never meant to actually
// work regardless if Bluetooth comes on within the timeout period or not. Return false.
if (!mBluetoothAdapter.isEnabled()) return false;
try {
while (mConnectedProfiles != sProfilesToConnect.size()) {
if (!mConditionAllProxiesConnected.await(
timeout, TimeUnit.MILLISECONDS)) {
Slog.e(TAG, "Timeout while waiting for proxies, Connected: "
+ mConnectedProfiles + "/" + sProfilesToConnect.size());
return false;
}
}
} catch (InterruptedException e) {
Slog.w(TAG, "waitForProxies: interrupted", e);
Thread.currentThread().interrupt();
return false;
}
return true;
}
/**
* Connect a given remote device on a specific Bluetooth profile
*
* @param profile BluetoothProfile.* based profile ID
* @param device The device you wish to connect
*/
@Override
public boolean bluetoothConnectToProfile(int profile, BluetoothDevice device) {
if (device == null) {
Slog.e(TAG, "Cannot connect to profile on null device");
return false;
}
logd("Trying to connect to %s (%s) Profile: %s", device.getName(), device.getAddress(),
Utils.getProfileName(profile));
mBluetoothProxyLock.lock();
try {
if (!isBluetoothConnectionProxyAvailable(profile)) {
if (!waitForProxies(PROXY_OPERATION_TIMEOUT_MS)
&& !isBluetoothConnectionProxyAvailable(profile)) {
Slog.e(TAG, "Cannot connect to Profile. Proxy Unavailable");
return false;
}
}
switch (profile) {
case BluetoothProfile.A2DP_SINK:
return mBluetoothA2dpSink.connect(device);
case BluetoothProfile.HEADSET_CLIENT:
return mBluetoothHeadsetClient.connect(device);
case BluetoothProfile.MAP_CLIENT:
return mBluetoothMapClient.connect(device);
case BluetoothProfile.PBAP_CLIENT:
return mBluetoothPbapClient.connect(device);
case BluetoothProfile.PAN:
return mBluetoothPan.connect(device);
default:
Slog.w(TAG, "Unknown Profile: " + Utils.getProfileName(profile));
break;
}
} finally {
mBluetoothProxyLock.unlock();
}
return false;
}
/**
* Disonnect a given remote device from a specific Bluetooth profile
*
* @param profile BluetoothProfile.* based profile ID
* @param device The device you wish to disconnect
*/
@Override
public boolean bluetoothDisconnectFromProfile(int profile, BluetoothDevice device) {
if (device == null) {
Slog.e(TAG, "Cannot disconnect from profile on null device");
return false;
}
logd("Trying to disconnect from %s (%s) Profile: %s", device.getName(), device.getAddress(),
Utils.getProfileName(profile));
mBluetoothProxyLock.lock();
try {
if (!isBluetoothConnectionProxyAvailable(profile)) {
if (!waitForProxies(PROXY_OPERATION_TIMEOUT_MS)
&& !isBluetoothConnectionProxyAvailable(profile)) {
Slog.e(TAG, "Cannot disconnect from Profile. Proxy Unavailable");
return false;
}
}
switch (profile) {
case BluetoothProfile.A2DP_SINK:
return mBluetoothA2dpSink.disconnect(device);
case BluetoothProfile.HEADSET_CLIENT:
return mBluetoothHeadsetClient.disconnect(device);
case BluetoothProfile.MAP_CLIENT:
return mBluetoothMapClient.disconnect(device);
case BluetoothProfile.PBAP_CLIENT:
return mBluetoothPbapClient.disconnect(device);
case BluetoothProfile.PAN:
return mBluetoothPan.disconnect(device);
default:
Slog.w(TAG, "Unknown Profile: " + Utils.getProfileName(profile));
break;
}
} finally {
mBluetoothProxyLock.unlock();
}
return false;
}
/**
* Get the priority of the given Bluetooth profile for the given remote device
*
* @param profile - Bluetooth profile
* @param device - remote Bluetooth device
*/
@Override
public int getProfilePriority(int profile, BluetoothDevice device) {
if (device == null) {
Slog.e(TAG, "Cannot get " + Utils.getProfileName(profile)
+ " profile priority on null device");
return BluetoothProfile.PRIORITY_UNDEFINED;
}
int priority;
mBluetoothProxyLock.lock();
try {
if (!isBluetoothConnectionProxyAvailable(profile)) {
if (!waitForProxies(PROXY_OPERATION_TIMEOUT_MS)
&& !isBluetoothConnectionProxyAvailable(profile)) {
Slog.e(TAG, "Cannot get " + Utils.getProfileName(profile)
+ " profile priority. Proxy Unavailable");
return BluetoothProfile.PRIORITY_UNDEFINED;
}
}
switch (profile) {
case BluetoothProfile.A2DP_SINK:
priority = mBluetoothA2dpSink.getPriority(device);
break;
case BluetoothProfile.HEADSET_CLIENT:
priority = mBluetoothHeadsetClient.getPriority(device);
break;
case BluetoothProfile.MAP_CLIENT:
priority = mBluetoothMapClient.getPriority(device);
break;
case BluetoothProfile.PBAP_CLIENT:
priority = mBluetoothPbapClient.getPriority(device);
break;
default:
Slog.w(TAG, "Unknown Profile: " + Utils.getProfileName(profile));
priority = BluetoothProfile.PRIORITY_UNDEFINED;
break;
}
} finally {
mBluetoothProxyLock.unlock();
}
logd("%s priority for %s (%s) = %d", Utils.getProfileName(profile), device.getName(),
device.getAddress(), priority);
return priority;
}
/**
* Set the priority of the given Bluetooth profile for the given remote device
*
* @param profile - Bluetooth profile
* @param device - remote Bluetooth device
* @param priority - priority to set
*/
@Override
public void setProfilePriority(int profile, BluetoothDevice device, int priority) {
if (device == null) {
Slog.e(TAG, "Cannot set " + Utils.getProfileName(profile)
+ " profile priority on null device");
return;
}
logd("Setting %s priority for %s (%s) to %d", Utils.getProfileName(profile),
device.getName(), device.getAddress(), priority);
mBluetoothProxyLock.lock();
try {
if (!isBluetoothConnectionProxyAvailable(profile)) {
if (!waitForProxies(PROXY_OPERATION_TIMEOUT_MS)
&& !isBluetoothConnectionProxyAvailable(profile)) {
Slog.e(TAG, "Cannot set " + Utils.getProfileName(profile)
+ " profile priority. Proxy Unavailable");
return;
}
}
switch (profile) {
case BluetoothProfile.A2DP_SINK:
mBluetoothA2dpSink.setPriority(device, priority);
break;
case BluetoothProfile.HEADSET_CLIENT:
mBluetoothHeadsetClient.setPriority(device, priority);
break;
case BluetoothProfile.MAP_CLIENT:
mBluetoothMapClient.setPriority(device, priority);
break;
case BluetoothProfile.PBAP_CLIENT:
mBluetoothPbapClient.setPriority(device, priority);
break;
default:
Slog.w(TAG, "Unknown Profile: " + Utils.getProfileName(profile));
break;
}
} finally {
mBluetoothProxyLock.unlock();
}
}
void dump(IndentingPrintWriter pw) {
pw.printf("Supported profiles: %s\n", sProfilesToConnect);
pw.printf("Number of connected profiles: %d\n", mConnectedProfiles);
pw.printf("Profiles status: %s\n", mBluetoothProfileStatus);
pw.printf("Proxy operation timeout: %d ms\n", PROXY_OPERATION_TIMEOUT_MS);
pw.printf("BluetoothAdapter: %s\n", mBluetoothAdapter);
pw.printf("BluetoothA2dpSink: %s\n", mBluetoothA2dpSink);
pw.printf("BluetoothHeadsetClient: %s\n", mBluetoothHeadsetClient);
pw.printf("BluetoothPbapClient: %s\n", mBluetoothPbapClient);
pw.printf("BluetoothMapClient: %s\n", mBluetoothMapClient);
pw.printf("BluetoothPan: %s\n", mBluetoothPan);
mFastPairProvider.dump(pw);
}
/**
* Log to debug if debug output is enabled
*/
private void logd(String message, Object... args) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Slog.d(TAG, String.format(message, args));
}
}
}