• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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(desiredIoCapability);
661 
662             runOnUiThread(() -> {
663                 updateStringStatusView(
664                         R.id.anti_spoofing_private_key_text_view,
665                         ANTI_SPOOFING_KEY_LABEL,
666                         antiSpoofingKeyString);
667                 FastPairSimulator.Options option = FastPairSimulator.Options.builder(modelId)
668                         .setAdvertisingModelId(
669                                 mAppLaunchSwitch.isChecked() ? MODEL_ID_APP_LAUNCH : modelId)
670                         .setBluetoothAddress(finalBluetoothAddress)
671                         .setTxPowerLevel(toTxPowerLevel(txPower))
672                         .setAdvertisingChangedCallback(isAdvertising -> updateStatusView())
673                         .setAntiSpoofingPrivateKey(antiSpoofingKey)
674                         .setUseRandomSaltForAccountKeyRotation(useRandomSaltForAccountKeyRotation)
675                         .setDataOnlyConnection(device != null && device.getDataOnlyConnection())
676                         .setShowsPasskeyConfirmation(
677                                 device.getDeviceType().equals(DeviceType.ANDROID_AUTO))
678                         .setRemoveAllDevicesDuringPairing(mRemoveAllDevicesDuringPairing)
679                         .build();
680                 Logger textViewLogger = new Logger(FastPairSimulator.TAG) {
681 
682                     @FormatMethod
683                     public void log(@Nullable Throwable exception, String message,
684                             Object... objects) {
685                         super.log(exception, message, objects);
686 
687                         String exceptionMessage = (exception == null) ? ""
688                                 : " - " + exception.getMessage();
689                         final String finalMessage =
690                                 String.format(message, objects) + exceptionMessage;
691 
692                         textView.post(() -> {
693                             String newText =
694                                     textView.getText() + "\n\n" + finalMessage;
695                             textView.setText(newText);
696                         });
697                     }
698                 };
699                 mFastPairSimulator =
700                         new FastPairSimulator(this, option, textViewLogger);
701                 mFastPairSimulator.setFirmwareVersion(firmwareVersion);
702                 mFailSwitch.setChecked(
703                         mFastPairSimulator.getShouldFailPairing());
704                 mAdvOptionSpinner.setSelection(0);
705                 setCapabilityToSimulator();
706 
707                 updateStringStatusView(R.id.bluetooth_address_text_view,
708                         "Bluetooth address",
709                         mFastPairSimulator.getBluetoothAddress());
710 
711                 updateStringStatusView(R.id.device_name_text_view,
712                         "Device name",
713                         mFastPairSimulator.getDeviceName());
714 
715                 resetButton.setText("Reset");
716                 resetButton.setEnabled(true);
717                 mModelIdSpinner.setEnabled(true);
718                 mAppLaunchSwitch.setEnabled(true);
719                 mFastPairSimulator.setDeviceNameCallback(deviceName ->
720                         updateStringStatusView(
721                                 R.id.device_name_text_view,
722                                 "Device name", deviceName));
723 
724                 if (desiredIoCapability == IO_CAPABILITY_IN
725                         || device.getDeviceType().equals(DeviceType.ANDROID_AUTO)) {
726                     mFastPairSimulator.setPasskeyEventCallback(mPasskeyEventCallback);
727                 }
728                 if (mInputStreamListener != null) {
729                     mInputStreamListener.setFastPairSimulator(mFastPairSimulator);
730                 }
731             });
732         });
733     }
734 
getIoCapabilityFromModelId(String modelId)735     private int getIoCapabilityFromModelId(String modelId) {
736         Device device = mModelsMap.get(modelId);
737         if (device == null) {
738             return IO_CAPABILITY_NONE;
739         } else {
740             if (getAntiSpoofingKey(modelId) == null) {
741                 return IO_CAPABILITY_NONE;
742             } else {
743                 switch (device.getDeviceType()) {
744                     case INPUT_DEVICE:
745                         return IO_CAPABILITY_IN;
746 
747                     case DEVICE_TYPE_UNSPECIFIED:
748                         return IO_CAPABILITY_NONE;
749 
750                     // Treats wearable to IO_CAPABILITY_KBDISP for simulator because there seems
751                     // no suitable
752                     // type.
753                     case WEARABLE:
754                         return IO_CAPABILITY_KBDISP;
755 
756                     default:
757                         return IO_CAPABILITY_IO;
758                 }
759             }
760         }
761     }
762 
763     @Nullable
getAccontKey()764     ByteString getAccontKey() {
765         if (mFastPairSimulator == null) {
766             return null;
767         }
768         return mFastPairSimulator.getAccountKey();
769     }
770 
771     @Nullable
getAntiSpoofingKey(String modelId)772     private byte[] getAntiSpoofingKey(String modelId) {
773         Device device = mModelsMap.get(modelId);
774         if (device != null
775                 && device.hasAntiSpoofingKeyPair()
776                 && !device.getAntiSpoofingKeyPair().getPrivateKey().isEmpty()) {
777             return base64().decode(device.getAntiSpoofingKeyPair().getPrivateKey().toStringUtf8());
778         } else if (ANTI_SPOOFING_PRIVATE_KEY_MAP.containsKey(modelId)) {
779             return base64().decode(ANTI_SPOOFING_PRIVATE_KEY_MAP.get(modelId));
780         } else {
781             return null;
782         }
783     }
784 
785     private final PasskeyEventCallback mPasskeyEventCallback = new PasskeyEventCallback() {
786         @Override
787         public void onPasskeyRequested(KeyInputCallback keyInputCallback) {
788             showInputPasskeyDialog(keyInputCallback);
789         }
790 
791         @Override
792         public void onPasskeyConfirmation(int passkey, Consumer<Boolean> isConfirmed) {
793             showConfirmPasskeyDialog(passkey, isConfirmed);
794         }
795 
796         @Override
797         public void onRemotePasskeyReceived(int passkey) {
798             if (mInputPasskeyDialog == null) {
799                 return;
800             }
801 
802             EditText userInputDialogEditText = mInputPasskeyDialog.findViewById(
803                     R.id.userInputDialog);
804             if (userInputDialogEditText == null) {
805                 return;
806             }
807 
808             userInputDialogEditText.setText(String.format("%d", passkey));
809         }
810     };
811 
showInputPasskeyDialog(KeyInputCallback keyInputCallback)812     private void showInputPasskeyDialog(KeyInputCallback keyInputCallback) {
813         if (mInputPasskeyDialog == null) {
814             View userInputView =
815                     LayoutInflater.from(getApplicationContext()).inflate(R.layout.user_input_dialog,
816                             null);
817             EditText userInputDialogEditText = userInputView.findViewById(R.id.userInputDialog);
818             userInputDialogEditText.setHint(R.string.passkey_input_hint);
819             userInputDialogEditText.setInputType(InputType.TYPE_CLASS_NUMBER);
820             mInputPasskeyDialog = new AlertDialog.Builder(MainActivity.this)
821                     .setView(userInputView)
822                     .setCancelable(false)
823                     .setPositiveButton(
824                             android.R.string.ok,
825                             (DialogInterface dialogBox, int id) -> {
826                                 String input = userInputDialogEditText.getText().toString();
827                                 keyInputCallback.onKeyInput(Integer.parseInt(input));
828                             })
829                     .setNegativeButton(android.R.string.cancel, /* listener= */ null)
830                     .setTitle(R.string.passkey_dialog_title)
831                     .create();
832         }
833         if (!mInputPasskeyDialog.isShowing()) {
834             mInputPasskeyDialog.show();
835         }
836     }
837 
showConfirmPasskeyDialog(int passkey, Consumer<Boolean> isConfirmed)838     private void showConfirmPasskeyDialog(int passkey, Consumer<Boolean> isConfirmed) {
839         runOnUiThread(() -> new AlertDialog.Builder(MainActivity.this)
840                 .setCancelable(false)
841                 .setTitle(R.string.confirm_passkey)
842                 .setMessage(String.valueOf(passkey))
843                 .setPositiveButton(android.R.string.ok,
844                         (d, w) -> isConfirmed.accept(true))
845                 .setNegativeButton(android.R.string.cancel,
846                         (d, w) -> isConfirmed.accept(false))
847                 .create()
848                 .show());
849     }
850 
851     @UiThread
updateStringStatusView(int id, String name, String value)852     private void updateStringStatusView(int id, String name, String value) {
853         ((TextView) findViewById(id)).setText(name + ": " + value);
854     }
855 
856     @UiThread
updateStatusView()857     private void updateStatusView() {
858         TextView remoteDeviceTextView = (TextView) findViewById(R.id.remote_device_text_view);
859         remoteDeviceTextView.setBackgroundColor(
860                 mBluetoothController.getRemoteDevice() != null ? LIGHT_GREEN : Color.LTGRAY);
861         String remoteDeviceString = mBluetoothController.getRemoteDeviceAsString();
862         remoteDeviceTextView.setText("Remote device: " + remoteDeviceString);
863 
864         updateBooleanStatusView(
865                 R.id.is_advertising_text_view,
866                 "BLE advertising",
867                 mFastPairSimulator != null && mFastPairSimulator.isAdvertising());
868 
869         updateStringStatusView(
870                 R.id.scan_mode_text_view,
871                 "Mode",
872                 FastPairSimulator.scanModeToString(mBluetoothController.getScanMode()));
873 
874         boolean isPaired = mBluetoothController.isPaired();
875         updateBooleanStatusView(R.id.is_paired_text_view, "Paired", isPaired);
876 
877         updateBooleanStatusView(
878                 R.id.is_connected_text_view, "Connected", mBluetoothController.isConnected());
879     }
880 
881     @UiThread
updateBooleanStatusView(int id, String name, boolean value)882     private void updateBooleanStatusView(int id, String name, boolean value) {
883         TextView view = (TextView) findViewById(id);
884         view.setBackgroundColor(value ? LIGHT_GREEN : Color.LTGRAY);
885         view.setText(name + ": " + (value ? "Yes" : "No"));
886     }
887 
getFromIntentOrPrefs(String key, String defaultValue)888     private String getFromIntentOrPrefs(String key, String defaultValue) {
889         Bundle extras = getIntent().getExtras();
890         extras = extras != null ? extras : new Bundle();
891         SharedPreferences prefs = getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE);
892         String value = extras.getString(key, prefs.getString(key, defaultValue));
893         if (value == null) {
894             prefs.edit().remove(key).apply();
895         } else {
896             prefs.edit().putString(key, value).apply();
897         }
898         return value;
899     }
900 
getFromIntentOrPrefs(String key, boolean defaultValue)901     private boolean getFromIntentOrPrefs(String key, boolean defaultValue) {
902         Bundle extras = getIntent().getExtras();
903         extras = extras != null ? extras : new Bundle();
904         SharedPreferences prefs = getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE);
905         boolean value = extras.getBoolean(key, prefs.getBoolean(key, defaultValue));
906         prefs.edit().putBoolean(key, value).apply();
907         return value;
908     }
909 
toTxPowerLevel(String txPowerLevelString)910     private static int toTxPowerLevel(String txPowerLevelString) {
911         switch (txPowerLevelString.toUpperCase()) {
912             case "3":
913             case "HIGH":
914                 return AdvertiseSettings.ADVERTISE_TX_POWER_HIGH;
915             case "2":
916             case "MEDIUM":
917                 return AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM;
918             case "1":
919             case "LOW":
920                 return AdvertiseSettings.ADVERTISE_TX_POWER_LOW;
921             case "0":
922             case "ULTRA_LOW":
923                 return AdvertiseSettings.ADVERTISE_TX_POWER_ULTRA_LOW;
924             default:
925                 throw new IllegalArgumentException(
926                         "Unexpected TxPower="
927                                 + txPowerLevelString
928                                 + ", please provide HIGH, MEDIUM, LOW, or ULTRA_LOW.");
929         }
930     }
931 
checkPermissions(String[] permissions)932     private boolean checkPermissions(String[] permissions) {
933         for (String permission : permissions) {
934             if (checkSelfPermission(permission) != PERMISSION_GRANTED) {
935                 return false;
936             }
937         }
938         return true;
939     }
940 
941     @Override
onDestroy()942     protected void onDestroy() {
943         mRemoteDevicesManager.destroy();
944 
945         if (mFastPairSimulator != null) {
946             mFastPairSimulator.destroy();
947             mBluetoothController.unregisterBluetoothStateReceiver();
948         }
949 
950         // Recover the IO capability.
951         mBluetoothController.setIoCapability(IO_CAPABILITY_IO);
952 
953         super.onDestroy();
954     }
955 
956     @Override
onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)957     public void onRequestPermissionsResult(
958             int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
959         super.onRequestPermissionsResult(requestCode, permissions, grantResults);
960         // Relaunch this activity.
961         recreate();
962     }
963 
startAdvertisingBatteryInformationBasedOnOption(int option)964     void startAdvertisingBatteryInformationBasedOnOption(int option) {
965         if (mFastPairSimulator == null) {
966             return;
967         }
968 
969         // Option 0 is "No battery info", it means simulator will not pack battery information when
970         // advertising. For the others with battery info, since we are simulating the Presto's
971         // behavior,
972         // there will always be three battery values.
973         switch (option) {
974             case 0:
975                 // Option "0: No battery info"
976                 mFastPairSimulator.clearBatteryValues();
977                 break;
978             case 1:
979                 // Option "1: Show L(⬆) + R(⬆) + C(⬆)"
980                 mFastPairSimulator.setSuppressBatteryNotification(false);
981                 mFastPairSimulator.setBatteryValues(new BatteryValue(true, 60),
982                         new BatteryValue(true, 61),
983                         new BatteryValue(true, 62));
984                 break;
985             case 2:
986                 // Option "2: Show L + R + C(unknown)"
987                 mFastPairSimulator.setSuppressBatteryNotification(false);
988                 mFastPairSimulator.setBatteryValues(new BatteryValue(false, 70),
989                         new BatteryValue(false, 71),
990                         new BatteryValue(false, -1));
991                 break;
992             case 3:
993                 // Option "3: Show L(low 10) + R(low 9) + C(low 25)"
994                 mFastPairSimulator.setSuppressBatteryNotification(false);
995                 mFastPairSimulator.setBatteryValues(new BatteryValue(false, 10),
996                         new BatteryValue(false, 9),
997                         new BatteryValue(false, 25));
998                 break;
999             case 4:
1000                 // Option "4: Suppress battery w/o level changes"
1001                 // Just change the suppress bit and keep the battery values the same as before.
1002                 mFastPairSimulator.setSuppressBatteryNotification(true);
1003                 break;
1004             case 5:
1005                 // Option "5: Suppress L(low 10) + R(11) + C"
1006                 mFastPairSimulator.setSuppressBatteryNotification(true);
1007                 mFastPairSimulator.setBatteryValues(new BatteryValue(false, 10),
1008                         new BatteryValue(false, 11),
1009                         new BatteryValue(false, 82));
1010                 break;
1011             case 6:
1012                 // Option "6: Suppress L(low ⬆) + R(low ⬆) + C(low 10)"
1013                 mFastPairSimulator.setSuppressBatteryNotification(true);
1014                 mFastPairSimulator.setBatteryValues(new BatteryValue(true, 10),
1015                         new BatteryValue(true, 9),
1016                         new BatteryValue(false, 10));
1017                 break;
1018             case 7:
1019                 // Option "7: Suppress L(low ⬆) + R(low ⬆) + C(low ⬆)"
1020                 mFastPairSimulator.setSuppressBatteryNotification(true);
1021                 mFastPairSimulator.setBatteryValues(new BatteryValue(true, 10),
1022                         new BatteryValue(true, 9),
1023                         new BatteryValue(true, 25));
1024                 break;
1025             case 8:
1026                 // Option "8: Show subsequent pairing notification"
1027                 mFastPairSimulator.setSuppressSubsequentPairingNotification(false);
1028                 break;
1029             case 9:
1030                 // Option "9: Suppress subsequent pairing notification"
1031                 mFastPairSimulator.setSuppressSubsequentPairingNotification(true);
1032                 break;
1033             default:
1034                 // Unknown option, do nothing.
1035                 return;
1036         }
1037 
1038         mFastPairSimulator.startAdvertising();
1039     }
1040 }
1041