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