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