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.mapclient; 18 19 import android.Manifest; 20 import android.app.PendingIntent; 21 import android.bluetooth.BluetoothAdapter; 22 import android.bluetooth.BluetoothDevice; 23 import android.bluetooth.BluetoothProfile; 24 import android.bluetooth.BluetoothUuid; 25 import android.bluetooth.IBluetoothMapClient; 26 import android.bluetooth.SdpMasRecord; 27 import android.content.BroadcastReceiver; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.IntentFilter; 31 import android.net.Uri; 32 import android.os.ParcelUuid; 33 import android.util.Log; 34 35 import com.android.bluetooth.Utils; 36 import com.android.bluetooth.btservice.AdapterService; 37 import com.android.bluetooth.btservice.ProfileService; 38 import com.android.internal.annotations.VisibleForTesting; 39 40 import java.util.ArrayList; 41 import java.util.Arrays; 42 import java.util.Iterator; 43 import java.util.List; 44 import java.util.Map; 45 import java.util.Set; 46 import java.util.concurrent.ConcurrentHashMap; 47 48 public class MapClientService extends ProfileService { 49 private static final String TAG = "MapClientService"; 50 51 static final boolean DBG = false; 52 static final boolean VDBG = false; 53 54 static final int MAXIMUM_CONNECTED_DEVICES = 4; 55 56 private static final String BLUETOOTH_PERM = android.Manifest.permission.BLUETOOTH; 57 58 private Map<BluetoothDevice, MceStateMachine> mMapInstanceMap = new ConcurrentHashMap<>(1); 59 private MnsService mMnsServer; 60 private BluetoothAdapter mAdapter; 61 private static MapClientService sMapClientService; 62 private MapBroadcastReceiver mMapReceiver = new MapBroadcastReceiver(); 63 getMapClientService()64 public static synchronized MapClientService getMapClientService() { 65 if (sMapClientService == null) { 66 Log.w(TAG, "getMapClientService(): service is null"); 67 return null; 68 } 69 if (!sMapClientService.isAvailable()) { 70 Log.w(TAG, "getMapClientService(): service is not available "); 71 return null; 72 } 73 return sMapClientService; 74 } 75 setMapClientService(MapClientService instance)76 private static synchronized void setMapClientService(MapClientService instance) { 77 if (DBG) { 78 Log.d(TAG, "setMapClientService(): set to: " + instance); 79 } 80 sMapClientService = instance; 81 } 82 83 @VisibleForTesting getInstanceMap()84 Map<BluetoothDevice, MceStateMachine> getInstanceMap() { 85 return mMapInstanceMap; 86 } 87 88 /** 89 * Connect the given Bluetooth device. 90 * 91 * @param device 92 * @return true if connection is successful, false otherwise. 93 */ connect(BluetoothDevice device)94 public synchronized boolean connect(BluetoothDevice device) { 95 if (device == null) { 96 throw new IllegalArgumentException("Null device"); 97 } 98 if (DBG) { 99 StringBuilder sb = new StringBuilder(); 100 dump(sb); 101 Log.d(TAG, "MAP connect device: " + device 102 + ", InstanceMap start state: " + sb.toString()); 103 } 104 if (getPriority(device) == BluetoothProfile.PRIORITY_OFF) { 105 Log.w(TAG, "Connection not allowed: <" + device.getAddress() + "> is PRIORITY_OFF"); 106 return false; 107 } 108 MceStateMachine mapStateMachine = mMapInstanceMap.get(device); 109 if (mapStateMachine == null) { 110 // a map state machine instance doesn't exist yet, create a new one if we can. 111 if (mMapInstanceMap.size() < MAXIMUM_CONNECTED_DEVICES) { 112 addDeviceToMapAndConnect(device); 113 return true; 114 } else { 115 // Maxed out on the number of allowed connections. 116 // see if some of the current connections can be cleaned-up, to make room. 117 removeUncleanAccounts(); 118 if (mMapInstanceMap.size() < MAXIMUM_CONNECTED_DEVICES) { 119 addDeviceToMapAndConnect(device); 120 return true; 121 } else { 122 Log.e(TAG, "Maxed out on the number of allowed MAP connections. " 123 + "Connect request rejected on " + device); 124 return false; 125 } 126 } 127 } 128 129 // statemachine already exists in the map. 130 int state = getConnectionState(device); 131 if (state == BluetoothProfile.STATE_CONNECTED 132 || state == BluetoothProfile.STATE_CONNECTING) { 133 Log.w(TAG, "Received connect request while already connecting/connected."); 134 return true; 135 } 136 137 // Statemachine exists but not in connecting or connected state! it should 138 // have been removed form the map. lets get rid of it and add a new one. 139 if (DBG) { 140 Log.d(TAG, "Statemachine exists for a device in unexpected state: " + state); 141 } 142 mMapInstanceMap.remove(device); 143 addDeviceToMapAndConnect(device); 144 if (DBG) { 145 StringBuilder sb = new StringBuilder(); 146 dump(sb); 147 Log.d(TAG, "MAP connect device: " + device 148 + ", InstanceMap end state: " + sb.toString()); 149 } 150 return true; 151 } 152 addDeviceToMapAndConnect(BluetoothDevice device)153 private synchronized void addDeviceToMapAndConnect(BluetoothDevice device) { 154 // When creating a new statemachine, its state is set to CONNECTING - which will trigger 155 // connect. 156 MceStateMachine mapStateMachine = new MceStateMachine(this, device); 157 mMapInstanceMap.put(device, mapStateMachine); 158 } 159 disconnect(BluetoothDevice device)160 public synchronized boolean disconnect(BluetoothDevice device) { 161 if (DBG) { 162 StringBuilder sb = new StringBuilder(); 163 dump(sb); 164 Log.d(TAG, "MAP disconnect device: " + device 165 + ", InstanceMap start state: " + sb.toString()); 166 } 167 MceStateMachine mapStateMachine = mMapInstanceMap.get(device); 168 // a map state machine instance doesn't exist. maybe it is already gone? 169 if (mapStateMachine == null) { 170 return false; 171 } 172 int connectionState = mapStateMachine.getState(); 173 if (connectionState != BluetoothProfile.STATE_CONNECTED 174 && connectionState != BluetoothProfile.STATE_CONNECTING) { 175 return false; 176 } 177 mapStateMachine.disconnect(); 178 if (DBG) { 179 StringBuilder sb = new StringBuilder(); 180 dump(sb); 181 Log.d(TAG, "MAP disconnect device: " + device 182 + ", InstanceMap start state: " + sb.toString()); 183 } 184 return true; 185 } 186 getConnectedDevices()187 public List<BluetoothDevice> getConnectedDevices() { 188 return getDevicesMatchingConnectionStates(new int[]{BluetoothAdapter.STATE_CONNECTED}); 189 } 190 getMceStateMachineForDevice(BluetoothDevice device)191 MceStateMachine getMceStateMachineForDevice(BluetoothDevice device) { 192 return mMapInstanceMap.get(device); 193 } 194 getDevicesMatchingConnectionStates(int[] states)195 public synchronized List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 196 if (DBG) Log.d(TAG, "getDevicesMatchingConnectionStates" + Arrays.toString(states)); 197 List<BluetoothDevice> deviceList = new ArrayList<>(); 198 Set<BluetoothDevice> bondedDevices = mAdapter.getBondedDevices(); 199 int connectionState; 200 for (BluetoothDevice device : bondedDevices) { 201 connectionState = getConnectionState(device); 202 if (DBG) Log.d(TAG, "Device: " + device + "State: " + connectionState); 203 for (int i = 0; i < states.length; i++) { 204 if (connectionState == states[i]) { 205 deviceList.add(device); 206 } 207 } 208 } 209 if (DBG) Log.d(TAG, deviceList.toString()); 210 return deviceList; 211 } 212 getConnectionState(BluetoothDevice device)213 public synchronized int getConnectionState(BluetoothDevice device) { 214 MceStateMachine mapStateMachine = mMapInstanceMap.get(device); 215 // a map state machine instance doesn't exist yet, create a new one if we can. 216 return (mapStateMachine == null) ? BluetoothProfile.STATE_DISCONNECTED 217 : mapStateMachine.getState(); 218 } 219 setPriority(BluetoothDevice device, int priority)220 public boolean setPriority(BluetoothDevice device, int priority) { 221 if (VDBG) { 222 Log.v(TAG, "Saved priority " + device + " = " + priority); 223 } 224 AdapterService.getAdapterService().getDatabase() 225 .setProfilePriority(device, BluetoothProfile.MAP_CLIENT, priority); 226 return true; 227 } 228 getPriority(BluetoothDevice device)229 public int getPriority(BluetoothDevice device) { 230 return AdapterService.getAdapterService().getDatabase() 231 .getProfilePriority(device, BluetoothProfile.MAP_CLIENT); 232 } 233 sendMessage(BluetoothDevice device, Uri[] contacts, String message, PendingIntent sentIntent, PendingIntent deliveredIntent)234 public synchronized boolean sendMessage(BluetoothDevice device, Uri[] contacts, String message, 235 PendingIntent sentIntent, PendingIntent deliveredIntent) { 236 MceStateMachine mapStateMachine = mMapInstanceMap.get(device); 237 return mapStateMachine != null 238 && mapStateMachine.sendMapMessage(contacts, message, sentIntent, deliveredIntent); 239 } 240 241 @Override initBinder()242 protected IProfileServiceBinder initBinder() { 243 return new Binder(this); 244 } 245 246 @Override start()247 protected boolean start() { 248 Log.e(TAG, "start()"); 249 250 if (mMnsServer == null) { 251 mMnsServer = MapUtils.newMnsServiceInstance(this); 252 if (mMnsServer == null) { 253 // this can't happen 254 Log.w(TAG, "MnsService is *not* created!"); 255 return false; 256 } 257 } 258 259 mAdapter = BluetoothAdapter.getDefaultAdapter(); 260 261 IntentFilter filter = new IntentFilter(); 262 filter.addAction(BluetoothDevice.ACTION_SDP_RECORD); 263 filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); 264 registerReceiver(mMapReceiver, filter); 265 removeUncleanAccounts(); 266 setMapClientService(this); 267 return true; 268 } 269 270 @Override stop()271 protected synchronized boolean stop() { 272 if (DBG) { 273 Log.d(TAG, "stop()"); 274 } 275 unregisterReceiver(mMapReceiver); 276 if (mMnsServer != null) { 277 mMnsServer.stop(); 278 } 279 for (MceStateMachine stateMachine : mMapInstanceMap.values()) { 280 if (stateMachine.getState() == BluetoothAdapter.STATE_CONNECTED) { 281 stateMachine.disconnect(); 282 } 283 stateMachine.doQuit(); 284 } 285 return true; 286 } 287 288 @Override cleanup()289 protected void cleanup() { 290 if (DBG) { 291 Log.d(TAG, "in Cleanup"); 292 } 293 removeUncleanAccounts(); 294 // TODO(b/72948646): should be moved to stop() 295 setMapClientService(null); 296 } 297 298 /** 299 * cleanupDevice removes the associated state machine from the instance map 300 * 301 * @param device BluetoothDevice address of remote device 302 */ 303 @VisibleForTesting cleanupDevice(BluetoothDevice device)304 public void cleanupDevice(BluetoothDevice device) { 305 if (DBG) { 306 StringBuilder sb = new StringBuilder(); 307 dump(sb); 308 Log.d(TAG, "Cleanup device: " + device + ", InstanceMap start state: " 309 + sb.toString()); 310 } 311 synchronized (mMapInstanceMap) { 312 MceStateMachine stateMachine = mMapInstanceMap.get(device); 313 if (stateMachine != null) { 314 mMapInstanceMap.remove(device); 315 } 316 } 317 if (DBG) { 318 StringBuilder sb = new StringBuilder(); 319 dump(sb); 320 Log.d(TAG, "Cleanup device: " + device + ", InstanceMap end state: " 321 + sb.toString()); 322 } 323 } 324 325 @VisibleForTesting removeUncleanAccounts()326 void removeUncleanAccounts() { 327 if (DBG) { 328 StringBuilder sb = new StringBuilder(); 329 dump(sb); 330 Log.d(TAG, "removeUncleanAccounts:InstanceMap end state: " 331 + sb.toString()); 332 } 333 Iterator iterator = mMapInstanceMap.entrySet().iterator(); 334 while (iterator.hasNext()) { 335 Map.Entry<BluetoothDevice, MceStateMachine> profileConnection = 336 (Map.Entry) iterator.next(); 337 if (profileConnection.getValue().getState() == BluetoothProfile.STATE_DISCONNECTED) { 338 iterator.remove(); 339 } 340 } 341 if (DBG) { 342 StringBuilder sb = new StringBuilder(); 343 dump(sb); 344 Log.d(TAG, "removeUncleanAccounts:InstanceMap end state: " 345 + sb.toString()); 346 } 347 } 348 getUnreadMessages(BluetoothDevice device)349 public synchronized boolean getUnreadMessages(BluetoothDevice device) { 350 MceStateMachine mapStateMachine = mMapInstanceMap.get(device); 351 if (mapStateMachine == null) { 352 return false; 353 } 354 return mapStateMachine.getUnreadMessages(); 355 } 356 357 /** 358 * Returns the SDP record's MapSupportedFeatures field (see Bluetooth MAP 1.4 spec, page 114). 359 * @param device The Bluetooth device to get this value for. 360 * @return the SDP record's MapSupportedFeatures field. 361 */ getSupportedFeatures(BluetoothDevice device)362 public synchronized int getSupportedFeatures(BluetoothDevice device) { 363 MceStateMachine mapStateMachine = mMapInstanceMap.get(device); 364 if (mapStateMachine == null) { 365 if (DBG) Log.d(TAG, "in getSupportedFeatures, returning 0"); 366 return 0; 367 } 368 return mapStateMachine.getSupportedFeatures(); 369 } 370 371 @Override dump(StringBuilder sb)372 public void dump(StringBuilder sb) { 373 super.dump(sb); 374 for (MceStateMachine stateMachine : mMapInstanceMap.values()) { 375 stateMachine.dump(sb); 376 } 377 } 378 379 //Binder object: Must be static class or memory leak may occur 380 381 /** 382 * This class implements the IClient interface - or actually it validates the 383 * preconditions for calling the actual functionality in the MapClientService, and calls it. 384 */ 385 private static class Binder extends IBluetoothMapClient.Stub implements IProfileServiceBinder { 386 private MapClientService mService; 387 Binder(MapClientService service)388 Binder(MapClientService service) { 389 if (VDBG) { 390 Log.v(TAG, "Binder()"); 391 } 392 mService = service; 393 } 394 getService()395 private MapClientService getService() { 396 if (!Utils.checkCaller()) { 397 Log.w(TAG, "MAP call not allowed for non-active user"); 398 return null; 399 } 400 401 if (mService != null && mService.isAvailable()) { 402 mService.enforceCallingOrSelfPermission(BLUETOOTH_PERM, 403 "Need BLUETOOTH permission"); 404 return mService; 405 } 406 return null; 407 } 408 409 @Override cleanup()410 public void cleanup() { 411 mService = null; 412 } 413 414 @Override isConnected(BluetoothDevice device)415 public boolean isConnected(BluetoothDevice device) { 416 if (VDBG) { 417 Log.v(TAG, "isConnected()"); 418 } 419 MapClientService service = getService(); 420 if (service == null) { 421 return false; 422 } 423 return service.getConnectionState(device) == BluetoothProfile.STATE_CONNECTED; 424 } 425 426 @Override connect(BluetoothDevice device)427 public boolean connect(BluetoothDevice device) { 428 if (VDBG) { 429 Log.v(TAG, "connect()"); 430 } 431 MapClientService service = getService(); 432 if (service == null) { 433 return false; 434 } 435 return service.connect(device); 436 } 437 438 @Override disconnect(BluetoothDevice device)439 public boolean disconnect(BluetoothDevice device) { 440 if (VDBG) { 441 Log.v(TAG, "disconnect()"); 442 } 443 MapClientService service = getService(); 444 if (service == null) { 445 return false; 446 } 447 return service.disconnect(device); 448 } 449 450 @Override getConnectedDevices()451 public List<BluetoothDevice> getConnectedDevices() { 452 if (VDBG) { 453 Log.v(TAG, "getConnectedDevices()"); 454 } 455 MapClientService service = getService(); 456 if (service == null) { 457 return new ArrayList<BluetoothDevice>(0); 458 } 459 return service.getConnectedDevices(); 460 } 461 462 @Override getDevicesMatchingConnectionStates(int[] states)463 public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 464 if (VDBG) { 465 Log.v(TAG, "getDevicesMatchingConnectionStates()"); 466 } 467 MapClientService service = getService(); 468 if (service == null) { 469 return new ArrayList<BluetoothDevice>(0); 470 } 471 return service.getDevicesMatchingConnectionStates(states); 472 } 473 474 @Override getConnectionState(BluetoothDevice device)475 public int getConnectionState(BluetoothDevice device) { 476 if (VDBG) { 477 Log.v(TAG, "getConnectionState()"); 478 } 479 MapClientService service = getService(); 480 if (service == null) { 481 return BluetoothProfile.STATE_DISCONNECTED; 482 } 483 return service.getConnectionState(device); 484 } 485 486 @Override setPriority(BluetoothDevice device, int priority)487 public boolean setPriority(BluetoothDevice device, int priority) { 488 MapClientService service = getService(); 489 if (service == null) { 490 return false; 491 } 492 return service.setPriority(device, priority); 493 } 494 495 @Override getPriority(BluetoothDevice device)496 public int getPriority(BluetoothDevice device) { 497 MapClientService service = getService(); 498 if (service == null) { 499 return BluetoothProfile.PRIORITY_UNDEFINED; 500 } 501 return service.getPriority(device); 502 } 503 504 @Override sendMessage(BluetoothDevice device, Uri[] contacts, String message, PendingIntent sentIntent, PendingIntent deliveredIntent)505 public boolean sendMessage(BluetoothDevice device, Uri[] contacts, String message, 506 PendingIntent sentIntent, PendingIntent deliveredIntent) { 507 MapClientService service = getService(); 508 if (service == null) { 509 return false; 510 } 511 if (DBG) Log.d(TAG, "Checking Permission of sendMessage"); 512 mService.enforceCallingOrSelfPermission(Manifest.permission.SEND_SMS, 513 "Need SEND_SMS permission"); 514 515 return service.sendMessage(device, contacts, message, sentIntent, deliveredIntent); 516 } 517 518 @Override getUnreadMessages(BluetoothDevice device)519 public boolean getUnreadMessages(BluetoothDevice device) { 520 MapClientService service = getService(); 521 if (service == null) { 522 return false; 523 } 524 mService.enforceCallingOrSelfPermission(Manifest.permission.READ_SMS, 525 "Need READ_SMS permission"); 526 return service.getUnreadMessages(device); 527 } 528 529 @Override getSupportedFeatures(BluetoothDevice device)530 public int getSupportedFeatures(BluetoothDevice device) { 531 MapClientService service = getService(); 532 if (service == null) { 533 if (DBG) { 534 Log.d(TAG, 535 "in MapClientService getSupportedFeatures stub, returning 0"); 536 } 537 return 0; 538 } 539 mService.enforceCallingOrSelfPermission(Manifest.permission.BLUETOOTH, 540 "Need BLUETOOTH permission"); 541 return service.getSupportedFeatures(device); 542 } 543 } 544 545 private class MapBroadcastReceiver extends BroadcastReceiver { 546 @Override onReceive(Context context, Intent intent)547 public void onReceive(Context context, Intent intent) { 548 String action = intent.getAction(); 549 if (DBG) { 550 Log.d(TAG, "onReceive: " + action); 551 } 552 if (!action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED) 553 && !action.equals(BluetoothDevice.ACTION_SDP_RECORD)) { 554 // we don't care about this intent 555 return; 556 } 557 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 558 if (device == null) { 559 Log.e(TAG, "broadcast has NO device param!"); 560 return; 561 } 562 if (DBG) { 563 Log.d(TAG, "broadcast has device: (" + device.getAddress() + ", " 564 + device.getName() + ")"); 565 } 566 MceStateMachine stateMachine = mMapInstanceMap.get(device); 567 if (stateMachine == null) { 568 Log.e(TAG, "No Statemachine found for the device from broadcast"); 569 return; 570 } 571 572 if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) { 573 if (stateMachine.getState() == BluetoothProfile.STATE_CONNECTED) { 574 stateMachine.disconnect(); 575 } 576 } 577 578 if (action.equals(BluetoothDevice.ACTION_SDP_RECORD)) { 579 ParcelUuid uuid = intent.getParcelableExtra(BluetoothDevice.EXTRA_UUID); 580 if (DBG) { 581 Log.d(TAG, "UUID of SDP: " + uuid); 582 } 583 584 if (uuid.equals(BluetoothUuid.MAS)) { 585 // Check if we have a valid SDP record. 586 SdpMasRecord masRecord = 587 intent.getParcelableExtra(BluetoothDevice.EXTRA_SDP_RECORD); 588 if (DBG) { 589 Log.d(TAG, "SDP = " + masRecord); 590 } 591 int status = intent.getIntExtra(BluetoothDevice.EXTRA_SDP_SEARCH_STATUS, -1); 592 if (masRecord == null) { 593 Log.w(TAG, "SDP search ended with no MAS record. Status: " + status); 594 return; 595 } 596 stateMachine.obtainMessage(MceStateMachine.MSG_MAS_SDP_DONE, 597 masRecord).sendToTarget(); 598 } 599 } 600 } 601 } 602 } 603