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