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