MapClientService.java revision b972c44e4b15e7c8341da1a05a88208132335273
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
17package com.android.bluetooth.mapclient;
18
19import android.Manifest;
20import android.app.PendingIntent;
21import android.bluetooth.BluetoothAdapter;
22import android.bluetooth.BluetoothDevice;
23import android.bluetooth.BluetoothProfile;
24import android.bluetooth.BluetoothUuid;
25import android.bluetooth.IBluetoothMapClient;
26import android.bluetooth.SdpMasRecord;
27import android.content.BroadcastReceiver;
28import android.content.Context;
29import android.content.Intent;
30import android.content.IntentFilter;
31import android.net.Uri;
32import android.os.ParcelUuid;
33import android.provider.Settings;
34import android.support.annotation.VisibleForTesting;
35import android.util.Log;
36
37import com.android.bluetooth.Utils;
38import com.android.bluetooth.btservice.ProfileService;
39
40import java.util.ArrayList;
41import java.util.Arrays;
42import java.util.Iterator;
43import java.util.List;
44import java.util.Map;
45import java.util.Set;
46import java.util.concurrent.ConcurrentHashMap;
47
48public 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
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
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
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     */
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
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
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
183    public List<BluetoothDevice> getConnectedDevices() {
184        return getDevicesMatchingConnectionStates(new int[]{BluetoothAdapter.STATE_CONNECTED});
185    }
186
187    MceStateMachine getMceStateMachineForDevice(BluetoothDevice device) {
188        return mMapInstanceMap.get(device);
189    }
190
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
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
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
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
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
240    protected IProfileServiceBinder initBinder() {
241        return new Binder(this);
242    }
243
244    @Override
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
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
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
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
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
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
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
367        Binder(MapClientService service) {
368            if (VDBG) {
369                Log.v(TAG, "Binder()");
370            }
371            mService = service;
372        }
373
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
389        public void cleanup() {
390            mService = null;
391        }
392
393        @Override
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
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
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
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
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
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
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
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
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
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
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