• 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 package com.android.bluetooth.gatt;
17 
18 import android.bluetooth.le.ScanFilter;
19 import android.bluetooth.le.ScanSettings;
20 import android.os.BatteryStatsManager;
21 import android.os.Binder;
22 import android.os.SystemClock;
23 import android.os.WorkSource;
24 
25 import com.android.bluetooth.BluetoothMetricsProto;
26 import com.android.bluetooth.BluetoothStatsLog;
27 import com.android.bluetooth.btservice.AdapterService;
28 import com.android.bluetooth.util.WorkSourceUtil;
29 
30 import java.text.DateFormat;
31 import java.text.SimpleDateFormat;
32 import java.util.ArrayList;
33 import java.util.Arrays;
34 import java.util.Date;
35 import java.util.HashMap;
36 import java.util.Iterator;
37 import java.util.List;
38 import java.util.Objects;
39 
40 /**
41  * ScanStats class helps keep track of information about scans
42  * on a per application basis.
43  * @hide
44  */
45 /*package*/ class AppScanStats {
46     private static final String TAG = AppScanStats.class.getSimpleName();
47 
48     static final DateFormat DATE_FORMAT = new SimpleDateFormat("MM-dd HH:mm:ss");
49 
50     // Weight is the duty cycle of the scan mode
51     static final int OPPORTUNISTIC_WEIGHT = 0;
52     static final int LOW_POWER_WEIGHT = 10;
53     static final int AMBIENT_DISCOVERY_WEIGHT = 20;
54     static final int BALANCED_WEIGHT = 25;
55     static final int LOW_LATENCY_WEIGHT = 100;
56 
57     static final int LARGE_SCAN_TIME_GAP_MS = 24000;
58 
59     // ContextMap here is needed to grab Apps and Connections
60     ContextMap mContextMap;
61 
62     // GattService is needed to add scan event protos to be dumped later
63     final GattService mGattService;
64 
65     // Battery stats is used to keep track of scans and result stats
66     BatteryStatsManager mBatteryStatsManager;
67 
68     private final AdapterService mAdapterService;
69 
70     class LastScan {
71         public long duration;
72         public long suspendDuration;
73         public long suspendStartTime;
74         public boolean isSuspended;
75         public long timestamp;
76         public boolean isOpportunisticScan;
77         public boolean isTimeout;
78         public boolean isBackgroundScan;
79         public boolean isFilterScan;
80         public boolean isCallbackScan;
81         public boolean isBatchScan;
82         public int results;
83         public int scannerId;
84         public int scanMode;
85         public int scanCallbackType;
86         public String filterString;
87 
LastScan(long timestamp, boolean isFilterScan, boolean isCallbackScan, int scannerId, int scanMode, int scanCallbackType)88         LastScan(long timestamp, boolean isFilterScan, boolean isCallbackScan, int scannerId,
89                 int scanMode, int scanCallbackType) {
90             this.duration = 0;
91             this.timestamp = timestamp;
92             this.isOpportunisticScan = false;
93             this.isTimeout = false;
94             this.isBackgroundScan = false;
95             this.isFilterScan = isFilterScan;
96             this.isCallbackScan = isCallbackScan;
97             this.isBatchScan = false;
98             this.scanMode = scanMode;
99             this.scanCallbackType = scanCallbackType;
100             this.results = 0;
101             this.scannerId = scannerId;
102             this.suspendDuration = 0;
103             this.suspendStartTime = 0;
104             this.isSuspended = false;
105             this.filterString = "";
106         }
107     }
108     public String appName;
109     public WorkSource mWorkSource; // Used for BatteryStatsManager
110     public final WorkSourceUtil mWorkSourceUtil; // Used for BluetoothStatsLog
111     private int mScansStarted = 0;
112     private int mScansStopped = 0;
113     public boolean isRegistered = false;
114     private long mScanStartTime = 0;
115     private long mTotalActiveTime = 0;
116     private long mTotalSuspendTime = 0;
117     private long mTotalScanTime = 0;
118     private long mOppScanTime = 0;
119     private long mLowPowerScanTime = 0;
120     private long mBalancedScanTime = 0;
121     private long mLowLantencyScanTime = 0;
122     private long mAmbientDiscoveryScanTime = 0;
123     private int mOppScan = 0;
124     private int mLowPowerScan = 0;
125     private int mBalancedScan = 0;
126     private int mLowLantencyScan = 0;
127     private int mAmbientDiscoveryScan = 0;
128     private List<LastScan> mLastScans = new ArrayList<LastScan>();
129     private HashMap<Integer, LastScan> mOngoingScans = new HashMap<Integer, LastScan>();
130     public long startTime = 0;
131     public long stopTime = 0;
132     public int results = 0;
133 
AppScanStats(String name, WorkSource source, ContextMap map, GattService service)134     AppScanStats(String name, WorkSource source, ContextMap map, GattService service) {
135         appName = name;
136         mContextMap = map;
137         mGattService = service;
138         mBatteryStatsManager = service.getSystemService(BatteryStatsManager.class);
139 
140         if (source == null) {
141             // Bill the caller if the work source isn't passed through
142             source = new WorkSource(Binder.getCallingUid(), appName);
143         }
144         mWorkSource = source;
145         mWorkSourceUtil = new WorkSourceUtil(source);
146         mAdapterService = Objects.requireNonNull(AdapterService.getAdapterService());
147     }
148 
addResult(int scannerId)149     synchronized void addResult(int scannerId) {
150         LastScan scan = getScanFromScannerId(scannerId);
151         if (scan != null) {
152             scan.results++;
153 
154             // Only update battery stats after receiving 100 new results in order
155             // to lower the cost of the binder transaction
156             if (scan.results % 100 == 0) {
157                 mBatteryStatsManager.reportBleScanResults(mWorkSource, 100);
158                 BluetoothStatsLog.write(BluetoothStatsLog.BLE_SCAN_RESULT_RECEIVED,
159                         mWorkSourceUtil.getUids(), mWorkSourceUtil.getTags(), 100);
160             }
161         }
162 
163         results++;
164     }
165 
isScanning()166     synchronized boolean isScanning() {
167         return !mOngoingScans.isEmpty();
168     }
169 
getScanFromScannerId(int scannerId)170     synchronized LastScan getScanFromScannerId(int scannerId) {
171         return mOngoingScans.get(scannerId);
172     }
173 
isScanTimeout(int scannerId)174     synchronized boolean isScanTimeout(int scannerId) {
175         LastScan onGoingScan = getScanFromScannerId(scannerId);
176         if (onGoingScan == null) {
177             return false;
178         }
179         return onGoingScan.isTimeout;
180     }
181 
recordScanStart(ScanSettings settings, List<ScanFilter> filters, boolean isFilterScan, boolean isCallbackScan, int scannerId)182     synchronized void recordScanStart(ScanSettings settings, List<ScanFilter> filters,
183             boolean isFilterScan, boolean isCallbackScan, int scannerId) {
184         LastScan existingScan = getScanFromScannerId(scannerId);
185         if (existingScan != null) {
186             return;
187         }
188         this.mScansStarted++;
189         startTime = SystemClock.elapsedRealtime();
190 
191         LastScan scan = new LastScan(startTime, isFilterScan, isCallbackScan, scannerId,
192                 settings.getScanMode(), settings.getCallbackType());
193         if (settings != null) {
194             scan.isOpportunisticScan = scan.scanMode == ScanSettings.SCAN_MODE_OPPORTUNISTIC;
195             scan.isBackgroundScan =
196                     (scan.scanCallbackType & ScanSettings.CALLBACK_TYPE_FIRST_MATCH) != 0;
197             scan.isBatchScan =
198                     settings.getCallbackType() == ScanSettings.CALLBACK_TYPE_ALL_MATCHES
199                     && settings.getReportDelayMillis() != 0;
200             switch (scan.scanMode) {
201                 case ScanSettings.SCAN_MODE_OPPORTUNISTIC:
202                     mOppScan++;
203                     break;
204                 case ScanSettings.SCAN_MODE_LOW_POWER:
205                     mLowPowerScan++;
206                     break;
207                 case ScanSettings.SCAN_MODE_BALANCED:
208                     mBalancedScan++;
209                     break;
210                 case ScanSettings.SCAN_MODE_LOW_LATENCY:
211                     mLowLantencyScan++;
212                     break;
213                 case ScanSettings.SCAN_MODE_AMBIENT_DISCOVERY:
214                     mAmbientDiscoveryScan++;
215                     break;
216             }
217         }
218 
219         if (isFilterScan) {
220             for (ScanFilter filter : filters) {
221                 scan.filterString +=
222                       "\n      └ " + filterToStringWithoutNullParam(filter);
223             }
224         }
225 
226         BluetoothMetricsProto.ScanEvent scanEvent = BluetoothMetricsProto.ScanEvent.newBuilder()
227                 .setScanEventType(BluetoothMetricsProto.ScanEvent.ScanEventType.SCAN_EVENT_START)
228                 .setScanTechnologyType(
229                         BluetoothMetricsProto.ScanEvent.ScanTechnologyType.SCAN_TECH_TYPE_LE)
230                 .setEventTimeMillis(System.currentTimeMillis())
231                 .setInitiator(truncateAppName(appName)).build();
232         mGattService.addScanEvent(scanEvent);
233 
234         if (!isScanning()) {
235             mScanStartTime = startTime;
236         }
237         boolean isUnoptimized =
238                 !(scan.isFilterScan || scan.isBackgroundScan || scan.isOpportunisticScan);
239         mBatteryStatsManager.reportBleScanStarted(mWorkSource, isUnoptimized);
240         BluetoothStatsLog.write(BluetoothStatsLog.BLE_SCAN_STATE_CHANGED,
241                 mWorkSourceUtil.getUids(), mWorkSourceUtil.getTags(),
242                 BluetoothStatsLog.BLE_SCAN_STATE_CHANGED__STATE__ON,
243                 scan.isFilterScan, scan.isBackgroundScan, scan.isOpportunisticScan);
244 
245         mOngoingScans.put(scannerId, scan);
246     }
247 
recordScanStop(int scannerId)248     synchronized void recordScanStop(int scannerId) {
249         LastScan scan = getScanFromScannerId(scannerId);
250         if (scan == null) {
251             return;
252         }
253         this.mScansStopped++;
254         stopTime = SystemClock.elapsedRealtime();
255         long scanDuration = stopTime - scan.timestamp;
256         scan.duration = scanDuration;
257         if (scan.isSuspended) {
258             long suspendDuration = stopTime - scan.suspendStartTime;
259             scan.suspendDuration += suspendDuration;
260             mTotalSuspendTime += suspendDuration;
261         }
262         mOngoingScans.remove(scannerId);
263         if (mLastScans.size() >= mAdapterService.getScanQuotaCount()) {
264             mLastScans.remove(0);
265         }
266         mLastScans.add(scan);
267 
268         BluetoothMetricsProto.ScanEvent scanEvent = BluetoothMetricsProto.ScanEvent.newBuilder()
269                 .setScanEventType(BluetoothMetricsProto.ScanEvent.ScanEventType.SCAN_EVENT_STOP)
270                 .setScanTechnologyType(
271                         BluetoothMetricsProto.ScanEvent.ScanTechnologyType.SCAN_TECH_TYPE_LE)
272                 .setEventTimeMillis(System.currentTimeMillis())
273                 .setInitiator(truncateAppName(appName))
274                 .setNumberResults(scan.results)
275                 .build();
276         mGattService.addScanEvent(scanEvent);
277 
278         mTotalScanTime += scanDuration;
279         long activeDuration = scanDuration - scan.suspendDuration;
280         mTotalActiveTime += activeDuration;
281         switch (scan.scanMode) {
282             case ScanSettings.SCAN_MODE_OPPORTUNISTIC:
283                 mOppScanTime += activeDuration;
284                 break;
285             case ScanSettings.SCAN_MODE_LOW_POWER:
286                 mLowPowerScanTime += activeDuration;
287                 break;
288             case ScanSettings.SCAN_MODE_BALANCED:
289                 mBalancedScanTime += activeDuration;
290                 break;
291             case ScanSettings.SCAN_MODE_LOW_LATENCY:
292                 mLowLantencyScanTime += activeDuration;
293                 break;
294             case ScanSettings.SCAN_MODE_AMBIENT_DISCOVERY:
295                 mAmbientDiscoveryScanTime += activeDuration;
296                 break;
297         }
298 
299         // Inform battery stats of any results it might be missing on scan stop
300         boolean isUnoptimized =
301                 !(scan.isFilterScan || scan.isBackgroundScan || scan.isOpportunisticScan);
302         mBatteryStatsManager.reportBleScanResults(mWorkSource, scan.results % 100);
303         mBatteryStatsManager.reportBleScanStopped(mWorkSource, isUnoptimized);
304         BluetoothStatsLog.write(BluetoothStatsLog.BLE_SCAN_RESULT_RECEIVED,
305                 mWorkSourceUtil.getUids(), mWorkSourceUtil.getTags(), scan.results % 100);
306         BluetoothStatsLog.write(BluetoothStatsLog.BLE_SCAN_STATE_CHANGED,
307                 mWorkSourceUtil.getUids(), mWorkSourceUtil.getTags(),
308                 BluetoothStatsLog.BLE_SCAN_STATE_CHANGED__STATE__OFF,
309                 scan.isFilterScan, scan.isBackgroundScan, scan.isOpportunisticScan);
310     }
311 
recordScanSuspend(int scannerId)312     synchronized void recordScanSuspend(int scannerId) {
313         LastScan scan = getScanFromScannerId(scannerId);
314         if (scan == null || scan.isSuspended) {
315             return;
316         }
317         scan.suspendStartTime = SystemClock.elapsedRealtime();
318         scan.isSuspended = true;
319     }
320 
recordScanResume(int scannerId)321     synchronized void recordScanResume(int scannerId) {
322         LastScan scan = getScanFromScannerId(scannerId);
323         long suspendDuration = 0;
324         if (scan == null || !scan.isSuspended) {
325             return;
326         }
327         scan.isSuspended = false;
328         stopTime = SystemClock.elapsedRealtime();
329         suspendDuration = stopTime - scan.suspendStartTime;
330         scan.suspendDuration += suspendDuration;
331         mTotalSuspendTime += suspendDuration;
332     }
333 
setScanTimeout(int scannerId)334     synchronized void setScanTimeout(int scannerId) {
335         if (!isScanning()) {
336             return;
337         }
338 
339         LastScan scan = getScanFromScannerId(scannerId);
340         if (scan != null) {
341             scan.isTimeout = true;
342         }
343     }
344 
isScanningTooFrequently()345     synchronized boolean isScanningTooFrequently() {
346         if (mLastScans.size() < mAdapterService.getScanQuotaCount()) {
347             return false;
348         }
349 
350         return (SystemClock.elapsedRealtime() - mLastScans.get(0).timestamp)
351                 < mAdapterService.getScanQuotaWindowMillis();
352     }
353 
isScanningTooLong()354     synchronized boolean isScanningTooLong() {
355         if (!isScanning()) {
356             return false;
357         }
358         return (SystemClock.elapsedRealtime() - mScanStartTime)
359                 > mAdapterService.getScanTimeoutMillis();
360     }
361 
hasRecentScan()362     synchronized boolean hasRecentScan() {
363         if (!isScanning() || mLastScans.isEmpty()) {
364             return false;
365         }
366         LastScan lastScan = mLastScans.get(mLastScans.size() - 1);
367         return ((SystemClock.elapsedRealtime() - lastScan.duration - lastScan.timestamp)
368                 < LARGE_SCAN_TIME_GAP_MS);
369     }
370 
371     // This function truncates the app name for privacy reasons. Apps with
372     // four part package names or more get truncated to three parts, and apps
373     // with three part package names names get truncated to two. Apps with two
374     // or less package names names are untouched.
375     // Examples: one.two.three.four => one.two.three
376     //           one.two.three => one.two
truncateAppName(String name)377     private String truncateAppName(String name) {
378         String initiator = name;
379         String[] nameSplit = initiator.split("\\.");
380         if (nameSplit.length > 3) {
381             initiator = nameSplit[0] + "." + nameSplit[1] + "." + nameSplit[2];
382         } else if (nameSplit.length == 3) {
383             initiator = nameSplit[0] + "." + nameSplit[1];
384         }
385 
386         return initiator;
387     }
388 
filterToStringWithoutNullParam(ScanFilter filter)389     private static String filterToStringWithoutNullParam(ScanFilter filter) {
390         String filterString = "BluetoothLeScanFilter [";
391         if (filter.getDeviceName() != null) {
392             filterString += " DeviceName=" + filter.getDeviceName();
393         }
394         if (filter.getDeviceAddress() != null) {
395             filterString += " DeviceAddress=" + filter.getDeviceAddress();
396         }
397         if (filter.getServiceUuid() != null) {
398             filterString += " ServiceUuid=" + filter.getServiceUuid();
399         }
400         if (filter.getServiceUuidMask() != null) {
401             filterString += " ServiceUuidMask=" + filter.getServiceUuidMask();
402         }
403         if (filter.getServiceSolicitationUuid() != null) {
404             filterString += " ServiceSolicitationUuid=" + filter.getServiceSolicitationUuid();
405         }
406         if (filter.getServiceSolicitationUuidMask() != null) {
407             filterString +=
408                   " ServiceSolicitationUuidMask=" + filter.getServiceSolicitationUuidMask();
409         }
410         if (filter.getServiceDataUuid() != null) {
411             filterString += " ServiceDataUuid=" + Objects.toString(filter.getServiceDataUuid());
412         }
413         if (filter.getServiceData() != null) {
414             filterString += " ServiceData=" + Arrays.toString(filter.getServiceData());
415         }
416         if (filter.getServiceDataMask() != null) {
417             filterString += " ServiceDataMask=" + Arrays.toString(filter.getServiceDataMask());
418         }
419         if (filter.getManufacturerId() >= 0) {
420             filterString += " ManufacturerId=" + filter.getManufacturerId();
421         }
422         if (filter.getManufacturerData() != null) {
423             filterString += " ManufacturerData=" + Arrays.toString(filter.getManufacturerData());
424         }
425         if (filter.getManufacturerDataMask() != null) {
426             filterString +=
427                   " ManufacturerDataMask=" +  Arrays.toString(filter.getManufacturerDataMask());
428         }
429         filterString += " ]";
430         return filterString;
431     }
432 
433 
scanModeToString(int scanMode)434     private static String scanModeToString(int scanMode) {
435         switch (scanMode) {
436             case ScanSettings.SCAN_MODE_OPPORTUNISTIC:
437                 return "OPPORTUNISTIC";
438             case ScanSettings.SCAN_MODE_LOW_LATENCY:
439                 return "LOW_LATENCY";
440             case ScanSettings.SCAN_MODE_BALANCED:
441                 return "BALANCED";
442             case ScanSettings.SCAN_MODE_LOW_POWER:
443                 return "LOW_POWER";
444             case ScanSettings.SCAN_MODE_AMBIENT_DISCOVERY:
445                 return "AMBIENT_DISCOVERY";
446             default:
447                 return "UNKNOWN(" + scanMode + ")";
448         }
449     }
450 
callbackTypeToString(int callbackType)451     private static String callbackTypeToString(int callbackType) {
452         switch (callbackType) {
453             case ScanSettings.CALLBACK_TYPE_ALL_MATCHES:
454                 return "ALL_MATCHES";
455             case ScanSettings.CALLBACK_TYPE_FIRST_MATCH:
456                 return "FIRST_MATCH";
457             case ScanSettings.CALLBACK_TYPE_MATCH_LOST:
458                 return "LOST";
459             default:
460                 return callbackType == (ScanSettings.CALLBACK_TYPE_FIRST_MATCH
461                     | ScanSettings.CALLBACK_TYPE_MATCH_LOST) ? "[FIRST_MATCH | LOST]" : "UNKNOWN: "
462                     + callbackType;
463         }
464     }
465 
dumpToString(StringBuilder sb)466     synchronized void dumpToString(StringBuilder sb) {
467         long currentTime = System.currentTimeMillis();
468         long currTime = SystemClock.elapsedRealtime();
469         long Score = 0;
470         long scanDuration = 0;
471         long suspendDuration = 0;
472         long activeDuration = 0;
473         long totalActiveTime = mTotalActiveTime;
474         long totalSuspendTime = mTotalSuspendTime;
475         long totalScanTime = mTotalScanTime;
476         long oppScanTime = mOppScanTime;
477         long lowPowerScanTime = mLowPowerScanTime;
478         long balancedScanTime = mBalancedScanTime;
479         long lowLatencyScanTime = mLowLantencyScanTime;
480         long ambientDiscoveryScanTime = mAmbientDiscoveryScanTime;
481         int oppScan = mOppScan;
482         int lowPowerScan = mLowPowerScan;
483         int balancedScan = mBalancedScan;
484         int lowLatencyScan = mLowLantencyScan;
485         int ambientDiscoveryScan = mAmbientDiscoveryScan;
486 
487         if (!mOngoingScans.isEmpty()) {
488             for (Integer key : mOngoingScans.keySet()) {
489                 LastScan scan = mOngoingScans.get(key);
490                 scanDuration = currTime - scan.timestamp;
491 
492                 if (scan.isSuspended) {
493                     suspendDuration = currTime - scan.suspendStartTime;
494                     totalSuspendTime += suspendDuration;
495                 }
496 
497                 totalScanTime += scanDuration;
498                 totalSuspendTime += suspendDuration;
499                 activeDuration = scanDuration - scan.suspendDuration - suspendDuration;
500                 totalActiveTime += activeDuration;
501                 switch (scan.scanMode) {
502                     case ScanSettings.SCAN_MODE_OPPORTUNISTIC:
503                         oppScanTime += activeDuration;
504                         break;
505                     case ScanSettings.SCAN_MODE_LOW_POWER:
506                         lowPowerScanTime += activeDuration;
507                         break;
508                     case ScanSettings.SCAN_MODE_BALANCED:
509                         balancedScanTime += activeDuration;
510                         break;
511                     case ScanSettings.SCAN_MODE_LOW_LATENCY:
512                         lowLatencyScanTime += activeDuration;
513                         break;
514                     case ScanSettings.SCAN_MODE_AMBIENT_DISCOVERY:
515                         ambientDiscoveryScan += activeDuration;
516                         break;
517                 }
518             }
519         }
520         Score = (oppScanTime * OPPORTUNISTIC_WEIGHT + lowPowerScanTime * LOW_POWER_WEIGHT
521               + balancedScanTime * BALANCED_WEIGHT + lowLatencyScanTime * LOW_LATENCY_WEIGHT
522               + ambientDiscoveryScanTime * AMBIENT_DISCOVERY_WEIGHT) / 100;
523 
524         sb.append("  " + appName);
525         if (isRegistered) {
526             sb.append(" (Registered)");
527         }
528 
529         sb.append("\n  LE scans (started/stopped)                                  : "
530                 + mScansStarted + " / " + mScansStopped);
531         sb.append("\n  Scan time in ms (active/suspend/total)                      : "
532                 + totalActiveTime + " / " + totalSuspendTime + " / " + totalScanTime);
533         sb.append("\n  Scan time with mode in ms "
534                 + "(Opp/LowPower/Balanced/LowLatency/AmbientDiscovery):"
535                 + oppScanTime + " / " + lowPowerScanTime + " / " + balancedScanTime + " / "
536                 + lowLatencyScanTime + " / " + ambientDiscoveryScanTime);
537         sb.append("\n  Scan mode counter (Opp/LowPower/Balanced/LowLatency/AmbientDiscovery):"
538                 + oppScan + " / " + lowPowerScan + " / " + balancedScan + " / " + lowLatencyScan
539                 + " / " + ambientDiscoveryScan);
540         sb.append("\n  Score                                                       : " + Score);
541         sb.append("\n  Total number of results                                     : " + results);
542 
543         if (!mLastScans.isEmpty()) {
544             sb.append("\n  Last " + mLastScans.size()
545                     + " scans                                                :");
546 
547             for (int i = 0; i < mLastScans.size(); i++) {
548                 LastScan scan = mLastScans.get(i);
549                 Date timestamp = new Date(currentTime - currTime + scan.timestamp);
550                 sb.append("\n    " + DATE_FORMAT.format(timestamp) + " - ");
551                 sb.append(scan.duration + "ms ");
552                 if (scan.isOpportunisticScan) {
553                     sb.append("Opp ");
554                 }
555                 if (scan.isBackgroundScan) {
556                     sb.append("Back ");
557                 }
558                 if (scan.isTimeout) {
559                     sb.append("Forced ");
560                 }
561                 if (scan.isFilterScan) {
562                     sb.append("Filter ");
563                 }
564                 sb.append(scan.results + " results");
565                 sb.append(" (" + scan.scannerId + ") ");
566                 if (scan.isCallbackScan) {
567                     sb.append("CB ");
568                 } else {
569                     sb.append("PI ");
570                 }
571                 if (scan.isBatchScan) {
572                     sb.append("Batch Scan");
573                 } else {
574                     sb.append("Regular Scan");
575                 }
576                 if (scan.suspendDuration != 0) {
577                     activeDuration = scan.duration - scan.suspendDuration;
578                     sb.append("\n      └ " + "Suspended Time: " + scan.suspendDuration
579                             + "ms, Active Time: " + activeDuration);
580                 }
581                 sb.append("\n      └ " + "Scan Config: [ ScanMode="
582                         + scanModeToString(scan.scanMode) + ", callbackType="
583                         + callbackTypeToString(scan.scanCallbackType) + " ]");
584                 if (scan.isFilterScan) {
585                     sb.append(scan.filterString);
586                 }
587             }
588         }
589 
590         if (!mOngoingScans.isEmpty()) {
591             sb.append("\n  Ongoing scans                                               :");
592             for (Integer key : mOngoingScans.keySet()) {
593                 LastScan scan = mOngoingScans.get(key);
594                 Date timestamp = new Date(currentTime - currTime + scan.timestamp);
595                 sb.append("\n    " + DATE_FORMAT.format(timestamp) + " - ");
596                 sb.append((currTime - scan.timestamp) + "ms ");
597                 if (scan.isOpportunisticScan) {
598                     sb.append("Opp ");
599                 }
600                 if (scan.isBackgroundScan) {
601                     sb.append("Back ");
602                 }
603                 if (scan.isTimeout) {
604                     sb.append("Forced ");
605                 }
606                 if (scan.isFilterScan) {
607                     sb.append("Filter ");
608                 }
609                 if (scan.isSuspended) {
610                     sb.append("Suspended ");
611                 }
612                 sb.append(scan.results + " results");
613                 sb.append(" (" + scan.scannerId + ") ");
614                 if (scan.isCallbackScan) {
615                     sb.append("CB ");
616                 } else {
617                     sb.append("PI ");
618                 }
619                 if (scan.isBatchScan) {
620                     sb.append("Batch Scan");
621                 } else {
622                     sb.append("Regular Scan");
623                 }
624                 if (scan.suspendStartTime != 0) {
625                     long duration = scan.suspendDuration + (scan.isSuspended ? (currTime
626                             - scan.suspendStartTime) : 0);
627                     activeDuration = scan.duration - scan.suspendDuration;
628                     sb.append("\n      └ " + "Suspended Time:" + scan.suspendDuration
629                             + "ms, Active Time:" + activeDuration);
630                 }
631                 sb.append("\n      └ " + "Scan Config: [ ScanMode="
632                         + scanModeToString(scan.scanMode) + ", callbackType="
633                         + callbackTypeToString(scan.scanCallbackType) + " ]");
634                 if (scan.isFilterScan) {
635                     sb.append(scan.filterString);
636                 }
637             }
638         }
639 
640         ContextMap.App appEntry = mContextMap.getByName(appName);
641         if (appEntry != null && isRegistered) {
642             sb.append("\n  Application ID                     : " + appEntry.id);
643             sb.append("\n  UUID                               : " + appEntry.uuid);
644 
645             List<ContextMap.Connection> connections = mContextMap.getConnectionByApp(appEntry.id);
646 
647             sb.append("\n  Connections: " + connections.size());
648 
649             Iterator<ContextMap.Connection> ii = connections.iterator();
650             while (ii.hasNext()) {
651                 ContextMap.Connection connection = ii.next();
652                 long connectionTime = currTime - connection.startTime;
653                 Date timestamp = new Date(currentTime - currTime + connection.startTime);
654                 sb.append("\n    " + DATE_FORMAT.format(timestamp) + " - ");
655                 sb.append((connectionTime) + "ms ");
656                 sb.append(": " + connection.address + " (" + connection.connId + ")");
657             }
658         }
659         sb.append("\n\n");
660     }
661 }
662