• 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 static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
20 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
21 import static android.bluetooth.BluetoothProfile.STATE_CONNECTED;
22 import static android.bluetooth.BluetoothProfile.STATE_CONNECTING;
23 import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED;
24 
25 import static java.util.Objects.requireNonNull;
26 import static java.util.Objects.requireNonNullElseGet;
27 
28 import android.app.PendingIntent;
29 import android.bluetooth.BluetoothAdapter;
30 import android.bluetooth.BluetoothDevice;
31 import android.bluetooth.BluetoothProfile;
32 import android.bluetooth.BluetoothUuid;
33 import android.bluetooth.SdpMasRecord;
34 import android.net.Uri;
35 import android.os.Handler;
36 import android.os.Looper;
37 import android.os.ParcelUuid;
38 import android.os.Parcelable;
39 import android.sysprop.BluetoothProperties;
40 import android.util.Log;
41 
42 import com.android.bluetooth.btservice.AdapterService;
43 import com.android.bluetooth.btservice.ProfileService;
44 import com.android.bluetooth.btservice.storage.DatabaseManager;
45 import com.android.internal.annotations.VisibleForTesting;
46 
47 import java.util.ArrayList;
48 import java.util.Arrays;
49 import java.util.Iterator;
50 import java.util.List;
51 import java.util.Map;
52 import java.util.concurrent.ConcurrentHashMap;
53 
54 public class MapClientService extends ProfileService {
55     private static final String TAG = MapClientService.class.getSimpleName();
56 
57     static final int MAXIMUM_CONNECTED_DEVICES = 4;
58 
59     private final Map<BluetoothDevice, MceStateMachine> mMapInstanceMap =
60             new ConcurrentHashMap<>(1);
61 
62     private final AdapterService mAdapterService;
63     private final DatabaseManager mDatabaseManager;
64     private final MnsService mMnsServer;
65     private final Looper mStateMachinesLooper;
66     private final Handler mHandler;
67 
68     private static MapClientService sMapClientService;
69 
MapClientService(AdapterService adapterService)70     public MapClientService(AdapterService adapterService) {
71         this(adapterService, null, null);
72     }
73 
74     @VisibleForTesting
MapClientService(AdapterService adapterService, Looper looper, MnsService mnsServer)75     MapClientService(AdapterService adapterService, Looper looper, MnsService mnsServer) {
76         super(requireNonNull(adapterService));
77         mAdapterService = adapterService;
78         mDatabaseManager = requireNonNull(adapterService.getDatabase());
79         mMnsServer = requireNonNullElseGet(mnsServer, () -> new MnsService(this));
80 
81         if (looper == null) {
82             mHandler = new Handler(requireNonNull(Looper.getMainLooper()));
83             mStateMachinesLooper = null;
84         } else {
85             mHandler = new Handler(looper);
86 
87             // MapClient is only using a common state machine looper for test.
88             // In real device, it use a thread per device connected to avoid congestion.
89             mStateMachinesLooper = looper;
90         }
91 
92         removeUncleanAccounts();
93         MapClientContent.clearAllContent(this);
94         setMapClientService(this);
95     }
96 
isEnabled()97     public static boolean isEnabled() {
98         return BluetoothProperties.isProfileMapClientEnabled().orElse(false);
99     }
100 
getMapClientService()101     public static synchronized MapClientService getMapClientService() {
102         if (sMapClientService == null) {
103             Log.w(TAG, "getMapClientService(): service is null");
104             return null;
105         }
106         if (!sMapClientService.isAvailable()) {
107             Log.w(TAG, "getMapClientService(): service is not available ");
108             return null;
109         }
110         return sMapClientService;
111     }
112 
113     @VisibleForTesting
setMapClientService(MapClientService instance)114     static synchronized void setMapClientService(MapClientService instance) {
115         Log.d(TAG, "setMapClientService(): set to: " + instance);
116         sMapClientService = instance;
117     }
118 
119     @VisibleForTesting
getInstanceMap()120     Map<BluetoothDevice, MceStateMachine> getInstanceMap() {
121         return mMapInstanceMap;
122     }
123 
124     /**
125      * Connect the given Bluetooth device.
126      *
127      * @return true if connection is successful, false otherwise.
128      */
connect(BluetoothDevice device)129     public synchronized boolean connect(BluetoothDevice device) {
130         if (device == null) {
131             throw new IllegalArgumentException("Null device");
132         }
133         Log.d(TAG, "connect(device= " + device + "): devices=" + mMapInstanceMap.keySet());
134         if (getConnectionPolicy(device) == CONNECTION_POLICY_FORBIDDEN) {
135             Log.w(
136                     TAG,
137                     "Connection not allowed: <"
138                             + device.getAddress()
139                             + "> is CONNECTION_POLICY_FORBIDDEN");
140             return false;
141         }
142         MceStateMachine mapStateMachine = mMapInstanceMap.get(device);
143         if (mapStateMachine == null) {
144             // a map state machine instance doesn't exist yet, create a new one if we can.
145             if (mMapInstanceMap.size() < MAXIMUM_CONNECTED_DEVICES) {
146                 addDeviceToMapAndConnect(device);
147                 return true;
148             } else {
149                 // Maxed out on the number of allowed connections.
150                 // see if some of the current connections can be cleaned-up, to make room.
151                 removeUncleanAccounts();
152                 if (mMapInstanceMap.size() < MAXIMUM_CONNECTED_DEVICES) {
153                     addDeviceToMapAndConnect(device);
154                     return true;
155                 } else {
156                     Log.e(
157                             TAG,
158                             "Maxed out on the number of allowed MAP connections. "
159                                     + "Connect request rejected on "
160                                     + device);
161                     return false;
162                 }
163             }
164         }
165 
166         // StateMachine already exists in the map.
167         int state = getConnectionState(device);
168         if (state == STATE_CONNECTED || state == STATE_CONNECTING) {
169             Log.w(TAG, "Received connect request while already connecting/connected.");
170             return true;
171         }
172 
173         // StateMachine exists but not in connecting or connected state! it should
174         // have been removed form the map. lets get rid of it and add a new one.
175         Log.d(TAG, "StateMachine exists for a device in unexpected state: " + state);
176         mMapInstanceMap.remove(device);
177         mapStateMachine.doQuit();
178 
179         addDeviceToMapAndConnect(device);
180         Log.d(TAG, "connect(device= " + device + "): end devices=" + mMapInstanceMap.keySet());
181         return true;
182     }
183 
addDeviceToMapAndConnect(BluetoothDevice device)184     private synchronized void addDeviceToMapAndConnect(BluetoothDevice device) {
185         // When creating a new StateMachine, its state is set to CONNECTING - which will trigger
186         // connect.
187         MceStateMachine mapStateMachine;
188         if (mStateMachinesLooper != null) {
189             mapStateMachine =
190                     new MceStateMachine(this, device, mAdapterService, mStateMachinesLooper);
191         } else {
192             mapStateMachine = new MceStateMachine(this, device, mAdapterService);
193         }
194         mMapInstanceMap.put(device, mapStateMachine);
195     }
196 
disconnect(BluetoothDevice device)197     public synchronized boolean disconnect(BluetoothDevice device) {
198         Log.d(TAG, "disconnect(device= " + device + "): devices=" + mMapInstanceMap.keySet());
199         MceStateMachine mapStateMachine = mMapInstanceMap.get(device);
200         // a map state machine instance doesn't exist. maybe it is already gone?
201         if (mapStateMachine == null) {
202             return false;
203         }
204         int connectionState = mapStateMachine.getState();
205         if (connectionState != STATE_CONNECTED && connectionState != STATE_CONNECTING) {
206             return false;
207         }
208         mapStateMachine.disconnect();
209         Log.d(TAG, "disconnect(device= " + device + "): end devices=" + mMapInstanceMap.keySet());
210         return true;
211     }
212 
getConnectedDevices()213     public List<BluetoothDevice> getConnectedDevices() {
214         return getDevicesMatchingConnectionStates(new int[] {BluetoothAdapter.STATE_CONNECTED});
215     }
216 
getMceStateMachineForDevice(BluetoothDevice device)217     MceStateMachine getMceStateMachineForDevice(BluetoothDevice device) {
218         return mMapInstanceMap.get(device);
219     }
220 
getDevicesMatchingConnectionStates(int[] states)221     public synchronized List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
222         Log.d(TAG, "getDevicesMatchingConnectionStates" + Arrays.toString(states));
223         List<BluetoothDevice> deviceList = new ArrayList<>();
224         BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices();
225         int connectionState;
226         for (BluetoothDevice device : bondedDevices) {
227             connectionState = getConnectionState(device);
228             Log.d(TAG, "Device: " + device + "State: " + connectionState);
229             for (int i = 0; i < states.length; i++) {
230                 if (connectionState == states[i]) {
231                     deviceList.add(device);
232                 }
233             }
234         }
235         Log.d(TAG, deviceList.toString());
236         return deviceList;
237     }
238 
getConnectionState(BluetoothDevice device)239     public synchronized int getConnectionState(BluetoothDevice device) {
240         MceStateMachine mapStateMachine = mMapInstanceMap.get(device);
241         // a map state machine instance doesn't exist yet, create a new one if we can.
242         return (mapStateMachine == null) ? STATE_DISCONNECTED : mapStateMachine.getState();
243     }
244 
245     /**
246      * Set connection policy of the profile and connects it if connectionPolicy is {@link
247      * BluetoothProfile#CONNECTION_POLICY_ALLOWED} or disconnects if connectionPolicy is {@link
248      * BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}
249      *
250      * <p>The device should already be paired. Connection policy can be one of: {@link
251      * BluetoothProfile#CONNECTION_POLICY_ALLOWED}, {@link
252      * BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}, {@link
253      * BluetoothProfile#CONNECTION_POLICY_UNKNOWN}
254      *
255      * @param device Paired bluetooth device
256      * @param connectionPolicy is the connection policy to set to for this profile
257      * @return true if connectionPolicy is set, false on error
258      */
setConnectionPolicy(BluetoothDevice device, int connectionPolicy)259     public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy) {
260         Log.v(TAG, "Saved connectionPolicy " + device + " = " + connectionPolicy);
261 
262         if (!mDatabaseManager.setProfileConnectionPolicy(
263                 device, BluetoothProfile.MAP_CLIENT, connectionPolicy)) {
264             return false;
265         }
266         if (connectionPolicy == CONNECTION_POLICY_ALLOWED) {
267             connect(device);
268         } else if (connectionPolicy == CONNECTION_POLICY_FORBIDDEN) {
269             disconnect(device);
270         }
271         return true;
272     }
273 
274     /**
275      * Get the connection policy of the profile.
276      *
277      * <p>The connection policy can be any of: {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED},
278      * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}, {@link
279      * BluetoothProfile#CONNECTION_POLICY_UNKNOWN}
280      *
281      * @param device Bluetooth device
282      * @return connection policy of the device
283      */
getConnectionPolicy(BluetoothDevice device)284     public int getConnectionPolicy(BluetoothDevice device) {
285         return mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.MAP_CLIENT);
286     }
287 
sendMessage( BluetoothDevice device, Uri[] contacts, String message, PendingIntent sentIntent, PendingIntent deliveredIntent)288     public synchronized boolean sendMessage(
289             BluetoothDevice device,
290             Uri[] contacts,
291             String message,
292             PendingIntent sentIntent,
293             PendingIntent deliveredIntent) {
294         MceStateMachine mapStateMachine = mMapInstanceMap.get(device);
295         return mapStateMachine != null
296                 && mapStateMachine.sendMapMessage(contacts, message, sentIntent, deliveredIntent);
297     }
298 
299     @Override
initBinder()300     public IProfileServiceBinder initBinder() {
301         return new MapClientServiceBinder(this);
302     }
303 
304     @Override
cleanup()305     public synchronized void cleanup() {
306         Log.i(TAG, "Cleanup MapClient Service");
307 
308         mMnsServer.stop();
309         for (MceStateMachine stateMachine : mMapInstanceMap.values()) {
310             if (stateMachine.getState() == BluetoothAdapter.STATE_CONNECTED) {
311                 stateMachine.disconnect();
312             }
313             stateMachine.doQuit();
314         }
315         mMapInstanceMap.clear();
316 
317         // Unregister Handler and stop all queued messages.
318         mHandler.removeCallbacksAndMessages(null);
319 
320         removeUncleanAccounts();
321 
322         setMapClientService(null);
323     }
324 
325     /**
326      * cleanupDevice removes the associated state machine from the instance map
327      *
328      * @param device BluetoothDevice address of remote device
329      * @param sm the state machine to clean up or {@code null} to clean up any state machine.
330      */
331     @VisibleForTesting
cleanupDevice(BluetoothDevice device, MceStateMachine sm)332     public void cleanupDevice(BluetoothDevice device, MceStateMachine sm) {
333         Log.d(TAG, "cleanup(device= " + device + "): devices=" + mMapInstanceMap.keySet());
334         synchronized (mMapInstanceMap) {
335             MceStateMachine stateMachine = mMapInstanceMap.get(device);
336             if (stateMachine != null) {
337                 if (sm == null || stateMachine == sm) {
338                     mMapInstanceMap.remove(device);
339                     stateMachine.doQuit();
340                 } else {
341                     Log.w(TAG, "Trying to clean up wrong state machine");
342                 }
343             }
344         }
345         Log.d(TAG, "cleanup(device= " + device + "): end devices=" + mMapInstanceMap.keySet());
346     }
347 
348     @VisibleForTesting
removeUncleanAccounts()349     void removeUncleanAccounts() {
350         Log.d(TAG, "removeUncleanAccounts(): devices=" + mMapInstanceMap.keySet());
351         Iterator iterator = mMapInstanceMap.entrySet().iterator();
352         while (iterator.hasNext()) {
353             Map.Entry<BluetoothDevice, MceStateMachine> profileConnection =
354                     (Map.Entry) iterator.next();
355             if (profileConnection.getValue().getState() == STATE_DISCONNECTED) {
356                 iterator.remove();
357             }
358         }
359         Log.d(TAG, "removeUncleanAccounts(): end devices=" + mMapInstanceMap.keySet());
360     }
361 
getUnreadMessages(BluetoothDevice device)362     public synchronized boolean getUnreadMessages(BluetoothDevice device) {
363         MceStateMachine mapStateMachine = mMapInstanceMap.get(device);
364         if (mapStateMachine == null) {
365             return false;
366         }
367         return mapStateMachine.getUnreadMessages();
368     }
369 
370     /**
371      * Returns the SDP record's MapSupportedFeatures field (see Bluetooth MAP 1.4 spec, page 114).
372      *
373      * @param device The Bluetooth device to get this value for.
374      * @return the SDP record's MapSupportedFeatures field.
375      */
getSupportedFeatures(BluetoothDevice device)376     public synchronized int getSupportedFeatures(BluetoothDevice device) {
377         MceStateMachine mapStateMachine = mMapInstanceMap.get(device);
378         if (mapStateMachine == null) {
379             Log.d(TAG, "in getSupportedFeatures, returning 0");
380             return 0;
381         }
382         return mapStateMachine.getSupportedFeatures();
383     }
384 
setMessageStatus( BluetoothDevice device, String handle, int status)385     public synchronized boolean setMessageStatus(
386             BluetoothDevice device, String handle, int status) {
387         MceStateMachine mapStateMachine = mMapInstanceMap.get(device);
388         if (mapStateMachine == null) {
389             return false;
390         }
391         return mapStateMachine.setMessageStatus(handle, status);
392     }
393 
394     @Override
dump(StringBuilder sb)395     public void dump(StringBuilder sb) {
396         super.dump(sb);
397         for (MceStateMachine stateMachine : mMapInstanceMap.values()) {
398             stateMachine.dump(sb);
399         }
400     }
401 
aclDisconnected(BluetoothDevice device, int transport)402     public void aclDisconnected(BluetoothDevice device, int transport) {
403         mHandler.post(() -> handleAclDisconnected(device, transport));
404     }
405 
handleAclDisconnected(BluetoothDevice device, int transport)406     private void handleAclDisconnected(BluetoothDevice device, int transport) {
407         MceStateMachine stateMachine = mMapInstanceMap.get(device);
408         if (stateMachine == null) {
409             Log.e(TAG, "No StateMachine found for the device=" + device);
410             return;
411         }
412 
413         Log.i(
414                 TAG,
415                 "Received ACL disconnection event, device=" + device + ", transport=" + transport);
416 
417         if (transport != BluetoothDevice.TRANSPORT_BREDR) {
418             return;
419         }
420 
421         if (stateMachine.getState() == STATE_CONNECTED) {
422             stateMachine.disconnect();
423         }
424     }
425 
receiveSdpSearchRecord( BluetoothDevice device, int status, Parcelable record, ParcelUuid uuid)426     public void receiveSdpSearchRecord(
427             BluetoothDevice device, int status, Parcelable record, ParcelUuid uuid) {
428         mHandler.post(() -> handleSdpSearchRecordReceived(device, status, record, uuid));
429     }
430 
handleSdpSearchRecordReceived( BluetoothDevice device, int status, Parcelable record, ParcelUuid uuid)431     private void handleSdpSearchRecordReceived(
432             BluetoothDevice device, int status, Parcelable record, ParcelUuid uuid) {
433         MceStateMachine stateMachine = mMapInstanceMap.get(device);
434         Log.d(TAG, "Received SDP Record, device=" + device + ", uuid=" + uuid);
435         if (stateMachine == null) {
436             Log.e(TAG, "No StateMachine found for the device=" + device);
437             return;
438         }
439         if (uuid.equals(BluetoothUuid.MAS)) {
440             // Check if we have a valid SDP record.
441             SdpMasRecord masRecord = (SdpMasRecord) record;
442             Log.d(TAG, "SDP complete, status: " + status + ", record:" + masRecord);
443             stateMachine.sendSdpResult(status, masRecord);
444         }
445     }
446 }
447