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