1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.car; 17 18 import android.bluetooth.BluetoothA2dpSink; 19 import android.bluetooth.BluetoothAdapter; 20 import android.bluetooth.BluetoothDevice; 21 import android.bluetooth.BluetoothHeadsetClient; 22 import android.bluetooth.BluetoothMapClient; 23 import android.bluetooth.BluetoothPan; 24 import android.bluetooth.BluetoothPbapClient; 25 import android.bluetooth.BluetoothProfile; 26 import android.car.ICarBluetoothUserService; 27 import android.util.Log; 28 import android.util.SparseBooleanArray; 29 30 import com.android.internal.util.Preconditions; 31 32 import java.util.Arrays; 33 import java.util.List; 34 import java.util.concurrent.TimeUnit; 35 import java.util.concurrent.locks.Condition; 36 import java.util.concurrent.locks.ReentrantLock; 37 38 public class CarBluetoothUserService extends ICarBluetoothUserService.Stub { 39 private static final String TAG = "CarBluetoothUserService"; 40 private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); 41 private final PerUserCarService mService; 42 private final BluetoothAdapter mBluetoothAdapter; 43 44 // Profiles we support 45 private static final List<Integer> sProfilesToConnect = Arrays.asList( 46 BluetoothProfile.HEADSET_CLIENT, 47 BluetoothProfile.PBAP_CLIENT, 48 BluetoothProfile.A2DP_SINK, 49 BluetoothProfile.MAP_CLIENT, 50 BluetoothProfile.PAN 51 ); 52 53 // Profile Proxies Objects to pair with above list. Access to these proxy objects will all be 54 // guarded by this classes implicit monitor lock. 55 private BluetoothA2dpSink mBluetoothA2dpSink = null; 56 private BluetoothHeadsetClient mBluetoothHeadsetClient = null; 57 private BluetoothPbapClient mBluetoothPbapClient = null; 58 private BluetoothMapClient mBluetoothMapClient = null; 59 private BluetoothPan mBluetoothPan = null; 60 61 // Concurrency variables for waitForProxyConnections. Used so we can block with a timeout while 62 // setting up or closing down proxy connections. 63 private final ReentrantLock mBluetoothProxyStatusLock; 64 private final Condition mConditionAllProxiesConnected; 65 private final Condition mConditionAllProxiesDisconnected; 66 private SparseBooleanArray mBluetoothProfileStatus; 67 private int mConnectedProfiles; 68 private static final int PROXY_OPERATION_TIMEOUT_MS = 8000; 69 70 /** 71 * Create a CarBluetoothUserService instance. 72 * 73 * @param serice - A reference to a PerUserCarService, so we can use its context to receive 74 * updates as a particular user. 75 */ CarBluetoothUserService(PerUserCarService service)76 public CarBluetoothUserService(PerUserCarService service) { 77 mService = service; 78 mConnectedProfiles = 0; 79 mBluetoothProfileStatus = new SparseBooleanArray(); 80 for (int profile : sProfilesToConnect) { 81 mBluetoothProfileStatus.put(profile, false); 82 } 83 mBluetoothProxyStatusLock = new ReentrantLock(); 84 mConditionAllProxiesConnected = mBluetoothProxyStatusLock.newCondition(); 85 mConditionAllProxiesDisconnected = mBluetoothProxyStatusLock.newCondition(); 86 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 87 } 88 89 /** 90 * Setup connections to the profile proxy objects that talk to the Bluetooth profile services. 91 * 92 * Connection requests are asynchronous in nature and return through the ProfileServiceListener 93 * below. Since callers expect that the proxies are initialized by the time we call this, we 94 * will block (with a timeout) until all proxies are connected. 95 */ 96 @Override setupBluetoothConnectionProxies()97 public void setupBluetoothConnectionProxies() { 98 logd("Initiate connections to profile proxies"); 99 Preconditions.checkNotNull(mBluetoothAdapter, "Bluetooth adapter cannot be null"); 100 mBluetoothProxyStatusLock.lock(); 101 try { 102 103 // Connect all the profiles that are unconnected, keep count so we can wait below 104 for (int profile : sProfilesToConnect) { 105 if (mBluetoothProfileStatus.get(profile, false)) { 106 logd(Utils.getProfileName(profile) + " is already connected"); 107 continue; 108 } 109 logd("Connecting " + Utils.getProfileName(profile)); 110 mBluetoothAdapter.getProfileProxy(mService.getApplicationContext(), 111 mProfileListener, profile); 112 } 113 114 // Wait for all the profiles to connect with a generous timeout just in case 115 while (mConnectedProfiles != sProfilesToConnect.size()) { 116 if (!mConditionAllProxiesConnected.await( 117 PROXY_OPERATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { 118 Log.e(TAG, "Timeout while waiting for all proxies to connect. Connected only " 119 + mConnectedProfiles + "/" + sProfilesToConnect.size()); 120 break; 121 } 122 } 123 } catch (InterruptedException e) { 124 Log.w(TAG, "setupBluetoothConnectionProxies: interrupted", e); 125 } finally { 126 mBluetoothProxyStatusLock.unlock(); 127 } 128 } 129 130 /** 131 * Close connections to the profile proxy objects 132 * 133 * Proxy disconnection requests are asynchronous in nature and return through the 134 * ProfileServiceListener below. This method will block (with a timeout) until all proxies have 135 * disconnected. 136 */ 137 @Override closeBluetoothConnectionProxies()138 public synchronized void closeBluetoothConnectionProxies() { 139 logd("Tear down profile proxy connections"); 140 Preconditions.checkNotNull(mBluetoothAdapter, "Bluetooth adapter cannot be null"); 141 mBluetoothProxyStatusLock.lock(); 142 try { 143 if (mBluetoothA2dpSink != null) { 144 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP_SINK, mBluetoothA2dpSink); 145 } 146 if (mBluetoothHeadsetClient != null) { 147 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET_CLIENT, 148 mBluetoothHeadsetClient); 149 } 150 if (mBluetoothPbapClient != null) { 151 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.PBAP_CLIENT, 152 mBluetoothPbapClient); 153 } 154 if (mBluetoothMapClient != null) { 155 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.MAP_CLIENT, 156 mBluetoothMapClient); 157 } 158 if (mBluetoothPan != null) { 159 mBluetoothAdapter.closeProfileProxy(BluetoothProfile.PAN, mBluetoothPan); 160 } 161 162 while (mConnectedProfiles != 0) { 163 if (!mConditionAllProxiesDisconnected.await( 164 PROXY_OPERATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { 165 Log.e(TAG, "Timeout while waiting for all proxies to disconnect. There are " 166 + mConnectedProfiles + "/" + sProfilesToConnect.size() + "still " 167 + "connected"); 168 break; 169 } 170 } 171 172 } catch (InterruptedException e) { 173 Log.w(TAG, "closeBluetoothConnectionProxies: interrupted", e); 174 } finally { 175 mBluetoothProxyStatusLock.unlock(); 176 } 177 } 178 179 /** 180 * Listen for and collect Bluetooth profile proxy connections and disconnections. 181 */ 182 private BluetoothProfile.ServiceListener mProfileListener = 183 new BluetoothProfile.ServiceListener() { 184 public void onServiceConnected(int profile, BluetoothProfile proxy) { 185 logd("OnServiceConnected profile: " + Utils.getProfileName(profile)); 186 187 // Grab the profile proxy object and update the status book keeping in one step so the 188 // book keeping and proxy objects never disagree 189 synchronized (this) { 190 switch (profile) { 191 case BluetoothProfile.A2DP_SINK: 192 mBluetoothA2dpSink = (BluetoothA2dpSink) proxy; 193 break; 194 case BluetoothProfile.HEADSET_CLIENT: 195 mBluetoothHeadsetClient = (BluetoothHeadsetClient) proxy; 196 break; 197 case BluetoothProfile.PBAP_CLIENT: 198 mBluetoothPbapClient = (BluetoothPbapClient) proxy; 199 break; 200 case BluetoothProfile.MAP_CLIENT: 201 mBluetoothMapClient = (BluetoothMapClient) proxy; 202 break; 203 case BluetoothProfile.PAN: 204 mBluetoothPan = (BluetoothPan) proxy; 205 break; 206 default: 207 logd("Unhandled profile connected: " + Utils.getProfileName(profile)); 208 break; 209 } 210 211 mBluetoothProxyStatusLock.lock(); 212 try { 213 if (!mBluetoothProfileStatus.get(profile, false)) { 214 mBluetoothProfileStatus.put(profile, true); 215 mConnectedProfiles++; 216 if (mConnectedProfiles == sProfilesToConnect.size()) { 217 logd("All profiles have connected"); 218 mConditionAllProxiesConnected.signal(); 219 } 220 } 221 } finally { 222 mBluetoothProxyStatusLock.unlock(); 223 } 224 } 225 } 226 227 public void onServiceDisconnected(int profile) { 228 logd("onServiceDisconnected profile: " + Utils.getProfileName(profile)); 229 230 // Null the profile proxy object and update the status book keeping in one step so the 231 // book keeping and proxy objects never disagree 232 synchronized (this) { 233 switch (profile) { 234 case BluetoothProfile.A2DP_SINK: 235 mBluetoothA2dpSink = null; 236 break; 237 case BluetoothProfile.HEADSET_CLIENT: 238 mBluetoothHeadsetClient = null; 239 break; 240 case BluetoothProfile.PBAP_CLIENT: 241 mBluetoothPbapClient = null; 242 break; 243 case BluetoothProfile.MAP_CLIENT: 244 mBluetoothMapClient = null; 245 break; 246 case BluetoothProfile.PAN: 247 mBluetoothPan = null; 248 break; 249 default: 250 logd("Unhandled profile disconnected: " + Utils.getProfileName(profile)); 251 break; 252 } 253 254 mBluetoothProxyStatusLock.lock(); 255 try { 256 if (mBluetoothProfileStatus.get(profile, false)) { 257 mBluetoothProfileStatus.put(profile, false); 258 mConnectedProfiles--; 259 if (mConnectedProfiles == 0) { 260 logd("All profiles have disconnected"); 261 mConditionAllProxiesDisconnected.signal(); 262 } 263 } 264 } finally { 265 mBluetoothProxyStatusLock.unlock(); 266 } 267 } 268 } 269 }; 270 271 /** 272 * Check if a proxy is available for the given profile to talk to the Profile's bluetooth 273 * service. 274 * @param profile - Bluetooth profile to check for 275 * @return - true if proxy available, false if not. 276 */ 277 @Override isBluetoothConnectionProxyAvailable(int profile)278 public boolean isBluetoothConnectionProxyAvailable(int profile) { 279 boolean proxyConnected = false; 280 mBluetoothProxyStatusLock.lock(); 281 try { 282 proxyConnected = mBluetoothProfileStatus.get(profile, false); 283 } finally { 284 mBluetoothProxyStatusLock.unlock(); 285 } 286 if (!proxyConnected) { 287 setupBluetoothConnectionProxies(); 288 return isBluetoothConnectionProxyAvailable(profile); 289 } 290 return proxyConnected; 291 } 292 293 @Override bluetoothConnectToProfile(int profile, BluetoothDevice device)294 public boolean bluetoothConnectToProfile(int profile, BluetoothDevice device) { 295 if (device == null) { 296 Log.e(TAG, "Cannot connect to profile on null device"); 297 return false; 298 } 299 logd("Trying to connect to " + device.getName() + " (" + device.getAddress() + ") Profile: " 300 + Utils.getProfileName(profile)); 301 synchronized (this) { 302 if (!isBluetoothConnectionProxyAvailable(profile)) { 303 Log.e(TAG, "Cannot connect to Profile. Proxy Unavailable"); 304 return false; 305 } 306 switch (profile) { 307 case BluetoothProfile.A2DP_SINK: 308 return mBluetoothA2dpSink.connect(device); 309 case BluetoothProfile.HEADSET_CLIENT: 310 return mBluetoothHeadsetClient.connect(device); 311 case BluetoothProfile.MAP_CLIENT: 312 return mBluetoothMapClient.connect(device); 313 case BluetoothProfile.PBAP_CLIENT: 314 return mBluetoothPbapClient.connect(device); 315 case BluetoothProfile.PAN: 316 return mBluetoothPan.connect(device); 317 default: 318 Log.w(TAG, "Unknown Profile: " + Utils.getProfileName(profile)); 319 break; 320 } 321 } 322 return false; 323 } 324 325 @Override bluetoothDisconnectFromProfile(int profile, BluetoothDevice device)326 public boolean bluetoothDisconnectFromProfile(int profile, BluetoothDevice device) { 327 if (device == null) { 328 Log.e(TAG, "Cannot disconnect from profile on null device"); 329 return false; 330 } 331 logd("Trying to disconnect from " + device.getName() + " (" + device.getAddress() 332 + ") Profile: " + Utils.getProfileName(profile)); 333 synchronized (this) { 334 if (!isBluetoothConnectionProxyAvailable(profile)) { 335 Log.e(TAG, "Cannot disconnect from profile. Proxy Unavailable"); 336 return false; 337 } 338 switch (profile) { 339 case BluetoothProfile.A2DP_SINK: 340 return mBluetoothA2dpSink.disconnect(device); 341 case BluetoothProfile.HEADSET_CLIENT: 342 return mBluetoothHeadsetClient.disconnect(device); 343 case BluetoothProfile.MAP_CLIENT: 344 return mBluetoothMapClient.disconnect(device); 345 case BluetoothProfile.PBAP_CLIENT: 346 return mBluetoothPbapClient.disconnect(device); 347 case BluetoothProfile.PAN: 348 return mBluetoothPan.disconnect(device); 349 default: 350 Log.w(TAG, "Unknown Profile: " + Utils.getProfileName(profile)); 351 break; 352 } 353 } 354 return false; 355 } 356 357 /** 358 * Get the priority of the given Bluetooth profile for the given remote device 359 * @param profile - Bluetooth profile 360 * @param device - remote Bluetooth device 361 */ 362 @Override getProfilePriority(int profile, BluetoothDevice device)363 public int getProfilePriority(int profile, BluetoothDevice device) { 364 if (device == null) { 365 Log.e(TAG, "Cannot get " + Utils.getProfileName(profile) 366 + " profile priority on null device"); 367 return BluetoothProfile.PRIORITY_UNDEFINED; 368 } 369 int priority; 370 synchronized (this) { 371 if (!isBluetoothConnectionProxyAvailable(profile)) { 372 Log.e(TAG, "Cannot get " + Utils.getProfileName(profile) 373 + " profile priority. Proxy Unavailable"); 374 return BluetoothProfile.PRIORITY_UNDEFINED; 375 } 376 switch (profile) { 377 case BluetoothProfile.A2DP_SINK: 378 priority = mBluetoothA2dpSink.getPriority(device); 379 break; 380 case BluetoothProfile.HEADSET_CLIENT: 381 priority = mBluetoothHeadsetClient.getPriority(device); 382 break; 383 case BluetoothProfile.MAP_CLIENT: 384 priority = mBluetoothMapClient.getPriority(device); 385 break; 386 case BluetoothProfile.PBAP_CLIENT: 387 priority = mBluetoothPbapClient.getPriority(device); 388 break; 389 default: 390 Log.w(TAG, "Unknown Profile: " + Utils.getProfileName(profile)); 391 priority = BluetoothProfile.PRIORITY_UNDEFINED; 392 break; 393 } 394 } 395 logd(Utils.getProfileName(profile) + " priority for " + device.getName() + " (" 396 + device.getAddress() + ") = " + priority); 397 return priority; 398 } 399 400 /** 401 * Set the priority of the given Bluetooth profile for the given remote device 402 * @param profile - Bluetooth profile 403 * @param device - remote Bluetooth device 404 * @param priority - priority to set 405 */ 406 @Override setProfilePriority(int profile, BluetoothDevice device, int priority)407 public void setProfilePriority(int profile, BluetoothDevice device, int priority) { 408 if (device == null) { 409 Log.e(TAG, "Cannot set " + Utils.getProfileName(profile) 410 + " profile priority on null device"); 411 return; 412 } 413 logd("Setting " + Utils.getProfileName(profile) + " priority for " + device.getName() + " (" 414 + device.getAddress() + ") to " + priority); 415 synchronized (this) { 416 if (!isBluetoothConnectionProxyAvailable(profile)) { 417 Log.e(TAG, "Cannot set " + Utils.getProfileName(profile) 418 + " profile priority. Proxy Unavailable"); 419 return; 420 } 421 switch (profile) { 422 case BluetoothProfile.A2DP_SINK: 423 mBluetoothA2dpSink.setPriority(device, priority); 424 break; 425 case BluetoothProfile.HEADSET_CLIENT: 426 mBluetoothHeadsetClient.setPriority(device, priority); 427 break; 428 case BluetoothProfile.MAP_CLIENT: 429 mBluetoothMapClient.setPriority(device, priority); 430 break; 431 case BluetoothProfile.PBAP_CLIENT: 432 mBluetoothPbapClient.setPriority(device, priority); 433 break; 434 default: 435 Log.w(TAG, "Unknown Profile: " + Utils.getProfileName(profile)); 436 break; 437 } 438 } 439 } 440 441 /** 442 * Log to debug if debug output is enabled 443 */ logd(String msg)444 private void logd(String msg) { 445 if (DBG) { 446 Log.d(TAG, msg); 447 } 448 } 449 } 450