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