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 17 package com.android.car.bluetooth; 18 19 import static com.android.car.bluetooth.FastPairAccountKeyStorage.AccountKey; 20 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; 21 22 import android.bluetooth.BluetoothAdapter; 23 import android.bluetooth.BluetoothDevice; 24 import android.bluetooth.BluetoothManager; 25 import android.bluetooth.le.AdvertiseData; 26 import android.bluetooth.le.AdvertisingSet; 27 import android.bluetooth.le.AdvertisingSetCallback; 28 import android.bluetooth.le.AdvertisingSetParameters; 29 import android.bluetooth.le.BluetoothLeAdvertiser; 30 import android.car.builtin.bluetooth.le.AdvertisingSetCallbackHelper; 31 import android.car.builtin.bluetooth.le.AdvertisingSetHelper; 32 import android.car.builtin.util.Slogf; 33 import android.content.Context; 34 import android.os.Handler; 35 import android.os.Message; 36 import android.os.ParcelUuid; 37 import android.util.Log; 38 39 import com.android.car.CarLog; 40 import com.android.car.CarServiceUtils; 41 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; 42 import com.android.car.internal.util.IndentingPrintWriter; 43 44 import java.nio.ByteBuffer; 45 import java.nio.ByteOrder; 46 import java.security.MessageDigest; 47 import java.util.ArrayList; 48 import java.util.Arrays; 49 import java.util.List; 50 import java.util.Objects; 51 import java.util.Random; 52 53 /** 54 * The FastPairAdvertiser is responsible for the BLE advertisement of either the model ID while 55 * in pairing mode or the stored account keys while not in pairing mode. 56 * 57 * This advertiser should always be advertising either the model ID or the account key filter if the 58 * Bluetooth adapter is on. 59 * 60 * Additionally, the Fast Pair Advertiser is the only entity allowed to receive notifications about 61 * our private address, which is used by the protocol to verify the remote device we're talking to. 62 * 63 * Advertisement packet formats and timing/intervals are described by the Fast Pair specification 64 */ 65 public class FastPairAdvertiser { 66 private static final String TAG = CarLog.tagFor(FastPairAdvertiser.class); 67 private static final boolean DBG = Slogf.isLoggable(TAG, Log.DEBUG); 68 69 public static final int STATE_STOPPED = 0; 70 public static final int STATE_STARTING = 1; 71 public static final int STATE_STARTED = 2; 72 public static final int STATE_STOPPING = 3; 73 74 // Service ID assigned for FastPair. 75 public static final ParcelUuid SERVICE_UUID = ParcelUuid 76 .fromString("0000FE2C-0000-1000-8000-00805f9b34fb"); 77 78 private static final byte ACCOUNT_KEY_FILTER_FLAGS = 0x00; 79 private static final byte SALT_FIELD_DESCRIPTOR = 0x11; 80 81 private final Context mContext; 82 private final BluetoothAdapter mBluetoothAdapter; 83 private BluetoothLeAdvertiser mBluetoothLeAdvertiser; 84 private AdvertisingSetParameters mAdvertisingSetParameters; 85 private AdvertisingSetCallback mAdvertisingSetCallback; 86 private AdvertiseData mData; 87 private int mTxPower = 0; 88 private Callbacks mCallbacks; 89 90 private final AdvertisingHandler mAdvertisingHandler; 91 92 /** 93 * Receive events from this FastPairAdvertiser 94 */ 95 public interface Callbacks { 96 /** 97 * Notify the Resolvable Private Address of the BLE advertiser. 98 * 99 * @param device The current LE address 100 */ onRpaUpdated(BluetoothDevice device)101 void onRpaUpdated(BluetoothDevice device); 102 } 103 FastPairAdvertiser(Context context)104 FastPairAdvertiser(Context context) { 105 mContext = context; 106 mBluetoothAdapter = mContext.getSystemService(BluetoothManager.class).getAdapter(); 107 Objects.requireNonNull(mBluetoothAdapter, "Bluetooth adapter cannot be null"); 108 mAdvertisingHandler = new AdvertisingHandler(); 109 initializeAdvertisingSetCallback(); 110 } 111 112 /** 113 * Advertise the Fast Pair model ID. 114 * 115 * Model ID advertisements have the following format: 116 * 117 * Octet | Type | Description | Value 118 * -------------------------------------------------------------------------------------------- 119 * 0-2 | uint24 | 24-bit Model ID | varies, example: 0x123456 120 * -------------------------------------------------------------------------------------------- 121 * 122 * Ensure advertising is stopped before switching the underlying advertising data. This can be 123 * done by calling stopAdvertising(). 124 */ advertiseModelId(int modelId, Callbacks callback)125 public void advertiseModelId(int modelId, Callbacks callback) { 126 if (DBG) { 127 Slogf.d(TAG, "advertiseModelId(id=0x%s)", Integer.toHexString(modelId)); 128 } 129 130 ByteBuffer modelIdBytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt( 131 modelId); 132 mAdvertisingHandler.startAdvertising(Arrays.copyOfRange(modelIdBytes.array(), 1, 4), 133 AdvertisingSetParameters.INTERVAL_LOW, callback); 134 } 135 136 /** 137 * Advertise the stored account keys. 138 * 139 * Account Keys advertisements have the following format: 140 * 141 * Octet | Type | Description | Value 142 * -------------------------------------------------------------------------------------------- 143 * 0 | uint8 | Flags, all bits reserved for future use | 0x00 144 * -------------------------------------------------------------------------------------------- 145 * 1-N | | Account Key Data | 0x00, if empty 146 * | | | bloom(account keys), otherwise 147 * -------------------------------------------------------------------------------------------- 148 * 149 * The Account Key Data has the following format: 150 * 151 * Octet | Type | Description | Value 152 * -------------------------------------------------------------------------------------------- 153 * 0 | uint8 | 0bLLLLTTTT (T=type, L=Length) | length=0bLLLL, 4 bit field length 154 * | | | type=0bTTTT, 0b0000 (show UI) 155 * | | | type=0bTTTT, 0b0010 (hide UI) 156 * -------------------------------------------------------------------------------------------- 157 * 1-N | | Account Key Filter | 0x00, if empty 158 * -------------------------------------------------------------------------------------------- 159 * N+1 | uint8 | Salt Field Length and Type | 0b00010001 160 * -------------------------------------------------------------------------------------------- 161 * N+2 | uint8 | Salt | varies 162 * -------------------------------------------------------------------------------------------- 163 * 164 * The Account Key Filter is a bloom filter representation of the stored keys. The filter alone 165 * requires 1.2 * <number of keys> + 3 bytes. This means an Account Key Filter packet is a total 166 * size of 4 (flags, filter field id + length, salt field id + length, salt) + 1.2 * <keys> + 3 167 * bytes. 168 * 169 * Keep this in mind when defining your max keys size, as it will directly impact the size of 170 * advertisement data and packet. Make sure your controller supports your maximum advertisement 171 * size. 172 * 173 * Ensure advertising is stopped before switching the underlying advertising data. This can be 174 * done by calling stopAdvertising(). 175 */ advertiseAccountKeys(List<AccountKey> accountKeys, Callbacks callback)176 public void advertiseAccountKeys(List<AccountKey> accountKeys, Callbacks callback) { 177 if (DBG) { 178 Slogf.d(TAG, "advertiseAccountKeys(keys=%s)", accountKeys); 179 } 180 181 // If we have account keys, then create a salt value and generate the account key filter 182 byte[] accountKeyFilter = null; 183 byte[] salt = null; 184 if (accountKeys != null && accountKeys.size() > 0) { 185 salt = new byte[1]; 186 new Random().nextBytes(salt); 187 accountKeyFilter = getAccountKeyFilter(accountKeys, salt[0]); 188 } 189 190 // If we have an account key filter, then create an advertisement payload using it and the 191 // salt. Otherwise, create an empty advertisement. 192 ByteBuffer accountKeyAdvertisement = null; 193 if (accountKeyFilter != null) { 194 int size = accountKeyFilter.length; 195 accountKeyAdvertisement = ByteBuffer.allocate(size + 4); // filter + 3b flags + 1b salt 196 accountKeyAdvertisement.put(ACCOUNT_KEY_FILTER_FLAGS); // Reserved Flags byte 197 accountKeyAdvertisement.put((byte) (size << 4)); // Length Type and Size, 0bLLLLTTTT 198 accountKeyAdvertisement.put(accountKeyFilter); // Account Key Bloom Results 199 accountKeyAdvertisement.put(SALT_FIELD_DESCRIPTOR); // Salt Field/Size, 0bLLLLTTTT 200 accountKeyAdvertisement.put(salt); // The actual 1 byte of salt 201 } else { 202 accountKeyAdvertisement = ByteBuffer.allocate(2); 203 accountKeyAdvertisement.put((byte) 0x00); // Reserved Flags Byte 204 accountKeyAdvertisement.put((byte) 0x00); // Empty Keys Byte 205 } 206 207 mAdvertisingHandler.startAdvertising(accountKeyAdvertisement.array(), 208 AdvertisingSetParameters.INTERVAL_MEDIUM, callback); 209 } 210 211 /** 212 * Calculate the account key filter, defined as the bloom of the set of account keys. 213 * 214 * @param keys The list of Fast Pair Account keys 215 * @param salt The salt to be used here, as well as appended to the Account Data Advertisment 216 * @return A byte array representing the account key filter 217 */ getAccountKeyFilter(List<AccountKey> keys, byte salt)218 byte[] getAccountKeyFilter(List<AccountKey> keys, byte salt) { 219 if (keys == null || keys.size() <= 0) { 220 Slogf.e(TAG, "Cannot generate account key filter, keys=%s, salt=%s", keys, salt); 221 return null; 222 } 223 224 int size = (int) (1.2 * keys.size()) + 3; 225 byte[] filter = new byte[size]; 226 227 for (AccountKey key : keys) { 228 byte[] v = Arrays.copyOf(key.toBytes(), 17); 229 v[16] = salt; 230 try { 231 byte[] hashed = MessageDigest.getInstance("SHA-256").digest(v); 232 ByteBuffer byteBuffer = ByteBuffer.wrap(hashed); 233 for (int j = 0; j < 8; j++) { 234 long k = Integer.toUnsignedLong(byteBuffer.getInt()) % (size * 8L); 235 filter[(int) (k / 8)] |= (byte) (1 << (k % 8)); 236 } 237 } catch (Exception e) { 238 Slogf.e(TAG, "Error calculating account key filter: %s", e); 239 return null; 240 } 241 } 242 return filter; 243 } 244 245 /** 246 * Stop advertising any data. 247 */ stopAdvertising()248 public void stopAdvertising() { 249 if (DBG) { 250 Slogf.d(TAG, "stoppingAdvertising"); 251 } 252 mAdvertisingHandler.stopAdvertising(); 253 } 254 255 /** 256 * Start a BLE advertisement using the given data, interval, and callbacks. 257 * 258 * Must be called on the Advertising Handler. 259 * 260 * @param data The data to advertise 261 * @param interval The interval at which to advertise 262 * @param callbacks The callback object to notify of FastPairAdvertiser events 263 */ startAdvertisingInternal(byte[] data, int interval, Callbacks callbacks)264 private boolean startAdvertisingInternal(byte[] data, int interval, Callbacks callbacks) { 265 if (DBG) { 266 Slogf.d(TAG, "startAdvertisingInternal(data=%s, internval=%d, cb=%s)", 267 Arrays.toString(data), interval, callbacks); 268 } 269 270 mBluetoothLeAdvertiser = mBluetoothAdapter.getBluetoothLeAdvertiser(); 271 if (mBluetoothLeAdvertiser == null) { 272 Slogf.e(TAG, "startAdvertisingInternal: Failed to get an advertiser."); 273 mBluetoothLeAdvertiser = null; 274 return false; 275 } 276 277 mAdvertisingSetParameters = new AdvertisingSetParameters.Builder() 278 .setLegacyMode(true) 279 .setInterval(interval) 280 .setScannable(true) 281 .setConnectable(true) 282 .build(); 283 mData = new AdvertiseData.Builder() 284 .addServiceUuid(SERVICE_UUID) 285 .addServiceData(SERVICE_UUID, data) 286 .setIncludeTxPowerLevel(true) 287 .build(); 288 mCallbacks = callbacks; 289 290 mBluetoothLeAdvertiser.startAdvertisingSet(mAdvertisingSetParameters, mData, null, null, 291 null, mAdvertisingSetCallback); 292 return true; 293 } 294 295 /** 296 * Stop advertising any data. 297 * 298 * This must be called on the Advertising Handler. 299 */ stopAdvertisingInternal()300 private void stopAdvertisingInternal() { 301 if (DBG) { 302 Slogf.d(TAG, "stoppingAdvertisingInternal"); 303 } 304 305 if (mBluetoothLeAdvertiser == null) return; 306 307 mBluetoothLeAdvertiser.stopAdvertisingSet(mAdvertisingSetCallback); 308 mTxPower = 0; 309 mBluetoothLeAdvertiser = null; 310 } 311 isAdvertising()312 public boolean isAdvertising() { 313 return getAdvertisingState() == STATE_STARTED; 314 } 315 getAdvertisingState()316 public int getAdvertisingState() { 317 return mAdvertisingHandler.getState(); 318 } 319 initializeAdvertisingSetCallback()320 private void initializeAdvertisingSetCallback() { 321 AdvertisingSetCallbackHelper.Callback proxy = 322 new AdvertisingSetCallbackHelper.Callback() { 323 @Override 324 public void onAdvertisingSetStarted(AdvertisingSet advertisingSet, int txPower, 325 int status) { 326 onAdvertisingSetStartedHandler(advertisingSet, txPower, status); 327 if (advertisingSet != null) { 328 AdvertisingSetHelper.getOwnAddress(advertisingSet); 329 } 330 } 331 332 @Override 333 public void onAdvertisingSetStopped(AdvertisingSet advertisingSet) { 334 onAdvertisingSetStoppedHandler(advertisingSet); 335 } 336 337 @Override 338 public void onOwnAddressRead(AdvertisingSet advertisingSet, int addressType, 339 String address) { 340 onOwnAddressReadHandler(addressType, address); 341 } 342 }; 343 344 mAdvertisingSetCallback = 345 AdvertisingSetCallbackHelper.createRealCallbackFromProxy(proxy); 346 } 347 348 // For {@link AdvertisingSetCallback#onAdvertisingSetStarted} and its proxy onAdvertisingSetStartedHandler(AdvertisingSet advertisingSet, int txPower, int status)349 private void onAdvertisingSetStartedHandler(AdvertisingSet advertisingSet, int txPower, 350 int status) { 351 if (DBG) { 352 Slogf.d(TAG, "onAdvertisingSetStarted(): txPower: %d, status: %d", txPower, status); 353 } 354 if (status != AdvertisingSetCallback.ADVERTISE_SUCCESS || advertisingSet == null) { 355 Slogf.w(TAG, "Failed to start advertising, status=%s, advertiser=%s", 356 BluetoothUtils.getAdvertisingCallbackStatusName(status), advertisingSet); 357 mAdvertisingHandler.advertisingStopped(); 358 return; 359 } 360 mTxPower = txPower; 361 mAdvertisingHandler.advertisingStarted(); 362 } 363 364 // For {@link AdvertisingSetCallback#onAdvertisingSetStopped} and its proxy onAdvertisingSetStoppedHandler(AdvertisingSet advertisingSet)365 private void onAdvertisingSetStoppedHandler(AdvertisingSet advertisingSet) { 366 if (DBG) Slogf.d(TAG, "onAdvertisingSetStopped()"); 367 mAdvertisingHandler.advertisingStopped(); 368 } 369 370 // For {@link AdvertisingSetCallback#onOwnAddressRead} and its proxy onOwnAddressReadHandler(int addressType, String address)371 private void onOwnAddressReadHandler(int addressType, String address) { 372 if (DBG) Slogf.d(TAG, "onOwnAddressRead Type= %d, Address= %s", addressType, address); 373 mCallbacks.onRpaUpdated(mBluetoothAdapter.getRemoteDevice(address)); 374 } 375 376 /** 377 * A handler that synchronizes advertising events 378 */ 379 // TODO (243161113): Clean this handler up to make it more clear and enable direct advertising 380 // data changes without stopping 381 private class AdvertisingHandler extends Handler { 382 private static final int MSG_ADVERTISING_STOPPED = 0; 383 private static final int MSG_START_ADVERTISING = 1; 384 private static final int MSG_ADVERTISING_STARTED = 2; 385 private static final int MSG_STOP_ADVERTISING = 3; 386 private static final int MSG_TIMEOUT = 4; 387 388 private static final int OPERATION_TIMEOUT_MS = 4000; 389 390 private int mState = STATE_STOPPED; 391 private final ArrayList<Message> mDeferredMessages = new ArrayList<Message>(); 392 393 private class AdvertisingRequest { 394 public final byte[] mData; 395 public final int mInterval; 396 public final Callbacks mCallback; 397 AdvertisingRequest(byte[] data, int interval, Callbacks callback)398 AdvertisingRequest(byte[] data, int interval, Callbacks callback) { 399 mInterval = interval; 400 mData = data; 401 mCallback = callback; 402 } 403 } 404 AdvertisingHandler()405 AdvertisingHandler() { 406 super(CarServiceUtils.getHandlerThread(FastPairProvider.THREAD_NAME).getLooper()); 407 } 408 startAdvertising(byte[] data, int interval, Callbacks callback)409 public void startAdvertising(byte[] data, int interval, Callbacks callback) { 410 if (DBG) Slogf.d(TAG, "HANDLER: startAdvertising(data=%s)", Arrays.toString(data)); 411 AdvertisingRequest request = new AdvertisingRequest(data, interval, callback); 412 sendMessage(obtainMessage(MSG_START_ADVERTISING, request)); 413 } 414 advertisingStarted()415 public void advertisingStarted() { 416 if (DBG) Slogf.d(TAG, "HANDLER: advertisingStart()"); 417 sendMessage(obtainMessage(MSG_ADVERTISING_STARTED)); 418 } 419 stopAdvertising()420 public void stopAdvertising() { 421 if (DBG) Slogf.d(TAG, "HANDLER: stopAdvertising()"); 422 sendMessage(obtainMessage(MSG_STOP_ADVERTISING)); 423 } 424 advertisingStopped()425 public void advertisingStopped() { 426 if (DBG) Slogf.d(TAG, "HANDLER: advertisingStop()"); 427 sendMessage(obtainMessage(MSG_ADVERTISING_STOPPED)); 428 } 429 queueOperationTimeout()430 private void queueOperationTimeout() { 431 removeMessages(MSG_TIMEOUT); 432 sendMessageDelayed(obtainMessage(MSG_TIMEOUT), OPERATION_TIMEOUT_MS); 433 } 434 435 @Override handleMessage(Message msg)436 public void handleMessage(Message msg) { 437 if (DBG) { 438 Slogf.i(TAG, "HANDLER: Received message %s, state=%s", messageToString(msg.what), 439 stateToString(mState)); 440 } 441 switch (msg.what) { 442 case MSG_ADVERTISING_STOPPED: 443 removeMessages(MSG_TIMEOUT); 444 transitionTo(STATE_STOPPED); 445 processDeferredMessages(); 446 break; 447 448 case MSG_START_ADVERTISING: 449 if (mState == STATE_STARTED) { 450 break; 451 } else if (mState != STATE_STOPPED) { 452 deferMessage(msg); 453 return; 454 } 455 AdvertisingRequest request = (AdvertisingRequest) msg.obj; 456 if (startAdvertisingInternal(request.mData, request.mInterval, 457 request.mCallback)) { 458 transitionTo(STATE_STARTING); 459 } 460 queueOperationTimeout(); 461 break; 462 463 case MSG_ADVERTISING_STARTED: 464 removeMessages(MSG_TIMEOUT); 465 transitionTo(STATE_STARTED); 466 processDeferredMessages(); 467 break; 468 469 case MSG_STOP_ADVERTISING: 470 if (mState == STATE_STOPPED) { 471 break; 472 } else if (mState != STATE_STARTED) { 473 deferMessage(msg); 474 return; 475 } 476 stopAdvertisingInternal(); 477 transitionTo(STATE_STOPPING); 478 queueOperationTimeout(); 479 break; 480 case MSG_TIMEOUT: 481 if (mState == STATE_STARTING) { 482 Slogf.w(TAG, "HANDLER: Timed out waiting for startAdvertising"); 483 stopAdvertisingInternal(); 484 } else if (mState == STATE_STOPPING) { 485 Slogf.w(TAG, "HANDLER: Timed out waiting for stopAdvertising"); 486 } else { 487 Slogf.e(TAG, "HANDLER: Unexpected timeout in state %s", 488 stateToString(mState)); 489 } 490 transitionTo(STATE_STOPPED); 491 processDeferredMessages(); 492 break; 493 494 default: 495 Slogf.e(TAG, "HANDLER: Unexpected message: %d", msg.what); 496 } 497 } 498 transitionTo(int state)499 private void transitionTo(int state) { 500 if (DBG) Slogf.d(TAG, "HANDLER: %s -> %s", stateToString(mState), stateToString(state)); 501 mState = state; 502 } 503 deferMessage(Message message)504 private void deferMessage(Message message) { 505 if (DBG) { 506 Slogf.i(TAG, "HANDLER: Deferred message, message=%s", 507 messageToString(message.what)); 508 } 509 510 Message copy = obtainMessage(); 511 copy.copyFrom(message); 512 mDeferredMessages.add(copy); 513 514 if (DBG) { 515 StringBuilder sb = new StringBuilder(); 516 sb.append("["); 517 for (Message m : mDeferredMessages) { 518 sb.append(" ").append(messageToString(m.what)); 519 } 520 sb.append(" ]"); 521 Slogf.d(TAG, "HANDLER: Deferred List: %s", sb.toString()); 522 } 523 } 524 processDeferredMessages()525 private void processDeferredMessages() { 526 if (DBG) { 527 Slogf.d(TAG, "HANDLER: Process deferred Messages, size=%d", 528 mDeferredMessages.size()); 529 } 530 for (int i = mDeferredMessages.size() - 1; i >= 0; i--) { 531 Message message = mDeferredMessages.get(i); 532 if (DBG) { 533 Slogf.i(TAG, "HANDLER: Adding deferred message to front, message=%s", 534 messageToString(message.what)); 535 } 536 sendMessageAtFrontOfQueue(message); 537 } 538 mDeferredMessages.clear(); 539 } 540 getState()541 public int getState() { 542 return mState; 543 } 544 messageToString(int message)545 private String messageToString(int message) { 546 switch (message) { 547 case MSG_ADVERTISING_STOPPED: 548 return "MSG_ADVERTISING_STOPPED"; 549 case MSG_START_ADVERTISING: 550 return "MSG_START_ADVERTISING"; 551 case MSG_ADVERTISING_STARTED: 552 return "MSG_ADVERTISING_STARTED"; 553 case MSG_STOP_ADVERTISING: 554 return "MSG_STOP_ADVERTISING"; 555 case MSG_TIMEOUT: 556 return "MSG_TIMEOUT"; 557 default: 558 return "Unknown"; 559 } 560 } 561 } 562 stateToString(int state)563 private String stateToString(int state) { 564 switch (state) { 565 case STATE_STOPPED: 566 return "STATE_STOPPED"; 567 case STATE_STARTING: 568 return "STATE_STARTING"; 569 case STATE_STARTED: 570 return "STATE_STARTED"; 571 case STATE_STOPPING: 572 return "STATE_STOPPING"; 573 default: 574 return "Unknown"; 575 } 576 } 577 578 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dump(IndentingPrintWriter writer)579 public void dump(IndentingPrintWriter writer) { 580 writer.println("FastPairAdvertiser:"); 581 writer.increaseIndent(); 582 writer.println("AdvertisingState : " + stateToString(getAdvertisingState())); 583 if (isAdvertising()) { 584 writer.println("Advertising Interval : " + mAdvertisingSetParameters.getInterval()); 585 writer.println("TX Power : " + mTxPower + "/" 586 + mAdvertisingSetParameters.getTxPowerLevel()); 587 writer.println("Advertising Data : " + mData); 588 } 589 writer.decreaseIndent(); 590 } 591 } 592