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