/* * 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 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); } } }