1 /* 2 * Copyright (C) 2008 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 android.bluetooth; 18 19 import android.Manifest; 20 import android.annotation.NonNull; 21 import android.annotation.RequiresNoPermission; 22 import android.annotation.RequiresPermission; 23 import android.annotation.SdkConstant; 24 import android.annotation.SuppressLint; 25 import android.annotation.SystemApi; 26 import android.annotation.SdkConstant.SdkConstantType; 27 import android.bluetooth.annotations.RequiresBluetoothConnectPermission; 28 import android.compat.annotation.UnsupportedAppUsage; 29 import android.content.Attributable; 30 import android.content.AttributionSource; 31 import android.content.Context; 32 import android.os.Binder; 33 import android.os.Build; 34 import android.os.IBinder; 35 import android.os.RemoteException; 36 import android.util.CloseGuard; 37 import android.util.Log; 38 39 import java.util.ArrayList; 40 import java.util.List; 41 42 /** 43 * This class provides the APIs to control the Bluetooth MAP 44 * Profile. 45 * 46 * @hide 47 */ 48 @SystemApi 49 public final class BluetoothMap implements BluetoothProfile, AutoCloseable { 50 51 private static final String TAG = "BluetoothMap"; 52 private static final boolean DBG = true; 53 private static final boolean VDBG = false; 54 55 private CloseGuard mCloseGuard; 56 57 /** @hide */ 58 @SuppressLint("ActionValue") 59 @SystemApi 60 @RequiresBluetoothConnectPermission 61 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) 62 @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) 63 public static final String ACTION_CONNECTION_STATE_CHANGED = 64 "android.bluetooth.map.profile.action.CONNECTION_STATE_CHANGED"; 65 66 /** 67 * There was an error trying to obtain the state 68 * 69 * @hide 70 */ 71 public static final int STATE_ERROR = -1; 72 73 /** @hide */ 74 public static final int RESULT_FAILURE = 0; 75 /** @hide */ 76 public static final int RESULT_SUCCESS = 1; 77 /** 78 * Connection canceled before completion. 79 * 80 * @hide 81 */ 82 public static final int RESULT_CANCELED = 2; 83 84 private final BluetoothAdapter mAdapter; 85 private final AttributionSource mAttributionSource; 86 private final BluetoothProfileConnector<IBluetoothMap> mProfileConnector = 87 new BluetoothProfileConnector(this, BluetoothProfile.MAP, 88 "BluetoothMap", IBluetoothMap.class.getName()) { 89 @Override 90 public IBluetoothMap getServiceInterface(IBinder service) { 91 return IBluetoothMap.Stub.asInterface(Binder.allowBlocking(service)); 92 } 93 }; 94 95 /** 96 * Create a BluetoothMap proxy object. 97 */ BluetoothMap(Context context, ServiceListener listener, BluetoothAdapter adapter)98 /* package */ BluetoothMap(Context context, ServiceListener listener, 99 BluetoothAdapter adapter) { 100 if (DBG) Log.d(TAG, "Create BluetoothMap proxy object"); 101 mAdapter = adapter; 102 mAttributionSource = adapter.getAttributionSource(); 103 mProfileConnector.connect(context, listener); 104 mCloseGuard = new CloseGuard(); 105 mCloseGuard.open("close"); 106 } 107 finalize()108 protected void finalize() { 109 if (mCloseGuard != null) { 110 mCloseGuard.warnIfOpen(); 111 } 112 close(); 113 } 114 115 /** 116 * Close the connection to the backing service. 117 * Other public functions of BluetoothMap will return default error 118 * results once close() has been called. Multiple invocations of close() 119 * are ok. 120 * 121 * @hide 122 */ 123 @SystemApi close()124 public void close() { 125 if (VDBG) log("close()"); 126 mProfileConnector.disconnect(); 127 } 128 getService()129 private IBluetoothMap getService() { 130 return mProfileConnector.getService(); 131 } 132 133 /** 134 * Get the current state of the BluetoothMap service. 135 * 136 * @return One of the STATE_ return codes, or STATE_ERROR if this proxy object is currently not 137 * connected to the Map service. 138 * 139 * @hide 140 */ 141 @RequiresBluetoothConnectPermission 142 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) getState()143 public int getState() { 144 if (VDBG) log("getState()"); 145 final IBluetoothMap service = getService(); 146 if (service != null) { 147 try { 148 return service.getState(mAttributionSource); 149 } catch (RemoteException e) { 150 Log.e(TAG, e.toString()); 151 } 152 } else { 153 Log.w(TAG, "Proxy not attached to service"); 154 if (DBG) log(Log.getStackTraceString(new Throwable())); 155 } 156 return BluetoothMap.STATE_ERROR; 157 } 158 159 /** 160 * Get the currently connected remote Bluetooth device (PCE). 161 * 162 * @return The remote Bluetooth device, or null if not in connected or connecting state, or if 163 * this proxy object is not connected to the Map service. 164 * 165 * @hide 166 */ 167 @RequiresBluetoothConnectPermission 168 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) getClient()169 public BluetoothDevice getClient() { 170 if (VDBG) log("getClient()"); 171 final IBluetoothMap service = getService(); 172 if (service != null) { 173 try { 174 return Attributable.setAttributionSource( 175 service.getClient(mAttributionSource), mAttributionSource); 176 } catch (RemoteException e) { 177 Log.e(TAG, e.toString()); 178 } 179 } else { 180 Log.w(TAG, "Proxy not attached to service"); 181 if (DBG) log(Log.getStackTraceString(new Throwable())); 182 } 183 return null; 184 } 185 186 /** 187 * Returns true if the specified Bluetooth device is connected. 188 * Returns false if not connected, or if this proxy object is not 189 * currently connected to the Map service. 190 * 191 * @hide 192 */ 193 @RequiresBluetoothConnectPermission 194 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) isConnected(BluetoothDevice device)195 public boolean isConnected(BluetoothDevice device) { 196 if (VDBG) log("isConnected(" + device + ")"); 197 final IBluetoothMap service = getService(); 198 if (service != null) { 199 try { 200 return service.isConnected(device, mAttributionSource); 201 } catch (RemoteException e) { 202 Log.e(TAG, e.toString()); 203 } 204 } else { 205 Log.w(TAG, "Proxy not attached to service"); 206 if (DBG) log(Log.getStackTraceString(new Throwable())); 207 } 208 return false; 209 } 210 211 /** 212 * Initiate connection. Initiation of outgoing connections is not 213 * supported for MAP server. 214 * 215 * @hide 216 */ 217 @RequiresNoPermission connect(BluetoothDevice device)218 public boolean connect(BluetoothDevice device) { 219 if (DBG) log("connect(" + device + ")" + "not supported for MAPS"); 220 return false; 221 } 222 223 /** 224 * Initiate disconnect. 225 * 226 * @param device Remote Bluetooth Device 227 * @return false on error, true otherwise 228 * 229 * @hide 230 */ 231 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 232 @RequiresBluetoothConnectPermission 233 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) disconnect(BluetoothDevice device)234 public boolean disconnect(BluetoothDevice device) { 235 if (DBG) log("disconnect(" + device + ")"); 236 final IBluetoothMap service = getService(); 237 if (service != null && isEnabled() && isValidDevice(device)) { 238 try { 239 return service.disconnect(device, mAttributionSource); 240 } catch (RemoteException e) { 241 Log.e(TAG, Log.getStackTraceString(new Throwable())); 242 return false; 243 } 244 } 245 if (service == null) Log.w(TAG, "Proxy not attached to service"); 246 return false; 247 } 248 249 /** 250 * Check class bits for possible Map support. 251 * This is a simple heuristic that tries to guess if a device with the 252 * given class bits might support Map. It is not accurate for all 253 * devices. It tries to err on the side of false positives. 254 * 255 * @return True if this device might support Map. 256 * 257 * @hide 258 */ doesClassMatchSink(BluetoothClass btClass)259 public static boolean doesClassMatchSink(BluetoothClass btClass) { 260 // TODO optimize the rule 261 switch (btClass.getDeviceClass()) { 262 case BluetoothClass.Device.COMPUTER_DESKTOP: 263 case BluetoothClass.Device.COMPUTER_LAPTOP: 264 case BluetoothClass.Device.COMPUTER_SERVER: 265 case BluetoothClass.Device.COMPUTER_UNCATEGORIZED: 266 return true; 267 default: 268 return false; 269 } 270 } 271 272 /** 273 * Get the list of connected devices. Currently at most one. 274 * 275 * @return list of connected devices 276 * 277 * @hide 278 */ 279 @SystemApi 280 @RequiresBluetoothConnectPermission 281 @RequiresPermission(allOf = { 282 android.Manifest.permission.BLUETOOTH_CONNECT, 283 android.Manifest.permission.BLUETOOTH_PRIVILEGED, 284 }) getConnectedDevices()285 public @NonNull List<BluetoothDevice> getConnectedDevices() { 286 if (DBG) log("getConnectedDevices()"); 287 final IBluetoothMap service = getService(); 288 if (service != null && isEnabled()) { 289 try { 290 return Attributable.setAttributionSource( 291 service.getConnectedDevices(mAttributionSource), mAttributionSource); 292 } catch (RemoteException e) { 293 Log.e(TAG, Log.getStackTraceString(new Throwable())); 294 return new ArrayList<BluetoothDevice>(); 295 } 296 } 297 if (service == null) Log.w(TAG, "Proxy not attached to service"); 298 return new ArrayList<BluetoothDevice>(); 299 } 300 301 /** 302 * Get the list of devices matching specified states. Currently at most one. 303 * 304 * @return list of matching devices 305 * 306 * @hide 307 */ 308 @RequiresBluetoothConnectPermission 309 @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) getDevicesMatchingConnectionStates(int[] states)310 public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 311 if (DBG) log("getDevicesMatchingStates()"); 312 final IBluetoothMap service = getService(); 313 if (service != null && isEnabled()) { 314 try { 315 return Attributable.setAttributionSource( 316 service.getDevicesMatchingConnectionStates(states, mAttributionSource), 317 mAttributionSource); 318 } catch (RemoteException e) { 319 Log.e(TAG, Log.getStackTraceString(new Throwable())); 320 return new ArrayList<BluetoothDevice>(); 321 } 322 } 323 if (service == null) Log.w(TAG, "Proxy not attached to service"); 324 return new ArrayList<BluetoothDevice>(); 325 } 326 327 /** 328 * Get connection state of device 329 * 330 * @return device connection state 331 * 332 * @hide 333 */ 334 @RequiresBluetoothConnectPermission 335 @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) getConnectionState(BluetoothDevice device)336 public int getConnectionState(BluetoothDevice device) { 337 if (DBG) log("getConnectionState(" + device + ")"); 338 final IBluetoothMap service = getService(); 339 if (service != null && isEnabled() && isValidDevice(device)) { 340 try { 341 return service.getConnectionState(device, mAttributionSource); 342 } catch (RemoteException e) { 343 Log.e(TAG, Log.getStackTraceString(new Throwable())); 344 return BluetoothProfile.STATE_DISCONNECTED; 345 } 346 } 347 if (service == null) Log.w(TAG, "Proxy not attached to service"); 348 return BluetoothProfile.STATE_DISCONNECTED; 349 } 350 351 /** 352 * Set priority of the profile 353 * 354 * <p> The device should already be paired. 355 * Priority can be one of {@link #PRIORITY_ON} or {@link #PRIORITY_OFF}, 356 * 357 * @param device Paired bluetooth device 358 * @param priority 359 * @return true if priority is set, false on error 360 * @hide 361 */ 362 @RequiresBluetoothConnectPermission 363 @RequiresPermission(allOf = { 364 android.Manifest.permission.BLUETOOTH_CONNECT, 365 android.Manifest.permission.BLUETOOTH_PRIVILEGED, 366 }) setPriority(BluetoothDevice device, int priority)367 public boolean setPriority(BluetoothDevice device, int priority) { 368 if (DBG) log("setPriority(" + device + ", " + priority + ")"); 369 return setConnectionPolicy(device, BluetoothAdapter.priorityToConnectionPolicy(priority)); 370 } 371 372 /** 373 * Set connection policy of the profile 374 * 375 * <p> The device should already be paired. 376 * Connection policy can be one of {@link #CONNECTION_POLICY_ALLOWED}, 377 * {@link #CONNECTION_POLICY_FORBIDDEN}, {@link #CONNECTION_POLICY_UNKNOWN} 378 * 379 * @param device Paired bluetooth device 380 * @param connectionPolicy is the connection policy to set to for this profile 381 * @return true if connectionPolicy is set, false on error 382 * @hide 383 */ 384 @SystemApi 385 @RequiresBluetoothConnectPermission 386 @RequiresPermission(allOf = { 387 android.Manifest.permission.BLUETOOTH_CONNECT, 388 android.Manifest.permission.BLUETOOTH_PRIVILEGED, 389 }) setConnectionPolicy(@onNull BluetoothDevice device, @ConnectionPolicy int connectionPolicy)390 public boolean setConnectionPolicy(@NonNull BluetoothDevice device, 391 @ConnectionPolicy int connectionPolicy) { 392 if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")"); 393 final IBluetoothMap service = getService(); 394 if (service != null && isEnabled() && isValidDevice(device)) { 395 if (connectionPolicy != BluetoothProfile.CONNECTION_POLICY_FORBIDDEN 396 && connectionPolicy != BluetoothProfile.CONNECTION_POLICY_ALLOWED) { 397 return false; 398 } 399 try { 400 return service.setConnectionPolicy(device, connectionPolicy, mAttributionSource); 401 } catch (RemoteException e) { 402 Log.e(TAG, Log.getStackTraceString(new Throwable())); 403 return false; 404 } 405 } 406 if (service == null) Log.w(TAG, "Proxy not attached to service"); 407 return false; 408 } 409 410 /** 411 * Get the priority of the profile. 412 * 413 * <p> The priority can be any of: 414 * {@link #PRIORITY_OFF}, {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED} 415 * 416 * @param device Bluetooth device 417 * @return priority of the device 418 * @hide 419 */ 420 @RequiresBluetoothConnectPermission 421 @RequiresPermission(allOf = { 422 android.Manifest.permission.BLUETOOTH_CONNECT, 423 android.Manifest.permission.BLUETOOTH_PRIVILEGED, 424 }) getPriority(BluetoothDevice device)425 public int getPriority(BluetoothDevice device) { 426 if (VDBG) log("getPriority(" + device + ")"); 427 return BluetoothAdapter.connectionPolicyToPriority(getConnectionPolicy(device)); 428 } 429 430 /** 431 * Get the connection policy of the profile. 432 * 433 * <p> The connection policy can be any of: 434 * {@link #CONNECTION_POLICY_ALLOWED}, {@link #CONNECTION_POLICY_FORBIDDEN}, 435 * {@link #CONNECTION_POLICY_UNKNOWN} 436 * 437 * @param device Bluetooth device 438 * @return connection policy of the device 439 * @hide 440 */ 441 @SystemApi 442 @RequiresBluetoothConnectPermission 443 @RequiresPermission(allOf = { 444 android.Manifest.permission.BLUETOOTH_CONNECT, 445 android.Manifest.permission.BLUETOOTH_PRIVILEGED, 446 }) getConnectionPolicy(@onNull BluetoothDevice device)447 public @ConnectionPolicy int getConnectionPolicy(@NonNull BluetoothDevice device) { 448 if (VDBG) log("getConnectionPolicy(" + device + ")"); 449 final IBluetoothMap service = getService(); 450 if (service != null && isEnabled() && isValidDevice(device)) { 451 try { 452 return service.getConnectionPolicy(device, mAttributionSource); 453 } catch (RemoteException e) { 454 Log.e(TAG, Log.getStackTraceString(new Throwable())); 455 return BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; 456 } 457 } 458 if (service == null) Log.w(TAG, "Proxy not attached to service"); 459 return BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; 460 } 461 log(String msg)462 private static void log(String msg) { 463 Log.d(TAG, msg); 464 } 465 isEnabled()466 private boolean isEnabled() { 467 return mAdapter.isEnabled(); 468 } 469 isValidDevice(BluetoothDevice device)470 private static boolean isValidDevice(BluetoothDevice device) { 471 return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress()); 472 } 473 } 474