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 static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED; 20 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; 21 import static android.bluetooth.BluetoothProfile.STATE_CONNECTED; 22 import static android.bluetooth.BluetoothProfile.STATE_CONNECTING; 23 import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; 24 25 import static java.util.Objects.requireNonNull; 26 import static java.util.Objects.requireNonNullElseGet; 27 28 import android.app.PendingIntent; 29 import android.bluetooth.BluetoothAdapter; 30 import android.bluetooth.BluetoothDevice; 31 import android.bluetooth.BluetoothProfile; 32 import android.bluetooth.BluetoothUuid; 33 import android.bluetooth.SdpMasRecord; 34 import android.net.Uri; 35 import android.os.Handler; 36 import android.os.Looper; 37 import android.os.ParcelUuid; 38 import android.os.Parcelable; 39 import android.sysprop.BluetoothProperties; 40 import android.util.Log; 41 42 import com.android.bluetooth.btservice.AdapterService; 43 import com.android.bluetooth.btservice.ProfileService; 44 import com.android.bluetooth.btservice.storage.DatabaseManager; 45 import com.android.internal.annotations.VisibleForTesting; 46 47 import java.util.ArrayList; 48 import java.util.Arrays; 49 import java.util.Iterator; 50 import java.util.List; 51 import java.util.Map; 52 import java.util.concurrent.ConcurrentHashMap; 53 54 public class MapClientService extends ProfileService { 55 private static final String TAG = MapClientService.class.getSimpleName(); 56 57 static final int MAXIMUM_CONNECTED_DEVICES = 4; 58 59 private final Map<BluetoothDevice, MceStateMachine> mMapInstanceMap = 60 new ConcurrentHashMap<>(1); 61 62 private final AdapterService mAdapterService; 63 private final DatabaseManager mDatabaseManager; 64 private final MnsService mMnsServer; 65 private final Looper mStateMachinesLooper; 66 private final Handler mHandler; 67 68 private static MapClientService sMapClientService; 69 MapClientService(AdapterService adapterService)70 public MapClientService(AdapterService adapterService) { 71 this(adapterService, null, null); 72 } 73 74 @VisibleForTesting MapClientService(AdapterService adapterService, Looper looper, MnsService mnsServer)75 MapClientService(AdapterService adapterService, Looper looper, MnsService mnsServer) { 76 super(requireNonNull(adapterService)); 77 mAdapterService = adapterService; 78 mDatabaseManager = requireNonNull(adapterService.getDatabase()); 79 mMnsServer = requireNonNullElseGet(mnsServer, () -> new MnsService(this)); 80 81 if (looper == null) { 82 mHandler = new Handler(requireNonNull(Looper.getMainLooper())); 83 mStateMachinesLooper = null; 84 } else { 85 mHandler = new Handler(looper); 86 87 // MapClient is only using a common state machine looper for test. 88 // In real device, it use a thread per device connected to avoid congestion. 89 mStateMachinesLooper = looper; 90 } 91 92 removeUncleanAccounts(); 93 MapClientContent.clearAllContent(this); 94 setMapClientService(this); 95 } 96 isEnabled()97 public static boolean isEnabled() { 98 return BluetoothProperties.isProfileMapClientEnabled().orElse(false); 99 } 100 getMapClientService()101 public static synchronized MapClientService getMapClientService() { 102 if (sMapClientService == null) { 103 Log.w(TAG, "getMapClientService(): service is null"); 104 return null; 105 } 106 if (!sMapClientService.isAvailable()) { 107 Log.w(TAG, "getMapClientService(): service is not available "); 108 return null; 109 } 110 return sMapClientService; 111 } 112 113 @VisibleForTesting setMapClientService(MapClientService instance)114 static synchronized void setMapClientService(MapClientService instance) { 115 Log.d(TAG, "setMapClientService(): set to: " + instance); 116 sMapClientService = instance; 117 } 118 119 @VisibleForTesting getInstanceMap()120 Map<BluetoothDevice, MceStateMachine> getInstanceMap() { 121 return mMapInstanceMap; 122 } 123 124 /** 125 * Connect the given Bluetooth device. 126 * 127 * @return true if connection is successful, false otherwise. 128 */ connect(BluetoothDevice device)129 public synchronized boolean connect(BluetoothDevice device) { 130 if (device == null) { 131 throw new IllegalArgumentException("Null device"); 132 } 133 Log.d(TAG, "connect(device= " + device + "): devices=" + mMapInstanceMap.keySet()); 134 if (getConnectionPolicy(device) == CONNECTION_POLICY_FORBIDDEN) { 135 Log.w( 136 TAG, 137 "Connection not allowed: <" 138 + device.getAddress() 139 + "> is CONNECTION_POLICY_FORBIDDEN"); 140 return false; 141 } 142 MceStateMachine mapStateMachine = mMapInstanceMap.get(device); 143 if (mapStateMachine == null) { 144 // a map state machine instance doesn't exist yet, create a new one if we can. 145 if (mMapInstanceMap.size() < MAXIMUM_CONNECTED_DEVICES) { 146 addDeviceToMapAndConnect(device); 147 return true; 148 } else { 149 // Maxed out on the number of allowed connections. 150 // see if some of the current connections can be cleaned-up, to make room. 151 removeUncleanAccounts(); 152 if (mMapInstanceMap.size() < MAXIMUM_CONNECTED_DEVICES) { 153 addDeviceToMapAndConnect(device); 154 return true; 155 } else { 156 Log.e( 157 TAG, 158 "Maxed out on the number of allowed MAP connections. " 159 + "Connect request rejected on " 160 + device); 161 return false; 162 } 163 } 164 } 165 166 // StateMachine already exists in the map. 167 int state = getConnectionState(device); 168 if (state == STATE_CONNECTED || state == STATE_CONNECTING) { 169 Log.w(TAG, "Received connect request while already connecting/connected."); 170 return true; 171 } 172 173 // StateMachine exists but not in connecting or connected state! it should 174 // have been removed form the map. lets get rid of it and add a new one. 175 Log.d(TAG, "StateMachine exists for a device in unexpected state: " + state); 176 mMapInstanceMap.remove(device); 177 mapStateMachine.doQuit(); 178 179 addDeviceToMapAndConnect(device); 180 Log.d(TAG, "connect(device= " + device + "): end devices=" + mMapInstanceMap.keySet()); 181 return true; 182 } 183 addDeviceToMapAndConnect(BluetoothDevice device)184 private synchronized void addDeviceToMapAndConnect(BluetoothDevice device) { 185 // When creating a new StateMachine, its state is set to CONNECTING - which will trigger 186 // connect. 187 MceStateMachine mapStateMachine; 188 if (mStateMachinesLooper != null) { 189 mapStateMachine = 190 new MceStateMachine(this, device, mAdapterService, mStateMachinesLooper); 191 } else { 192 mapStateMachine = new MceStateMachine(this, device, mAdapterService); 193 } 194 mMapInstanceMap.put(device, mapStateMachine); 195 } 196 disconnect(BluetoothDevice device)197 public synchronized boolean disconnect(BluetoothDevice device) { 198 Log.d(TAG, "disconnect(device= " + device + "): devices=" + mMapInstanceMap.keySet()); 199 MceStateMachine mapStateMachine = mMapInstanceMap.get(device); 200 // a map state machine instance doesn't exist. maybe it is already gone? 201 if (mapStateMachine == null) { 202 return false; 203 } 204 int connectionState = mapStateMachine.getState(); 205 if (connectionState != STATE_CONNECTED && connectionState != STATE_CONNECTING) { 206 return false; 207 } 208 mapStateMachine.disconnect(); 209 Log.d(TAG, "disconnect(device= " + device + "): end devices=" + mMapInstanceMap.keySet()); 210 return true; 211 } 212 getConnectedDevices()213 public List<BluetoothDevice> getConnectedDevices() { 214 return getDevicesMatchingConnectionStates(new int[] {BluetoothAdapter.STATE_CONNECTED}); 215 } 216 getMceStateMachineForDevice(BluetoothDevice device)217 MceStateMachine getMceStateMachineForDevice(BluetoothDevice device) { 218 return mMapInstanceMap.get(device); 219 } 220 getDevicesMatchingConnectionStates(int[] states)221 public synchronized List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 222 Log.d(TAG, "getDevicesMatchingConnectionStates" + Arrays.toString(states)); 223 List<BluetoothDevice> deviceList = new ArrayList<>(); 224 BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices(); 225 int connectionState; 226 for (BluetoothDevice device : bondedDevices) { 227 connectionState = getConnectionState(device); 228 Log.d(TAG, "Device: " + device + "State: " + connectionState); 229 for (int i = 0; i < states.length; i++) { 230 if (connectionState == states[i]) { 231 deviceList.add(device); 232 } 233 } 234 } 235 Log.d(TAG, deviceList.toString()); 236 return deviceList; 237 } 238 getConnectionState(BluetoothDevice device)239 public synchronized int getConnectionState(BluetoothDevice device) { 240 MceStateMachine mapStateMachine = mMapInstanceMap.get(device); 241 // a map state machine instance doesn't exist yet, create a new one if we can. 242 return (mapStateMachine == null) ? STATE_DISCONNECTED : mapStateMachine.getState(); 243 } 244 245 /** 246 * Set connection policy of the profile and connects it if connectionPolicy is {@link 247 * BluetoothProfile#CONNECTION_POLICY_ALLOWED} or disconnects if connectionPolicy is {@link 248 * BluetoothProfile#CONNECTION_POLICY_FORBIDDEN} 249 * 250 * <p>The device should already be paired. Connection policy can be one of: {@link 251 * BluetoothProfile#CONNECTION_POLICY_ALLOWED}, {@link 252 * BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}, {@link 253 * BluetoothProfile#CONNECTION_POLICY_UNKNOWN} 254 * 255 * @param device Paired bluetooth device 256 * @param connectionPolicy is the connection policy to set to for this profile 257 * @return true if connectionPolicy is set, false on error 258 */ setConnectionPolicy(BluetoothDevice device, int connectionPolicy)259 public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy) { 260 Log.v(TAG, "Saved connectionPolicy " + device + " = " + connectionPolicy); 261 262 if (!mDatabaseManager.setProfileConnectionPolicy( 263 device, BluetoothProfile.MAP_CLIENT, connectionPolicy)) { 264 return false; 265 } 266 if (connectionPolicy == CONNECTION_POLICY_ALLOWED) { 267 connect(device); 268 } else if (connectionPolicy == CONNECTION_POLICY_FORBIDDEN) { 269 disconnect(device); 270 } 271 return true; 272 } 273 274 /** 275 * Get the connection policy of the profile. 276 * 277 * <p>The connection policy can be any of: {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED}, 278 * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}, {@link 279 * BluetoothProfile#CONNECTION_POLICY_UNKNOWN} 280 * 281 * @param device Bluetooth device 282 * @return connection policy of the device 283 */ getConnectionPolicy(BluetoothDevice device)284 public int getConnectionPolicy(BluetoothDevice device) { 285 return mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.MAP_CLIENT); 286 } 287 sendMessage( BluetoothDevice device, Uri[] contacts, String message, PendingIntent sentIntent, PendingIntent deliveredIntent)288 public synchronized boolean sendMessage( 289 BluetoothDevice device, 290 Uri[] contacts, 291 String message, 292 PendingIntent sentIntent, 293 PendingIntent deliveredIntent) { 294 MceStateMachine mapStateMachine = mMapInstanceMap.get(device); 295 return mapStateMachine != null 296 && mapStateMachine.sendMapMessage(contacts, message, sentIntent, deliveredIntent); 297 } 298 299 @Override initBinder()300 public IProfileServiceBinder initBinder() { 301 return new MapClientServiceBinder(this); 302 } 303 304 @Override cleanup()305 public synchronized void cleanup() { 306 Log.i(TAG, "Cleanup MapClient Service"); 307 308 mMnsServer.stop(); 309 for (MceStateMachine stateMachine : mMapInstanceMap.values()) { 310 if (stateMachine.getState() == BluetoothAdapter.STATE_CONNECTED) { 311 stateMachine.disconnect(); 312 } 313 stateMachine.doQuit(); 314 } 315 mMapInstanceMap.clear(); 316 317 // Unregister Handler and stop all queued messages. 318 mHandler.removeCallbacksAndMessages(null); 319 320 removeUncleanAccounts(); 321 322 setMapClientService(null); 323 } 324 325 /** 326 * cleanupDevice removes the associated state machine from the instance map 327 * 328 * @param device BluetoothDevice address of remote device 329 * @param sm the state machine to clean up or {@code null} to clean up any state machine. 330 */ 331 @VisibleForTesting cleanupDevice(BluetoothDevice device, MceStateMachine sm)332 public void cleanupDevice(BluetoothDevice device, MceStateMachine sm) { 333 Log.d(TAG, "cleanup(device= " + device + "): devices=" + mMapInstanceMap.keySet()); 334 synchronized (mMapInstanceMap) { 335 MceStateMachine stateMachine = mMapInstanceMap.get(device); 336 if (stateMachine != null) { 337 if (sm == null || stateMachine == sm) { 338 mMapInstanceMap.remove(device); 339 stateMachine.doQuit(); 340 } else { 341 Log.w(TAG, "Trying to clean up wrong state machine"); 342 } 343 } 344 } 345 Log.d(TAG, "cleanup(device= " + device + "): end devices=" + mMapInstanceMap.keySet()); 346 } 347 348 @VisibleForTesting removeUncleanAccounts()349 void removeUncleanAccounts() { 350 Log.d(TAG, "removeUncleanAccounts(): devices=" + mMapInstanceMap.keySet()); 351 Iterator iterator = mMapInstanceMap.entrySet().iterator(); 352 while (iterator.hasNext()) { 353 Map.Entry<BluetoothDevice, MceStateMachine> profileConnection = 354 (Map.Entry) iterator.next(); 355 if (profileConnection.getValue().getState() == STATE_DISCONNECTED) { 356 iterator.remove(); 357 } 358 } 359 Log.d(TAG, "removeUncleanAccounts(): end devices=" + mMapInstanceMap.keySet()); 360 } 361 getUnreadMessages(BluetoothDevice device)362 public synchronized boolean getUnreadMessages(BluetoothDevice device) { 363 MceStateMachine mapStateMachine = mMapInstanceMap.get(device); 364 if (mapStateMachine == null) { 365 return false; 366 } 367 return mapStateMachine.getUnreadMessages(); 368 } 369 370 /** 371 * Returns the SDP record's MapSupportedFeatures field (see Bluetooth MAP 1.4 spec, page 114). 372 * 373 * @param device The Bluetooth device to get this value for. 374 * @return the SDP record's MapSupportedFeatures field. 375 */ getSupportedFeatures(BluetoothDevice device)376 public synchronized int getSupportedFeatures(BluetoothDevice device) { 377 MceStateMachine mapStateMachine = mMapInstanceMap.get(device); 378 if (mapStateMachine == null) { 379 Log.d(TAG, "in getSupportedFeatures, returning 0"); 380 return 0; 381 } 382 return mapStateMachine.getSupportedFeatures(); 383 } 384 setMessageStatus( BluetoothDevice device, String handle, int status)385 public synchronized boolean setMessageStatus( 386 BluetoothDevice device, String handle, int status) { 387 MceStateMachine mapStateMachine = mMapInstanceMap.get(device); 388 if (mapStateMachine == null) { 389 return false; 390 } 391 return mapStateMachine.setMessageStatus(handle, status); 392 } 393 394 @Override dump(StringBuilder sb)395 public void dump(StringBuilder sb) { 396 super.dump(sb); 397 for (MceStateMachine stateMachine : mMapInstanceMap.values()) { 398 stateMachine.dump(sb); 399 } 400 } 401 aclDisconnected(BluetoothDevice device, int transport)402 public void aclDisconnected(BluetoothDevice device, int transport) { 403 mHandler.post(() -> handleAclDisconnected(device, transport)); 404 } 405 handleAclDisconnected(BluetoothDevice device, int transport)406 private void handleAclDisconnected(BluetoothDevice device, int transport) { 407 MceStateMachine stateMachine = mMapInstanceMap.get(device); 408 if (stateMachine == null) { 409 Log.e(TAG, "No StateMachine found for the device=" + device); 410 return; 411 } 412 413 Log.i( 414 TAG, 415 "Received ACL disconnection event, device=" + device + ", transport=" + transport); 416 417 if (transport != BluetoothDevice.TRANSPORT_BREDR) { 418 return; 419 } 420 421 if (stateMachine.getState() == STATE_CONNECTED) { 422 stateMachine.disconnect(); 423 } 424 } 425 receiveSdpSearchRecord( BluetoothDevice device, int status, Parcelable record, ParcelUuid uuid)426 public void receiveSdpSearchRecord( 427 BluetoothDevice device, int status, Parcelable record, ParcelUuid uuid) { 428 mHandler.post(() -> handleSdpSearchRecordReceived(device, status, record, uuid)); 429 } 430 handleSdpSearchRecordReceived( BluetoothDevice device, int status, Parcelable record, ParcelUuid uuid)431 private void handleSdpSearchRecordReceived( 432 BluetoothDevice device, int status, Parcelable record, ParcelUuid uuid) { 433 MceStateMachine stateMachine = mMapInstanceMap.get(device); 434 Log.d(TAG, "Received SDP Record, device=" + device + ", uuid=" + uuid); 435 if (stateMachine == null) { 436 Log.e(TAG, "No StateMachine found for the device=" + device); 437 return; 438 } 439 if (uuid.equals(BluetoothUuid.MAS)) { 440 // Check if we have a valid SDP record. 441 SdpMasRecord masRecord = (SdpMasRecord) record; 442 Log.d(TAG, "SDP complete, status: " + status + ", record:" + masRecord); 443 stateMachine.sendSdpResult(status, masRecord); 444 } 445 } 446 } 447