diff options
author | Android Auto Companion <no-reply@google.com> | 2021-12-08 09:12:15 -0800 |
---|---|---|
committer | Dan Harms <danharms@google.com> | 2021-12-08 10:29:21 -0800 |
commit | a34dde2db1dbc86486affc058137d780aad5c89b (patch) | |
tree | 4153645045318b9b277a0d0e0892e00b089a18ab | |
parent | 6b2e6918ffcc38ee3bf4b801fd2ff4e8c85125a8 (diff) | |
download | CompanionDeviceSupport-a34dde2db1dbc86486affc058137d780aad5c89b.tar.gz |
Project import generated by Copybara
Included changes:
Release-Id: aae-companiondevice-android_20211208.00_RC00
Change-Id: I9f2aeb7c9e7be964084475b5a30313c741e7e7a1
28 files changed, 615 insertions, 191 deletions
diff --git a/companiondevice/AndroidManifest.xml b/companiondevice/AndroidManifest.xml index a377a30..d579588 100644 --- a/companiondevice/AndroidManifest.xml +++ b/companiondevice/AndroidManifest.xml @@ -84,6 +84,7 @@ android:resource="@bool/enable_feature_coordinator" /> <meta-data android:name="com.google.android.connecteddevice.supported_oob_channels" android:resource="@array/supported_oob_channels"/> + </service> <service android:name="com.google.android.connecteddevice.service.ConnectedDeviceFgUserService" diff --git a/companiondevice/build.gradle b/companiondevice/build.gradle index 2f64c1e..5a50a1b 100644 --- a/companiondevice/build.gradle +++ b/companiondevice/build.gradle @@ -13,7 +13,7 @@ android { applicationId "com.google.android.companiondevicesupport" minSdkVersion 29 targetSdkVersion 30 - versionCode 1117 + versionCode 1181 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/companiondevice/res/layout-w1240dp-land/suw_companion_qr_code_landing_fragment.xml b/companiondevice/res/layout-w1240dp-land/suw_companion_qr_code_landing_fragment.xml index 6fdf3bd..c50dbe5 100644 --- a/companiondevice/res/layout-w1240dp-land/suw_companion_qr_code_landing_fragment.xml +++ b/companiondevice/res/layout-w1240dp-land/suw_companion_qr_code_landing_fragment.xml @@ -46,7 +46,7 @@ android:layout_marginVertical="?attr/contentMarginVertical" style="@style/CompanionTitleTextStyle" /> <TextView - android:id="@+id/add_associated_device_subtitle" + android:id="@+id/connect_to_car_instruction" android:text="@string/add_associated_device_subtitle" android:layout_width="wrap_content" android:layout_height="wrap_content" diff --git a/companiondevice/res/layout-w1240dp-land/suw_companion_setup_profile_fragment.xml b/companiondevice/res/layout-w1240dp-land/suw_companion_setup_profile_fragment.xml new file mode 100644 index 0000000..ebb110e --- /dev/null +++ b/companiondevice/res/layout-w1240dp-land/suw_companion_setup_profile_fragment.xml @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="horizontal" + android:layout_marginHorizontal="?attr/pageMarginHorizontal"> + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:orientation="vertical" + android:layout_weight="@integer/suw_title_column_weight" + android:paddingVertical="?attr/pageMarginVertical" + android:layout_marginEnd="@dimen/suw_column_inner_padding_horizontal"> + <TextView + android:id="@+id/suw_setup_profile_title" + android:text="@string/suw_setup_profile_title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="left" + android:layout_marginVertical="?attr/contentMarginVertical" + style="@style/CompanionTitleTextStyle" /> + <TextView + android:id="@+id/suw_setup_profile_content" + android:text="@string/suw_setup_profile_content" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="left" + style="@style/CompanionSubtitleTextStyle" /> + </LinearLayout> + <View style="@style/SuwVerticalDividerStyle"/> + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:orientation="vertical" + android:gravity="center" + android:layout_weight="@integer/suw_content_column_weight" + android:layout_marginVertical="?attr/pageMarginVertical" + android:layout_marginStart="@dimen/suw_column_inner_padding_horizontal" > + <ImageView + android:id="@+id/qr_code" + android:layout_width="@dimen/qr_code_size" + android:layout_height="@dimen/qr_code_size" + tools:ignore="ContentDescription" /> + <TextView + android:id="@+id/connect_to_car_instruction" + android:text="@string/suw_qr_instruction_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginVertical="?attr/contentMarginVertical" + android:gravity="center" + style="@style/CompanionSubtitleTextStyle" /> + </LinearLayout> +</LinearLayout> diff --git a/companiondevice/res/layout/companion_qr_code_landing_fragment.xml b/companiondevice/res/layout/companion_qr_code_landing_fragment.xml index ba122f7..55b2356 100644 --- a/companiondevice/res/layout/companion_qr_code_landing_fragment.xml +++ b/companiondevice/res/layout/companion_qr_code_landing_fragment.xml @@ -44,7 +44,7 @@ android:layout_marginVertical="?attr/contentMarginVertical" style="@style/CompanionTitleTextStyle" /> <TextView - android:id="@+id/add_associated_device_subtitle" + android:id="@+id/connect_to_car_instruction" android:text="@string/add_associated_device_subtitle" android:layout_width="wrap_content" android:layout_height="wrap_content" diff --git a/companiondevice/res/layout/suw_companion_setup_profile_fragment.xml b/companiondevice/res/layout/suw_companion_setup_profile_fragment.xml new file mode 100644 index 0000000..a1a12d2 --- /dev/null +++ b/companiondevice/res/layout/suw_companion_setup_profile_fragment.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fillViewport="true"> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:gravity="center_horizontal" + android:paddingVertical="?attr/pageMarginVertical" + android:layout_marginHorizontal="?attr/pageMarginHorizontal"> + <TextView + android:id="@+id/suw_setup_profile_title" + android:text="@string/suw_setup_profile_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center" + android:layout_marginVertical="?attr/contentMarginVertical" + style="@style/CompanionTitleTextStyle" /> + <TextView + android:id="@+id/suw_setup_profile_content" + android:text="@string/suw_setup_profile_content" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center" + style="@style/CompanionSubtitleTextStyle" /> + <View + android:layout_marginBottom="?attr/companionDividerMargin" + style="@style/HorizontalDividerStyle" /> + <ImageView + android:id="@+id/qr_code" + android:layout_width="@dimen/qr_code_size" + android:layout_height="@dimen/qr_code_size" + tools:ignore="ContentDescription" /> + <TextView + android:id="@+id/connect_to_car_instruction" + android:text="@string/suw_qr_instruction_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginVertical="?attr/contentMarginVertical" + android:gravity="center" + style="@style/CompanionSubtitleTextStyle" /> + </LinearLayout> +</ScrollView> diff --git a/companiondevice/res/values/config.xml b/companiondevice/res/values/config.xml index 74d3b93..e2f7b56 100644 --- a/companiondevice/res/values/config.xml +++ b/companiondevice/res/values/config.xml @@ -54,4 +54,5 @@ <bool name="enable_feature_coordinator">true</bool> <bool name="enable_qr_code">false</bool> + <bool name="enable_passenger">false</bool> </resources> diff --git a/companiondevice/res/values/strings.xml b/companiondevice/res/values/strings.xml index d583a94..97259ad 100644 --- a/companiondevice/res/values/strings.xml +++ b/companiondevice/res/values/strings.xml @@ -184,4 +184,19 @@ <!-- Prefix of the BLE device name. [CHAR LIMIT=20]--> <string name="ble_device_name_prefix">Vehicle\u00A0</string> + + <!-- QR code uri scheme. --> + <string name="uri_scheme" translatable="false">companion</string> + <!-- QR code uri authority. --> + <string name="uri_authority" translatable="false">demo.companiondevice.com</string> + <!-- QR code uri path. --> + <string name="uri_path" translatable="false">associate</string> + + <!-- Car SUW setup profile page title. [CHAR LIMIT=40]--> + <string name="suw_setup_profile_title">Setup profile with phone</string> + <!-- Car SUW setup profile page content. [CHAR LIMIT=200]--> + <string name="suw_setup_profile_content">Scan QR code to get started. If you don\'t have a MyCompanion app, you can download it on Google Play and the App Store.</string> + <!-- Car SUW setup profile page instructions for user to scan the QR code. [CHAR LIMIT=150]--> + <string name="suw_qr_instruction_text">Scan QR code to connect to<br /> <b><xliff:g id="car_name" example="MyVehicle">%1$s</xliff:g></b> <xliff:g id="advertised_car_name" example="(Vehicle 0000)">%2$s</xliff:g></string> + </resources> diff --git a/companiondevice/src/com/google/android/companiondevicesupport/AssociationActivity.java b/companiondevice/src/com/google/android/companiondevicesupport/AssociationActivity.java index e799eb3..5d2e314 100644 --- a/companiondevice/src/com/google/android/companiondevicesupport/AssociationActivity.java +++ b/companiondevice/src/com/google/android/companiondevicesupport/AssociationActivity.java @@ -55,6 +55,7 @@ public class AssociationActivity extends FragmentActivity { private static final String ASSOCIATION_ERROR_FRAGMENT_TAG = "AssociationErrorFragment"; private static final String TURN_ON_BLUETOOTH_DIALOG_TAG = "TurnOnBluetoothDialog"; private static final String EXTRA_AUTH_IS_SETUP_WIZARD = "is_setup_wizard"; + private static final String EXTRA_AUTH_IS_SETUP_PROFILE = "is_setup_profile_association"; private static final String EXTRA_USE_IMMERSIVE_MODE = "useImmersiveMode"; private static final String EXTRA_HIDE_SKIP_BUTTON = "hide_skip_button"; @@ -62,6 +63,7 @@ public class AssociationActivity extends FragmentActivity { private CarSetupWizardCompatLayout carSetupWizardLayout; private AssociatedDeviceViewModel model; private boolean isStartedForSuw = false; + private boolean isStartedForSetupProfile = false; private boolean isImmersive = false; private boolean hideSkipButton = false; @@ -101,6 +103,8 @@ public class AssociationActivity extends FragmentActivity { return; } isStartedForSuw = intent.getBooleanExtra(EXTRA_AUTH_IS_SETUP_WIZARD, /* defaultValue= */ false); + isStartedForSetupProfile = + intent.getBooleanExtra(EXTRA_AUTH_IS_SETUP_PROFILE, /* defaultValue= */ false); isImmersive = intent.getBooleanExtra(EXTRA_USE_IMMERSIVE_MODE, /* defaultValue= */ false); hideSkipButton = intent.getBooleanExtra(EXTRA_HIDE_SKIP_BUTTON, /* defaultValue= */ false); } @@ -295,8 +299,12 @@ public class AssociationActivity extends FragmentActivity { if (fragment != null && fragment.isVisible()) { return; } - fragment = CompanionQrCodeLandingFragment.newInstance(isStartedForSuw); + fragment = + CompanionQrCodeLandingFragment.newInstance(isStartedForSuw, isStartedForSetupProfile); launchFragment(fragment, COMPANION_LANDING_FRAGMENT_TAG); + if (isStartedForSetupProfile) { + showSkipButton(); + } } private void showTurnOnBluetoothFragment() { diff --git a/companiondevice/src/com/google/android/companiondevicesupport/CompanionQrCodeLandingFragment.java b/companiondevice/src/com/google/android/companiondevicesupport/CompanionQrCodeLandingFragment.java index cde7c51..200c8e9 100644 --- a/companiondevice/src/com/google/android/companiondevicesupport/CompanionQrCodeLandingFragment.java +++ b/companiondevice/src/com/google/android/companiondevicesupport/CompanionQrCodeLandingFragment.java @@ -20,6 +20,7 @@ import static com.google.android.connecteddevice.util.SafeLog.loge; import android.bluetooth.BluetoothAdapter; import android.graphics.Bitmap; +import android.net.Uri; import android.os.Bundle; import androidx.fragment.app.Fragment; import android.text.Html; @@ -30,27 +31,39 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.LayoutRes; +import androidx.annotation.Nullable; import androidx.lifecycle.ViewModelProvider; +import com.google.android.connecteddevice.model.StartAssociationResponse; import com.google.android.connecteddevice.model.TransportProtocols; import com.google.android.connecteddevice.ui.AssociatedDeviceViewModel; import com.google.android.connecteddevice.ui.AssociatedDeviceViewModelFactory; +import com.google.android.connecteddevice.ui.CompanionUriBuilder; +import com.google.android.connecteddevice.ui.QrCodeGenerator; import java.util.Arrays; import java.util.List; /** Fragment that provides association instructions. */ public class CompanionQrCodeLandingFragment extends Fragment { private static final String IS_STARTED_FOR_SUW_KEY = "isStartedForSuw"; + private static final String IS_STARTED_FOR_SETUP_PROFILE_KEY = "isSetupProfile"; private static final String TAG = "CompanionQrCodeLandingFragment"; + private boolean isStartedForSetupProfile = false; + @Nullable private View instructionsView = null; + @Nullable private View addButtonView = null; /** * Creates a new instance of {@link CompanionQrCodeLandingFragment}. * * @param isStartedForSUW If the fragment is created for car setup wizard. + * @param isStartedForSetupProfile If the fragment is created for car setup wizard and car profile + * setup. This value will take effect only if the {@code isStartedForSUW} is {@code true}. * @return {@link CompanionQrCodeLandingFragment} instance. */ - static CompanionQrCodeLandingFragment newInstance(boolean isStartedForSUW) { + static CompanionQrCodeLandingFragment newInstance( + boolean isStartedForSUW, boolean isStartedForSetupProfile) { Bundle bundle = new Bundle(); bundle.putBoolean(IS_STARTED_FOR_SUW_KEY, isStartedForSUW); + bundle.putBoolean(IS_STARTED_FOR_SETUP_PROFILE_KEY, isStartedForSetupProfile); CompanionQrCodeLandingFragment fragment = new CompanionQrCodeLandingFragment(); fragment.setArguments(bundle); return fragment; @@ -59,16 +72,25 @@ public class CompanionQrCodeLandingFragment extends Fragment { @Override public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - @LayoutRes - int layout = - getArguments().getBoolean(IS_STARTED_FOR_SUW_KEY) - ? R.layout.suw_companion_qr_code_landing_fragment - : R.layout.companion_qr_code_landing_fragment; + boolean isStartedForSuw = getArguments().getBoolean(IS_STARTED_FOR_SUW_KEY); + isStartedForSetupProfile = getArguments().getBoolean(IS_STARTED_FOR_SETUP_PROFILE_KEY); + @LayoutRes int layout; + if (isStartedForSuw) { + if (isStartedForSetupProfile) { + layout = R.layout.suw_companion_setup_profile_fragment; + } else { + layout = R.layout.suw_companion_qr_code_landing_fragment; + } + } else { + layout = R.layout.companion_qr_code_landing_fragment; + } return inflater.inflate(layout, container, false); } @Override public void onViewCreated(View view, Bundle bundle) { + instructionsView = view.findViewById(R.id.association_qr_code_instructions); + addButtonView = view.findViewById(R.id.add_button_and_divider); List<String> transportProtocols = Arrays.asList(getResources().getStringArray(R.array.transport_protocols)); AssociatedDeviceViewModel model = @@ -79,13 +101,19 @@ public class CompanionQrCodeLandingFragment extends Fragment { transportProtocols.contains(TransportProtocols.PROTOCOL_SPP), getResources().getString(R.string.ble_device_name_prefix))) .get(AssociatedDeviceViewModel.class); - TextView connectToCarTextView = view.findViewById(R.id.add_associated_device_subtitle); + + TextView connectToCarTextView = view.findViewById(R.id.connect_to_car_instruction); model .getAdvertisedCarName() .observe(/* owner= */ this, carName -> setCarName(connectToCarTextView, carName)); + model.getAssociationResponse().observe(/* owner= */ this, this::processAssociationResponse); + if (isStartedForSetupProfile) { + // Directly start advertisement in SUW setup profile association flow. + model.startAssociation(); + return; + } view.findViewById(R.id.add_associated_device_button) .setOnClickListener(l -> model.startAssociation()); - model.getQrCode().observe(/* owner= */ this, code -> setImageView(code, view)); } private void setCarName(TextView textView, String carName) { @@ -102,25 +130,59 @@ public class CompanionQrCodeLandingFragment extends Fragment { carName = "(" + carName + ")"; } String bluetoothName = BluetoothAdapter.getDefaultAdapter().getName(); - String connectToCarText = getString(R.string.qr_instruction_text, bluetoothName, carName); + int textId = + isStartedForSetupProfile ? R.string.suw_qr_instruction_text : R.string.qr_instruction_text; + String connectToCarText = getString(textId, bluetoothName, carName); Spanned styledConnectToCarText = Html.fromHtml(connectToCarText, Html.FROM_HTML_MODE_LEGACY); textView.setText(styledConnectToCarText); } - private void setImageView(Bitmap bitmap, View view) { - ImageView qrImage = view.findViewById(R.id.qr_code); - View instruction = view.findViewById(R.id.association_qr_code_instructions); - View addButton = view.findViewById(R.id.add_button_and_divider); + /** + * Set the QR code image view when the association response is available. Hide the instruction and + * add button if they are not null. + * + * @param response the association started successfully response. + */ + private void processAssociationResponse(StartAssociationResponse response) { + if (response == null) { + loge( + TAG, + "Association response is null during QR code generation when association " + + "started successfully, ignore."); + return; + } + + ImageView qrImage = getView().findViewById(R.id.qr_code); if (qrImage == null) { loge(TAG, "No valid ImageView to show QR code."); return; } + + Uri uri = + new CompanionUriBuilder() + .scheme(getResources().getString(R.string.uri_scheme)) + .authority(getResources().getString(R.string.uri_authority)) + .appendPath(getResources().getString(R.string.uri_path)) + .oobData(response.getOobData()) + .deviceId(response.getDeviceIdentifier()) + .appendQueryParameter( + IS_STARTED_FOR_SETUP_PROFILE_KEY, String.valueOf(isStartedForSetupProfile)) + .build(); + Bitmap bitmap = + QrCodeGenerator.createQrCode( + uri.toString(), getResources().getDimensionPixelSize(R.dimen.qr_code_size)); if (bitmap == null) { + loge(TAG, "QR code could not be generated, ignore."); return; } - instruction.setVisibility(View.GONE); - addButton.setVisibility(View.GONE); + qrImage.setImageBitmap(bitmap); qrImage.setVisibility(View.VISIBLE); + if (instructionsView != null) { + instructionsView.setVisibility(View.GONE); + } + if (addButtonView != null) { + addButtonView.setVisibility(View.GONE); + } } } diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/ConnectedDeviceManager.java b/libs/connecteddevice/src/com/google/android/connecteddevice/ConnectedDeviceManager.java index 18638f8..6cf5668 100644 --- a/libs/connecteddevice/src/com/google/android/connecteddevice/ConnectedDeviceManager.java +++ b/libs/connecteddevice/src/com/google/android/connecteddevice/ConnectedDeviceManager.java @@ -138,7 +138,7 @@ public class ConnectedDeviceManager { this.carBluetoothManager = carBluetoothManager; this.connectionExecutor = connectionExecutor; this.carBluetoothManager.registerCallback(generateCarManagerCallback(), callbackExecutor); - this.storage.setAssociatedDeviceCallback(associatedDeviceCallback); + this.storage.registerAssociatedDeviceCallback(associatedDeviceCallback); this.storageExecutor = storageExecutor; } diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/core/FeatureCoordinator.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/core/FeatureCoordinator.kt index f4ca509..0860ace 100644 --- a/libs/connecteddevice/src/com/google/android/connecteddevice/core/FeatureCoordinator.kt +++ b/libs/connecteddevice/src/com/google/android/connecteddevice/core/FeatureCoordinator.kt @@ -77,7 +77,7 @@ constructor( init { controller.registerCallback(createDeviceControllerCallback(), callbackExecutor) - storage.setAssociatedDeviceCallback(createStorageAssociatedDeviceCallback()) + storage.registerAssociatedDeviceCallback(createStorageAssociatedDeviceCallback()) } /** Initiate connections with all enabled [AssociatedDevice]s. */ @@ -283,11 +283,17 @@ constructor( } override fun claimAssociatedDevice(deviceId: String) { + logd(TAG, "Claiming device $deviceId. Updating storage and disconnecting.") + controller.disconnectDevice(UUID.fromString(deviceId)) storage.claimAssociatedDevice(deviceId) + controller.initiateConnectionToDevice(UUID.fromString(deviceId)) } override fun removeAssociatedDeviceClaim(deviceId: String) { + logd(TAG, "Removing claim on device $deviceId. Updating storage and disconnecting.") + controller.disconnectDevice(UUID.fromString(deviceId)) storage.removeAssociatedDeviceClaim(deviceId) + controller.initiateConnectionToDevice(UUID.fromString(deviceId)) } private fun startAssociationInternal( diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/core/MultiProtocolDeviceController.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/core/MultiProtocolDeviceController.kt index 66369a3..73c8d9b 100644 --- a/libs/connecteddevice/src/com/google/android/connecteddevice/core/MultiProtocolDeviceController.kt +++ b/libs/connecteddevice/src/com/google/android/connecteddevice/core/MultiProtocolDeviceController.kt @@ -64,6 +64,8 @@ import java.util.concurrent.atomic.AtomicReference * * @property protocols List of supported protocols. * @property storage Storage necessary to generate reconnect challenge. + * @property enablePassenger Whether passenger devices automatically connect. When `true`, newly + * associated devices will remain unclaimed by default. * @property callbackExecutor Executor on which callbacks are executed. */ class MultiProtocolDeviceController @@ -72,8 +74,9 @@ constructor( private val protocols: Set<ConnectionProtocol>, private val storage: ConnectedDeviceStorage, private val oobRunner: OobRunner, - private val callbackExecutor: Executor = Executors.newSingleThreadExecutor(), - private val associationServiceUuid: UUID + private val associationServiceUuid: UUID, + private val enablePassenger: Boolean, + private val callbackExecutor: Executor = Executors.newSingleThreadExecutor() ) : DeviceController { private val connectedRemoteDevices = ConcurrentHashMap<UUID, ConnectedRemoteDevice>() @@ -84,6 +87,23 @@ constructor( private val associatedDevices = CopyOnWriteArrayList<AssociatedDevice>() private val driverDevices = CopyOnWriteArrayList<AssociatedDevice>() + private val storageCallback = + object : ConnectedDeviceStorage.AssociatedDeviceCallback { + override fun onAssociatedDeviceAdded(device: AssociatedDevice) { + // Device population is handled locally when a new device is added. + } + + override fun onAssociatedDeviceRemoved(device: AssociatedDevice) { + logd(TAG, "An associated device has been removed. Repopulating devices from storage.") + populateDevices() + } + + override fun onAssociatedDeviceUpdated(device: AssociatedDevice) { + logd(TAG, "An associated device has been updated. Repopulating devices from storage.") + populateDevices() + } + } + override val connectedDevices: List<ConnectedDevice> get() { val devices = mutableListOf<ConnectedDevice>() @@ -114,43 +134,27 @@ constructor( } init { - callbackExecutor.execute { - while (true) { - try { - logd(TAG, "Populating associated devices from storage.") - associatedDevices.clear() - driverDevices.clear() - associatedDevices.addAll(storage.allAssociatedDevices) - driverDevices.addAll(storage.driverAssociatedDevices) - break - } catch (sqliteException: SQLiteCantOpenDatabaseException) { - loge( - TAG, - "Caught transient exception while retrieving devices. Trying again.", - sqliteException - ) - try { - Thread.sleep(ASSOCIATED_DEVICE_RETRY_MS) - } catch (interrupted: InterruptedException) { - loge(TAG, "Sleep interrupted.", interrupted) - break - } - } - } - logd(TAG, "Devices populated successfully.") - } - - // TODO(b/192656006) Add registration for updates to associated devices to keep in sync + storage.registerAssociatedDeviceCallback(storageCallback) } override fun start() { - logd(TAG, "Starting controller.") - val associatedDevices = storage.driverAssociatedDevices - for (device in associatedDevices) { + logd(TAG, "Starting controller and initiating connections with driver devices.") + populateDevices() + val driverDevices = storage.driverAssociatedDevices + for (device in driverDevices) { if (device.isConnectionEnabled) { initiateConnectionToDevice(UUID.fromString(device.deviceId)) } } + if (!enablePassenger) { + logd(TAG, "The passenger experience is disabled. Skipping discovery of passenger devices.") + return + } + logd(TAG, "Initiating connections with passenger devices.") + val passengerDevices = storage.passengerAssociatedDevices + for (device in passengerDevices) { + initiateConnectionToDevice(UUID.fromString(device.deviceId)) + } } override fun reset() { @@ -310,6 +314,34 @@ constructor( callbacks.remove(callback) } + private fun populateDevices() { + callbackExecutor.execute { + while (true) { + try { + logd(TAG, "Populating associated devices from storage.") + associatedDevices.clear() + driverDevices.clear() + associatedDevices.addAll(storage.allAssociatedDevices) + driverDevices.addAll(storage.driverAssociatedDevices) + logd(TAG, "Devices populated successfully.") + break + } catch (sqliteException: SQLiteCantOpenDatabaseException) { + loge( + TAG, + "Caught transient exception while retrieving devices. Trying again.", + sqliteException + ) + try { + Thread.sleep(ASSOCIATED_DEVICE_RETRY_MS) + } catch (interrupted: InterruptedException) { + loge(TAG, "Sleep interrupted.", interrupted) + break + } + } + } + } + } + /** * Create challenge for connection advertisement. * @@ -710,8 +742,14 @@ constructor( /* deviceName= */ null, /* isConnectionEnabled= */ true ) - storage.addAssociatedDeviceForDriver(associatedDevice) - driverDevices.add(associatedDevice) + if (enablePassenger) { + logd(TAG, "Saving newly associated device $deviceId as unclaimed.") + storage.addAssociatedDeviceForUser(AssociatedDevice.UNCLAIMED_USER_ID, associatedDevice) + } else { + logd(TAG, "Saving newly associated device $deviceId as a driver's device.") + storage.addAssociatedDeviceForDriver(associatedDevice) + driverDevices.add(associatedDevice) + } associatedDevices.add(associatedDevice) } diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/notificationmsg/NotificationMsgService.java b/libs/connecteddevice/src/com/google/android/connecteddevice/notificationmsg/NotificationMsgService.java index 497506e..4982181 100644 --- a/libs/connecteddevice/src/com/google/android/connecteddevice/notificationmsg/NotificationMsgService.java +++ b/libs/connecteddevice/src/com/google/android/connecteddevice/notificationmsg/NotificationMsgService.java @@ -31,11 +31,13 @@ import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.Typeface; import android.os.Binder; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.os.IBinder; +import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; import androidx.core.app.RemoteInput; -import androidx.annotation.Nullable; import com.google.android.connecteddevice.model.ConnectedDevice; import com.google.android.connecteddevice.notificationmsg.common.ConversationKey; import com.google.android.connecteddevice.notificationmsg.proto.NotificationMsg; @@ -171,6 +173,13 @@ public class NotificationMsgService extends MetaDataService { * started. */ private void sendServiceRunningNotification() { + // TODO(b/201677355) A new rule in S restricts + // the ability to start foreground services to only active UI + // however, this needs to be fixed properly as Assistant needs the foreground service + // for replies to work + if (VERSION.SDK_INT > VERSION_CODES.R) { + return; + } NotificationManager notificationManager = getSystemService(NotificationManager.class); // Create notification channel for app running notification NotificationChannel appRunningNotificationChannel = diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/notificationmsg/common/BaseNotificationDelegate.java b/libs/connecteddevice/src/com/google/android/connecteddevice/notificationmsg/common/BaseNotificationDelegate.java index 260bbca..803c74a 100644 --- a/libs/connecteddevice/src/com/google/android/connecteddevice/notificationmsg/common/BaseNotificationDelegate.java +++ b/libs/connecteddevice/src/com/google/android/connecteddevice/notificationmsg/common/BaseNotificationDelegate.java @@ -29,11 +29,11 @@ import android.graphics.Typeface; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; +import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat.Action; -import androidx.core.graphics.drawable.IconCompat; -import androidx.annotation.Nullable; import androidx.core.app.Person; +import androidx.core.graphics.drawable.IconCompat; import com.google.common.collect.Iterables; import java.util.ArrayList; import java.util.HashMap; diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/notificationmsg/common/TelecomUtils.java b/libs/connecteddevice/src/com/google/android/connecteddevice/notificationmsg/common/TelecomUtils.java index 0705f2f..63a29c7 100644 --- a/libs/connecteddevice/src/com/google/android/connecteddevice/notificationmsg/common/TelecomUtils.java +++ b/libs/connecteddevice/src/com/google/android/connecteddevice/notificationmsg/common/TelecomUtils.java @@ -24,9 +24,9 @@ import android.graphics.Canvas; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; +import androidx.annotation.Nullable; import androidx.core.graphics.drawable.RoundedBitmapDrawable; import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; -import androidx.annotation.Nullable; /** Telecom utility methods. */ public class TelecomUtils { diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/notificationmsg/common/Utils.java b/libs/connecteddevice/src/com/google/android/connecteddevice/notificationmsg/common/Utils.java index 870de69..687fed8 100644 --- a/libs/connecteddevice/src/com/google/android/connecteddevice/notificationmsg/common/Utils.java +++ b/libs/connecteddevice/src/com/google/android/connecteddevice/notificationmsg/common/Utils.java @@ -27,12 +27,12 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Typeface; import android.graphics.drawable.Drawable; -import androidx.core.graphics.drawable.RoundedBitmapDrawable; -import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; import android.text.BidiFormatter; import android.text.TextDirectionHeuristics; import android.text.TextUtils; import androidx.annotation.Nullable; +import androidx.core.graphics.drawable.RoundedBitmapDrawable; +import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; import com.google.android.connecteddevice.notificationmsg.proto.NotificationMsg; import com.google.android.connecteddevice.notificationmsg.proto.NotificationMsg.AvatarIconSync; import com.google.android.connecteddevice.notificationmsg.proto.NotificationMsg.ConversationNotification; diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceService.java b/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceService.java index 95c062c..3daa450 100644 --- a/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceService.java +++ b/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceService.java @@ -137,6 +137,9 @@ public final class ConnectedDeviceService extends TrunkService { private static final String META_SUPPORTED_OOB_CHANNELS = "com.google.android.connecteddevice.supported_oob_channels"; + private static final String META_ENABLE_PASSENGER = + "com.google.android.connecteddevice.enable_passenger"; + private static final boolean PROXY_ENABLED_BY_DEFAULT = false; private static final String DEFAULT_RECONNECT_UUID = "000000e0-0000-1000-8000-00805f9b34fb"; @@ -160,6 +163,8 @@ public final class ConnectedDeviceService extends TrunkService { private static final boolean ENABLE_FEATURE_COORDINATOR_BY_DEFAULT = false; + private static final boolean ENABLE_PASSENGER_BY_DEFAULT = false; + private static final String[] DEFAULT_TRANSPORT_PROTOCOLS = { TransportProtocols.PROTOCOL_BLE_PERIPHERAL }; @@ -275,8 +280,10 @@ public final class ConnectedDeviceService extends TrunkService { META_SUPPORTED_OOB_CHANNELS, /* defaultValue= */ new String[0])))); UUID associationUuid = UUID.fromString(requireMetaString(META_ASSOCIATION_SERVICE_UUID)); + boolean enablePassenger = getMetaBoolean(META_ENABLE_PASSENGER, ENABLE_PASSENGER_BY_DEFAULT); DeviceController deviceController = - new MultiProtocolDeviceController(protocols, storage, oobRunner, associationUuid); + new MultiProtocolDeviceController( + protocols, storage, oobRunner, associationUuid, enablePassenger); featureCoordinator = new FeatureCoordinator(deviceController, storage, loggingManager); logd(TAG, "Wrapping FeatureCoordinator in legacy binders for backwards compatibility."); connectedDeviceManagerBinder = createConnectedDeviceManagerWrapper(); diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/storage/ConnectedDeviceStorage.java b/libs/connecteddevice/src/com/google/android/connecteddevice/storage/ConnectedDeviceStorage.java index 3fadd88..389d201 100644 --- a/libs/connecteddevice/src/com/google/android/connecteddevice/storage/ConnectedDeviceStorage.java +++ b/libs/connecteddevice/src/com/google/android/connecteddevice/storage/ConnectedDeviceStorage.java @@ -28,12 +28,15 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.room.Room; import com.google.android.connecteddevice.model.AssociatedDevice; +import com.google.android.connecteddevice.util.ThreadSafeCallbacks; import java.security.InvalidKeyException; import java.security.InvalidParameterException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; import java.util.UUID; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; @@ -55,11 +58,14 @@ public class ConnectedDeviceStorage { private final CryptoHelper cryptoHelper; + private final Executor callbackExecutor; + private SharedPreferences sharedPreferences; private UUID uniqueId; - private AssociatedDeviceCallback associatedDeviceCallback; + private final ThreadSafeCallbacks<AssociatedDeviceCallback> callbacks = + new ThreadSafeCallbacks<>(); public ConnectedDeviceStorage(@NonNull Context context) { this( @@ -68,31 +74,30 @@ public class ConnectedDeviceStorage { Room.databaseBuilder(context, ConnectedDeviceDatabase.class, DATABASE_NAME) .fallbackToDestructiveMigration() .build() - .associatedDeviceDao()); + .associatedDeviceDao(), + Executors.newSingleThreadExecutor()); } @VisibleForTesting public ConnectedDeviceStorage( @NonNull Context context, @NonNull CryptoHelper cryptoHelper, - @NonNull AssociatedDeviceDao associatedDeviceDatabase) { + @NonNull AssociatedDeviceDao associatedDeviceDatabase, + @NonNull Executor callbackExecutor) { this.context = context; this.cryptoHelper = cryptoHelper; this.associatedDeviceDatabase = associatedDeviceDatabase; + this.callbackExecutor = callbackExecutor; } - /** - * Set a callback for associated device updates. - * - * @param callback {@link AssociatedDeviceCallback} to set. - */ - public void setAssociatedDeviceCallback(@NonNull AssociatedDeviceCallback callback) { - associatedDeviceCallback = callback; + /** Register an {@link AssociatedDeviceCallback} for associated device updates. */ + public void registerAssociatedDeviceCallback(@NonNull AssociatedDeviceCallback callback) { + callbacks.add(callback, callbackExecutor); } - /** Clear the callback for association device callback updates. */ - public void clearAssociationDeviceCallback() { - associatedDeviceCallback = null; + /** Unregister an {@link AssociatedDeviceCallback} from associated device updates. */ + public void unregisterAssociatedDeviceCallback(@NonNull AssociatedDeviceCallback callback) { + callbacks.remove(callback); } /** @@ -377,9 +382,7 @@ public class ConnectedDeviceStorage { AssociatedDeviceEntity entity = new AssociatedDeviceEntity(userId, device, /* isConnectionEnabled= */ true); associatedDeviceDatabase.addOrReplaceAssociatedDevice(entity); - if (associatedDeviceCallback != null) { - associatedDeviceCallback.onAssociatedDeviceAdded(device); - } + callbacks.invoke(callback -> callback.onAssociatedDeviceAdded(device)); } /** @@ -427,9 +430,7 @@ public class ConnectedDeviceStorage { private void updateName(AssociatedDeviceEntity entity, String name) { entity.name = name; associatedDeviceDatabase.addOrReplaceAssociatedDevice(entity); - if (associatedDeviceCallback != null) { - associatedDeviceCallback.onAssociatedDeviceUpdated(entity.toAssociatedDevice()); - } + callbacks.invoke(callback -> callback.onAssociatedDeviceUpdated(entity.toAssociatedDevice())); } /** @@ -443,9 +444,7 @@ public class ConnectedDeviceStorage { return; } associatedDeviceDatabase.removeAssociatedDevice(entity); - if (associatedDeviceCallback != null) { - associatedDeviceCallback.onAssociatedDeviceRemoved(entity.toAssociatedDevice()); - } + callbacks.invoke(callback -> callback.onAssociatedDeviceRemoved(entity.toAssociatedDevice())); } /** @@ -470,9 +469,7 @@ public class ConnectedDeviceStorage { } entity.isConnectionEnabled = isConnectionEnabled; associatedDeviceDatabase.addOrReplaceAssociatedDevice(entity); - if (associatedDeviceCallback != null) { - associatedDeviceCallback.onAssociatedDeviceUpdated(entity.toAssociatedDevice()); - } + callbacks.invoke(callback -> callback.onAssociatedDeviceUpdated(entity.toAssociatedDevice())); } /** @@ -502,9 +499,7 @@ public class ConnectedDeviceStorage { entity.userId = ActivityManager.getCurrentUser(); associatedDeviceDatabase.addOrReplaceAssociatedDevice(entity); - if (associatedDeviceCallback != null) { - associatedDeviceCallback.onAssociatedDeviceUpdated(entity.toAssociatedDevice()); - } + callbacks.invoke(callback -> callback.onAssociatedDeviceUpdated(entity.toAssociatedDevice())); } /** Removes the claim on the identified associated device leaving it in an unclaimed state. */ @@ -522,9 +517,7 @@ public class ConnectedDeviceStorage { entity.userId = AssociatedDevice.UNCLAIMED_USER_ID; associatedDeviceDatabase.addOrReplaceAssociatedDevice(entity); - if (associatedDeviceCallback != null) { - associatedDeviceCallback.onAssociatedDeviceUpdated(entity.toAssociatedDevice()); - } + callbacks.invoke(callback -> callback.onAssociatedDeviceUpdated(entity.toAssociatedDevice())); } /** Callback for association device related events. */ diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/ui/AssociatedDeviceViewModel.java b/libs/connecteddevice/src/com/google/android/connecteddevice/ui/AssociatedDeviceViewModel.java index d319aec..171ba8c 100644 --- a/libs/connecteddevice/src/com/google/android/connecteddevice/ui/AssociatedDeviceViewModel.java +++ b/libs/connecteddevice/src/com/google/android/connecteddevice/ui/AssociatedDeviceViewModel.java @@ -28,8 +28,6 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.graphics.Bitmap; -import android.net.Uri; import android.os.ParcelUuid; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -60,12 +58,6 @@ public class AssociatedDeviceViewModel extends AndroidViewModel { private static final Duration DISCOVERABLE_DURATION = Duration.ofMinutes(2); - private static final String SCHEME = "https"; - - private static final String AUTHORITY = "demo.companiondevice.com"; - - private static final int QR_CODE_SIZE_IN_PIXEL = 200; - /** States of association process. */ public enum AssociationState { NONE, @@ -82,7 +74,8 @@ public class AssociatedDeviceViewModel extends AndroidViewModel { private final MutableLiveData<AssociatedDeviceDetails> currentDeviceDetails = new MutableLiveData<>(null); private final MutableLiveData<String> advertisedCarName = new MutableLiveData<>(null); - private final MutableLiveData<Bitmap> bitmap = new MutableLiveData<>(null); + private final MutableLiveData<StartAssociationResponse> associationResponse = + new MutableLiveData<>(null); private final MutableLiveData<String> pairingCode = new MutableLiveData<>(null); private final MutableLiveData<Integer> bluetoothState = new MutableLiveData<>(BluetoothAdapter.STATE_OFF); @@ -199,9 +192,10 @@ public class AssociatedDeviceViewModel extends AndroidViewModel { getApplication().startActivity(intent); } - /** Resets the value of {@link #associationState}. */ + /** Resets the value of {@link #associationState} and {@link #associationResponse}. */ public void resetAssociationState() { associationState.postValue(AssociationState.NONE); + associationResponse.postValue(null); } /** Gets the name that is being advertised by the car. */ @@ -209,9 +203,9 @@ public class AssociatedDeviceViewModel extends AndroidViewModel { return advertisedCarName; } - /** Gets the Qr code bitmap. */ - public LiveData<Bitmap> getQrCode() { - return bitmap; + /** Gets the response from a successful request to start association. */ + public LiveData<StartAssociationResponse> getAssociationResponse() { + return associationResponse; } /** Gets the generated pairing code. */ @@ -345,6 +339,7 @@ public class AssociatedDeviceViewModel extends AndroidViewModel { @Override public void onAssociatedDeviceAdded(@NonNull AssociatedDevice device) { + resetAssociationState(); addOrUpdateAssociatedDevice(device); } @@ -375,21 +370,7 @@ public class AssociatedDeviceViewModel extends AndroidViewModel { new IAssociationCallback.Stub() { @Override public void onAssociationStartSuccess(StartAssociationResponse response) { - Uri uri = - new CompanionUriBuilder() - .scheme(SCHEME) - .authority(AUTHORITY) - .oobData(response.getOobData()) - .deviceId(response.getDeviceIdentifier()) - .build(); - - Bitmap code = QrCodeGenerator.createQrCode(uri.toString(), QR_CODE_SIZE_IN_PIXEL); - if (code == null) { - loge(TAG, "QR code is null, ignore."); - return; - } - bitmap.postValue(code); - + associationResponse.postValue(response); associationState.postValue(AssociationState.STARTED); String deviceName = response.getDeviceName(); if (!deviceName.isEmpty()) { diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/ui/CompanionUriBuilder.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/ui/CompanionUriBuilder.kt index 86b5ba8..bff0608 100644 --- a/libs/connecteddevice/src/com/google/android/connecteddevice/ui/CompanionUriBuilder.kt +++ b/libs/connecteddevice/src/com/google/android/connecteddevice/ui/CompanionUriBuilder.kt @@ -25,10 +25,10 @@ import com.google.android.connecteddevice.model.OobData import com.google.protobuf.ByteString /** Build Uri which will be used to communicate with remote device. */ -class CompanionUriBuilder { +class CompanionUriBuilder(private val uri: Uri? = null) { private var oobData: OobData? = null private var deviceId: ByteArray? = null - private val builder = Uri.Builder() + private val builder: Uri.Builder = uri?.buildUpon() ?: Uri.Builder() fun scheme(scheme: String) = apply { builder.scheme(scheme) } fun authority(authority: String) = apply { builder.authority(authority) } @@ -51,21 +51,23 @@ class CompanionUriBuilder { fun build(): Uri { val oobData = this.oobData - val oobAssociationData = OutOfBandAssociationData.newBuilder().run { - if (deviceId != null) { - setDeviceIdentifier(ByteString.copyFrom(deviceId)) - } - if (oobData != null) { - val token = OutOfBandAssociationToken.newBuilder().run { - setEncryptionKey(ByteString.copyFrom(oobData.encryptionKey)) - setIhuIv(ByteString.copyFrom(oobData.ihuIv)) - setMobileIv(ByteString.copyFrom(oobData.mobileIv)) - build() + val oobAssociationData = + OutOfBandAssociationData.newBuilder().run { + if (deviceId != null) { + setDeviceIdentifier(ByteString.copyFrom(deviceId)) + } + if (oobData != null) { + val token = + OutOfBandAssociationToken.newBuilder().run { + setEncryptionKey(ByteString.copyFrom(oobData.encryptionKey)) + setIhuIv(ByteString.copyFrom(oobData.ihuIv)) + setMobileIv(ByteString.copyFrom(oobData.mobileIv)) + build() + } + setToken(token) } - setToken(token) + build() } - build() - } builder.appendQueryParameter( OOB_DATA_PARAMETER_KEY, Base64.encodeToString(oobAssociationData.toByteArray(), Base64.URL_SAFE) diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/ConnectedDeviceManagerTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/ConnectedDeviceManagerTest.java index ff95fe0..873e42c 100644 --- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/ConnectedDeviceManagerTest.java +++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/ConnectedDeviceManagerTest.java @@ -92,7 +92,7 @@ public class ConnectedDeviceManagerTest { directExecutor, directExecutor, directExecutor); - verify(mockStorage).setAssociatedDeviceCallback(callbackCaptor.capture()); + verify(mockStorage).registerAssociatedDeviceCallback(callbackCaptor.capture()); when(mockStorage.getDriverAssociatedDevices()).thenReturn(userDevices); when(mockStorage.getDriverAssociatedDeviceIds()).thenReturn(userDeviceIds); associatedDeviceCallback = callbackCaptor.getValue(); diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/ChannelResolverTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/ChannelResolverTest.kt index c1ae7b4..9228f86 100644 --- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/ChannelResolverTest.kt +++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/ChannelResolverTest.kt @@ -93,7 +93,8 @@ class ChannelResolverTest { .setQueryExecutor(directExecutor()) .build() val database = connectedDeviceDatabase.associatedDeviceDao() - spyStorage = spy(ConnectedDeviceStorage(context, Base64CryptoHelper(), database)) + spyStorage = + spy(ConnectedDeviceStorage(context, Base64CryptoHelper(), database, directExecutor())) whenever(mockStreamFactory.createProtocolStream(any(), any())).thenReturn(mockStream) whenever(spyStorage.hashWithChallengeSecret(any(), any())).thenReturn(TEST_CHALLENGE_RESPONSE) channelResolver = diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannelTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannelTest.kt index e3232dd..3dc968a 100644 --- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannelTest.kt +++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannelTest.kt @@ -83,7 +83,8 @@ class MultiProtocolSecureChannelTest { .setQueryExecutor(directExecutor()) .build() .associatedDeviceDao() - spyStorage = spy(ConnectedDeviceStorage(context, Base64CryptoHelper(), database)) + spyStorage = + spy(ConnectedDeviceStorage(context, Base64CryptoHelper(), database, directExecutor())) whenever(spyStorage.uniqueId).thenReturn(SERVER_DEVICE_ID) } diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannelV4Test.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannelV4Test.kt index cc5e0e3..17927c5 100644 --- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannelV4Test.kt +++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannelV4Test.kt @@ -68,7 +68,7 @@ class MultiProtocolSecureChannelV4Test { .setQueryExecutor(directExecutor()) .build() .associatedDeviceDao() - storage = ConnectedDeviceStorage(context, Base64CryptoHelper(), database) + storage = ConnectedDeviceStorage(context, Base64CryptoHelper(), database, directExecutor()) } @Test fun processVerificationCodeMessage_oobVerification_verifyOobCode() { diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/core/FeatureCoordinatorTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/core/FeatureCoordinatorTest.kt index 23ab293..7cb2bfe 100644 --- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/core/FeatureCoordinatorTest.kt +++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/core/FeatureCoordinatorTest.kt @@ -946,20 +946,24 @@ class FeatureCoordinatorTest { } @Test - fun claimAssociatedDevice_claimsDevice() { - val deviceId = UUID.randomUUID().toString() + fun claimAssociatedDevice_disconnectsAndClaimsDeviceAndInitiatesReconnection() { + val deviceId = UUID.randomUUID() - coordinator.claimAssociatedDevice(deviceId) + coordinator.claimAssociatedDevice(deviceId.toString()) - verify(mockStorage).claimAssociatedDevice(deviceId) + verify(mockController).disconnectDevice(deviceId) + verify(mockStorage).claimAssociatedDevice(deviceId.toString()) + verify(mockController).initiateConnectionToDevice(deviceId) } @Test - fun removeAssociatedDeviceClaim_removesClaim() { - val deviceId = UUID.randomUUID().toString() + fun removeAssociatedDeviceClaim_disconnectsAndRemovesClaimAndInitiatesReconnection() { + val deviceId = UUID.randomUUID() - coordinator.removeAssociatedDeviceClaim(deviceId) + coordinator.removeAssociatedDeviceClaim(deviceId.toString()) - verify(mockStorage).removeAssociatedDeviceClaim(deviceId) + verify(mockController).disconnectDevice(deviceId) + verify(mockStorage).removeAssociatedDeviceClaim(deviceId.toString()) + verify(mockController).initiateConnectionToDevice(deviceId) } } diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/core/MultiProtocolDeviceControllerTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/core/MultiProtocolDeviceControllerTest.kt index d8d07ba..2fae8f9 100644 --- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/core/MultiProtocolDeviceControllerTest.kt +++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/core/MultiProtocolDeviceControllerTest.kt @@ -92,15 +92,17 @@ class MultiProtocolDeviceControllerTest { .setQueryExecutor(directExecutor()) .build() val database = connectedDeviceDatabase.associatedDeviceDao() - spyStorage = spy(ConnectedDeviceStorage(context, Base64CryptoHelper(), database)) + spyStorage = + spy(ConnectedDeviceStorage(context, Base64CryptoHelper(), database, directExecutor())) whenever(spyStorage.hashWithChallengeSecret(any(), any())).thenReturn(TEST_CHALLENGE) deviceController = MultiProtocolDeviceController( protocols, spyStorage, mockOobRunner, - directExecutor(), - testAssociationServiceUuid + testAssociationServiceUuid, + enablePassenger = false, + callbackExecutor = directExecutor() ) deviceController.registerCallback(mockCallback, directExecutor()) secureChannel = @@ -121,13 +123,14 @@ class MultiProtocolDeviceControllerTest { } @Test - fun init_retriesStorageOnException() { + fun start_retriesStorageOnException() { val transientErrorStorage = object : ConnectedDeviceStorage( context, Base64CryptoHelper(), - connectedDeviceDatabase.associatedDeviceDao() + connectedDeviceDatabase.associatedDeviceDao(), + directExecutor() ) { var attempts = 0 override fun getAllAssociatedDevices(): MutableList<AssociatedDevice> { @@ -140,17 +143,93 @@ class MultiProtocolDeviceControllerTest { } MultiProtocolDeviceController( - protocols, - transientErrorStorage, - mockOobRunner, - directExecutor(), - testAssociationServiceUuid - ) + protocols, + transientErrorStorage, + mockOobRunner, + testAssociationServiceUuid, + enablePassenger = false, + callbackExecutor = directExecutor() + ) + .start() assertThat(transientErrorStorage.attempts).isEqualTo(2) } @Test + fun start_connectsToPassengerDevicesWhenPassengerEnabled() { + val driverId = UUID.randomUUID() + val driverDevice = + AssociatedDevice( + driverId.toString(), + /* deviceAddress= */ "", + /* deviceName= */ null, + /* isConnectionEnabled= */ true + ) + val passengerId = UUID.randomUUID() + val passengerDevice = + AssociatedDevice( + passengerId.toString(), + /* deviceAddress= */ "", + /* deviceName= */ null, + /* isConnectionEnabled= */ true + ) + whenever(spyStorage.driverAssociatedDevices).thenReturn(listOf(driverDevice)) + whenever(spyStorage.passengerAssociatedDevices).thenReturn(listOf(passengerDevice)) + whenever(spyStorage.allAssociatedDevices).thenReturn(listOf(driverDevice, passengerDevice)) + deviceController = + MultiProtocolDeviceController( + protocols, + spyStorage, + mockOobRunner, + testAssociationServiceUuid, + enablePassenger = true, + callbackExecutor = directExecutor() + ) + + deviceController.start() + + verify(testConnectionProtocol).startConnectionDiscovery(eq(driverId), any(), any()) + verify(testConnectionProtocol).startConnectionDiscovery(eq(passengerId), any(), any()) + } + + @Test + fun start_doesNotConnectToPassengerDevicesWhenPassengerDisabled() { + val driverId = UUID.randomUUID() + val driverDevice = + AssociatedDevice( + driverId.toString(), + /* deviceAddress= */ "", + /* deviceName= */ null, + /* isConnectionEnabled= */ true + ) + val passengerId = UUID.randomUUID() + val passengerDevice = + AssociatedDevice( + passengerId.toString(), + /* deviceAddress= */ "", + /* deviceName= */ null, + /* isConnectionEnabled= */ true + ) + whenever(spyStorage.driverAssociatedDevices).thenReturn(listOf(driverDevice)) + whenever(spyStorage.passengerAssociatedDevices).thenReturn(listOf(passengerDevice)) + whenever(spyStorage.allAssociatedDevices).thenReturn(listOf(driverDevice, passengerDevice)) + deviceController = + MultiProtocolDeviceController( + protocols, + spyStorage, + mockOobRunner, + testAssociationServiceUuid, + enablePassenger = false, + callbackExecutor = directExecutor() + ) + + deviceController.start() + + verify(testConnectionProtocol).startConnectionDiscovery(eq(driverId), any(), any()) + verify(testConnectionProtocol, never()).startConnectionDiscovery(eq(passengerId), any(), any()) + } + + @Test fun startAssociation_startedWithoutIdentifier() { val deviceName = "TestDeviceName" @@ -274,10 +353,12 @@ class MultiProtocolDeviceControllerTest { protocols, spyStorage, mockOobRunner, - directExecutor(), - testAssociationServiceUuid + testAssociationServiceUuid, + enablePassenger = false, + callbackExecutor = directExecutor() ) deviceController.registerCallback(mockCallback, directExecutor()) + deviceController.start() deviceController.initiateConnectionToDevice(deviceId) argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply { verify(testConnectionProtocol).startConnectionDiscovery(any(), any(), capture()) @@ -665,6 +746,95 @@ class MultiProtocolDeviceControllerTest { } @Test + fun handleSecureChannelMessage_firstMessagePersistsDeviceAsDriverWhenPassengerDisabled() { + val deviceName = "TestDeviceName" + val deviceId = UUID.randomUUID() + val testIdentifier = UUID.randomUUID() + val secret = ByteUtils.randomBytes(CHALLENGE_SECRET_BYTES) + val testDeviceMessage = + DeviceMessage.createOutgoingMessage( + null, + true, + OperationType.CLIENT_MESSAGE, + ByteUtils.uuidToBytes(deviceId) + secret + ) + deviceController = + MultiProtocolDeviceController( + protocols, + spyStorage, + mockOobRunner, + testAssociationServiceUuid, + enablePassenger = false, + callbackExecutor = directExecutor() + ) + + deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier) + argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply { + verify(testConnectionProtocol) + .startAssociationDiscovery(eq(deviceName), capture(), eq(testIdentifier)) + firstValue.onDeviceConnected(UUID.randomUUID().toString()) + } + + deviceController.handleSecureChannelMessage( + testDeviceMessage, + deviceController.getConnectedDevice( + deviceController.associationPendingDeviceId.get() ?: fail("Null device id.") + ) + ?: fail("Failed to find the device.") + ) + + argumentCaptor<AssociatedDevice>().apply { + verify(spyStorage).addAssociatedDeviceForDriver(capture()) + assertThat(firstValue.deviceId).isEqualTo(deviceId.toString()) + } + } + + @Test + fun handleSecureChannelMessage_firstMessagePersistsDeviceAsUnclaimedWhenPassengerEnabled() { + val deviceName = "TestDeviceName" + val deviceId = UUID.randomUUID() + val testIdentifier = UUID.randomUUID() + val secret = ByteUtils.randomBytes(CHALLENGE_SECRET_BYTES) + val testDeviceMessage = + DeviceMessage.createOutgoingMessage( + null, + true, + OperationType.CLIENT_MESSAGE, + ByteUtils.uuidToBytes(deviceId) + secret + ) + deviceController = + MultiProtocolDeviceController( + protocols, + spyStorage, + mockOobRunner, + testAssociationServiceUuid, + enablePassenger = true, + callbackExecutor = directExecutor() + ) + + deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier) + argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply { + verify(testConnectionProtocol) + .startAssociationDiscovery(eq(deviceName), capture(), eq(testIdentifier)) + firstValue.onDeviceConnected(UUID.randomUUID().toString()) + } + + deviceController.handleSecureChannelMessage( + testDeviceMessage, + deviceController.getConnectedDevice( + deviceController.associationPendingDeviceId.get() ?: fail("Null device id.") + ) + ?: fail("Failed to find the device.") + ) + + argumentCaptor<AssociatedDevice>().apply { + verify(spyStorage) + .addAssociatedDeviceForUser(eq(AssociatedDevice.UNCLAIMED_USER_ID), capture()) + assertThat(firstValue.deviceId).isEqualTo(deviceId.toString()) + } + } + + @Test fun connectedDevices_returnsAllConnectedDevices() { val activeUserDeviceId = UUID.randomUUID() val activeUserDevice = @@ -699,12 +869,13 @@ class MultiProtocolDeviceControllerTest { protocols, spyStorage, mockOobRunner, - directExecutor(), - testAssociationServiceUuid + testAssociationServiceUuid, + enablePassenger = false, + callbackExecutor = directExecutor() ) deviceController.registerCallback(mockCallback, directExecutor()) + deviceController.start() - deviceController.initiateConnectionToDevice(activeUserDeviceId) argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply { verify(testConnectionProtocol) .startConnectionDiscovery(eq(activeUserDeviceId), any(), capture()) @@ -793,8 +964,9 @@ class MultiProtocolDeviceControllerTest { setOf(protocol1, protocol2), spyStorage, mockOobRunner, - directExecutor(), - testAssociationServiceUuid + testAssociationServiceUuid, + enablePassenger = false, + callbackExecutor = directExecutor() ) .apply { registerCallback(mockCallback, directExecutor()) } deviceController.initiateConnectionToDevice(deviceId) @@ -830,8 +1002,9 @@ class MultiProtocolDeviceControllerTest { setOf(protocol), spyStorage, mockOobRunner, - directExecutor(), - testAssociationServiceUuid + testAssociationServiceUuid, + enablePassenger = false, + callbackExecutor = directExecutor() ) .apply { registerCallback(mockCallback, directExecutor()) } deviceController.initiateConnectionToDevice(deviceId) @@ -867,11 +1040,12 @@ class MultiProtocolDeviceControllerTest { setOf(protocol), spyStorage, mockOobRunner, - directExecutor(), - testAssociationServiceUuid + testAssociationServiceUuid, + enablePassenger = false, + callbackExecutor = directExecutor() ) .apply { registerCallback(mockCallback, directExecutor()) } - deviceController.initiateConnectionToDevice(deviceId) + deviceController.start() argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply { verify(protocol).startConnectionDiscovery(any(), any(), capture()) firstValue.onDeviceConnected(testProtocolId) @@ -994,18 +1168,6 @@ class MultiProtocolDeviceControllerTest { } } - private fun startReconnection() { - val deviceName = "TestDeviceName" - val testIdentifier = UUID.randomUUID() - - deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier) - argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply { - verify(testConnectionProtocol) - .startAssociationDiscovery(eq(deviceName), capture(), eq(testIdentifier)) - firstValue.onDeviceConnected(UUID.randomUUID().toString()) - } - } - private class Base64CryptoHelper : CryptoHelper { override fun encrypt(value: ByteArray?): String? = Base64.encodeToString(value, Base64.DEFAULT) diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/storage/ConnectedDeviceStorageTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/storage/ConnectedDeviceStorageTest.java index ecf057e..bdfbe1d 100644 --- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/storage/ConnectedDeviceStorageTest.java +++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/storage/ConnectedDeviceStorageTest.java @@ -62,7 +62,8 @@ public final class ConnectedDeviceStorageTest { .build(); AssociatedDeviceDao database = connectedDeviceDatabase.associatedDeviceDao(); - connectedDeviceStorage = new ConnectedDeviceStorage(context, new FakeCryptoHelper(), database); + connectedDeviceStorage = + new ConnectedDeviceStorage(context, new FakeCryptoHelper(), database, directExecutor()); addedAssociatedDevices = new ArrayList<>(); } @@ -233,7 +234,7 @@ public final class ConnectedDeviceStorageTest { @Test public void setAssociatedDeviceName_issuesCallbackOnNameChange() { AssociatedDeviceCallback callback = mock(AssociatedDeviceCallback.class); - connectedDeviceStorage.setAssociatedDeviceCallback(callback); + connectedDeviceStorage.registerAssociatedDeviceCallback(callback); AssociatedDevice device = new AssociatedDevice( UUID.randomUUID().toString(), @@ -299,7 +300,7 @@ public final class ConnectedDeviceStorageTest { @Test public void updateAssociatedDeviceName_issuesCallbackOnNameChange() { AssociatedDeviceCallback callback = mock(AssociatedDeviceCallback.class); - connectedDeviceStorage.setAssociatedDeviceCallback(callback); + connectedDeviceStorage.registerAssociatedDeviceCallback(callback); AssociatedDevice device = new AssociatedDevice( UUID.randomUUID().toString(), @@ -372,7 +373,7 @@ public final class ConnectedDeviceStorageTest { @Test public void addAssociatedDeviceForUser_invokesCallback() { AssociatedDeviceCallback callback = mock(AssociatedDeviceCallback.class); - connectedDeviceStorage.setAssociatedDeviceCallback(callback); + connectedDeviceStorage.registerAssociatedDeviceCallback(callback); AssociatedDevice device = addRandomAssociatedDevice(ACTIVE_USER_ID); @@ -382,7 +383,7 @@ public final class ConnectedDeviceStorageTest { @Test public void removeAssociatedDeviceForUser_invokesCallback() { AssociatedDeviceCallback callback = mock(AssociatedDeviceCallback.class); - connectedDeviceStorage.setAssociatedDeviceCallback(callback); + connectedDeviceStorage.registerAssociatedDeviceCallback(callback); AssociatedDevice device = addRandomAssociatedDevice(ACTIVE_USER_ID); connectedDeviceStorage.removeAssociatedDevice(device.getDeviceId()); @@ -393,7 +394,7 @@ public final class ConnectedDeviceStorageTest { @Test public void updateAssociatedDeviceName_invokesCallback() { AssociatedDeviceCallback callback = mock(AssociatedDeviceCallback.class); - connectedDeviceStorage.setAssociatedDeviceCallback(callback); + connectedDeviceStorage.registerAssociatedDeviceCallback(callback); AssociatedDevice device = addRandomAssociatedDevice(ACTIVE_USER_ID); String newName = "New Name"; @@ -407,7 +408,7 @@ public final class ConnectedDeviceStorageTest { @Test public void claimAssociatedDevice_setsCurrentUserIdOnAssociatedDevice() { AssociatedDeviceCallback callback = mock(AssociatedDeviceCallback.class); - connectedDeviceStorage.setAssociatedDeviceCallback(callback); + connectedDeviceStorage.registerAssociatedDeviceCallback(callback); AssociatedDevice device = addRandomAssociatedDevice(AssociatedDevice.UNCLAIMED_USER_ID); connectedDeviceStorage.claimAssociatedDevice(device.getDeviceId()); @@ -425,7 +426,7 @@ public final class ConnectedDeviceStorageTest { @Test public void removeAssociatedDeviceClaim_setsUnclaimedUserIdOnAssociatedDevice() { AssociatedDeviceCallback callback = mock(AssociatedDeviceCallback.class); - connectedDeviceStorage.setAssociatedDeviceCallback(callback); + connectedDeviceStorage.registerAssociatedDeviceCallback(callback); AssociatedDevice device = addRandomAssociatedDevice(AssociatedDevice.UNCLAIMED_USER_ID); connectedDeviceStorage.removeAssociatedDeviceClaim(device.getDeviceId()); |