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