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