• 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(
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