blob: 60bc5617920e10bb838421d6145522ad8e27e8b5 [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.Log;
import android.util.SparseBooleanArray;
import com.android.internal.util.Preconditions;
import java.util.Arrays;
import java.util.List;
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 = "CarBluetoothUserService";
private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
private final PerUserCarService mService;
private final BluetoothAdapter mBluetoothAdapter;
// 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
);
// Profile Proxies Objects to pair with above list. Access to these proxy objects will all be
// guarded by this classes implicit monitor lock.
private BluetoothA2dpSink mBluetoothA2dpSink = null;
private BluetoothHeadsetClient mBluetoothHeadsetClient = null;
private BluetoothPbapClient mBluetoothPbapClient = null;
private BluetoothMapClient mBluetoothMapClient = null;
private BluetoothPan mBluetoothPan = null;
// Concurrency variables for waitForProxyConnections. Used so we can block with a timeout while
// setting up or closing down proxy connections.
private final ReentrantLock mBluetoothProxyStatusLock;
private final Condition mConditionAllProxiesConnected;
private final Condition mConditionAllProxiesDisconnected;
private SparseBooleanArray mBluetoothProfileStatus;
private int mConnectedProfiles;
private static final int PROXY_OPERATION_TIMEOUT_MS = 8000;
/**
* Create a CarBluetoothUserService instance.
*
* @param serice - 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);
}
mBluetoothProxyStatusLock = new ReentrantLock();
mConditionAllProxiesConnected = mBluetoothProxyStatusLock.newCondition();
mConditionAllProxiesDisconnected = mBluetoothProxyStatusLock.newCondition();
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
}
/**
* Setup connections to the profile proxy objects that talk to the Bluetooth profile services.
*
* Connection requests are asynchronous in nature and return through the ProfileServiceListener
* below. Since callers expect that the proxies are initialized by the time we call this, we
* will block (with a timeout) until all proxies are connected.
*/
@Override
public void setupBluetoothConnectionProxies() {
logd("Initiate connections to profile proxies");
Preconditions.checkNotNull(mBluetoothAdapter, "Bluetooth adapter cannot be null");
mBluetoothProxyStatusLock.lock();
try {
// Connect all the profiles that are unconnected, keep count so we can wait below
for (int profile : sProfilesToConnect) {
if (mBluetoothProfileStatus.get(profile, false)) {
logd(Utils.getProfileName(profile) + " is already connected");
continue;
}
logd("Connecting " + Utils.getProfileName(profile));
mBluetoothAdapter.getProfileProxy(mService.getApplicationContext(),
mProfileListener, profile);
}
// Wait for all the profiles to connect with a generous timeout just in case
while (mConnectedProfiles != sProfilesToConnect.size()) {
if (!mConditionAllProxiesConnected.await(
PROXY_OPERATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
Log.e(TAG, "Timeout while waiting for all proxies to connect. Connected only "
+ mConnectedProfiles + "/" + sProfilesToConnect.size());
break;
}
}
} catch (InterruptedException e) {
Log.w(TAG, "setupBluetoothConnectionProxies: interrupted", e);
} finally {
mBluetoothProxyStatusLock.unlock();
}
}
/**
* Close connections to the profile proxy objects
*
* Proxy disconnection requests are asynchronous in nature and return through the
* ProfileServiceListener below. This method will block (with a timeout) until all proxies have
* disconnected.
*/
@Override
public synchronized void closeBluetoothConnectionProxies() {
logd("Tear down profile proxy connections");
Preconditions.checkNotNull(mBluetoothAdapter, "Bluetooth adapter cannot be null");
mBluetoothProxyStatusLock.lock();
try {
if (mBluetoothA2dpSink != null) {
mBluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP_SINK, mBluetoothA2dpSink);
}
if (mBluetoothHeadsetClient != null) {
mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET_CLIENT,
mBluetoothHeadsetClient);
}
if (mBluetoothPbapClient != null) {
mBluetoothAdapter.closeProfileProxy(BluetoothProfile.PBAP_CLIENT,
mBluetoothPbapClient);
}
if (mBluetoothMapClient != null) {
mBluetoothAdapter.closeProfileProxy(BluetoothProfile.MAP_CLIENT,
mBluetoothMapClient);
}
if (mBluetoothPan != null) {
mBluetoothAdapter.closeProfileProxy(BluetoothProfile.PAN, mBluetoothPan);
}
while (mConnectedProfiles != 0) {
if (!mConditionAllProxiesDisconnected.await(
PROXY_OPERATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
Log.e(TAG, "Timeout while waiting for all proxies to disconnect. There are "
+ mConnectedProfiles + "/" + sProfilesToConnect.size() + "still "
+ "connected");
break;
}
}
} catch (InterruptedException e) {
Log.w(TAG, "closeBluetoothConnectionProxies: interrupted", e);
} finally {
mBluetoothProxyStatusLock.unlock();
}
}
/**
* 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: " + 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
synchronized (this) {
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: " + Utils.getProfileName(profile));
break;
}
mBluetoothProxyStatusLock.lock();
try {
if (!mBluetoothProfileStatus.get(profile, false)) {
mBluetoothProfileStatus.put(profile, true);
mConnectedProfiles++;
if (mConnectedProfiles == sProfilesToConnect.size()) {
logd("All profiles have connected");
mConditionAllProxiesConnected.signal();
}
}
} finally {
mBluetoothProxyStatusLock.unlock();
}
}
}
public void onServiceDisconnected(int profile) {
logd("onServiceDisconnected profile: " + Utils.getProfileName(profile));
// Null the profile proxy object and update the status book keeping in one step so the
// book keeping and proxy objects never disagree
synchronized (this) {
switch (profile) {
case BluetoothProfile.A2DP_SINK:
mBluetoothA2dpSink = null;
break;
case BluetoothProfile.HEADSET_CLIENT:
mBluetoothHeadsetClient = null;
break;
case BluetoothProfile.PBAP_CLIENT:
mBluetoothPbapClient = null;
break;
case BluetoothProfile.MAP_CLIENT:
mBluetoothMapClient = null;
break;
case BluetoothProfile.PAN:
mBluetoothPan = null;
break;
default:
logd("Unhandled profile disconnected: " + Utils.getProfileName(profile));
break;
}
mBluetoothProxyStatusLock.lock();
try {
if (mBluetoothProfileStatus.get(profile, false)) {
mBluetoothProfileStatus.put(profile, false);
mConnectedProfiles--;
if (mConnectedProfiles == 0) {
logd("All profiles have disconnected");
mConditionAllProxiesDisconnected.signal();
}
}
} finally {
mBluetoothProxyStatusLock.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) {
boolean proxyConnected = false;
mBluetoothProxyStatusLock.lock();
try {
proxyConnected = mBluetoothProfileStatus.get(profile, false);
} finally {
mBluetoothProxyStatusLock.unlock();
}
if (!proxyConnected) {
setupBluetoothConnectionProxies();
return isBluetoothConnectionProxyAvailable(profile);
}
return proxyConnected;
}
@Override
public boolean bluetoothConnectToProfile(int profile, BluetoothDevice device) {
if (device == null) {
Log.e(TAG, "Cannot connect to profile on null device");
return false;
}
logd("Trying to connect to " + device.getName() + " (" + device.getAddress() + ") Profile: "
+ Utils.getProfileName(profile));
synchronized (this) {
if (!isBluetoothConnectionProxyAvailable(profile)) {
Log.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:
Log.w(TAG, "Unknown Profile: " + Utils.getProfileName(profile));
break;
}
}
return false;
}
@Override
public boolean bluetoothDisconnectFromProfile(int profile, BluetoothDevice device) {
if (device == null) {
Log.e(TAG, "Cannot disconnect from profile on null device");
return false;
}
logd("Trying to disconnect from " + device.getName() + " (" + device.getAddress()
+ ") Profile: " + Utils.getProfileName(profile));
synchronized (this) {
if (!isBluetoothConnectionProxyAvailable(profile)) {
Log.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:
Log.w(TAG, "Unknown Profile: " + Utils.getProfileName(profile));
break;
}
}
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) {
Log.e(TAG, "Cannot get " + Utils.getProfileName(profile)
+ " profile priority on null device");
return BluetoothProfile.PRIORITY_UNDEFINED;
}
int priority;
synchronized (this) {
if (!isBluetoothConnectionProxyAvailable(profile)) {
Log.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:
Log.w(TAG, "Unknown Profile: " + Utils.getProfileName(profile));
priority = BluetoothProfile.PRIORITY_UNDEFINED;
break;
}
}
logd(Utils.getProfileName(profile) + " priority for " + 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) {
Log.e(TAG, "Cannot set " + Utils.getProfileName(profile)
+ " profile priority on null device");
return;
}
logd("Setting " + Utils.getProfileName(profile) + " priority for " + device.getName() + " ("
+ device.getAddress() + ") to " + priority);
synchronized (this) {
if (!isBluetoothConnectionProxyAvailable(profile)) {
Log.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:
Log.w(TAG, "Unknown Profile: " + Utils.getProfileName(profile));
break;
}
}
}
/**
* Log to debug if debug output is enabled
*/
private void logd(String msg) {
if (DBG) {
Log.d(TAG, msg);
}
}
}