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 android.bluetooth; 18 19 import static android.Manifest.permission.BLUETOOTH_CONNECT; 20 import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; 21 import static android.Manifest.permission.RECEIVE_SMS; 22 import static android.Manifest.permission.SEND_SMS; 23 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED; 24 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; 25 import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; 26 import static android.bluetooth.BluetoothUtils.isValidDevice; 27 28 import android.annotation.NonNull; 29 import android.annotation.Nullable; 30 import android.annotation.RequiresNoPermission; 31 import android.annotation.RequiresPermission; 32 import android.annotation.SdkConstant; 33 import android.annotation.SdkConstant.SdkConstantType; 34 import android.annotation.SuppressLint; 35 import android.annotation.SystemApi; 36 import android.app.PendingIntent; 37 import android.bluetooth.annotations.RequiresBluetoothConnectPermission; 38 import android.compat.annotation.UnsupportedAppUsage; 39 import android.content.AttributionSource; 40 import android.content.Context; 41 import android.net.Uri; 42 import android.os.Build; 43 import android.os.IBinder; 44 import android.os.RemoteException; 45 import android.util.CloseGuard; 46 import android.util.Log; 47 48 import java.util.Arrays; 49 import java.util.Collection; 50 import java.util.Collections; 51 import java.util.List; 52 53 /** 54 * This class provides the APIs to control the Bluetooth MAP MCE Profile. 55 * 56 * @hide 57 */ 58 @SystemApi 59 public final class BluetoothMapClient implements BluetoothProfile, AutoCloseable { 60 private static final String TAG = BluetoothMapClient.class.getSimpleName(); 61 62 private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); 63 private static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE); 64 65 private final CloseGuard mCloseGuard; 66 67 /** 68 * Intent used to broadcast the change in connection state of the MAP Client profile. 69 * 70 * <p>This intent will have 3 extras: 71 * 72 * <ul> 73 * <li>{@link #EXTRA_STATE} - The current state of the profile. 74 * <li>{@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile. 75 * <li>{@link BluetoothDevice#EXTRA_DEVICE} - The remote device. 76 * </ul> 77 * 78 * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of {@link 79 * #STATE_DISCONNECTED}, {@link #STATE_CONNECTING}, {@link #STATE_CONNECTED}, {@link 80 * #STATE_DISCONNECTING}. 81 * 82 * @hide 83 */ 84 @SystemApi 85 @SuppressLint("ActionValue") 86 @RequiresBluetoothConnectPermission 87 @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) 88 @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) 89 public static final String ACTION_CONNECTION_STATE_CHANGED = 90 "android.bluetooth.mapmce.profile.action.CONNECTION_STATE_CHANGED"; 91 92 /** @hide */ 93 @RequiresPermission(RECEIVE_SMS) 94 @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) 95 public static final String ACTION_MESSAGE_RECEIVED = 96 "android.bluetooth.mapmce.profile.action.MESSAGE_RECEIVED"; 97 98 /* Actions to be used for pending intents */ 99 /** @hide */ 100 @RequiresBluetoothConnectPermission 101 @RequiresPermission(BLUETOOTH_CONNECT) 102 @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) 103 public static final String ACTION_MESSAGE_SENT_SUCCESSFULLY = 104 "android.bluetooth.mapmce.profile.action.MESSAGE_SENT_SUCCESSFULLY"; 105 106 /** @hide */ 107 @RequiresBluetoothConnectPermission 108 @RequiresPermission(BLUETOOTH_CONNECT) 109 @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) 110 public static final String ACTION_MESSAGE_DELIVERED_SUCCESSFULLY = 111 "android.bluetooth.mapmce.profile.action.MESSAGE_DELIVERED_SUCCESSFULLY"; 112 113 /** 114 * Action to notify read status changed 115 * 116 * @hide 117 */ 118 @RequiresBluetoothConnectPermission 119 @RequiresPermission(BLUETOOTH_CONNECT) 120 @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) 121 public static final String ACTION_MESSAGE_READ_STATUS_CHANGED = 122 "android.bluetooth.mapmce.profile.action.MESSAGE_READ_STATUS_CHANGED"; 123 124 /** 125 * Action to notify deleted status changed 126 * 127 * @hide 128 */ 129 @RequiresBluetoothConnectPermission 130 @RequiresPermission(BLUETOOTH_CONNECT) 131 @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) 132 public static final String ACTION_MESSAGE_DELETED_STATUS_CHANGED = 133 "android.bluetooth.mapmce.profile.action.MESSAGE_DELETED_STATUS_CHANGED"; 134 135 /** 136 * Extras used in ACTION_MESSAGE_RECEIVED intent. NOTE: HANDLE is only valid for a single 137 * session with the device. 138 */ 139 /** @hide */ 140 public static final String EXTRA_MESSAGE_HANDLE = 141 "android.bluetooth.mapmce.profile.extra.MESSAGE_HANDLE"; 142 143 /** @hide */ 144 public static final String EXTRA_MESSAGE_TIMESTAMP = 145 "android.bluetooth.mapmce.profile.extra.MESSAGE_TIMESTAMP"; 146 147 /** @hide */ 148 public static final String EXTRA_MESSAGE_READ_STATUS = 149 "android.bluetooth.mapmce.profile.extra.MESSAGE_READ_STATUS"; 150 151 /** @hide */ 152 public static final String EXTRA_SENDER_CONTACT_URI = 153 "android.bluetooth.mapmce.profile.extra.SENDER_CONTACT_URI"; 154 155 /** @hide */ 156 public static final String EXTRA_SENDER_CONTACT_NAME = 157 "android.bluetooth.mapmce.profile.extra.SENDER_CONTACT_NAME"; 158 159 /** 160 * Used as a boolean extra in ACTION_MESSAGE_DELETED_STATUS_CHANGED Contains the MAP message 161 * deleted status Possible values are: true: deleted false: undeleted 162 * 163 * @hide 164 */ 165 public static final String EXTRA_MESSAGE_DELETED_STATUS = 166 "android.bluetooth.mapmce.profile.extra.MESSAGE_DELETED_STATUS"; 167 168 /** 169 * Extra used in ACTION_MESSAGE_READ_STATUS_CHANGED or ACTION_MESSAGE_DELETED_STATUS_CHANGED 170 * Possible values are: 0: failure 1: success 171 * 172 * @hide 173 */ 174 public static final String EXTRA_RESULT_CODE = "android.bluetooth.device.extra.RESULT_CODE"; 175 176 /** 177 * There was an error trying to obtain the state 178 * 179 * @hide 180 */ 181 public static final int STATE_ERROR = -1; 182 183 /** @hide */ 184 public static final int RESULT_FAILURE = 0; 185 186 /** @hide */ 187 public static final int RESULT_SUCCESS = 1; 188 189 /** 190 * Connection canceled before completion. 191 * 192 * @hide 193 */ 194 public static final int RESULT_CANCELED = 2; 195 196 /* 197 * UNREAD, READ, UNDELETED, DELETED are passed as parameters 198 * to setMessageStatus to indicate the messages new state. 199 */ 200 201 /** @hide */ 202 public static final int UNREAD = 0; 203 204 /** @hide */ 205 public static final int READ = 1; 206 207 /** @hide */ 208 public static final int UNDELETED = 2; 209 210 /** @hide */ 211 public static final int DELETED = 3; 212 213 private final BluetoothAdapter mAdapter; 214 private final AttributionSource mAttributionSource; 215 216 private IBluetoothMapClient mService; 217 218 /** Create a BluetoothMapClient proxy object. */ BluetoothMapClient(Context context, BluetoothAdapter adapter)219 /* package */ BluetoothMapClient(Context context, BluetoothAdapter adapter) { 220 if (DBG) Log.d(TAG, "Create BluetoothMapClient proxy object"); 221 mAdapter = adapter; 222 mAttributionSource = adapter.getAttributionSource(); 223 mService = null; 224 mCloseGuard = new CloseGuard(); 225 mCloseGuard.open("close"); 226 } 227 228 /** @hide */ 229 @Override 230 @SuppressWarnings("Finalize") // TODO(b/314811467) finalize()231 protected void finalize() { 232 if (mCloseGuard != null) { 233 mCloseGuard.warnIfOpen(); 234 } 235 close(); 236 } 237 238 /** 239 * Close the connection to the backing service. Other public functions of BluetoothMap will 240 * return default error results once close() has been called. Multiple invocations of close() 241 * are ok. 242 * 243 * @hide 244 */ 245 @Override close()246 public void close() { 247 mAdapter.closeProfileProxy(this); 248 if (mCloseGuard != null) { 249 mCloseGuard.close(); 250 } 251 } 252 253 /** @hide */ 254 @Override 255 @RequiresNoPermission onServiceConnected(IBinder service)256 public void onServiceConnected(IBinder service) { 257 mService = IBluetoothMapClient.Stub.asInterface(service); 258 } 259 260 /** @hide */ 261 @Override 262 @RequiresNoPermission onServiceDisconnected()263 public void onServiceDisconnected() { 264 mService = null; 265 } 266 getService()267 private IBluetoothMapClient getService() { 268 return mService; 269 } 270 271 /** @hide */ 272 @Override 273 @RequiresNoPermission getAdapter()274 public BluetoothAdapter getAdapter() { 275 return mAdapter; 276 } 277 278 /** 279 * Initiate connection. Initiation of outgoing connections is not supported for MAP server. 280 * 281 * @hide 282 */ 283 @RequiresBluetoothConnectPermission 284 @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) connect(BluetoothDevice device)285 public boolean connect(BluetoothDevice device) { 286 if (DBG) Log.d(TAG, "connect(" + device + ")" + "for MAPS MCE"); 287 final IBluetoothMapClient service = getService(); 288 if (service == null) { 289 Log.w(TAG, "Proxy not attached to service"); 290 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 291 } else if (isEnabled() && isValidDevice(device)) { 292 try { 293 return service.connect(device, mAttributionSource); 294 } catch (RemoteException e) { 295 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 296 } 297 } 298 return false; 299 } 300 301 /** 302 * Initiate disconnect. 303 * 304 * @param device Remote Bluetooth Device 305 * @return false on error, true otherwise 306 * @hide 307 */ 308 @RequiresBluetoothConnectPermission 309 @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) disconnect(BluetoothDevice device)310 public boolean disconnect(BluetoothDevice device) { 311 if (DBG) Log.d(TAG, "disconnect(" + device + ")"); 312 final IBluetoothMapClient service = getService(); 313 if (service == null) { 314 Log.w(TAG, "Proxy not attached to service"); 315 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 316 } else if (isEnabled() && isValidDevice(device)) { 317 try { 318 return service.disconnect(device, mAttributionSource); 319 } catch (RemoteException e) { 320 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 321 } 322 } 323 return false; 324 } 325 326 /** 327 * {@inheritDoc} 328 * 329 * @hide 330 */ 331 @SystemApi 332 @Override 333 @RequiresBluetoothConnectPermission 334 @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) getConnectedDevices()335 public @NonNull List<BluetoothDevice> getConnectedDevices() { 336 if (DBG) Log.d(TAG, "getConnectedDevices()"); 337 final IBluetoothMapClient service = getService(); 338 if (service == null) { 339 Log.w(TAG, "Proxy not attached to service"); 340 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 341 } else if (isEnabled()) { 342 try { 343 return Attributable.setAttributionSource( 344 service.getConnectedDevices(mAttributionSource), mAttributionSource); 345 } catch (RemoteException e) { 346 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 347 } 348 } 349 return Collections.emptyList(); 350 } 351 352 /** 353 * {@inheritDoc} 354 * 355 * @hide 356 */ 357 @SystemApi 358 @Override 359 @RequiresBluetoothConnectPermission 360 @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) 361 @NonNull getDevicesMatchingConnectionStates(@onNull int[] states)362 public List<BluetoothDevice> getDevicesMatchingConnectionStates(@NonNull int[] states) { 363 if (DBG) Log.d(TAG, "getDevicesMatchingStates()"); 364 final IBluetoothMapClient service = getService(); 365 if (service == null) { 366 Log.w(TAG, "Proxy not attached to service"); 367 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 368 } else if (isEnabled()) { 369 try { 370 return Attributable.setAttributionSource( 371 service.getDevicesMatchingConnectionStates(states, mAttributionSource), 372 mAttributionSource); 373 } catch (RemoteException e) { 374 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 375 } 376 } 377 return Collections.emptyList(); 378 } 379 380 /** 381 * {@inheritDoc} 382 * 383 * @hide 384 */ 385 @SystemApi 386 @Override 387 @RequiresBluetoothConnectPermission 388 @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) getConnectionState(@onNull BluetoothDevice device)389 public @BtProfileState int getConnectionState(@NonNull BluetoothDevice device) { 390 if (DBG) Log.d(TAG, "getConnectionState(" + device + ")"); 391 final IBluetoothMapClient service = getService(); 392 if (service == null) { 393 Log.w(TAG, "Proxy not attached to service"); 394 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 395 } else if (isEnabled() && isValidDevice(device)) { 396 try { 397 return service.getConnectionState(device, mAttributionSource); 398 } catch (RemoteException e) { 399 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 400 } 401 } 402 return STATE_DISCONNECTED; 403 } 404 405 /** 406 * Set priority of the profile 407 * 408 * <p>The device should already be paired. Priority can be one of {@link #PRIORITY_ON} or {@link 409 * #PRIORITY_OFF}, 410 * 411 * @param device Paired bluetooth device 412 * @return true if priority is set, false on error 413 * @hide 414 */ 415 @RequiresBluetoothConnectPermission 416 @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) setPriority(BluetoothDevice device, int priority)417 public boolean setPriority(BluetoothDevice device, int priority) { 418 if (DBG) Log.d(TAG, "setPriority(" + device + ", " + priority + ")"); 419 return setConnectionPolicy(device, BluetoothAdapter.priorityToConnectionPolicy(priority)); 420 } 421 422 /** 423 * Set connection policy of the profile 424 * 425 * <p>The device should already be paired. Connection policy can be one of {@link 426 * #CONNECTION_POLICY_ALLOWED}, {@link #CONNECTION_POLICY_FORBIDDEN}, {@link 427 * #CONNECTION_POLICY_UNKNOWN} 428 * 429 * @param device Paired bluetooth device 430 * @param connectionPolicy is the connection policy to set to for this profile 431 * @return true if connectionPolicy is set, false on error 432 * @hide 433 */ 434 @SystemApi 435 @RequiresBluetoothConnectPermission 436 @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) setConnectionPolicy( @onNull BluetoothDevice device, @ConnectionPolicy int connectionPolicy)437 public boolean setConnectionPolicy( 438 @NonNull BluetoothDevice device, @ConnectionPolicy int connectionPolicy) { 439 if (DBG) Log.d(TAG, "setConnectionPolicy(" + device + ", " + connectionPolicy + ")"); 440 final IBluetoothMapClient service = getService(); 441 if (service == null) { 442 Log.w(TAG, "Proxy not attached to service"); 443 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 444 } else if (isEnabled() 445 && isValidDevice(device) 446 && (connectionPolicy == CONNECTION_POLICY_FORBIDDEN 447 || connectionPolicy == CONNECTION_POLICY_ALLOWED)) { 448 try { 449 return service.setConnectionPolicy(device, connectionPolicy, mAttributionSource); 450 } catch (RemoteException e) { 451 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 452 } 453 } 454 return false; 455 } 456 457 /** 458 * Get the priority of the profile. 459 * 460 * <p>The priority can be any of: {@link #PRIORITY_OFF}, {@link #PRIORITY_ON}, {@link 461 * #PRIORITY_UNDEFINED} 462 * 463 * @param device Bluetooth device 464 * @return priority of the device 465 * @hide 466 */ 467 @RequiresBluetoothConnectPermission 468 @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) getPriority(BluetoothDevice device)469 public int getPriority(BluetoothDevice device) { 470 if (VDBG) Log.d(TAG, "getPriority(" + device + ")"); 471 return BluetoothAdapter.connectionPolicyToPriority(getConnectionPolicy(device)); 472 } 473 474 /** 475 * Get the connection policy of the profile. 476 * 477 * <p>The connection policy can be any of: {@link #CONNECTION_POLICY_ALLOWED}, {@link 478 * #CONNECTION_POLICY_FORBIDDEN}, {@link #CONNECTION_POLICY_UNKNOWN} 479 * 480 * @param device Bluetooth device 481 * @return connection policy of the device 482 * @hide 483 */ 484 @SystemApi 485 @RequiresBluetoothConnectPermission 486 @RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}) getConnectionPolicy(@onNull BluetoothDevice device)487 public @ConnectionPolicy int getConnectionPolicy(@NonNull BluetoothDevice device) { 488 if (VDBG) Log.d(TAG, "getConnectionPolicy(" + device + ")"); 489 final IBluetoothMapClient service = getService(); 490 if (service == null) { 491 Log.w(TAG, "Proxy not attached to service"); 492 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 493 } else if (isEnabled() && isValidDevice(device)) { 494 try { 495 return service.getConnectionPolicy(device, mAttributionSource); 496 } catch (RemoteException e) { 497 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 498 } 499 } 500 return CONNECTION_POLICY_FORBIDDEN; 501 } 502 503 /** 504 * Send a message. 505 * 506 * <p>Send an SMS message to either the contacts primary number or the telephone number 507 * specified. 508 * 509 * @param device Bluetooth device 510 * @param contacts Uri Collection of the contacts 511 * @param message Message to be sent 512 * @param sentIntent intent issued when message is sent 513 * @param deliveredIntent intent issued when message is delivered 514 * @return true if the message is enqueued, false on error 515 * @hide 516 */ 517 @SystemApi 518 @RequiresBluetoothConnectPermission 519 @RequiresPermission(allOf = {BLUETOOTH_CONNECT, SEND_SMS}) sendMessage( @onNull BluetoothDevice device, @NonNull Collection<Uri> contacts, @NonNull String message, @Nullable PendingIntent sentIntent, @Nullable PendingIntent deliveredIntent)520 public boolean sendMessage( 521 @NonNull BluetoothDevice device, 522 @NonNull Collection<Uri> contacts, 523 @NonNull String message, 524 @Nullable PendingIntent sentIntent, 525 @Nullable PendingIntent deliveredIntent) { 526 return sendMessage( 527 device, 528 contacts.toArray(new Uri[contacts.size()]), 529 message, 530 sentIntent, 531 deliveredIntent); 532 } 533 534 /** 535 * Send a message. 536 * 537 * <p>Send an SMS message to either the contacts primary number or the telephone number 538 * specified. 539 * 540 * @param device Bluetooth device 541 * @param contacts Uri[] of the contacts 542 * @param message Message to be sent 543 * @param sentIntent intent issued when message is sent 544 * @param deliveredIntent intent issued when message is delivered 545 * @return true if the message is enqueued, false on error 546 * @hide 547 */ 548 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 549 @RequiresBluetoothConnectPermission 550 @RequiresPermission(allOf = {BLUETOOTH_CONNECT, SEND_SMS}) sendMessage( BluetoothDevice device, Uri[] contacts, String message, PendingIntent sentIntent, PendingIntent deliveredIntent)551 public boolean sendMessage( 552 BluetoothDevice device, 553 Uri[] contacts, 554 String message, 555 PendingIntent sentIntent, 556 PendingIntent deliveredIntent) { 557 if (DBG) { 558 Log.d(TAG, "sendMessage(" + device + ", " + Arrays.toString(contacts) + ", " + message); 559 } 560 final IBluetoothMapClient service = getService(); 561 if (service == null) { 562 Log.w(TAG, "Proxy not attached to service"); 563 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 564 } else if (isEnabled() && isValidDevice(device)) { 565 try { 566 return service.sendMessage( 567 device, contacts, message, sentIntent, deliveredIntent, mAttributionSource); 568 } catch (RemoteException e) { 569 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 570 } 571 } 572 return false; 573 } 574 isEnabled()575 private boolean isEnabled() { 576 return mAdapter.isEnabled(); 577 } 578 } 579