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 com.android.bluetooth.mapclient;
18 
19 import android.Manifest;
20 import android.annotation.RequiresPermission;
21 import android.app.PendingIntent;
22 import android.bluetooth.BluetoothAdapter;
23 import android.bluetooth.BluetoothDevice;
24 import android.bluetooth.BluetoothProfile;
25 import android.bluetooth.BluetoothUuid;
26 import android.bluetooth.IBluetoothMapClient;
27 import android.bluetooth.SdpMasRecord;
28 import android.content.Attributable;
29 import android.content.AttributionSource;
30 import android.content.BroadcastReceiver;
31 import android.content.Context;
32 import android.content.Intent;
33 import android.content.IntentFilter;
34 import android.net.Uri;
35 import android.os.ParcelUuid;
36 import android.util.Log;
37 
38 import com.android.bluetooth.Utils;
39 import com.android.bluetooth.btservice.AdapterService;
40 import com.android.bluetooth.btservice.ProfileService;
41 import com.android.bluetooth.btservice.storage.DatabaseManager;
42 import com.android.internal.annotations.VisibleForTesting;
43 
44 import java.util.ArrayList;
45 import java.util.Arrays;
46 import java.util.Iterator;
47 import java.util.List;
48 import java.util.Map;
49 import java.util.Objects;
50 import java.util.concurrent.ConcurrentHashMap;
51 
52 public class MapClientService extends ProfileService {
53     private static final String TAG = "MapClientService";
54 
55     static final boolean DBG = false;
56     static final boolean VDBG = false;
57 
58     static final int MAXIMUM_CONNECTED_DEVICES = 4;
59 
60     private Map<BluetoothDevice, MceStateMachine> mMapInstanceMap = new ConcurrentHashMap<>(1);
61     private MnsService mMnsServer;
62 
63     private AdapterService mAdapterService;
64     private DatabaseManager mDatabaseManager;
65     private static MapClientService sMapClientService;
66     private MapBroadcastReceiver mMapReceiver;
67 
getMapClientService()68     public static synchronized MapClientService getMapClientService() {
69         if (sMapClientService == null) {
70             Log.w(TAG, "getMapClientService(): service is null");
71             return null;
72         }
73         if (!sMapClientService.isAvailable()) {
74             Log.w(TAG, "getMapClientService(): service is not available ");
75             return null;
76         }
77         return sMapClientService;
78     }
79 
setMapClientService(MapClientService instance)80     private static synchronized void setMapClientService(MapClientService instance) {
81         if (DBG) {
82             Log.d(TAG, "setMapClientService(): set to: " + instance);
83         }
84         sMapClientService = instance;
85     }
86 
87     @VisibleForTesting
getInstanceMap()88     Map<BluetoothDevice, MceStateMachine> getInstanceMap() {
89         return mMapInstanceMap;
90     }
91 
92     /**
93      * Connect the given Bluetooth device.
94      *
95      * @param device
96      * @return true if connection is successful, false otherwise.
97      */
98     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
connect(BluetoothDevice device)99     public synchronized boolean connect(BluetoothDevice device) {
100         enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
101                 "Need BLUETOOTH_PRIVILEGED permission");
102         if (device == null) {
103             throw new IllegalArgumentException("Null device");
104         }
105         if (DBG) {
106             StringBuilder sb = new StringBuilder();
107             dump(sb);
108             Log.d(TAG, "MAP connect device: " + device
109                     + ", InstanceMap start state: " + sb.toString());
110         }
111         if (getConnectionPolicy(device) == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
112             Log.w(TAG, "Connection not allowed: <" + device.getAddress()
113                     + "> is CONNECTION_POLICY_FORBIDDEN");
114             return false;
115         }
116         MceStateMachine mapStateMachine = mMapInstanceMap.get(device);
117         if (mapStateMachine == null) {
118             // a map state machine instance doesn't exist yet, create a new one if we can.
119             if (mMapInstanceMap.size() < MAXIMUM_CONNECTED_DEVICES) {
120                 addDeviceToMapAndConnect(device);
121                 return true;
122             } else {
123                 // Maxed out on the number of allowed connections.
124                 // see if some of the current connections can be cleaned-up, to make room.
125                 removeUncleanAccounts();
126                 if (mMapInstanceMap.size() < MAXIMUM_CONNECTED_DEVICES) {
127                     addDeviceToMapAndConnect(device);
128                     return true;
129                 } else {
130                     Log.e(TAG, "Maxed out on the number of allowed MAP connections. "
131                             + "Connect request rejected on " + device);
132                     return false;
133                 }
134             }
135         }
136 
137         // statemachine already exists in the map.
138         int state = getConnectionState(device);
139         if (state == BluetoothProfile.STATE_CONNECTED
140                 || state == BluetoothProfile.STATE_CONNECTING) {
141             Log.w(TAG, "Received connect request while already connecting/connected.");
142             return true;
143         }
144 
145         // Statemachine exists but not in connecting or connected state! it should
146         // have been removed form the map. lets get rid of it and add a new one.
147         if (DBG) {
148             Log.d(TAG, "Statemachine exists for a device in unexpected state: " + state);
149         }
150         mMapInstanceMap.remove(device);
151         addDeviceToMapAndConnect(device);
152         if (DBG) {
153             StringBuilder sb = new StringBuilder();
154             dump(sb);
155             Log.d(TAG, "MAP connect device: " + device
156                     + ", InstanceMap end state: " + sb.toString());
157         }
158         return true;
159     }
160 
addDeviceToMapAndConnect(BluetoothDevice device)161     private synchronized void addDeviceToMapAndConnect(BluetoothDevice device) {
162         // When creating a new statemachine, its state is set to CONNECTING - which will trigger
163         // connect.
164         MceStateMachine mapStateMachine = new MceStateMachine(this, device);
165         mMapInstanceMap.put(device, mapStateMachine);
166     }
167 
168     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
disconnect(BluetoothDevice device)169     public synchronized boolean disconnect(BluetoothDevice device) {
170         enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
171                 "Need BLUETOOTH_PRIVILEGED permission");
172         if (DBG) {
173             StringBuilder sb = new StringBuilder();
174             dump(sb);
175             Log.d(TAG, "MAP disconnect device: " + device
176                     + ", InstanceMap start state: " + sb.toString());
177         }
178         MceStateMachine mapStateMachine = mMapInstanceMap.get(device);
179         // a map state machine instance doesn't exist. maybe it is already gone?
180         if (mapStateMachine == null) {
181             return false;
182         }
183         int connectionState = mapStateMachine.getState();
184         if (connectionState != BluetoothProfile.STATE_CONNECTED
185                 && connectionState != BluetoothProfile.STATE_CONNECTING) {
186             return false;
187         }
188         mapStateMachine.disconnect();
189         if (DBG) {
190             StringBuilder sb = new StringBuilder();
191             dump(sb);
192             Log.d(TAG, "MAP disconnect device: " + device
193                     + ", InstanceMap start state: " + sb.toString());
194         }
195         return true;
196     }
197 
getConnectedDevices()198     public List<BluetoothDevice> getConnectedDevices() {
199         return getDevicesMatchingConnectionStates(new int[]{BluetoothAdapter.STATE_CONNECTED});
200     }
201 
getMceStateMachineForDevice(BluetoothDevice device)202     MceStateMachine getMceStateMachineForDevice(BluetoothDevice device) {
203         return mMapInstanceMap.get(device);
204     }
205 
getDevicesMatchingConnectionStates(int[] states)206     public synchronized List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
207         if (DBG) Log.d(TAG, "getDevicesMatchingConnectionStates" + Arrays.toString(states));
208         List<BluetoothDevice> deviceList = new ArrayList<>();
209         BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices();
210         int connectionState;
211         for (BluetoothDevice device : bondedDevices) {
212             connectionState = getConnectionState(device);
213             if (DBG) Log.d(TAG, "Device: " + device + "State: " + connectionState);
214             for (int i = 0; i < states.length; i++) {
215                 if (connectionState == states[i]) {
216                     deviceList.add(device);
217                 }
218             }
219         }
220         if (DBG) Log.d(TAG, deviceList.toString());
221         return deviceList;
222     }
223 
getConnectionState(BluetoothDevice device)224     public synchronized int getConnectionState(BluetoothDevice device) {
225         MceStateMachine mapStateMachine = mMapInstanceMap.get(device);
226         // a map state machine instance doesn't exist yet, create a new one if we can.
227         return (mapStateMachine == null) ? BluetoothProfile.STATE_DISCONNECTED
228                 : mapStateMachine.getState();
229     }
230 
231     /**
232      * Set connection policy of the profile and connects it if connectionPolicy is
233      * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED} or disconnects if connectionPolicy is
234      * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}
235      *
236      * <p> The device should already be paired.
237      * Connection policy can be one of:
238      * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED},
239      * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN},
240      * {@link BluetoothProfile#CONNECTION_POLICY_UNKNOWN}
241      *
242      * @param device Paired bluetooth device
243      * @param connectionPolicy is the connection policy to set to for this profile
244      * @return true if connectionPolicy is set, false on error
245      */
246     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
setConnectionPolicy(BluetoothDevice device, int connectionPolicy)247     public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy) {
248         if (VDBG) {
249             Log.v(TAG, "Saved connectionPolicy " + device + " = " + connectionPolicy);
250         }
251         enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
252                 "Need BLUETOOTH_PRIVILEGED permission");
253 
254         if (!mDatabaseManager.setProfileConnectionPolicy(device, BluetoothProfile.MAP_CLIENT,
255                   connectionPolicy)) {
256             return false;
257         }
258         if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED) {
259             connect(device);
260         } else if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
261             disconnect(device);
262         }
263         return true;
264     }
265 
266     /**
267      * Get the connection policy of the profile.
268      *
269      * <p> The connection policy can be any of:
270      * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED},
271      * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN},
272      * {@link BluetoothProfile#CONNECTION_POLICY_UNKNOWN}
273      *
274      * @param device Bluetooth device
275      * @return connection policy of the device
276      * @hide
277      */
278     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
getConnectionPolicy(BluetoothDevice device)279     public int getConnectionPolicy(BluetoothDevice device) {
280         enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
281                 "Need BLUETOOTH_PRIVILEGED permission");
282         return mDatabaseManager
283                 .getProfileConnectionPolicy(device, BluetoothProfile.MAP_CLIENT);
284     }
285 
sendMessage(BluetoothDevice device, Uri[] contacts, String message, PendingIntent sentIntent, PendingIntent deliveredIntent)286     public synchronized boolean sendMessage(BluetoothDevice device, Uri[] contacts, String message,
287             PendingIntent sentIntent, PendingIntent deliveredIntent) {
288         MceStateMachine mapStateMachine = mMapInstanceMap.get(device);
289         return mapStateMachine != null
290                 && mapStateMachine.sendMapMessage(contacts, message, sentIntent, deliveredIntent);
291     }
292 
293     @Override
initBinder()294     public IProfileServiceBinder initBinder() {
295         return new Binder(this);
296     }
297 
298     @Override
start()299     protected synchronized boolean start() {
300         Log.e(TAG, "start()");
301 
302         mAdapterService = AdapterService.getAdapterService();
303         mDatabaseManager = Objects.requireNonNull(AdapterService.getAdapterService().getDatabase(),
304                 "DatabaseManager cannot be null when MapClientService starts");
305 
306         if (mMnsServer == null) {
307             mMnsServer = MapUtils.newMnsServiceInstance(this);
308             if (mMnsServer == null) {
309                 // this can't happen
310                 Log.w(TAG, "MnsService is *not* created!");
311                 return false;
312             }
313         }
314 
315         mMapReceiver = new MapBroadcastReceiver();
316         IntentFilter filter = new IntentFilter();
317         filter.addAction(BluetoothDevice.ACTION_SDP_RECORD);
318         filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
319         registerReceiver(mMapReceiver, filter);
320         removeUncleanAccounts();
321         MapClientContent.clearAllContent(this);
322         setMapClientService(this);
323         return true;
324     }
325 
326     @Override
stop()327     protected synchronized boolean stop() {
328         if (DBG) {
329             Log.d(TAG, "stop()");
330         }
331 
332         if (mMapReceiver != null) {
333             unregisterReceiver(mMapReceiver);
334             mMapReceiver = null;
335         }
336         if (mMnsServer != null) {
337             mMnsServer.stop();
338         }
339         for (MceStateMachine stateMachine : mMapInstanceMap.values()) {
340             if (stateMachine.getState() == BluetoothAdapter.STATE_CONNECTED) {
341                 stateMachine.disconnect();
342             }
343             stateMachine.doQuit();
344         }
345         return true;
346     }
347 
348     @Override
cleanup()349     protected void cleanup() {
350         if (DBG) {
351             Log.d(TAG, "in Cleanup");
352         }
353         removeUncleanAccounts();
354         // TODO(b/72948646): should be moved to stop()
355         setMapClientService(null);
356     }
357 
358     /**
359      * cleanupDevice removes the associated state machine from the instance map
360      *
361      * @param device BluetoothDevice address of remote device
362      */
363     @VisibleForTesting
cleanupDevice(BluetoothDevice device)364     public void cleanupDevice(BluetoothDevice device) {
365         if (DBG) {
366             StringBuilder sb = new StringBuilder();
367             dump(sb);
368             Log.d(TAG, "Cleanup device: " + device + ", InstanceMap start state: "
369                     + sb.toString());
370         }
371         synchronized (mMapInstanceMap) {
372             MceStateMachine stateMachine = mMapInstanceMap.get(device);
373             if (stateMachine != null) {
374                 mMapInstanceMap.remove(device);
375             }
376         }
377         if (DBG) {
378             StringBuilder sb = new StringBuilder();
379             dump(sb);
380             Log.d(TAG, "Cleanup device: " + device + ", InstanceMap end state: "
381                     + sb.toString());
382         }
383     }
384 
385     @VisibleForTesting
removeUncleanAccounts()386     void removeUncleanAccounts() {
387         if (DBG) {
388             StringBuilder sb = new StringBuilder();
389             dump(sb);
390             Log.d(TAG, "removeUncleanAccounts:InstanceMap end state: "
391                     + sb.toString());
392         }
393         Iterator iterator = mMapInstanceMap.entrySet().iterator();
394         while (iterator.hasNext()) {
395             Map.Entry<BluetoothDevice, MceStateMachine> profileConnection =
396                     (Map.Entry) iterator.next();
397             if (profileConnection.getValue().getState() == BluetoothProfile.STATE_DISCONNECTED) {
398                 iterator.remove();
399             }
400         }
401         if (DBG) {
402             StringBuilder sb = new StringBuilder();
403             dump(sb);
404             Log.d(TAG, "removeUncleanAccounts:InstanceMap end state: "
405                     + sb.toString());
406         }
407     }
408 
getUnreadMessages(BluetoothDevice device)409     public synchronized boolean getUnreadMessages(BluetoothDevice device) {
410         MceStateMachine mapStateMachine = mMapInstanceMap.get(device);
411         if (mapStateMachine == null) {
412             return false;
413         }
414         return mapStateMachine.getUnreadMessages();
415     }
416 
417     /**
418      * Returns the SDP record's MapSupportedFeatures field (see Bluetooth MAP 1.4 spec, page 114).
419      * @param device The Bluetooth device to get this value for.
420      * @return the SDP record's MapSupportedFeatures field.
421      */
getSupportedFeatures(BluetoothDevice device)422     public synchronized int getSupportedFeatures(BluetoothDevice device) {
423         MceStateMachine mapStateMachine = mMapInstanceMap.get(device);
424         if (mapStateMachine == null) {
425             if (DBG) Log.d(TAG, "in getSupportedFeatures, returning 0");
426             return 0;
427         }
428         return mapStateMachine.getSupportedFeatures();
429     }
430 
setMessageStatus(BluetoothDevice device, String handle, int status)431     public synchronized boolean setMessageStatus(BluetoothDevice device, String handle, int status) {
432         MceStateMachine mapStateMachine = mMapInstanceMap.get(device);
433         if (mapStateMachine == null) {
434             return false;
435         }
436         return mapStateMachine.setMessageStatus(handle, status);
437     }
438 
439     @Override
dump(StringBuilder sb)440     public void dump(StringBuilder sb) {
441         super.dump(sb);
442         for (MceStateMachine stateMachine : mMapInstanceMap.values()) {
443             stateMachine.dump(sb);
444         }
445     }
446 
447     //Binder object: Must be static class or memory leak may occur
448 
449     /**
450      * This class implements the IClient interface - or actually it validates the
451      * preconditions for calling the actual functionality in the MapClientService, and calls it.
452      */
453     private static class Binder extends IBluetoothMapClient.Stub implements IProfileServiceBinder {
454         private MapClientService mService;
455 
Binder(MapClientService service)456         Binder(MapClientService service) {
457             if (VDBG) {
458                 Log.v(TAG, "Binder()");
459             }
460             mService = service;
461         }
462 
463         @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
getService(AttributionSource source)464         private MapClientService getService(AttributionSource source) {
465             if (!(MapUtils.isSystemUser() || Utils.checkCallerIsSystemOrActiveUser(TAG))
466                     || !Utils.checkServiceAvailable(mService, TAG)
467                     || !Utils.checkConnectPermissionForDataDelivery(mService, source, TAG)) {
468                 return null;
469             }
470             return mService;
471         }
472 
473         @Override
cleanup()474         public void cleanup() {
475             mService = null;
476         }
477 
478         @Override
isConnected(BluetoothDevice device, AttributionSource source)479         public boolean isConnected(BluetoothDevice device, AttributionSource source) {
480             if (VDBG) {
481                 Log.v(TAG, "isConnected()");
482             }
483             Attributable.setAttributionSource(device, source);
484             MapClientService service = getService(source);
485             if (service == null) {
486                 return false;
487             }
488             return service.getConnectionState(device) == BluetoothProfile.STATE_CONNECTED;
489         }
490 
491         @Override
connect(BluetoothDevice device, AttributionSource source)492         public boolean connect(BluetoothDevice device, AttributionSource source) {
493             if (VDBG) {
494                 Log.v(TAG, "connect()");
495             }
496             Attributable.setAttributionSource(device, source);
497             MapClientService service = getService(source);
498             if (service == null) {
499                 return false;
500             }
501             return service.connect(device);
502         }
503 
504         @Override
disconnect(BluetoothDevice device, AttributionSource source)505         public boolean disconnect(BluetoothDevice device, AttributionSource source) {
506             if (VDBG) {
507                 Log.v(TAG, "disconnect()");
508             }
509             Attributable.setAttributionSource(device, source);
510             MapClientService service = getService(source);
511             if (service == null) {
512                 return false;
513             }
514             return service.disconnect(device);
515         }
516 
517         @Override
getConnectedDevices(AttributionSource source)518         public List<BluetoothDevice> getConnectedDevices(AttributionSource source) {
519             if (VDBG) {
520                 Log.v(TAG, "getConnectedDevices()");
521             }
522             MapClientService service = getService(source);
523             if (service == null) {
524                 return new ArrayList<BluetoothDevice>(0);
525             }
526             return service.getConnectedDevices();
527         }
528 
529         @Override
getDevicesMatchingConnectionStates(int[] states, AttributionSource source)530         public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states,
531                 AttributionSource source) {
532             if (VDBG) {
533                 Log.v(TAG, "getDevicesMatchingConnectionStates()");
534             }
535             MapClientService service = getService(source);
536             if (service == null) {
537                 return new ArrayList<BluetoothDevice>(0);
538             }
539             return service.getDevicesMatchingConnectionStates(states);
540         }
541 
542         @Override
getConnectionState(BluetoothDevice device, AttributionSource source)543         public int getConnectionState(BluetoothDevice device, AttributionSource source) {
544             if (VDBG) {
545                 Log.v(TAG, "getConnectionState()");
546             }
547             Attributable.setAttributionSource(device, source);
548             MapClientService service = getService(source);
549             if (service == null) {
550                 return BluetoothProfile.STATE_DISCONNECTED;
551             }
552             return service.getConnectionState(device);
553         }
554 
555         @Override
setConnectionPolicy(BluetoothDevice device, int connectionPolicy, AttributionSource source)556         public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy,
557                 AttributionSource source) {
558             Attributable.setAttributionSource(device, source);
559             MapClientService service = getService(source);
560             if (service == null) {
561                 return false;
562             }
563             return service.setConnectionPolicy(device, connectionPolicy);
564         }
565 
566         @Override
getConnectionPolicy(BluetoothDevice device, AttributionSource source)567         public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) {
568             Attributable.setAttributionSource(device, source);
569             MapClientService service = getService(source);
570             if (service == null) {
571                 return BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
572             }
573             return service.getConnectionPolicy(device);
574         }
575 
576         @Override
sendMessage(BluetoothDevice device, Uri[] contacts, String message, PendingIntent sentIntent, PendingIntent deliveredIntent, AttributionSource source)577         public boolean sendMessage(BluetoothDevice device, Uri[] contacts, String message,
578                 PendingIntent sentIntent, PendingIntent deliveredIntent, AttributionSource source) {
579             Attributable.setAttributionSource(device, source);
580             MapClientService service = getService(source);
581             if (service == null) {
582                 return false;
583             }
584             if (DBG) Log.d(TAG, "Checking Permission of sendMessage");
585             mService.enforceCallingOrSelfPermission(Manifest.permission.SEND_SMS,
586                     "Need SEND_SMS permission");
587 
588             return service.sendMessage(device, contacts, message, sentIntent, deliveredIntent);
589         }
590 
591         @Override
getUnreadMessages(BluetoothDevice device, AttributionSource source)592         public boolean getUnreadMessages(BluetoothDevice device, AttributionSource source) {
593             Attributable.setAttributionSource(device, source);
594             MapClientService service = getService(source);
595             if (service == null) {
596                 return false;
597             }
598             mService.enforceCallingOrSelfPermission(Manifest.permission.READ_SMS,
599                     "Need READ_SMS permission");
600             return service.getUnreadMessages(device);
601         }
602 
603         @Override
getSupportedFeatures(BluetoothDevice device, AttributionSource source)604         public int getSupportedFeatures(BluetoothDevice device, AttributionSource source) {
605             Attributable.setAttributionSource(device, source);
606             MapClientService service = getService(source);
607             if (service == null) {
608                 if (DBG) {
609                     Log.d(TAG,
610                             "in MapClientService getSupportedFeatures stub, returning 0");
611                 }
612                 return 0;
613             }
614             return service.getSupportedFeatures(device);
615         }
616 
617         @Override
setMessageStatus(BluetoothDevice device, String handle, int status, AttributionSource source)618         public boolean setMessageStatus(BluetoothDevice device, String handle, int status,
619                 AttributionSource source) {
620             Attributable.setAttributionSource(device, source);
621             MapClientService service = getService(source);
622             if (service == null) {
623                 return false;
624             }
625             mService.enforceCallingOrSelfPermission(Manifest.permission.READ_SMS,
626                     "Need READ_SMS permission");
627             return service.setMessageStatus(device, handle, status);
628         }
629     }
630 
631     private class MapBroadcastReceiver extends BroadcastReceiver {
632         @Override
onReceive(Context context, Intent intent)633         public void onReceive(Context context, Intent intent) {
634             String action = intent.getAction();
635             if (DBG) {
636                 Log.d(TAG, "onReceive: " + action);
637             }
638             if (!action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)
639                     && !action.equals(BluetoothDevice.ACTION_SDP_RECORD)) {
640                 // we don't care about this intent
641                 return;
642             }
643             BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
644             if (device == null) {
645                 Log.e(TAG, "broadcast has NO device param!");
646                 return;
647             }
648             if (DBG) {
649                 Log.d(TAG, "broadcast has device: (" + device.getAddress() + ")");
650             }
651             MceStateMachine stateMachine = mMapInstanceMap.get(device);
652             if (stateMachine == null) {
653                 Log.e(TAG, "No Statemachine found for the device from broadcast");
654                 return;
655             }
656 
657             if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
658                 if (stateMachine.getState() == BluetoothProfile.STATE_CONNECTED) {
659                     stateMachine.disconnect();
660                 }
661             }
662 
663             if (action.equals(BluetoothDevice.ACTION_SDP_RECORD)) {
664                 ParcelUuid uuid = intent.getParcelableExtra(BluetoothDevice.EXTRA_UUID);
665                 if (DBG) {
666                     Log.d(TAG, "UUID of SDP: " + uuid);
667                 }
668 
669                 if (uuid.equals(BluetoothUuid.MAS)) {
670                     // Check if we have a valid SDP record.
671                     SdpMasRecord masRecord =
672                             intent.getParcelableExtra(BluetoothDevice.EXTRA_SDP_RECORD);
673                     if (DBG) {
674                         Log.d(TAG, "SDP = " + masRecord);
675                     }
676                     int status = intent.getIntExtra(BluetoothDevice.EXTRA_SDP_SEARCH_STATUS, -1);
677                     if (masRecord == null) {
678                         Log.w(TAG, "SDP search ended with no MAS record. Status: " + status);
679                         return;
680                     }
681                     stateMachine.obtainMessage(MceStateMachine.MSG_MAS_SDP_DONE,
682                             masRecord).sendToTarget();
683                 }
684             }
685         }
686     }
687 }
688