ContextMap.java revision 9d48f7ed161cb7c201b91f21fb1be36522a562e0
1/*
2 * Copyright (C) 2013 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 */
16package com.android.bluetooth.gatt;
17
18import android.bluetooth.le.ScanSettings;
19import android.content.Context;
20import android.os.Binder;
21import android.os.IBinder;
22import android.os.IBinder.DeathRecipient;
23import android.os.IInterface;
24import android.os.RemoteException;
25import android.util.Log;
26import java.text.DateFormat;
27import java.text.SimpleDateFormat;
28import java.util.ArrayList;
29import java.util.Date;
30import java.util.HashSet;
31import java.util.Iterator;
32import java.util.LinkedList;
33import java.util.List;
34import java.util.NoSuchElementException;
35import java.util.Set;
36import java.util.UUID;
37import java.util.HashMap;
38import java.util.Map;
39
40import com.android.bluetooth.btservice.BluetoothProto;
41
42/**
43 * Helper class that keeps track of registered GATT applications.
44 * This class manages application callbacks and keeps track of GATT connections.
45 * @hide
46 */
47/*package*/ class ContextMap<T> {
48    private static final String TAG = GattServiceConfig.TAG_PREFIX + "ContextMap";
49
50    static final DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
51    static final int NUM_SCAN_EVENTS_KEPT = 20;
52    ArrayList<BluetoothProto.ScanEvent> mScanEvents =
53      new ArrayList<BluetoothProto.ScanEvent>(NUM_SCAN_EVENTS_KEPT);
54
55    /**
56     * ScanStats class helps keep track of information about scans
57     * on a per application basis.
58     */
59    class ScanStats {
60        static final int NUM_SCAN_DURATIONS_KEPT = 5;
61
62        String appName;
63        int scansStarted = 0;
64        int scansStopped = 0;
65        boolean isScanning = false;
66        boolean isRegistered = false;
67        boolean isOpportunisticScan = false;
68        boolean isBackgroundScan = false;
69        long minScanTime = Long.MAX_VALUE;
70        long maxScanTime = 0;
71        long totalScanTime = 0;
72        List<Long> lastScans = new ArrayList<Long>(NUM_SCAN_DURATIONS_KEPT + 1);
73        List<Long> lastScanTimestamps = new ArrayList<Long>(NUM_SCAN_DURATIONS_KEPT + 1);
74        long startTime = 0;
75        long stopTime = 0;
76
77        public ScanStats(String name) {
78            appName = name;
79        }
80
81        void recordScanStart(ScanSettings settings) {
82            if (isScanning)
83                return;
84
85            this.scansStarted++;
86            isScanning = true;
87            startTime = System.currentTimeMillis();
88            if (settings != null) {
89                isOpportunisticScan = settings.getScanMode() == ScanSettings.SCAN_MODE_OPPORTUNISTIC;
90                isBackgroundScan = (settings.getCallbackType() & ScanSettings.CALLBACK_TYPE_FIRST_MATCH) != 0;
91            }
92
93            BluetoothProto.ScanEvent scanEvent = new BluetoothProto.ScanEvent();
94            scanEvent.setScanEventType(BluetoothProto.ScanEvent.SCAN_EVENT_START);
95            scanEvent.setScanTechnologyType(BluetoothProto.ScanEvent.SCAN_TECH_TYPE_LE);
96            scanEvent.setInitiator(appName);
97            scanEvent.setEventTimeMillis(System.currentTimeMillis());
98
99            lastScanTimestamps.add(startTime);
100            if (lastScanTimestamps.size() > NUM_SCAN_DURATIONS_KEPT) {
101                lastScanTimestamps.remove(0);
102            }
103
104            synchronized(mScanEvents) {
105                if(mScanEvents.size() == NUM_SCAN_EVENTS_KEPT)
106                    mScanEvents.remove(0);
107                mScanEvents.add(scanEvent);
108            }
109        }
110
111        void recordScanStop() {
112            if (!isScanning)
113              return;
114
115            this.scansStopped++;
116            isScanning = false;
117            stopTime = System.currentTimeMillis();
118            long currTime = stopTime - startTime;
119
120            isOpportunisticScan = false;
121            isBackgroundScan = false;
122
123            minScanTime = Math.min(currTime, minScanTime);
124            maxScanTime = Math.max(currTime, maxScanTime);
125            totalScanTime += currTime;
126            lastScans.add(currTime);
127            if (lastScans.size() > NUM_SCAN_DURATIONS_KEPT) {
128                lastScans.remove(0);
129            }
130
131            BluetoothProto.ScanEvent scanEvent = new BluetoothProto.ScanEvent();
132            scanEvent.setScanEventType(BluetoothProto.ScanEvent.SCAN_EVENT_STOP);
133            scanEvent.setScanTechnologyType(BluetoothProto.ScanEvent.SCAN_TECH_TYPE_LE);
134            scanEvent.setInitiator(appName);
135            scanEvent.setEventTimeMillis(System.currentTimeMillis());
136            synchronized(mScanEvents) {
137                if (mScanEvents.size() == NUM_SCAN_EVENTS_KEPT)
138                    mScanEvents.remove(0);
139                mScanEvents.add(scanEvent);
140            }
141        }
142    }
143
144    /**
145     * Connection class helps map connection IDs to device addresses.
146     */
147    class Connection {
148        int connId;
149        String address;
150        int appId;
151
152        Connection(int connId, String address,int appId) {
153            this.connId = connId;
154            this.address = address;
155            this.appId = appId;
156        }
157    }
158
159    /**
160     * Application entry mapping UUIDs to appIDs and callbacks.
161     */
162    class App {
163        /** The UUID of the application */
164        UUID uuid;
165
166        /** The id of the application */
167        int id;
168
169        /** The package name of the application */
170        String name;
171
172        /** Application callbacks */
173        T callback;
174
175        /** Death receipient */
176        private IBinder.DeathRecipient mDeathRecipient;
177
178        /** Flag to signal that transport is congested */
179        Boolean isCongested = false;
180
181        /** Internal callback info queue, waiting to be send on congestion clear */
182        private List<CallbackInfo> congestionQueue = new ArrayList<CallbackInfo>();
183
184        /**
185         * Creates a new app context.
186         */
187        App(UUID uuid, T callback, String name) {
188            this.uuid = uuid;
189            this.callback = callback;
190            this.name = name;
191        }
192
193        /**
194         * Link death recipient
195         */
196        void linkToDeath(IBinder.DeathRecipient deathRecipient) {
197            try {
198                IBinder binder = ((IInterface)callback).asBinder();
199                binder.linkToDeath(deathRecipient, 0);
200                mDeathRecipient = deathRecipient;
201            } catch (RemoteException e) {
202                Log.e(TAG, "Unable to link deathRecipient for app id " + id);
203            }
204        }
205
206        /**
207         * Unlink death recipient
208         */
209        void unlinkToDeath() {
210            if (mDeathRecipient != null) {
211                try {
212                    IBinder binder = ((IInterface)callback).asBinder();
213                    binder.unlinkToDeath(mDeathRecipient,0);
214                } catch (NoSuchElementException e) {
215                    Log.e(TAG, "Unable to unlink deathRecipient for app id " + id);
216                }
217            }
218        }
219
220        void queueCallback(CallbackInfo callbackInfo) {
221            congestionQueue.add(callbackInfo);
222        }
223
224        CallbackInfo popQueuedCallback() {
225            if (congestionQueue.size() == 0) return null;
226            return congestionQueue.remove(0);
227        }
228    }
229
230    /** Our internal application list */
231    List<App> mApps = new ArrayList<App>();
232
233    /** Internal map to keep track of logging information by app name */
234    HashMap<String, ScanStats> mScanStats = new HashMap<String, ScanStats>();
235
236    /** Internal list of connected devices **/
237    Set<Connection> mConnections = new HashSet<Connection>();
238
239    /**
240     * Add an entry to the application context list.
241     */
242    void add(UUID uuid, T callback, Context context) {
243        String appName = context.getPackageManager().getNameForUid(
244                             Binder.getCallingUid());
245        if (appName == null) {
246            // Assign an app name if one isn't found
247            appName = "Unknown App (UID: " + Binder.getCallingUid() + ")";
248        }
249        synchronized (mApps) {
250            mApps.add(new App(uuid, callback, appName));
251            ScanStats scanStats = mScanStats.get(appName);
252            if (scanStats == null) {
253                scanStats = new ScanStats(appName);
254                mScanStats.put(appName, scanStats);
255            }
256            scanStats.isRegistered = true;
257        }
258    }
259
260    /**
261     * Remove the context for a given UUID
262     */
263    void remove(UUID uuid) {
264        synchronized (mApps) {
265            Iterator<App> i = mApps.iterator();
266            while (i.hasNext()) {
267                App entry = i.next();
268                if (entry.uuid.equals(uuid)) {
269                    entry.unlinkToDeath();
270                    mScanStats.get(entry.name).isRegistered = false;
271                    i.remove();
272                    break;
273                }
274            }
275        }
276    }
277
278    /**
279     * Remove the context for a given application ID.
280     */
281    void remove(int id) {
282        synchronized (mApps) {
283            Iterator<App> i = mApps.iterator();
284            while (i.hasNext()) {
285                App entry = i.next();
286                if (entry.id == id) {
287                    entry.unlinkToDeath();
288                    mScanStats.get(entry.name).isRegistered = false;
289                    i.remove();
290                    break;
291                }
292            }
293        }
294    }
295
296    /**
297     * Add a new connection for a given application ID.
298     */
299    void addConnection(int id, int connId, String address) {
300        synchronized (mConnections) {
301            App entry = getById(id);
302            if (entry != null){
303                mConnections.add(new Connection(connId, address, id));
304            }
305        }
306    }
307
308    /**
309     * Remove a connection with the given ID.
310     */
311    void removeConnection(int id, int connId) {
312        synchronized (mConnections) {
313            Iterator<Connection> i = mConnections.iterator();
314            while (i.hasNext()) {
315                Connection connection = i.next();
316                if (connection.connId == connId) {
317                    i.remove();
318                    break;
319                }
320            }
321        }
322    }
323
324    /**
325     * Get an application context by ID.
326     */
327    App getById(int id) {
328        Iterator<App> i = mApps.iterator();
329        while (i.hasNext()) {
330            App entry = i.next();
331            if (entry.id == id) return entry;
332        }
333        Log.e(TAG, "Context not found for ID " + id);
334        return null;
335    }
336
337    /**
338     * Get an application context by UUID.
339     */
340    App getByUuid(UUID uuid) {
341        Iterator<App> i = mApps.iterator();
342        while (i.hasNext()) {
343            App entry = i.next();
344            if (entry.uuid.equals(uuid)) return entry;
345        }
346        Log.e(TAG, "Context not found for UUID " + uuid);
347        return null;
348    }
349
350    /**
351     * Get an application context by the calling Apps name.
352     */
353    App getByName(String name) {
354        Iterator<App> i = mApps.iterator();
355        while (i.hasNext()) {
356            App entry = i.next();
357            if (entry.name.equals(name)) return entry;
358        }
359        Log.e(TAG, "Context not found for name " + name);
360        return null;
361    }
362
363    /**
364     * Get Logging info by ID
365     */
366    ScanStats getScanStatsById(int id) {
367        App temp = getById(id);
368        if (temp != null) {
369            return mScanStats.get(temp.name);
370        }
371        return null;
372    }
373
374    /**
375     * Get Logging info by application name
376     */
377    ScanStats getScanStatsByName(String name) {
378        return mScanStats.get(name);
379    }
380
381    /**
382     * Get the device addresses for all connected devices
383     */
384    Set<String> getConnectedDevices() {
385        Set<String> addresses = new HashSet<String>();
386        Iterator<Connection> i = mConnections.iterator();
387        while (i.hasNext()) {
388            Connection connection = i.next();
389            addresses.add(connection.address);
390        }
391        return addresses;
392    }
393
394    /**
395     * Get an application context by a connection ID.
396     */
397    App getByConnId(int connId) {
398        Iterator<Connection> ii = mConnections.iterator();
399        while (ii.hasNext()) {
400            Connection connection = ii.next();
401            if (connection.connId == connId){
402                return getById(connection.appId);
403            }
404        }
405        return null;
406    }
407
408    /**
409     * Returns a connection ID for a given device address.
410     */
411    Integer connIdByAddress(int id, String address) {
412        App entry = getById(id);
413        if (entry == null) return null;
414
415        Iterator<Connection> i = mConnections.iterator();
416        while (i.hasNext()) {
417            Connection connection = i.next();
418            if (connection.address.equals(address) && connection.appId == id)
419                return connection.connId;
420        }
421        return null;
422    }
423
424    /**
425     * Returns the device address for a given connection ID.
426     */
427    String addressByConnId(int connId) {
428        Iterator<Connection> i = mConnections.iterator();
429        while (i.hasNext()) {
430            Connection connection = i.next();
431            if (connection.connId == connId) return connection.address;
432        }
433        return null;
434    }
435
436    List<Connection> getConnectionByApp(int appId) {
437        List<Connection> currentConnections = new ArrayList<Connection>();
438        Iterator<Connection> i = mConnections.iterator();
439        while (i.hasNext()) {
440            Connection connection = i.next();
441            if (connection.appId == appId)
442                currentConnections.add(connection);
443        }
444        return currentConnections;
445    }
446
447    /**
448     * Erases all application context entries.
449     */
450    void clear() {
451        synchronized (mApps) {
452            Iterator<App> i = mApps.iterator();
453            while (i.hasNext()) {
454                App entry = i.next();
455                entry.unlinkToDeath();
456                i.remove();
457            }
458        }
459
460        synchronized (mConnections) {
461            mConnections.clear();
462        }
463    }
464
465    /**
466     * Returns connect device map with addr and appid
467     */
468    Map<Integer, String> getConnectedMap(){
469        Map<Integer, String> connectedmap = new HashMap<Integer, String>();
470        for(Connection conn: mConnections){
471            connectedmap.put(conn.appId, conn.address);
472        }
473        return connectedmap;
474    }
475
476    /**
477     * Logs debug information.
478     */
479    void dump(StringBuilder sb) {
480        long currTime = System.currentTimeMillis();
481
482        sb.append("  Entries: " + mScanStats.size() + "\n\n");
483
484        Iterator<Map.Entry<String,ScanStats>> it = mScanStats.entrySet().iterator();
485        while (it.hasNext()) {
486            Map.Entry<String, ScanStats> entry = it.next();
487
488            String name = entry.getKey();
489            ScanStats scanStats = entry.getValue();
490
491            long maxScanTime = scanStats.maxScanTime;
492            long minScanTime = scanStats.minScanTime;
493            long currScanTime = 0;
494
495            if (scanStats.isScanning) {
496                currScanTime = currTime - scanStats.startTime;
497                minScanTime = Math.min(currScanTime, minScanTime);
498                maxScanTime = Math.max(currScanTime, maxScanTime);
499            }
500
501            if (minScanTime == Long.MAX_VALUE) {
502                minScanTime = 0;
503            }
504
505            long lastScan = 0;
506            if (scanStats.stopTime != 0) {
507                lastScan = currTime - scanStats.stopTime;
508            }
509
510            long avgScanTime = 0;
511            if (scanStats.scansStarted > 0) {
512                avgScanTime = (scanStats.totalScanTime + currScanTime) /
513                              scanStats.scansStarted;
514            }
515
516            sb.append("  " + name);
517            if (scanStats.isRegistered) sb.append(" (Registered)");
518            if (scanStats.isOpportunisticScan) sb.append(" (Opportunistic)");
519            if (scanStats.isBackgroundScan) sb.append(" (Background)");
520            sb.append("\n");
521
522            sb.append("  LE scans (started/stopped)       : " +
523                      scanStats.scansStarted + " / " +
524                      scanStats.scansStopped + "\n");
525            sb.append("  Scan time in ms (min/max/avg)    : " +
526                      minScanTime + " / " +
527                      maxScanTime + " / " +
528                      avgScanTime + "\n");
529
530            if (scanStats.lastScans.size() != 0) {
531                sb.append("  Last " + scanStats.lastScans.size() +
532                          " scans (timestamp - duration):\n");
533
534                for (int i = 0; i < scanStats.lastScans.size(); i++) {
535                    Date timestamp = new Date(scanStats.lastScanTimestamps.get(i));
536                    sb.append("    " + dateFormat.format(timestamp) + " - ");
537                    sb.append(scanStats.lastScans.get(i) + "ms\n");
538                }
539            }
540
541            if (scanStats.isRegistered) {
542                App appEntry = getByName(name);
543                sb.append("  Application ID                   : " +
544                          appEntry.id + "\n");
545                sb.append("  UUID                             : " +
546                          appEntry.uuid + "\n");
547
548                if (scanStats.isScanning) {
549                    sb.append("  Current scan duration in ms      : " +
550                              currScanTime + "\n");
551                }
552
553                List<Connection> connections = getConnectionByApp(appEntry.id);
554                sb.append("  Connections: " + connections.size() + "\n");
555
556                Iterator<Connection> ii = connections.iterator();
557                while(ii.hasNext()) {
558                    Connection connection = ii.next();
559                    sb.append("    " + connection.connId + ": " +
560                              connection.address + "\n");
561                }
562            }
563            sb.append("\n");
564        }
565    }
566
567    void dumpProto(BluetoothProto.BluetoothLog proto) {
568        synchronized(mScanEvents) {
569            for (BluetoothProto.ScanEvent event : mScanEvents) {
570                proto.addScanEvent(event);
571            }
572        }
573    }
574}
575