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