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