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