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