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