1 /* 2 * Copyright (C) 2022 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 android.nearby.fastpair.provider.simulator.app; 18 19 import static android.content.pm.PackageManager.PERMISSION_GRANTED; 20 import static android.nearby.fastpair.provider.simulator.SimulatorStreamProtocol.Event.Code.BLUETOOTH_STATE_BOND; 21 import static android.nearby.fastpair.provider.simulator.SimulatorStreamProtocol.Event.Code.BLUETOOTH_STATE_CONNECTION; 22 import static android.nearby.fastpair.provider.simulator.SimulatorStreamProtocol.Event.Code.BLUETOOTH_STATE_SCAN_MODE; 23 24 import static com.google.common.io.BaseEncoding.base16; 25 import static com.google.common.io.BaseEncoding.base64; 26 27 import android.Manifest.permission; 28 import android.annotation.SuppressLint; 29 import android.app.Activity; 30 import android.app.AlertDialog; 31 import android.bluetooth.BluetoothDevice; 32 import android.bluetooth.BluetoothProfile; 33 import android.bluetooth.le.AdvertiseSettings; 34 import android.content.DialogInterface; 35 import android.content.SharedPreferences; 36 import android.graphics.Color; 37 import android.nearby.fastpair.provider.EventStreamProtocol.EventGroup; 38 import android.nearby.fastpair.provider.FastPairSimulator; 39 import android.nearby.fastpair.provider.FastPairSimulator.BatteryValue; 40 import android.nearby.fastpair.provider.FastPairSimulator.KeyInputCallback; 41 import android.nearby.fastpair.provider.FastPairSimulator.PasskeyEventCallback; 42 import android.nearby.fastpair.provider.bluetooth.BluetoothController; 43 import android.nearby.fastpair.provider.simulator.SimulatorStreamProtocol.Event; 44 import android.nearby.fastpair.provider.simulator.testing.RemoteDevice; 45 import android.nearby.fastpair.provider.simulator.testing.RemoteDevicesManager; 46 import android.nearby.fastpair.provider.simulator.testing.StreamIOHandlerFactory; 47 import android.nearby.fastpair.provider.utils.Logger; 48 import android.net.Uri; 49 import android.os.Bundle; 50 import android.text.InputType; 51 import android.text.TextUtils; 52 import android.text.method.ScrollingMovementMethod; 53 import android.view.LayoutInflater; 54 import android.view.Menu; 55 import android.view.MenuItem; 56 import android.view.View; 57 import android.widget.AdapterView; 58 import android.widget.AdapterView.OnItemSelectedListener; 59 import android.widget.ArrayAdapter; 60 import android.widget.Button; 61 import android.widget.CompoundButton; 62 import android.widget.EditText; 63 import android.widget.Spinner; 64 import android.widget.Switch; 65 import android.widget.TextView; 66 import android.widget.Toast; 67 68 import androidx.annotation.NonNull; 69 import androidx.annotation.Nullable; 70 import androidx.annotation.UiThread; 71 import androidx.core.util.Consumer; 72 73 import com.google.common.base.Ascii; 74 import com.google.common.base.Preconditions; 75 import com.google.common.base.Strings; 76 import com.google.common.collect.ImmutableMap; 77 import com.google.errorprone.annotations.FormatMethod; 78 import com.google.protobuf.ByteString; 79 80 import java.io.File; 81 import java.io.IOException; 82 import java.util.LinkedHashMap; 83 import java.util.Locale; 84 import java.util.Map; 85 import java.util.concurrent.Executors; 86 87 import service.proto.Rpcs.AntiSpoofingKeyPair; 88 import service.proto.Rpcs.Device; 89 import service.proto.Rpcs.DeviceType; 90 91 /** 92 * Simulates a Fast Pair device (e.g. a headset). 93 * 94 * <p>See README in this directory, and {http://go/fast-pair-spec}. 95 */ 96 @SuppressLint("SetTextI18n") 97 public class MainActivity extends Activity { 98 public static final String TAG = "FastPairProviderSimulatorApp"; 99 private final Logger mLogger = new Logger(TAG); 100 101 /** Device has a display and the ability to input Yes/No. */ 102 private static final int IO_CAPABILITY_IO = 1; 103 104 /** Device only has a keyboard for entry but no display. */ 105 private static final int IO_CAPABILITY_IN = 2; 106 107 /** Device has no Input or Output capability. */ 108 private static final int IO_CAPABILITY_NONE = 3; 109 110 /** Device has a display and a full keyboard. */ 111 private static final int IO_CAPABILITY_KBDISP = 4; 112 113 private static final String SHARED_PREFS_NAME = 114 "android.nearby.fastpair.provider.simulator.app"; 115 private static final String EXTRA_MODEL_ID = "MODEL_ID"; 116 private static final String EXTRA_BLUETOOTH_ADDRESS = "BLUETOOTH_ADDRESS"; 117 private static final String EXTRA_TX_POWER_LEVEL = "TX_POWER_LEVEL"; 118 private static final String EXTRA_FIRMWARE_VERSION = "FIRMWARE_VERSION"; 119 private static final String EXTRA_SUPPORT_DYNAMIC_SIZE = "SUPPORT_DYNAMIC_SIZE"; 120 private static final String EXTRA_USE_RANDOM_SALT_FOR_ACCOUNT_KEY_ROTATION = 121 "USE_RANDOM_SALT_FOR_ACCOUNT_KEY_ROTATION"; 122 private static final String EXTRA_REMOTE_DEVICE_ID = "REMOTE_DEVICE_ID"; 123 private static final String EXTRA_USE_NEW_GATT_CHARACTERISTICS_ID = 124 "USE_NEW_GATT_CHARACTERISTICS_ID"; 125 public static final String EXTRA_REMOVE_ALL_DEVICES_DURING_PAIRING = 126 "REMOVE_ALL_DEVICES_DURING_PAIRING"; 127 private static final String KEY_ACCOUNT_NAME = "ACCOUNT_NAME"; 128 private static final String[] PERMISSIONS = 129 new String[]{permission.BLUETOOTH, permission.BLUETOOTH_ADMIN, permission.GET_ACCOUNTS}; 130 private static final int LIGHT_GREEN = 0xFFC8FFC8; 131 private static final String ANTI_SPOOFING_KEY_LABEL = "Anti-spoofing key"; 132 133 private static final ImmutableMap<String, String> ANTI_SPOOFING_PRIVATE_KEY_MAP = 134 new ImmutableMap.Builder<String, String>() 135 .put("361A2E", "/1rMqyJRGeOK6vkTNgM70xrytxdKg14mNQkITeusK20=") 136 .put("00000D", "03/MAmUPTGNsN+2iA/1xASXoPplDh3Ha5/lk2JgEBx4=") 137 .put("00000C", "Cbj9eCJrTdDgSYxLkqtfADQi86vIaMvxJsQ298sZYWE=") 138 // BLE only devices 139 .put("49426D", "I5QFOJW0WWFgKKZiwGchuseXsq/p9RN/aYtNsGEVGT0=") 140 .put("01E5CE", "FbHt8STpHJDd4zFQFjimh4Zt7IU94U28MOEIXgUEeCw=") 141 .put("8D13B9", "mv++LcJB1n0mbLNGWlXCv/8Gb6aldctrJC4/Ma/Q3Rg=") 142 .put("9AB0F6", "9eKQNwJUr5vCg0c8rtOXkJcWTAsBmmvEKSgXIqAd50Q=") 143 // Android Auto 144 .put("8E083D", "hGQeREDKM/H1834zWMmTIe0Ap4Zl5igThgE62OtdcKA=") 145 .buildOrThrow(); 146 147 private static final Uri REMOTE_DEVICE_INPUT_STREAM_URI = 148 Uri.fromFile(new File("/data/local/nearby/tmp/read.pipe")); 149 150 private static final Uri REMOTE_DEVICE_OUTPUT_STREAM_URI = 151 Uri.fromFile(new File("/data/local/nearby/tmp/write.pipe")); 152 153 private static final String MODEL_ID_DEFAULT = "00000C"; 154 155 private static final String MODEL_ID_APP_LAUNCH = "60EB56"; 156 157 private static final int MODEL_ID_LENGTH = 6; 158 159 private BluetoothController mBluetoothController; 160 private final BluetoothController.EventListener mEventListener = 161 new BluetoothController.EventListener() { 162 163 @Override 164 public void onBondStateChanged(int bondState) { 165 sendEventToRemoteDevice( 166 Event.newBuilder().setCode(BLUETOOTH_STATE_BOND).setBondState( 167 bondState)); 168 updateStatusView(); 169 } 170 171 @Override 172 public void onConnectionStateChanged(int connectionState) { 173 sendEventToRemoteDevice( 174 Event.newBuilder() 175 .setCode(BLUETOOTH_STATE_CONNECTION) 176 .setConnectionState(connectionState)); 177 updateStatusView(); 178 } 179 180 @Override 181 public void onScanModeChange(int mode) { 182 sendEventToRemoteDevice( 183 Event.newBuilder().setCode(BLUETOOTH_STATE_SCAN_MODE).setScanMode( 184 mode)); 185 updateStatusView(); 186 } 187 188 @Override 189 public void onA2DPSinkProfileConnected() { 190 reset(); 191 } 192 }; 193 194 @Nullable 195 private FastPairSimulator mFastPairSimulator; 196 @Nullable 197 private AlertDialog mInputPasskeyDialog; 198 private Switch mFailSwitch; 199 private Switch mAppLaunchSwitch; 200 private Spinner mAdvOptionSpinner; 201 private Spinner mEventStreamSpinner; 202 private EventGroup mEventGroup; 203 private SharedPreferences mSharedPreferences; 204 private Spinner mModelIdSpinner; 205 private final RemoteDevicesManager mRemoteDevicesManager = new RemoteDevicesManager(); 206 @Nullable 207 private RemoteDeviceListener mInputStreamListener; 208 @Nullable 209 String mRemoteDeviceId; 210 private final Map<String, Device> mModelsMap = new LinkedHashMap<>(); 211 private boolean mRemoveAllDevicesDuringPairing = true; 212 sendEventToRemoteDevice(Event.Builder eventBuilder)213 void sendEventToRemoteDevice(Event.Builder eventBuilder) { 214 if (mRemoteDeviceId == null) { 215 return; 216 } 217 218 mLogger.log("Send data to output stream: %s", eventBuilder.getCode().getNumber()); 219 mRemoteDevicesManager.writeDataToRemoteDevice( 220 mRemoteDeviceId, 221 eventBuilder.build().toByteString(), 222 FutureCallbackWrapper.createDefaultIOCallback(this)); 223 } 224 225 @Override onCreate(Bundle savedInstanceState)226 protected void onCreate(Bundle savedInstanceState) { 227 super.onCreate(savedInstanceState); 228 229 setContentView(R.layout.activity_main); 230 231 mSharedPreferences = getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE); 232 233 mRemoveAllDevicesDuringPairing = 234 getIntent().getBooleanExtra(EXTRA_REMOVE_ALL_DEVICES_DURING_PAIRING, true); 235 236 mFailSwitch = findViewById(R.id.fail_switch); 237 mFailSwitch.setOnCheckedChangeListener((CompoundButton buttonView, boolean isChecked) -> { 238 if (mFastPairSimulator != null) { 239 mFastPairSimulator.setShouldFailPairing(isChecked); 240 } 241 }); 242 243 mAppLaunchSwitch = findViewById(R.id.app_launch_switch); 244 mAppLaunchSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> reset()); 245 246 mAdvOptionSpinner = findViewById(R.id.adv_option_spinner); 247 mEventStreamSpinner = findViewById(R.id.event_stream_spinner); 248 ArrayAdapter<CharSequence> advOptionAdapter = 249 ArrayAdapter.createFromResource( 250 this, R.array.adv_options, android.R.layout.simple_spinner_item); 251 ArrayAdapter<CharSequence> eventStreamAdapter = 252 ArrayAdapter.createFromResource( 253 this, R.array.event_stream_options, android.R.layout.simple_spinner_item); 254 advOptionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 255 mAdvOptionSpinner.setAdapter(advOptionAdapter); 256 mEventStreamSpinner.setAdapter(eventStreamAdapter); 257 mAdvOptionSpinner.setOnItemSelectedListener(new OnItemSelectedListener() { 258 @Override 259 public void onItemSelected(AdapterView<?> adapterView, View view, int position, 260 long id) { 261 startAdvertisingBatteryInformationBasedOnOption(position); 262 } 263 264 @Override 265 public void onNothingSelected(AdapterView<?> adapterView) { 266 } 267 }); 268 mEventStreamSpinner.setOnItemSelectedListener(new OnItemSelectedListener() { 269 @Override 270 public void onItemSelected(AdapterView<?> parent, View view, int position, 271 long id) { 272 switch (EventGroup.forNumber(position + 1)) { 273 case BLUETOOTH: 274 mEventGroup = EventGroup.BLUETOOTH; 275 break; 276 case LOGGING: 277 mEventGroup = EventGroup.LOGGING; 278 break; 279 case DEVICE: 280 mEventGroup = EventGroup.DEVICE; 281 break; 282 default: 283 // fall through 284 } 285 } 286 287 @Override 288 public void onNothingSelected(AdapterView<?> parent) { 289 } 290 }); 291 setupModelIdSpinner(); 292 setupRemoteDevices(); 293 if (checkPermissions(PERMISSIONS)) { 294 mBluetoothController = new BluetoothController(this, mEventListener); 295 mBluetoothController.registerBluetoothStateReceiver(); 296 mBluetoothController.enableBluetooth(); 297 mBluetoothController.connectA2DPSinkProfile(); 298 299 if (mSharedPreferences.getString(KEY_ACCOUNT_NAME, "").isEmpty()) { 300 putFixedModelLocal(); 301 resetModelIdSpinner(); 302 reset(); 303 } 304 } else { 305 requestPermissions(PERMISSIONS, 0 /* requestCode */); 306 } 307 } 308 309 @Override onCreateOptionsMenu(Menu menu)310 public boolean onCreateOptionsMenu(Menu menu) { 311 getMenuInflater().inflate(R.menu.menu, menu); 312 menu.findItem(R.id.use_new_gatt_characteristics_id).setChecked( 313 getFromIntentOrPrefs( 314 EXTRA_USE_NEW_GATT_CHARACTERISTICS_ID, /* defaultValue= */ false)); 315 return true; 316 } 317 318 @Override onOptionsItemSelected(MenuItem item)319 public boolean onOptionsItemSelected(MenuItem item) { 320 if (item.getItemId() == R.id.sign_out_menu_item) { 321 recreate(); 322 return true; 323 } else if (item.getItemId() == R.id.reset_account_keys_menu_item) { 324 resetAccountKeys(); 325 return true; 326 } else if (item.getItemId() == R.id.reset_device_name_menu_item) { 327 resetDeviceName(); 328 return true; 329 } else if (item.getItemId() == R.id.set_firmware_version) { 330 setFirmware(); 331 return true; 332 } else if (item.getItemId() == R.id.set_simulator_capability) { 333 setSimulatorCapability(); 334 return true; 335 } else if (item.getItemId() == R.id.use_new_gatt_characteristics_id) { 336 if (!item.isChecked()) { 337 item.setChecked(true); 338 mSharedPreferences.edit() 339 .putBoolean(EXTRA_USE_NEW_GATT_CHARACTERISTICS_ID, true).apply(); 340 } else { 341 item.setChecked(false); 342 mSharedPreferences.edit() 343 .putBoolean(EXTRA_USE_NEW_GATT_CHARACTERISTICS_ID, false).apply(); 344 } 345 reset(); 346 return true; 347 } 348 return super.onOptionsItemSelected(item); 349 } 350 setFirmware()351 private void setFirmware() { 352 View firmwareInputView = 353 LayoutInflater.from(getApplicationContext()).inflate(R.layout.user_input_dialog, 354 null); 355 EditText userInputDialogEditText = firmwareInputView.findViewById(R.id.userInputDialog); 356 new AlertDialog.Builder(MainActivity.this) 357 .setView(firmwareInputView) 358 .setCancelable(false) 359 .setPositiveButton(android.R.string.ok, (dialogBox, id) -> { 360 String input = userInputDialogEditText.getText().toString(); 361 mSharedPreferences.edit().putString(EXTRA_FIRMWARE_VERSION, 362 input).apply(); 363 reset(); 364 }) 365 .setNegativeButton(android.R.string.cancel, null) 366 .setTitle(R.string.firmware_dialog_title) 367 .show(); 368 } 369 setSimulatorCapability()370 private void setSimulatorCapability() { 371 String[] capabilityKeys = new String[]{EXTRA_SUPPORT_DYNAMIC_SIZE}; 372 String[] capabilityNames = new String[]{"Dynamic Buffer Size"}; 373 // Default values. 374 boolean[] capabilitySelected = new boolean[]{false}; 375 // Get from preferences if exist. 376 for (int i = 0; i < capabilityKeys.length; i++) { 377 capabilitySelected[i] = 378 mSharedPreferences.getBoolean(capabilityKeys[i], capabilitySelected[i]); 379 } 380 381 new AlertDialog.Builder(MainActivity.this) 382 .setMultiChoiceItems( 383 capabilityNames, 384 capabilitySelected, 385 (dialog, which, isChecked) -> capabilitySelected[which] = isChecked) 386 .setCancelable(false) 387 .setPositiveButton( 388 android.R.string.ok, 389 (dialogBox, id) -> { 390 for (int i = 0; i < capabilityKeys.length; i++) { 391 mSharedPreferences 392 .edit() 393 .putBoolean(capabilityKeys[i], capabilitySelected[i]) 394 .apply(); 395 } 396 setCapabilityToSimulator(); 397 }) 398 .setNegativeButton(android.R.string.cancel, null) 399 .setTitle("Simulator Capability") 400 .show(); 401 } 402 setCapabilityToSimulator()403 private void setCapabilityToSimulator() { 404 if (mFastPairSimulator != null) { 405 mFastPairSimulator.setDynamicBufferSize( 406 getFromIntentOrPrefs(EXTRA_SUPPORT_DYNAMIC_SIZE, false)); 407 } 408 } 409 getModelIdString(long id)410 private static String getModelIdString(long id) { 411 String result = Ascii.toUpperCase(Long.toHexString(id)); 412 while (result.length() < MODEL_ID_LENGTH) { 413 result = "0" + result; 414 } 415 return result; 416 } 417 putFixedModelLocal()418 private void putFixedModelLocal() { 419 mModelsMap.put( 420 "00000C", 421 Device.newBuilder() 422 .setId(12) 423 .setAntiSpoofingKeyPair(AntiSpoofingKeyPair.newBuilder().build()) 424 .setDeviceType(DeviceType.HEADPHONES) 425 .build()); 426 } 427 setupModelIdSpinner()428 private void setupModelIdSpinner() { 429 mModelIdSpinner = findViewById(R.id.model_id_spinner); 430 431 ArrayAdapter<String> modelIdAdapter = 432 new ArrayAdapter<>(this, android.R.layout.simple_spinner_item); 433 modelIdAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 434 mModelIdSpinner.setAdapter(modelIdAdapter); 435 resetModelIdSpinner(); 436 mModelIdSpinner.setOnItemSelectedListener(new OnItemSelectedListener() { 437 @Override 438 public void onItemSelected(AdapterView<?> parent, View view, int position, 439 long id) { 440 setModelId(mModelsMap.keySet().toArray(new String[0])[position]); 441 } 442 443 @Override 444 public void onNothingSelected(AdapterView<?> adapterView) { 445 } 446 }); 447 } 448 setupRemoteDevices()449 private void setupRemoteDevices() { 450 if (Strings.isNullOrEmpty(getIntent().getStringExtra(EXTRA_REMOTE_DEVICE_ID))) { 451 mLogger.log("Can't get remote device id"); 452 return; 453 } 454 mRemoteDeviceId = getIntent().getStringExtra(EXTRA_REMOTE_DEVICE_ID); 455 mInputStreamListener = new RemoteDeviceListener(this); 456 457 try { 458 mRemoteDevicesManager.registerRemoteDevice( 459 mRemoteDeviceId, 460 new RemoteDevice( 461 mRemoteDeviceId, 462 StreamIOHandlerFactory.createStreamIOHandler( 463 StreamIOHandlerFactory.Type.LOCAL_FILE, 464 REMOTE_DEVICE_INPUT_STREAM_URI, 465 REMOTE_DEVICE_OUTPUT_STREAM_URI), 466 mInputStreamListener)); 467 } catch (IOException e) { 468 mLogger.log(e, "Failed to create stream IO handler"); 469 } 470 } 471 472 @SuppressWarnings({"unchecked", "rawtypes"}) 473 @UiThread resetModelIdSpinner()474 private void resetModelIdSpinner() { 475 ArrayAdapter adapter = (ArrayAdapter) mModelIdSpinner.getAdapter(); 476 if (adapter == null) { 477 return; 478 } 479 480 adapter.clear(); 481 if (!mModelsMap.isEmpty()) { 482 for (String modelId : mModelsMap.keySet()) { 483 adapter.add(modelId + "-" + mModelsMap.get(modelId).getName()); 484 } 485 mModelIdSpinner.setEnabled(true); 486 int newPos = getPositionFromModelId(getModelId()); 487 if (newPos < 0) { 488 String newModelId = mModelsMap.keySet().iterator().next(); 489 Toast.makeText(this, 490 "Can't find Model ID " + getModelId() + " from console, reset it to " 491 + newModelId, Toast.LENGTH_SHORT).show(); 492 setModelId(newModelId); 493 newPos = 0; 494 } 495 mModelIdSpinner.setSelection(newPos, /* animate= */ false); 496 } else { 497 mModelIdSpinner.setEnabled(false); 498 } 499 } 500 getModelId()501 private String getModelId() { 502 return getFromIntentOrPrefs(EXTRA_MODEL_ID, MODEL_ID_DEFAULT).toUpperCase(Locale.US); 503 } 504 setModelId(String modelId)505 private boolean setModelId(String modelId) { 506 String validModelId = getValidModelId(modelId); 507 if (TextUtils.isEmpty(validModelId)) { 508 mLogger.log("Can't do setModelId because inputted modelId is invalid!"); 509 return false; 510 } 511 512 if (getModelId().equals(validModelId)) { 513 return false; 514 } 515 mSharedPreferences.edit().putString(EXTRA_MODEL_ID, validModelId).apply(); 516 reset(); 517 return true; 518 } 519 520 @Nullable getValidModelId(String modelId)521 private static String getValidModelId(String modelId) { 522 if (TextUtils.isEmpty(modelId) || modelId.length() < MODEL_ID_LENGTH) { 523 return null; 524 } 525 526 return modelId.substring(0, MODEL_ID_LENGTH).toUpperCase(Locale.US); 527 } 528 getPositionFromModelId(String modelId)529 private int getPositionFromModelId(String modelId) { 530 int i = 0; 531 for (String id : mModelsMap.keySet()) { 532 if (id.equals(modelId)) { 533 return i; 534 } 535 i++; 536 } 537 return -1; 538 } 539 resetAccountKeys()540 private void resetAccountKeys() { 541 if (mFastPairSimulator != null) { 542 mFastPairSimulator.resetAccountKeys(); 543 mFastPairSimulator.startAdvertising(); 544 } 545 } 546 resetDeviceName()547 private void resetDeviceName() { 548 if (mFastPairSimulator != null) { 549 mFastPairSimulator.resetDeviceName(); 550 } 551 } 552 553 /** Called via activity_main.xml */ onResetButtonClicked(View view)554 public void onResetButtonClicked(View view) { 555 reset(); 556 } 557 558 /** Called via activity_main.xml */ onSendEventStreamMessageButtonClicked(View view)559 public void onSendEventStreamMessageButtonClicked(View view) { 560 if (mFastPairSimulator != null) { 561 mFastPairSimulator.sendEventStreamMessageToRfcommDevices(mEventGroup); 562 } 563 } 564 reset()565 void reset() { 566 Button resetButton = findViewById(R.id.reset_button); 567 if (mModelsMap.isEmpty() || !resetButton.isEnabled()) { 568 return; 569 } 570 resetButton.setText("Resetting..."); 571 resetButton.setEnabled(false); 572 mModelIdSpinner.setEnabled(false); 573 mAppLaunchSwitch.setEnabled(false); 574 575 if (mFastPairSimulator != null) { 576 mFastPairSimulator.stopAdvertising(); 577 578 if (mBluetoothController.getRemoteDevice() != null) { 579 if (mRemoveAllDevicesDuringPairing) { 580 mFastPairSimulator.removeBond(mBluetoothController.getRemoteDevice()); 581 } 582 mBluetoothController.clearRemoteDevice(); 583 } 584 // To be safe, also unpair from all phones (this covers the case where you kill + 585 // relaunch the 586 // simulator while paired). 587 if (mRemoveAllDevicesDuringPairing) { 588 mFastPairSimulator.disconnectAllBondedDevices(); 589 } 590 // Sometimes a device will still be connected even though it's not bonded. :( Clear 591 // that too. 592 BluetoothProfile profileProxy = mBluetoothController.getA2DPSinkProfileProxy(); 593 for (BluetoothDevice device : profileProxy.getConnectedDevices()) { 594 mFastPairSimulator.disconnect(profileProxy, device); 595 } 596 } 597 updateStatusView(); 598 599 if (mFastPairSimulator != null) { 600 mFastPairSimulator.destroy(); 601 } 602 TextView textView = (TextView) findViewById(R.id.text_view); 603 textView.setText(""); 604 textView.setMovementMethod(new ScrollingMovementMethod()); 605 606 String modelId = getModelId(); 607 608 String txPower = getFromIntentOrPrefs(EXTRA_TX_POWER_LEVEL, "HIGH"); 609 updateStringStatusView(R.id.tx_power_text_view, "TxPower", txPower); 610 611 String bluetoothAddress = getFromIntentOrPrefs(EXTRA_BLUETOOTH_ADDRESS, ""); 612 613 String firmwareVersion = getFromIntentOrPrefs(EXTRA_FIRMWARE_VERSION, "1.1"); 614 try { 615 Preconditions.checkArgument(base16().decode(bluetoothAddress).length == 6); 616 } catch (IllegalArgumentException e) { 617 mLogger.log("Invalid BLUETOOTH_ADDRESS extra (%s), using default.", bluetoothAddress); 618 bluetoothAddress = null; 619 } 620 final String finalBluetoothAddress = bluetoothAddress; 621 622 updateStringStatusView( 623 R.id.anti_spoofing_private_key_text_view, ANTI_SPOOFING_KEY_LABEL, "Loading..."); 624 625 boolean useRandomSaltForAccountKeyRotation = 626 getFromIntentOrPrefs(EXTRA_USE_RANDOM_SALT_FOR_ACCOUNT_KEY_ROTATION, false); 627 628 Executors.newSingleThreadExecutor().execute(() -> { 629 // Fetch the anti-spoofing key corresponding to this model ID (if it 630 // exists). 631 // The account must have Project Viewer permission for the project 632 // that owns 633 // the model ID (normally discoverer-test or discoverer-devices). 634 byte[] antiSpoofingKey = getAntiSpoofingKey(modelId); 635 String antiSpoofingKeyString; 636 Device device = mModelsMap.get(modelId); 637 if (antiSpoofingKey != null) { 638 antiSpoofingKeyString = base64().encode(antiSpoofingKey); 639 } else { 640 if (mSharedPreferences.getString(KEY_ACCOUNT_NAME, "").isEmpty()) { 641 antiSpoofingKeyString = "Can't fetch, no account"; 642 } else { 643 if (device == null) { 644 antiSpoofingKeyString = String.format(Locale.US, 645 "Can't find model %s from console", modelId); 646 } else if (!device.hasAntiSpoofingKeyPair()) { 647 antiSpoofingKeyString = String.format(Locale.US, 648 "Can't find AntiSpoofingKeyPair for model %s", modelId); 649 } else if (device.getAntiSpoofingKeyPair().getPrivateKey().isEmpty()) { 650 antiSpoofingKeyString = String.format(Locale.US, 651 "Can't find privateKey for model %s", modelId); 652 } else { 653 antiSpoofingKeyString = "Unknown error"; 654 } 655 } 656 } 657 658 int desiredIoCapability = getIoCapabilityFromModelId(modelId); 659 660 mBluetoothController.setIoCapability( 661 /*ioCapabilityClassic=*/ desiredIoCapability, 662 /*ioCapabilityBLE=*/ desiredIoCapability); 663 664 runOnUiThread(() -> { 665 updateStringStatusView( 666 R.id.anti_spoofing_private_key_text_view, 667 ANTI_SPOOFING_KEY_LABEL, 668 antiSpoofingKeyString); 669 FastPairSimulator.Options option = FastPairSimulator.Options.builder(modelId) 670 .setAdvertisingModelId( 671 mAppLaunchSwitch.isChecked() ? MODEL_ID_APP_LAUNCH : modelId) 672 .setBluetoothAddress(finalBluetoothAddress) 673 .setTxPowerLevel(toTxPowerLevel(txPower)) 674 .setAdvertisingChangedCallback(isAdvertising -> updateStatusView()) 675 .setAntiSpoofingPrivateKey(antiSpoofingKey) 676 .setUseRandomSaltForAccountKeyRotation(useRandomSaltForAccountKeyRotation) 677 .setDataOnlyConnection(device != null && device.getDataOnlyConnection()) 678 .setShowsPasskeyConfirmation( 679 device.getDeviceType().equals(DeviceType.ANDROID_AUTO)) 680 .setRemoveAllDevicesDuringPairing(mRemoveAllDevicesDuringPairing) 681 .build(); 682 Logger textViewLogger = new Logger(FastPairSimulator.TAG) { 683 684 @FormatMethod 685 public void log(@Nullable Throwable exception, String message, 686 Object... objects) { 687 super.log(exception, message, objects); 688 689 String exceptionMessage = (exception == null) ? "" 690 : " - " + exception.getMessage(); 691 final String finalMessage = 692 String.format(message, objects) + exceptionMessage; 693 694 textView.post(() -> { 695 String newText = 696 textView.getText() + "\n\n" + finalMessage; 697 textView.setText(newText); 698 }); 699 } 700 }; 701 mFastPairSimulator = 702 new FastPairSimulator(this, option, textViewLogger); 703 mFastPairSimulator.setFirmwareVersion(firmwareVersion); 704 mFailSwitch.setChecked( 705 mFastPairSimulator.getShouldFailPairing()); 706 mAdvOptionSpinner.setSelection(0); 707 setCapabilityToSimulator(); 708 709 updateStringStatusView(R.id.bluetooth_address_text_view, 710 "Bluetooth address", 711 mFastPairSimulator.getBluetoothAddress()); 712 713 updateStringStatusView(R.id.device_name_text_view, 714 "Device name", 715 mFastPairSimulator.getDeviceName()); 716 717 resetButton.setText("Reset"); 718 resetButton.setEnabled(true); 719 mModelIdSpinner.setEnabled(true); 720 mAppLaunchSwitch.setEnabled(true); 721 mFastPairSimulator.setDeviceNameCallback(deviceName -> 722 updateStringStatusView( 723 R.id.device_name_text_view, 724 "Device name", deviceName)); 725 726 if (desiredIoCapability == IO_CAPABILITY_IN 727 || device.getDeviceType().equals(DeviceType.ANDROID_AUTO)) { 728 mFastPairSimulator.setPasskeyEventCallback(mPasskeyEventCallback); 729 } 730 if (mInputStreamListener != null) { 731 mInputStreamListener.setFastPairSimulator(mFastPairSimulator); 732 } 733 }); 734 }); 735 } 736 getIoCapabilityFromModelId(String modelId)737 private int getIoCapabilityFromModelId(String modelId) { 738 Device device = mModelsMap.get(modelId); 739 if (device == null) { 740 return IO_CAPABILITY_NONE; 741 } else { 742 if (getAntiSpoofingKey(modelId) == null) { 743 return IO_CAPABILITY_NONE; 744 } else { 745 switch (device.getDeviceType()) { 746 case INPUT_DEVICE: 747 return IO_CAPABILITY_IN; 748 749 case DEVICE_TYPE_UNSPECIFIED: 750 return IO_CAPABILITY_NONE; 751 752 // Treats wearable to IO_CAPABILITY_KBDISP for simulator because there seems 753 // no suitable 754 // type. 755 case WEARABLE: 756 return IO_CAPABILITY_KBDISP; 757 758 default: 759 return IO_CAPABILITY_IO; 760 } 761 } 762 } 763 } 764 765 @Nullable getAccontKey()766 ByteString getAccontKey() { 767 if (mFastPairSimulator == null) { 768 return null; 769 } 770 return mFastPairSimulator.getAccountKey(); 771 } 772 773 @Nullable getAntiSpoofingKey(String modelId)774 private byte[] getAntiSpoofingKey(String modelId) { 775 Device device = mModelsMap.get(modelId); 776 if (device != null 777 && device.hasAntiSpoofingKeyPair() 778 && !device.getAntiSpoofingKeyPair().getPrivateKey().isEmpty()) { 779 return base64().decode(device.getAntiSpoofingKeyPair().getPrivateKey().toStringUtf8()); 780 } else if (ANTI_SPOOFING_PRIVATE_KEY_MAP.containsKey(modelId)) { 781 return base64().decode(ANTI_SPOOFING_PRIVATE_KEY_MAP.get(modelId)); 782 } else { 783 return null; 784 } 785 } 786 787 private final PasskeyEventCallback mPasskeyEventCallback = new PasskeyEventCallback() { 788 @Override 789 public void onPasskeyRequested(KeyInputCallback keyInputCallback) { 790 showInputPasskeyDialog(keyInputCallback); 791 } 792 793 @Override 794 public void onPasskeyConfirmation(int passkey, Consumer<Boolean> isConfirmed) { 795 showConfirmPasskeyDialog(passkey, isConfirmed); 796 } 797 798 @Override 799 public void onRemotePasskeyReceived(int passkey) { 800 if (mInputPasskeyDialog == null) { 801 return; 802 } 803 804 EditText userInputDialogEditText = mInputPasskeyDialog.findViewById( 805 R.id.userInputDialog); 806 if (userInputDialogEditText == null) { 807 return; 808 } 809 810 userInputDialogEditText.setText(String.format("%d", passkey)); 811 } 812 }; 813 showInputPasskeyDialog(KeyInputCallback keyInputCallback)814 private void showInputPasskeyDialog(KeyInputCallback keyInputCallback) { 815 if (mInputPasskeyDialog == null) { 816 View userInputView = 817 LayoutInflater.from(getApplicationContext()).inflate(R.layout.user_input_dialog, 818 null); 819 EditText userInputDialogEditText = userInputView.findViewById(R.id.userInputDialog); 820 userInputDialogEditText.setHint(R.string.passkey_input_hint); 821 userInputDialogEditText.setInputType(InputType.TYPE_CLASS_NUMBER); 822 mInputPasskeyDialog = new AlertDialog.Builder(MainActivity.this) 823 .setView(userInputView) 824 .setCancelable(false) 825 .setPositiveButton( 826 android.R.string.ok, 827 (DialogInterface dialogBox, int id) -> { 828 String input = userInputDialogEditText.getText().toString(); 829 keyInputCallback.onKeyInput(Integer.parseInt(input)); 830 }) 831 .setNegativeButton(android.R.string.cancel, /* listener= */ null) 832 .setTitle(R.string.passkey_dialog_title) 833 .create(); 834 } 835 if (!mInputPasskeyDialog.isShowing()) { 836 mInputPasskeyDialog.show(); 837 } 838 } 839 showConfirmPasskeyDialog(int passkey, Consumer<Boolean> isConfirmed)840 private void showConfirmPasskeyDialog(int passkey, Consumer<Boolean> isConfirmed) { 841 runOnUiThread(() -> new AlertDialog.Builder(MainActivity.this) 842 .setCancelable(false) 843 .setTitle(R.string.confirm_passkey) 844 .setMessage(String.valueOf(passkey)) 845 .setPositiveButton(android.R.string.ok, 846 (d, w) -> isConfirmed.accept(true)) 847 .setNegativeButton(android.R.string.cancel, 848 (d, w) -> isConfirmed.accept(false)) 849 .create() 850 .show()); 851 } 852 853 @UiThread updateStringStatusView(int id, String name, String value)854 private void updateStringStatusView(int id, String name, String value) { 855 ((TextView) findViewById(id)).setText(name + ": " + value); 856 } 857 858 @UiThread updateStatusView()859 private void updateStatusView() { 860 TextView remoteDeviceTextView = (TextView) findViewById(R.id.remote_device_text_view); 861 remoteDeviceTextView.setBackgroundColor( 862 mBluetoothController.getRemoteDevice() != null ? LIGHT_GREEN : Color.LTGRAY); 863 String remoteDeviceString = mBluetoothController.getRemoteDeviceAsString(); 864 remoteDeviceTextView.setText("Remote device: " + remoteDeviceString); 865 866 updateBooleanStatusView( 867 R.id.is_advertising_text_view, 868 "BLE advertising", 869 mFastPairSimulator != null && mFastPairSimulator.isAdvertising()); 870 871 updateStringStatusView( 872 R.id.scan_mode_text_view, 873 "Mode", 874 FastPairSimulator.scanModeToString(mBluetoothController.getScanMode())); 875 876 boolean isPaired = mBluetoothController.isPaired(); 877 updateBooleanStatusView(R.id.is_paired_text_view, "Paired", isPaired); 878 879 updateBooleanStatusView( 880 R.id.is_connected_text_view, "Connected", mBluetoothController.isConnected()); 881 } 882 883 @UiThread updateBooleanStatusView(int id, String name, boolean value)884 private void updateBooleanStatusView(int id, String name, boolean value) { 885 TextView view = (TextView) findViewById(id); 886 view.setBackgroundColor(value ? LIGHT_GREEN : Color.LTGRAY); 887 view.setText(name + ": " + (value ? "Yes" : "No")); 888 } 889 getFromIntentOrPrefs(String key, String defaultValue)890 private String getFromIntentOrPrefs(String key, String defaultValue) { 891 Bundle extras = getIntent().getExtras(); 892 extras = extras != null ? extras : new Bundle(); 893 SharedPreferences prefs = getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE); 894 String value = extras.getString(key, prefs.getString(key, defaultValue)); 895 if (value == null) { 896 prefs.edit().remove(key).apply(); 897 } else { 898 prefs.edit().putString(key, value).apply(); 899 } 900 return value; 901 } 902 getFromIntentOrPrefs(String key, boolean defaultValue)903 private boolean getFromIntentOrPrefs(String key, boolean defaultValue) { 904 Bundle extras = getIntent().getExtras(); 905 extras = extras != null ? extras : new Bundle(); 906 SharedPreferences prefs = getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE); 907 boolean value = extras.getBoolean(key, prefs.getBoolean(key, defaultValue)); 908 prefs.edit().putBoolean(key, value).apply(); 909 return value; 910 } 911 toTxPowerLevel(String txPowerLevelString)912 private static int toTxPowerLevel(String txPowerLevelString) { 913 switch (txPowerLevelString.toUpperCase()) { 914 case "3": 915 case "HIGH": 916 return AdvertiseSettings.ADVERTISE_TX_POWER_HIGH; 917 case "2": 918 case "MEDIUM": 919 return AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM; 920 case "1": 921 case "LOW": 922 return AdvertiseSettings.ADVERTISE_TX_POWER_LOW; 923 case "0": 924 case "ULTRA_LOW": 925 return AdvertiseSettings.ADVERTISE_TX_POWER_ULTRA_LOW; 926 default: 927 throw new IllegalArgumentException( 928 "Unexpected TxPower=" 929 + txPowerLevelString 930 + ", please provide HIGH, MEDIUM, LOW, or ULTRA_LOW."); 931 } 932 } 933 checkPermissions(String[] permissions)934 private boolean checkPermissions(String[] permissions) { 935 for (String permission : permissions) { 936 if (checkSelfPermission(permission) != PERMISSION_GRANTED) { 937 return false; 938 } 939 } 940 return true; 941 } 942 943 @Override onDestroy()944 protected void onDestroy() { 945 mRemoteDevicesManager.destroy(); 946 947 if (mFastPairSimulator != null) { 948 mFastPairSimulator.destroy(); 949 mBluetoothController.unregisterBluetoothStateReceiver(); 950 } 951 952 // Recover the IO capability. 953 mBluetoothController.setIoCapability( 954 /*ioCapabilityClassic=*/ IO_CAPABILITY_IO, /*ioCapabilityBLE=*/ 955 IO_CAPABILITY_KBDISP); 956 957 super.onDestroy(); 958 } 959 960 @Override onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)961 public void onRequestPermissionsResult( 962 int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 963 super.onRequestPermissionsResult(requestCode, permissions, grantResults); 964 // Relaunch this activity. 965 recreate(); 966 } 967 startAdvertisingBatteryInformationBasedOnOption(int option)968 void startAdvertisingBatteryInformationBasedOnOption(int option) { 969 if (mFastPairSimulator == null) { 970 return; 971 } 972 973 // Option 0 is "No battery info", it means simulator will not pack battery information when 974 // advertising. For the others with battery info, since we are simulating the Presto's 975 // behavior, 976 // there will always be three battery values. 977 switch (option) { 978 case 0: 979 // Option "0: No battery info" 980 mFastPairSimulator.clearBatteryValues(); 981 break; 982 case 1: 983 // Option "1: Show L(⬆) + R(⬆) + C(⬆)" 984 mFastPairSimulator.setSuppressBatteryNotification(false); 985 mFastPairSimulator.setBatteryValues(new BatteryValue(true, 60), 986 new BatteryValue(true, 61), 987 new BatteryValue(true, 62)); 988 break; 989 case 2: 990 // Option "2: Show L + R + C(unknown)" 991 mFastPairSimulator.setSuppressBatteryNotification(false); 992 mFastPairSimulator.setBatteryValues(new BatteryValue(false, 70), 993 new BatteryValue(false, 71), 994 new BatteryValue(false, -1)); 995 break; 996 case 3: 997 // Option "3: Show L(low 10) + R(low 9) + C(low 25)" 998 mFastPairSimulator.setSuppressBatteryNotification(false); 999 mFastPairSimulator.setBatteryValues(new BatteryValue(false, 10), 1000 new BatteryValue(false, 9), 1001 new BatteryValue(false, 25)); 1002 break; 1003 case 4: 1004 // Option "4: Suppress battery w/o level changes" 1005 // Just change the suppress bit and keep the battery values the same as before. 1006 mFastPairSimulator.setSuppressBatteryNotification(true); 1007 break; 1008 case 5: 1009 // Option "5: Suppress L(low 10) + R(11) + C" 1010 mFastPairSimulator.setSuppressBatteryNotification(true); 1011 mFastPairSimulator.setBatteryValues(new BatteryValue(false, 10), 1012 new BatteryValue(false, 11), 1013 new BatteryValue(false, 82)); 1014 break; 1015 case 6: 1016 // Option "6: Suppress L(low ⬆) + R(low ⬆) + C(low 10)" 1017 mFastPairSimulator.setSuppressBatteryNotification(true); 1018 mFastPairSimulator.setBatteryValues(new BatteryValue(true, 10), 1019 new BatteryValue(true, 9), 1020 new BatteryValue(false, 10)); 1021 break; 1022 case 7: 1023 // Option "7: Suppress L(low ⬆) + R(low ⬆) + C(low ⬆)" 1024 mFastPairSimulator.setSuppressBatteryNotification(true); 1025 mFastPairSimulator.setBatteryValues(new BatteryValue(true, 10), 1026 new BatteryValue(true, 9), 1027 new BatteryValue(true, 25)); 1028 break; 1029 case 8: 1030 // Option "8: Show subsequent pairing notification" 1031 mFastPairSimulator.setSuppressSubsequentPairingNotification(false); 1032 break; 1033 case 9: 1034 // Option "9: Suppress subsequent pairing notification" 1035 mFastPairSimulator.setSuppressSubsequentPairingNotification(true); 1036 break; 1037 default: 1038 // Unknown option, do nothing. 1039 return; 1040 } 1041 1042 mFastPairSimulator.startAdvertising(); 1043 } 1044 } 1045