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