1 /*
2  * Copyright (C) 2021 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.BluetoothProtoEnums;
20 import android.bluetooth.le.AdvertiseData;
21 import android.bluetooth.le.AdvertisingSetParameters;
22 import android.bluetooth.le.PeriodicAdvertisingParameters;
23 import android.os.ParcelUuid;
24 import android.util.SparseArray;
25 
26 import androidx.annotation.VisibleForTesting;
27 
28 import com.android.bluetooth.btservice.MetricsLogger;
29 
30 import java.time.Duration;
31 import java.time.Instant;
32 import java.time.ZoneId;
33 import java.time.format.DateTimeFormatter;
34 import java.util.ArrayList;
35 import java.util.List;
36 import java.util.Map;
37 
38 /** ScanStats class helps keep track of information about scans on a per application basis. */
39 @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
40 public class AppAdvertiseStats {
41     private static final String TAG = AppAdvertiseStats.class.getSimpleName();
42 
43     private static DateTimeFormatter sDateFormat =
44             DateTimeFormatter.ofPattern("MM-dd HH:mm:ss").withZone(ZoneId.systemDefault());
45 
46     static final String[] PHY_LE_STRINGS = {"LE_1M", "LE_2M", "LE_CODED"};
47     static final int UUID_STRING_FILTER_LEN = 8;
48 
49     // ContextMap here is needed to grab Apps and Connections
50     ContextMap mContextMap;
51 
52     // GattService is needed to add scan event protos to be dumped later
53     GattService mGattService;
54 
55     static class AppAdvertiserData {
56         public boolean includeDeviceName = false;
57         public boolean includeTxPowerLevel = false;
58         public SparseArray<byte[]> manufacturerData;
59         public Map<ParcelUuid, byte[]> serviceData;
60         public List<ParcelUuid> serviceUuids;
61 
AppAdvertiserData( boolean includeDeviceName, boolean includeTxPowerLevel, SparseArray<byte[]> manufacturerData, Map<ParcelUuid, byte[]> serviceData, List<ParcelUuid> serviceUuids)62         AppAdvertiserData(
63                 boolean includeDeviceName,
64                 boolean includeTxPowerLevel,
65                 SparseArray<byte[]> manufacturerData,
66                 Map<ParcelUuid, byte[]> serviceData,
67                 List<ParcelUuid> serviceUuids) {
68             this.includeDeviceName = includeDeviceName;
69             this.includeTxPowerLevel = includeTxPowerLevel;
70             this.manufacturerData = manufacturerData;
71             this.serviceData = serviceData;
72             this.serviceUuids = serviceUuids;
73         }
74     }
75 
76     static class AppAdvertiserRecord {
77         public Instant startTime = null;
78         public Instant stopTime = null;
79         public int duration = 0;
80         public int maxExtendedAdvertisingEvents = 0;
81 
AppAdvertiserRecord(Instant startTime)82         AppAdvertiserRecord(Instant startTime) {
83             this.startTime = startTime;
84         }
85     }
86 
87     private String mAppName;
88     private int mId;
89     private boolean mAdvertisingEnabled = false;
90     private boolean mPeriodicAdvertisingEnabled = false;
91     private int mPrimaryPhy = BluetoothDevice.PHY_LE_1M;
92     private int mSecondaryPhy = BluetoothDevice.PHY_LE_1M;
93     private int mInterval = 0;
94     private int mTxPowerLevel = 0;
95     private boolean mLegacy = false;
96     private boolean mAnonymous = false;
97     private boolean mConnectable = false;
98     private boolean mScannable = false;
99     private AppAdvertiserData mAdvertisingData = null;
100     private AppAdvertiserData mScanResponseData = null;
101     private AppAdvertiserData mPeriodicAdvertisingData = null;
102     private boolean mPeriodicIncludeTxPower = false;
103     private int mPeriodicInterval = 0;
104     public ArrayList<AppAdvertiserRecord> mAdvertiserRecords = new ArrayList<AppAdvertiserRecord>();
105 
106     @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
AppAdvertiseStats(int id, String name, ContextMap map, GattService service)107     public AppAdvertiseStats(int id, String name, ContextMap map, GattService service) {
108         this.mId = id;
109         this.mAppName = name;
110         this.mContextMap = map;
111         this.mGattService = service;
112     }
113 
recordAdvertiseStart( AdvertisingSetParameters parameters, AdvertiseData advertiseData, AdvertiseData scanResponse, PeriodicAdvertisingParameters periodicParameters, AdvertiseData periodicData, int duration, int maxExtAdvEvents)114     void recordAdvertiseStart(
115             AdvertisingSetParameters parameters,
116             AdvertiseData advertiseData,
117             AdvertiseData scanResponse,
118             PeriodicAdvertisingParameters periodicParameters,
119             AdvertiseData periodicData,
120             int duration,
121             int maxExtAdvEvents) {
122         mAdvertisingEnabled = true;
123         AppAdvertiserRecord record = new AppAdvertiserRecord(Instant.now());
124         record.duration = duration;
125         record.maxExtendedAdvertisingEvents = maxExtAdvEvents;
126         mAdvertiserRecords.add(record);
127         if (mAdvertiserRecords.size() > 5) {
128             mAdvertiserRecords.remove(0);
129         }
130 
131         if (parameters != null) {
132             mPrimaryPhy = parameters.getPrimaryPhy();
133             mSecondaryPhy = parameters.getSecondaryPhy();
134             mInterval = parameters.getInterval();
135             mTxPowerLevel = parameters.getTxPowerLevel();
136             mLegacy = parameters.isLegacy();
137             mAnonymous = parameters.isAnonymous();
138             mConnectable = parameters.isConnectable();
139             mScannable = parameters.isScannable();
140         }
141 
142         if (advertiseData != null) {
143             mAdvertisingData =
144                     new AppAdvertiserData(
145                             advertiseData.getIncludeDeviceName(),
146                             advertiseData.getIncludeTxPowerLevel(),
147                             advertiseData.getManufacturerSpecificData(),
148                             advertiseData.getServiceData(),
149                             advertiseData.getServiceUuids());
150         }
151 
152         if (scanResponse != null) {
153             mScanResponseData =
154                     new AppAdvertiserData(
155                             scanResponse.getIncludeDeviceName(),
156                             scanResponse.getIncludeTxPowerLevel(),
157                             scanResponse.getManufacturerSpecificData(),
158                             scanResponse.getServiceData(),
159                             scanResponse.getServiceUuids());
160         }
161 
162         if (periodicData != null) {
163             mPeriodicAdvertisingData =
164                     new AppAdvertiserData(
165                             periodicData.getIncludeDeviceName(),
166                             periodicData.getIncludeTxPowerLevel(),
167                             periodicData.getManufacturerSpecificData(),
168                             periodicData.getServiceData(),
169                             periodicData.getServiceUuids());
170         }
171 
172         if (periodicParameters != null) {
173             mPeriodicAdvertisingEnabled = true;
174             mPeriodicIncludeTxPower = periodicParameters.getIncludeTxPower();
175             mPeriodicInterval = periodicParameters.getInterval();
176         }
177         recordAdvertiseEnableCount(true, mConnectable, mPeriodicAdvertisingEnabled);
178     }
179 
recordAdvertiseStart(int duration, int maxExtAdvEvents)180     void recordAdvertiseStart(int duration, int maxExtAdvEvents) {
181         recordAdvertiseStart(null, null, null, null, null, duration, maxExtAdvEvents);
182     }
183 
recordAdvertiseStop()184     void recordAdvertiseStop() {
185         recordAdvertiseEnableCount(false, mConnectable, mPeriodicAdvertisingEnabled);
186         if (!mAdvertiserRecords.isEmpty()) {
187             AppAdvertiserRecord record = mAdvertiserRecords.get(mAdvertiserRecords.size() - 1);
188             record.stopTime = Instant.now();
189             Duration duration = Duration.between(record.startTime, record.stopTime);
190             recordAdvertiseDurationCount(duration, mConnectable, mPeriodicAdvertisingEnabled);
191         }
192         mAdvertisingEnabled = false;
193         mPeriodicAdvertisingEnabled = false;
194     }
195 
recordAdvertiseInstanceCount(int instanceCount)196     static void recordAdvertiseInstanceCount(int instanceCount) {
197         if (instanceCount < 5) {
198             MetricsLogger.getInstance().cacheCount(BluetoothProtoEnums.LE_ADV_INSTANCE_COUNT_5, 1);
199         } else if (instanceCount < 10) {
200             MetricsLogger.getInstance().cacheCount(BluetoothProtoEnums.LE_ADV_INSTANCE_COUNT_10, 1);
201         } else if (instanceCount < 15) {
202             MetricsLogger.getInstance().cacheCount(BluetoothProtoEnums.LE_ADV_INSTANCE_COUNT_15, 1);
203         } else {
204             MetricsLogger.getInstance()
205                     .cacheCount(BluetoothProtoEnums.LE_ADV_INSTANCE_COUNT_15P, 1);
206         }
207     }
208 
recordAdvertiseErrorCount(int key)209     static void recordAdvertiseErrorCount(int key) {
210         if (key != BluetoothProtoEnums.LE_ADV_ERROR_ON_START_COUNT) {
211             return;
212         }
213         MetricsLogger.getInstance().cacheCount(key, 1);
214     }
215 
enableAdvertisingSet(boolean enable, int duration, int maxExtAdvEvents)216     void enableAdvertisingSet(boolean enable, int duration, int maxExtAdvEvents) {
217         if (enable) {
218             // if the advertisingSet have not been disabled, skip enabling.
219             if (!mAdvertisingEnabled) {
220                 recordAdvertiseStart(duration, maxExtAdvEvents);
221             }
222         } else {
223             // if the advertisingSet have not been enabled, skip disabling.
224             if (mAdvertisingEnabled) {
225                 recordAdvertiseStop();
226             }
227         }
228     }
229 
setAdvertisingData(AdvertiseData data)230     void setAdvertisingData(AdvertiseData data) {
231         if (mAdvertisingData == null) {
232             mAdvertisingData =
233                     new AppAdvertiserData(
234                             data.getIncludeDeviceName(),
235                             data.getIncludeTxPowerLevel(),
236                             data.getManufacturerSpecificData(),
237                             data.getServiceData(),
238                             data.getServiceUuids());
239         } else if (data != null) {
240             mAdvertisingData.includeDeviceName = data.getIncludeDeviceName();
241             mAdvertisingData.includeTxPowerLevel = data.getIncludeTxPowerLevel();
242             mAdvertisingData.manufacturerData = data.getManufacturerSpecificData();
243             mAdvertisingData.serviceData = data.getServiceData();
244             mAdvertisingData.serviceUuids = data.getServiceUuids();
245         }
246     }
247 
setScanResponseData(AdvertiseData data)248     void setScanResponseData(AdvertiseData data) {
249         if (mScanResponseData == null) {
250             mScanResponseData =
251                     new AppAdvertiserData(
252                             data.getIncludeDeviceName(),
253                             data.getIncludeTxPowerLevel(),
254                             data.getManufacturerSpecificData(),
255                             data.getServiceData(),
256                             data.getServiceUuids());
257         } else if (data != null) {
258             mScanResponseData.includeDeviceName = data.getIncludeDeviceName();
259             mScanResponseData.includeTxPowerLevel = data.getIncludeTxPowerLevel();
260             mScanResponseData.manufacturerData = data.getManufacturerSpecificData();
261             mScanResponseData.serviceData = data.getServiceData();
262             mScanResponseData.serviceUuids = data.getServiceUuids();
263         }
264     }
265 
setAdvertisingParameters(AdvertisingSetParameters parameters)266     void setAdvertisingParameters(AdvertisingSetParameters parameters) {
267         if (parameters != null) {
268             mPrimaryPhy = parameters.getPrimaryPhy();
269             mSecondaryPhy = parameters.getSecondaryPhy();
270             mInterval = parameters.getInterval();
271             mTxPowerLevel = parameters.getTxPowerLevel();
272             mLegacy = parameters.isLegacy();
273             mAnonymous = parameters.isAnonymous();
274             mConnectable = parameters.isConnectable();
275             mScannable = parameters.isScannable();
276         }
277     }
278 
setPeriodicAdvertisingParameters(PeriodicAdvertisingParameters parameters)279     void setPeriodicAdvertisingParameters(PeriodicAdvertisingParameters parameters) {
280         if (parameters != null) {
281             mPeriodicIncludeTxPower = parameters.getIncludeTxPower();
282             mPeriodicInterval = parameters.getInterval();
283         }
284     }
285 
setPeriodicAdvertisingData(AdvertiseData data)286     void setPeriodicAdvertisingData(AdvertiseData data) {
287         if (mPeriodicAdvertisingData == null) {
288             mPeriodicAdvertisingData =
289                     new AppAdvertiserData(
290                             data.getIncludeDeviceName(),
291                             data.getIncludeTxPowerLevel(),
292                             data.getManufacturerSpecificData(),
293                             data.getServiceData(),
294                             data.getServiceUuids());
295         } else if (data != null) {
296             mPeriodicAdvertisingData.includeDeviceName = data.getIncludeDeviceName();
297             mPeriodicAdvertisingData.includeTxPowerLevel = data.getIncludeTxPowerLevel();
298             mPeriodicAdvertisingData.manufacturerData = data.getManufacturerSpecificData();
299             mPeriodicAdvertisingData.serviceData = data.getServiceData();
300             mPeriodicAdvertisingData.serviceUuids = data.getServiceUuids();
301         }
302     }
303 
onPeriodicAdvertiseEnabled(boolean enable)304     void onPeriodicAdvertiseEnabled(boolean enable) {
305         mPeriodicAdvertisingEnabled = enable;
306     }
307 
setId(int id)308     void setId(int id) {
309         this.mId = id;
310     }
311 
recordAdvertiseDurationCount( Duration duration, boolean isConnectable, boolean inPeriodic)312     private static void recordAdvertiseDurationCount(
313             Duration duration, boolean isConnectable, boolean inPeriodic) {
314         if (duration.compareTo(Duration.ofMinutes(1)) < 0) {
315             MetricsLogger.getInstance()
316                     .cacheCount(BluetoothProtoEnums.LE_ADV_DURATION_COUNT_TOTAL_1M, 1);
317             if (isConnectable) {
318                 MetricsLogger.getInstance()
319                         .cacheCount(BluetoothProtoEnums.LE_ADV_DURATION_COUNT_CONNECTABLE_1M, 1);
320             }
321             if (inPeriodic) {
322                 MetricsLogger.getInstance()
323                         .cacheCount(BluetoothProtoEnums.LE_ADV_DURATION_COUNT_PERIODIC_1M, 1);
324             }
325         } else if (duration.compareTo(Duration.ofMinutes(30)) < 0) {
326             MetricsLogger.getInstance()
327                     .cacheCount(BluetoothProtoEnums.LE_ADV_DURATION_COUNT_TOTAL_30M, 1);
328             if (isConnectable) {
329                 MetricsLogger.getInstance()
330                         .cacheCount(BluetoothProtoEnums.LE_ADV_DURATION_COUNT_CONNECTABLE_30M, 1);
331             }
332             if (inPeriodic) {
333                 MetricsLogger.getInstance()
334                         .cacheCount(BluetoothProtoEnums.LE_ADV_DURATION_COUNT_PERIODIC_30M, 1);
335             }
336         } else if (duration.compareTo(Duration.ofHours(1)) < 0) {
337             MetricsLogger.getInstance()
338                     .cacheCount(BluetoothProtoEnums.LE_ADV_DURATION_COUNT_TOTAL_1H, 1);
339             if (isConnectable) {
340                 MetricsLogger.getInstance()
341                         .cacheCount(BluetoothProtoEnums.LE_ADV_DURATION_COUNT_CONNECTABLE_1H, 1);
342             }
343             if (inPeriodic) {
344                 MetricsLogger.getInstance()
345                         .cacheCount(BluetoothProtoEnums.LE_ADV_DURATION_COUNT_PERIODIC_1H, 1);
346             }
347         } else if (duration.compareTo(Duration.ofHours(3)) < 0) {
348             MetricsLogger.getInstance()
349                     .cacheCount(BluetoothProtoEnums.LE_ADV_DURATION_COUNT_TOTAL_3H, 1);
350             if (isConnectable) {
351                 MetricsLogger.getInstance()
352                         .cacheCount(BluetoothProtoEnums.LE_ADV_DURATION_COUNT_CONNECTABLE_3H, 1);
353             }
354             if (inPeriodic) {
355                 MetricsLogger.getInstance()
356                         .cacheCount(BluetoothProtoEnums.LE_ADV_DURATION_COUNT_PERIODIC_3H, 1);
357             }
358         } else {
359             MetricsLogger.getInstance()
360                     .cacheCount(BluetoothProtoEnums.LE_ADV_DURATION_COUNT_TOTAL_3HP, 1);
361             if (isConnectable) {
362                 MetricsLogger.getInstance()
363                         .cacheCount(BluetoothProtoEnums.LE_ADV_DURATION_COUNT_CONNECTABLE_3HP, 1);
364             }
365             if (inPeriodic) {
366                 MetricsLogger.getInstance()
367                         .cacheCount(BluetoothProtoEnums.LE_ADV_DURATION_COUNT_PERIODIC_3HP, 1);
368             }
369         }
370     }
371 
recordAdvertiseEnableCount( boolean enable, boolean isConnectable, boolean inPeriodic)372     private static void recordAdvertiseEnableCount(
373             boolean enable, boolean isConnectable, boolean inPeriodic) {
374         if (enable) {
375             MetricsLogger.getInstance().cacheCount(BluetoothProtoEnums.LE_ADV_COUNT_ENABLE, 1);
376             if (isConnectable) {
377                 MetricsLogger.getInstance()
378                         .cacheCount(BluetoothProtoEnums.LE_ADV_COUNT_CONNECTABLE_ENABLE, 1);
379             }
380             if (inPeriodic) {
381                 MetricsLogger.getInstance()
382                         .cacheCount(BluetoothProtoEnums.LE_ADV_COUNT_PERIODIC_ENABLE, 1);
383             }
384         } else {
385             MetricsLogger.getInstance().cacheCount(BluetoothProtoEnums.LE_ADV_COUNT_DISABLE, 1);
386             if (isConnectable) {
387                 MetricsLogger.getInstance()
388                         .cacheCount(BluetoothProtoEnums.LE_ADV_COUNT_CONNECTABLE_DISABLE, 1);
389             }
390             if (inPeriodic) {
391                 MetricsLogger.getInstance()
392                         .cacheCount(BluetoothProtoEnums.LE_ADV_COUNT_PERIODIC_DISABLE, 1);
393             }
394         }
395     }
396 
dumpAppAdvertiserData(StringBuilder sb, AppAdvertiserData advData)397     private static void dumpAppAdvertiserData(StringBuilder sb, AppAdvertiserData advData) {
398         sb.append(
399                 "\n          └Include Device Name                          : "
400                         + advData.includeDeviceName);
401         sb.append(
402                 "\n          └Include Tx Power Level                       : "
403                         + advData.includeTxPowerLevel);
404 
405         if (advData.manufacturerData.size() > 0) {
406             sb.append(
407                     "\n          └Manufacturer Data (length of data)           : "
408                             + advData.manufacturerData.size());
409         }
410 
411         if (!advData.serviceData.isEmpty()) {
412             sb.append("\n          └Service Data(UUID, length of data)           : ");
413             for (ParcelUuid uuid : advData.serviceData.keySet()) {
414                 sb.append(
415                         "\n            ["
416                                 + uuid.toString().substring(0, UUID_STRING_FILTER_LEN)
417                                 + "-xxxx-xxxx-xxxx-xxxxxxxxxxxx, "
418                                 + advData.serviceData.get(uuid).length
419                                 + "]");
420             }
421         }
422 
423         if (!advData.serviceUuids.isEmpty()) {
424             sb.append(
425                     "\n          └Service Uuids                                : \n            "
426                             + advData.serviceUuids.toString().substring(0, UUID_STRING_FILTER_LEN)
427                             + "-xxxx-xxxx-xxxx-xxxxxxxxxxxx");
428         }
429     }
430 
dumpPhyString(int phy)431     private static String dumpPhyString(int phy) {
432         if (phy > PHY_LE_STRINGS.length) {
433             return Integer.toString(phy);
434         } else {
435             return PHY_LE_STRINGS[phy - 1];
436         }
437     }
438 
dumpAppAdvertiseStats(StringBuilder sb, AppAdvertiseStats stats)439     private static void dumpAppAdvertiseStats(StringBuilder sb, AppAdvertiseStats stats) {
440         sb.append("\n      └Advertising:");
441         sb.append("\n        └Interval(0.625ms)                              : " + stats.mInterval);
442         sb.append(
443                 "\n        └TX POWER(dbm)                                  : "
444                         + stats.mTxPowerLevel);
445         sb.append(
446                 "\n        └Primary Phy                                    : "
447                         + dumpPhyString(stats.mPrimaryPhy));
448         sb.append(
449                 "\n        └Secondary Phy                                  : "
450                         + dumpPhyString(stats.mSecondaryPhy));
451         sb.append("\n        └Legacy                                         : " + stats.mLegacy);
452         sb.append(
453                 "\n        └Anonymous                                      : " + stats.mAnonymous);
454         sb.append(
455                 "\n        └Connectable                                    : "
456                         + stats.mConnectable);
457         sb.append(
458                 "\n        └Scannable                                      : " + stats.mScannable);
459 
460         if (stats.mAdvertisingData != null) {
461             sb.append("\n        └Advertise Data:");
462             dumpAppAdvertiserData(sb, stats.mAdvertisingData);
463         }
464 
465         if (stats.mScanResponseData != null) {
466             sb.append("\n        └Scan Response:");
467             dumpAppAdvertiserData(sb, stats.mScanResponseData);
468         }
469 
470         if (stats.mPeriodicInterval > 0) {
471             sb.append(
472                     "\n      └Periodic Advertising Enabled                     : "
473                             + stats.mPeriodicAdvertisingEnabled);
474             sb.append(
475                     "\n        └Periodic Include TxPower                       : "
476                             + stats.mPeriodicIncludeTxPower);
477             sb.append(
478                     "\n        └Periodic Interval(1.25ms)                      : "
479                             + stats.mPeriodicInterval);
480         }
481 
482         if (stats.mPeriodicAdvertisingData != null) {
483             sb.append("\n        └Periodic Advertise Data:");
484             dumpAppAdvertiserData(sb, stats.mPeriodicAdvertisingData);
485         }
486 
487         sb.append("\n");
488     }
489 
dumpToString(StringBuilder sb, AppAdvertiseStats stats)490     static void dumpToString(StringBuilder sb, AppAdvertiseStats stats) {
491         Instant currentTime = Instant.now();
492 
493         sb.append("\n    " + stats.mAppName);
494         sb.append("\n     Advertising ID                                     : " + stats.mId);
495         for (int i = 0; i < stats.mAdvertiserRecords.size(); i++) {
496             AppAdvertiserRecord record = stats.mAdvertiserRecords.get(i);
497 
498             sb.append("\n      " + (i + 1) + ":");
499             sb.append(
500                     "\n        └Start time                                     : "
501                             + sDateFormat.format(record.startTime));
502             if (record.stopTime == null) {
503                 Duration timeElapsed = Duration.between(record.startTime, currentTime);
504                 sb.append(
505                         "\n        └Elapsed time                                   : "
506                                 + timeElapsed.toMillis()
507                                 + "ms");
508             } else {
509                 sb.append(
510                         "\n        └Stop time                                      : "
511                                 + sDateFormat.format(record.stopTime));
512             }
513             sb.append(
514                     "\n        └Duration(10ms unit)                            : "
515                             + record.duration);
516             sb.append(
517                     "\n        └Maximum number of extended advertising events  : "
518                             + record.maxExtendedAdvertisingEvents);
519         }
520 
521         dumpAppAdvertiseStats(sb, stats);
522     }
523 }
524