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