• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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