1 /* 2 * Copyright (c) 2016 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 17 package com.android.bluetooth.pbapclient; 18 19 import android.accounts.Account; 20 import android.accounts.AccountManager; 21 import android.annotation.RequiresPermission; 22 import android.bluetooth.BluetoothDevice; 23 import android.bluetooth.BluetoothHeadsetClient; 24 import android.bluetooth.BluetoothProfile; 25 import android.bluetooth.IBluetoothPbapClient; 26 import android.content.Attributable; 27 import android.content.AttributionSource; 28 import android.content.BroadcastReceiver; 29 import android.content.ComponentName; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.content.IntentFilter; 33 import android.provider.CallLog; 34 import android.util.Log; 35 36 import com.android.bluetooth.R; 37 import com.android.bluetooth.Utils; 38 import com.android.bluetooth.btservice.AdapterService; 39 import com.android.bluetooth.btservice.ProfileService; 40 import com.android.bluetooth.btservice.storage.DatabaseManager; 41 import com.android.bluetooth.hfpclient.connserv.HfpClientConnectionService; 42 import com.android.bluetooth.sdp.SdpManager; 43 44 import java.util.ArrayList; 45 import java.util.List; 46 import java.util.Map; 47 import java.util.Objects; 48 import java.util.concurrent.ConcurrentHashMap; 49 50 /** 51 * Provides Bluetooth Phone Book Access Profile Client profile. 52 * 53 * @hide 54 */ 55 public class PbapClientService extends ProfileService { 56 private static final boolean DBG = com.android.bluetooth.pbapclient.Utils.DBG; 57 private static final boolean VDBG = com.android.bluetooth.pbapclient.Utils.VDBG; 58 59 private static final String TAG = "PbapClientService"; 60 private static final String SERVICE_NAME = "Phonebook Access PCE"; 61 // MAXIMUM_DEVICES set to 10 to prevent an excessive number of simultaneous devices. 62 private static final int MAXIMUM_DEVICES = 10; 63 private Map<BluetoothDevice, PbapClientStateMachine> mPbapClientStateMachineMap = 64 new ConcurrentHashMap<>(); 65 private static PbapClientService sPbapClientService; 66 private PbapBroadcastReceiver mPbapBroadcastReceiver = new PbapBroadcastReceiver(); 67 private int mSdpHandle = -1; 68 69 private DatabaseManager mDatabaseManager; 70 71 @Override initBinder()72 public IProfileServiceBinder initBinder() { 73 return new BluetoothPbapClientBinder(this); 74 } 75 76 @Override start()77 protected boolean start() { 78 if (VDBG) { 79 Log.v(TAG, "onStart"); 80 } 81 82 mDatabaseManager = Objects.requireNonNull(AdapterService.getAdapterService().getDatabase(), 83 "DatabaseManager cannot be null when PbapClientService starts"); 84 85 IntentFilter filter = new IntentFilter(); 86 filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); 87 // delay initial download until after the user is unlocked to add an account. 88 filter.addAction(Intent.ACTION_USER_UNLOCKED); 89 // To remove call logs when PBAP was never connected while calls were made, 90 // we also listen for HFP to become disconnected. 91 filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED); 92 try { 93 registerReceiver(mPbapBroadcastReceiver, filter); 94 } catch (Exception e) { 95 Log.w(TAG, "Unable to register pbapclient receiver", e); 96 } 97 98 removeUncleanAccounts(); 99 registerSdpRecord(); 100 setPbapClientService(this); 101 return true; 102 } 103 104 @Override stop()105 protected boolean stop() { 106 setPbapClientService(null); 107 cleanUpSdpRecord(); 108 try { 109 unregisterReceiver(mPbapBroadcastReceiver); 110 } catch (Exception e) { 111 Log.w(TAG, "Unable to unregister pbapclient receiver", e); 112 } 113 for (PbapClientStateMachine pbapClientStateMachine : mPbapClientStateMachineMap.values()) { 114 pbapClientStateMachine.doQuit(); 115 } 116 removeUncleanAccounts(); 117 return true; 118 } 119 cleanupDevice(BluetoothDevice device)120 void cleanupDevice(BluetoothDevice device) { 121 if (DBG) Log.d(TAG, "Cleanup device: " + device); 122 synchronized (mPbapClientStateMachineMap) { 123 PbapClientStateMachine pbapClientStateMachine = mPbapClientStateMachineMap.get(device); 124 if (pbapClientStateMachine != null) { 125 mPbapClientStateMachineMap.remove(device); 126 } 127 } 128 } 129 removeUncleanAccounts()130 private void removeUncleanAccounts() { 131 // Find all accounts that match the type "pbap" and delete them. 132 AccountManager accountManager = AccountManager.get(this); 133 Account[] accounts = 134 accountManager.getAccountsByType(getString(R.string.pbap_account_type)); 135 if (VDBG) Log.v(TAG, "Found " + accounts.length + " unclean accounts"); 136 for (Account acc : accounts) { 137 Log.w(TAG, "Deleting " + acc); 138 try { 139 getContentResolver().delete(CallLog.Calls.CONTENT_URI, 140 CallLog.Calls.PHONE_ACCOUNT_ID + "=?", new String[]{acc.name}); 141 } catch (IllegalArgumentException e) { 142 Log.w(TAG, "Call Logs could not be deleted, they may not exist yet."); 143 } 144 // The device ID is the name of the account. 145 accountManager.removeAccountExplicitly(acc); 146 } 147 } 148 removeHfpCallLog(String accountName, Context context)149 private void removeHfpCallLog(String accountName, Context context) { 150 if (DBG) Log.d(TAG, "Removing call logs from " + accountName); 151 // Delete call logs belonging to accountName==BD_ADDR that also match 152 // component name "hfpclient". 153 ComponentName componentName = new ComponentName(context, HfpClientConnectionService.class); 154 String selectionFilter = CallLog.Calls.PHONE_ACCOUNT_ID + "=? AND " 155 + CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME + "=?"; 156 String[] selectionArgs = new String[]{accountName, componentName.flattenToString()}; 157 try { 158 getContentResolver().delete(CallLog.Calls.CONTENT_URI, selectionFilter, selectionArgs); 159 } catch (IllegalArgumentException e) { 160 Log.w(TAG, "Call Logs could not be deleted, they may not exist yet."); 161 } 162 } 163 registerSdpRecord()164 private void registerSdpRecord() { 165 SdpManager sdpManager = SdpManager.getDefaultManager(); 166 if (sdpManager == null) { 167 Log.e(TAG, "SdpManager is null"); 168 return; 169 } 170 mSdpHandle = sdpManager.createPbapPceRecord(SERVICE_NAME, 171 PbapClientConnectionHandler.PBAP_V1_2); 172 } 173 cleanUpSdpRecord()174 private void cleanUpSdpRecord() { 175 if (mSdpHandle < 0) { 176 Log.e(TAG, "cleanUpSdpRecord, SDP record never created"); 177 return; 178 } 179 int sdpHandle = mSdpHandle; 180 mSdpHandle = -1; 181 SdpManager sdpManager = SdpManager.getDefaultManager(); 182 if (sdpManager == null) { 183 Log.e(TAG, "cleanUpSdpRecord failed, sdpManager is null, sdpHandle=" + sdpHandle); 184 return; 185 } 186 Log.i(TAG, "cleanUpSdpRecord, mSdpHandle=" + sdpHandle); 187 if (!sdpManager.removeSdpRecord(sdpHandle)) { 188 Log.e(TAG, "cleanUpSdpRecord, removeSdpRecord failed, sdpHandle=" + sdpHandle); 189 } 190 } 191 192 193 private class PbapBroadcastReceiver extends BroadcastReceiver { 194 @Override onReceive(Context context, Intent intent)195 public void onReceive(Context context, Intent intent) { 196 String action = intent.getAction(); 197 if (DBG) Log.v(TAG, "onReceive" + action); 198 if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) { 199 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 200 if (getConnectionState(device) == BluetoothProfile.STATE_CONNECTED) { 201 disconnect(device); 202 } 203 } else if (action.equals(Intent.ACTION_USER_UNLOCKED)) { 204 for (PbapClientStateMachine stateMachine : mPbapClientStateMachineMap.values()) { 205 stateMachine.resumeDownload(); 206 } 207 } else if (action.equals(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED)) { 208 // PbapClientConnectionHandler has code to remove calllogs when PBAP disconnects. 209 // However, if PBAP was never connected/enabled in the first place, and calls are 210 // made over HFP, these calllogs will not be removed when the device disconnects. 211 // This code ensures callogs are still removed in this case. 212 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 213 int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); 214 215 if (newState == BluetoothProfile.STATE_DISCONNECTED) { 216 if (DBG) { 217 Log.d(TAG, "Received intent to disconnect HFP with " + device); 218 } 219 // HFP client stores entries in calllog.db by BD_ADDR and component name 220 removeHfpCallLog(device.getAddress(), context); 221 } 222 } 223 } 224 } 225 226 /** 227 * Handler for incoming service calls 228 */ 229 private static class BluetoothPbapClientBinder extends IBluetoothPbapClient.Stub 230 implements IProfileServiceBinder { 231 private PbapClientService mService; 232 BluetoothPbapClientBinder(PbapClientService svc)233 BluetoothPbapClientBinder(PbapClientService svc) { 234 mService = svc; 235 } 236 237 @Override cleanup()238 public void cleanup() { 239 mService = null; 240 } 241 242 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) getService(AttributionSource source)243 private PbapClientService getService(AttributionSource source) { 244 if (!Utils.checkCallerIsSystemOrActiveUser(TAG) 245 || !Utils.checkServiceAvailable(mService, TAG) 246 || !Utils.checkConnectPermissionForDataDelivery(mService, source, TAG)) { 247 return null; 248 } 249 return mService; 250 } 251 252 @Override connect(BluetoothDevice device, AttributionSource source)253 public boolean connect(BluetoothDevice device, AttributionSource source) { 254 Attributable.setAttributionSource(device, source); 255 PbapClientService service = getService(source); 256 if (DBG) { 257 Log.d(TAG, "PbapClient Binder connect "); 258 } 259 if (service == null) { 260 Log.e(TAG, "PbapClient Binder connect no service"); 261 return false; 262 } 263 return service.connect(device); 264 } 265 266 @Override disconnect(BluetoothDevice device, AttributionSource source)267 public boolean disconnect(BluetoothDevice device, AttributionSource source) { 268 Attributable.setAttributionSource(device, source); 269 PbapClientService service = getService(source); 270 if (service == null) { 271 return false; 272 } 273 return service.disconnect(device); 274 } 275 276 @Override getConnectedDevices(AttributionSource source)277 public List<BluetoothDevice> getConnectedDevices(AttributionSource source) { 278 PbapClientService service = getService(source); 279 if (service == null) { 280 return new ArrayList<BluetoothDevice>(0); 281 } 282 return service.getConnectedDevices(); 283 } 284 285 @Override getDevicesMatchingConnectionStates(int[] states, AttributionSource source)286 public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states, 287 AttributionSource source) { 288 PbapClientService service = getService(source); 289 if (service == null) { 290 return new ArrayList<BluetoothDevice>(0); 291 } 292 return service.getDevicesMatchingConnectionStates(states); 293 } 294 295 @Override getConnectionState(BluetoothDevice device, AttributionSource source)296 public int getConnectionState(BluetoothDevice device, AttributionSource source) { 297 Attributable.setAttributionSource(device, source); 298 PbapClientService service = getService(source); 299 if (service == null) { 300 return BluetoothProfile.STATE_DISCONNECTED; 301 } 302 return service.getConnectionState(device); 303 } 304 305 @Override setConnectionPolicy(BluetoothDevice device, int connectionPolicy, AttributionSource source)306 public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy, 307 AttributionSource source) { 308 Attributable.setAttributionSource(device, source); 309 PbapClientService service = getService(source); 310 if (service == null) { 311 return false; 312 } 313 return service.setConnectionPolicy(device, connectionPolicy); 314 } 315 316 @Override getConnectionPolicy(BluetoothDevice device, AttributionSource source)317 public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) { 318 Attributable.setAttributionSource(device, source); 319 PbapClientService service = getService(source); 320 if (service == null) { 321 return BluetoothProfile.CONNECTION_POLICY_UNKNOWN; 322 } 323 return service.getConnectionPolicy(device); 324 } 325 326 327 } 328 329 // API methods getPbapClientService()330 public static synchronized PbapClientService getPbapClientService() { 331 if (sPbapClientService == null) { 332 Log.w(TAG, "getPbapClientService(): service is null"); 333 return null; 334 } 335 if (!sPbapClientService.isAvailable()) { 336 Log.w(TAG, "getPbapClientService(): service is not available"); 337 return null; 338 } 339 return sPbapClientService; 340 } 341 setPbapClientService(PbapClientService instance)342 private static synchronized void setPbapClientService(PbapClientService instance) { 343 if (VDBG) { 344 Log.v(TAG, "setPbapClientService(): set to: " + instance); 345 } 346 sPbapClientService = instance; 347 } 348 349 @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) connect(BluetoothDevice device)350 public boolean connect(BluetoothDevice device) { 351 if (device == null) { 352 throw new IllegalArgumentException("Null device"); 353 } 354 enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, 355 "Need BLUETOOTH_PRIVILEGED permission"); 356 if (DBG) Log.d(TAG, "Received request to ConnectPBAPPhonebook " + device.getAddress()); 357 if (getConnectionPolicy(device) <= BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) { 358 return false; 359 } 360 synchronized (mPbapClientStateMachineMap) { 361 PbapClientStateMachine pbapClientStateMachine = mPbapClientStateMachineMap.get(device); 362 if (pbapClientStateMachine == null 363 && mPbapClientStateMachineMap.size() < MAXIMUM_DEVICES) { 364 pbapClientStateMachine = new PbapClientStateMachine(this, device); 365 pbapClientStateMachine.start(); 366 mPbapClientStateMachineMap.put(device, pbapClientStateMachine); 367 return true; 368 } else { 369 Log.w(TAG, "Received connect request while already connecting/connected."); 370 return false; 371 } 372 } 373 } 374 375 /** 376 * Disconnects the pbap client profile from the passed in device 377 * 378 * @param device is the device with which we will disconnect the pbap client profile 379 * @return true if we disconnected the pbap client profile, false otherwise 380 */ 381 @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) disconnect(BluetoothDevice device)382 public boolean disconnect(BluetoothDevice device) { 383 if (device == null) { 384 throw new IllegalArgumentException("Null device"); 385 } 386 enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, 387 "Need BLUETOOTH_PRIVILEGED permission"); 388 PbapClientStateMachine pbapClientStateMachine = mPbapClientStateMachineMap.get(device); 389 if (pbapClientStateMachine != null) { 390 pbapClientStateMachine.disconnect(device); 391 return true; 392 393 } else { 394 Log.w(TAG, "disconnect() called on unconnected device."); 395 return false; 396 } 397 } 398 getConnectedDevices()399 public List<BluetoothDevice> getConnectedDevices() { 400 int[] desiredStates = {BluetoothProfile.STATE_CONNECTED}; 401 return getDevicesMatchingConnectionStates(desiredStates); 402 } 403 getDevicesMatchingConnectionStates(int[] states)404 private List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 405 List<BluetoothDevice> deviceList = new ArrayList<BluetoothDevice>(0); 406 for (Map.Entry<BluetoothDevice, PbapClientStateMachine> stateMachineEntry : 407 mPbapClientStateMachineMap 408 .entrySet()) { 409 int currentDeviceState = stateMachineEntry.getValue().getConnectionState(); 410 for (int state : states) { 411 if (currentDeviceState == state) { 412 deviceList.add(stateMachineEntry.getKey()); 413 break; 414 } 415 } 416 } 417 return deviceList; 418 } 419 420 /** 421 * Get the current connection state of the profile 422 * 423 * @param device is the remote bluetooth device 424 * @return {@link BluetoothProfile#STATE_DISCONNECTED} if this profile is disconnected, 425 * {@link BluetoothProfile#STATE_CONNECTING} if this profile is being connected, 426 * {@link BluetoothProfile#STATE_CONNECTED} if this profile is connected, or 427 * {@link BluetoothProfile#STATE_DISCONNECTING} if this profile is being disconnected 428 */ getConnectionState(BluetoothDevice device)429 public int getConnectionState(BluetoothDevice device) { 430 if (device == null) { 431 throw new IllegalArgumentException("Null device"); 432 } 433 PbapClientStateMachine pbapClientStateMachine = mPbapClientStateMachineMap.get(device); 434 if (pbapClientStateMachine == null) { 435 return BluetoothProfile.STATE_DISCONNECTED; 436 } else { 437 return pbapClientStateMachine.getConnectionState(device); 438 } 439 } 440 441 /** 442 * Set connection policy of the profile and connects it if connectionPolicy is 443 * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED} or disconnects if connectionPolicy is 444 * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN} 445 * 446 * <p> The device should already be paired. 447 * Connection policy can be one of: 448 * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED}, 449 * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}, 450 * {@link BluetoothProfile#CONNECTION_POLICY_UNKNOWN} 451 * 452 * @param device Paired bluetooth device 453 * @param connectionPolicy is the connection policy to set to for this profile 454 * @return true if connectionPolicy is set, false on error 455 */ 456 @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) setConnectionPolicy(BluetoothDevice device, int connectionPolicy)457 public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy) { 458 if (device == null) { 459 throw new IllegalArgumentException("Null device"); 460 } 461 enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, 462 "Need BLUETOOTH_PRIVILEGED permission"); 463 if (DBG) { 464 Log.d(TAG, "Saved connectionPolicy " + device + " = " + connectionPolicy); 465 } 466 467 if (!mDatabaseManager.setProfileConnectionPolicy(device, BluetoothProfile.PBAP_CLIENT, 468 connectionPolicy)) { 469 return false; 470 } 471 if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED) { 472 connect(device); 473 } else if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) { 474 disconnect(device); 475 } 476 return true; 477 } 478 479 /** 480 * Get the connection policy of the profile. 481 * 482 * <p> The connection policy can be any of: 483 * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED}, 484 * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}, 485 * {@link BluetoothProfile#CONNECTION_POLICY_UNKNOWN} 486 * 487 * @param device Bluetooth device 488 * @return connection policy of the device 489 * @hide 490 */ 491 @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) getConnectionPolicy(BluetoothDevice device)492 public int getConnectionPolicy(BluetoothDevice device) { 493 if (device == null) { 494 throw new IllegalArgumentException("Null device"); 495 } 496 enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, 497 "Need BLUETOOTH_PRIVILEGED permission"); 498 return mDatabaseManager 499 .getProfileConnectionPolicy(device, BluetoothProfile.PBAP_CLIENT); 500 } 501 502 @Override dump(StringBuilder sb)503 public void dump(StringBuilder sb) { 504 super.dump(sb); 505 for (PbapClientStateMachine stateMachine : mPbapClientStateMachineMap.values()) { 506 stateMachine.dump(sb); 507 } 508 } 509 } 510