Project import generated by Copybara
Included changes:
Release-Id: aae-companiondevice-android_20220202.00_RC00
Change-Id: Ib2767be29ae02933a60f44a7d6f749f459ed54b5
diff --git a/companiondevice/AndroidManifest.xml b/companiondevice/AndroidManifest.xml
index d579588..de87f10 100644
--- a/companiondevice/AndroidManifest.xml
+++ b/companiondevice/AndroidManifest.xml
@@ -16,7 +16,9 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.google.android.companiondevicesupport">
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.google.android.companiondevicesupport"
+ tools:ignore="all">
<!-- Needed for BLE scanning/advertising -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
@@ -78,10 +80,6 @@
android:resource="@bool/enable_proxy" />
<meta-data android:name="com.google.android.connecteddevice.transport_protocols"
android:resource="@array/transport_protocols" />
- <meta-data android:name="com.google.android.connecteddevice.enable_capabilities_exchange"
- android:resource="@bool/enable_capabilities" />
- <meta-data android:name="com.google.android.connecteddevice.enable_feature_coordinator"
- android:resource="@bool/enable_feature_coordinator" />
<meta-data android:name="com.google.android.connecteddevice.supported_oob_channels"
android:resource="@array/supported_oob_channels"/>
@@ -101,8 +99,6 @@
android:resource="@array/early_fg_user_services"/>
<meta-data android:name="com.google.android.connecteddevice.unlock_services"
android:resource="@array/unlock_fg_user_services"/>
- <meta-data android:name="com.google.android.connecteddevice.enable_feature_coordinator"
- android:resource="@bool/enable_feature_coordinator" />
</service>
<service
android:name="com.google.android.connecteddevice.service.SppService"
@@ -205,6 +201,8 @@
android:resource="@string/trusted_device_feature_title" />
<meta-data android:name="com.android.settings.category"
android:value="com.android.settings.category.ia.security" />
+ <meta-data android:name="com.android.settings.icon_tintable"
+ android:value="true" />
</activity>
</application>
</manifest>
diff --git a/companiondevice/build.gradle b/companiondevice/build.gradle
index 5a50a1b..20f8f2c 100644
--- a/companiondevice/build.gradle
+++ b/companiondevice/build.gradle
@@ -1,4 +1,5 @@
apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
buildscript {
repositories {
@@ -13,7 +14,7 @@
applicationId "com.google.android.companiondevicesupport"
minSdkVersion 29
targetSdkVersion 30
- versionCode 1181
+ versionCode 1249
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
diff --git a/companiondevice/res/anim/fade_in.xml b/companiondevice/res/anim/fade_in.xml
new file mode 100644
index 0000000..bff039f
--- /dev/null
+++ b/companiondevice/res/anim/fade_in.xml
@@ -0,0 +1,21 @@
+<?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.
+ -->
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@android:integer/config_shortAnimTime"
+ android:interpolator="@android:anim/decelerate_interpolator"
+ android:fromAlpha="0"
+ android:toAlpha="1" />
diff --git a/companiondevice/res/anim/fade_out.xml b/companiondevice/res/anim/fade_out.xml
new file mode 100644
index 0000000..35c88a3
--- /dev/null
+++ b/companiondevice/res/anim/fade_out.xml
@@ -0,0 +1,21 @@
+<?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.
+ -->
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@android:integer/config_shortAnimTime"
+ android:interpolator="@android:anim/decelerate_interpolator"
+ android:fromAlpha="1"
+ android:toAlpha="0" />
diff --git a/companiondevice/res/drawable/ic_baseline_star_24.xml b/companiondevice/res/drawable/ic_baseline_star_24.xml
new file mode 100644
index 0000000..2a59f29
--- /dev/null
+++ b/companiondevice/res/drawable/ic_baseline_star_24.xml
@@ -0,0 +1,27 @@
+<?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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"/>
+</vector>
diff --git a/companiondevice/res/drawable/ic_baseline_star_border_24.xml b/companiondevice/res/drawable/ic_baseline_star_border_24.xml
new file mode 100644
index 0000000..fd73f5d
--- /dev/null
+++ b/companiondevice/res/drawable/ic_baseline_star_border_24.xml
@@ -0,0 +1,27 @@
+<?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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z"/>
+</vector>
diff --git a/companiondevice/res/layout/associated_device_detail_fragment.xml b/companiondevice/res/layout/associated_device_detail_fragment.xml
index 84a1743..5cd268b 100644
--- a/companiondevice/res/layout/associated_device_detail_fragment.xml
+++ b/companiondevice/res/layout/associated_device_detail_fragment.xml
@@ -96,6 +96,35 @@
android:textAppearance="@style/SettingsItemButtonTextStyle" />
</LinearLayout>
<LinearLayout
+ android:id="@+id/claim_button"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center"
+ android:layout_weight="1"
+ android:background="@drawable/car_ui_list_item_background"
+ android:clickable="true"
+ android:minHeight="?attr/companionListItemMinHeight">
+ <ImageView
+ android:id="@+id/claim_button_icon"
+ android:layout_width="?attr/companionSecondaryIconSize"
+ android:layout_height="?attr/companionSecondaryIconSize"
+ android:scaleType="fitXY"
+ android:src="@drawable/ic_baseline_star_border_24"
+ android:tint="?attr/companionColorAccent"
+ tools:ignore="ContentDescription" />
+ <TextView
+ android:id="@+id/claim_button_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/settings_list_item_text_no_icon_margin_start"
+ android:layout_marginEnd="@dimen/settings_list_item_text_margin_end"
+ android:layout_marginBottom="@dimen/settings_list_item_content_margin_bottom"
+ android:layout_marginTop="@dimen/settings_list_item_content_margin_top"
+ android:text="@string/unclaimed_device"
+ android:textAppearance="@style/SettingsItemButtonTextStyle" />
+ </LinearLayout>
+ <LinearLayout
android:id="@+id/forget_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
diff --git a/companiondevice/res/layout/associated_devices_list_fragment.xml b/companiondevice/res/layout/associated_devices_list_fragment.xml
new file mode 100644
index 0000000..6f419c8
--- /dev/null
+++ b/companiondevice/res/layout/associated_devices_list_fragment.xml
@@ -0,0 +1,26 @@
+<?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.
+ -->
+<FrameLayout 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">
+
+ <com.android.car.ui.recyclerview.CarUiRecyclerView
+ android:id="@+id/associated_devices_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+</FrameLayout>
diff --git a/companiondevice/res/values/colors.xml b/companiondevice/res/values/colors.xml
index cce576f..14520d0 100644
--- a/companiondevice/res/values/colors.xml
+++ b/companiondevice/res/values/colors.xml
@@ -19,6 +19,7 @@
<color name="connection_color_connected">#34A853</color>
<color name="connection_color_disconnected">#EA4335</color>
<color name="connection_color_not_detected">#C4C4C4</color>
+ <color name="connection_color_detected">#FFC107</color>
<color name="car_red_300">@*android:color/car_red_300</color>
<color name="divider_color">#5F6368</color>
diff --git a/companiondevice/res/values/config.xml b/companiondevice/res/values/config.xml
index e2f7b56..13ae152 100644
--- a/companiondevice/res/values/config.xml
+++ b/companiondevice/res/values/config.xml
@@ -50,8 +50,6 @@
</string-array>
<bool name="enable_proxy">false</bool>
- <bool name="enable_capabilities">false</bool>
- <bool name="enable_feature_coordinator">true</bool>
<bool name="enable_qr_code">false</bool>
<bool name="enable_passenger">false</bool>
diff --git a/companiondevice/res/values/strings.xml b/companiondevice/res/values/strings.xml
index 97259ad..ecf5294 100644
--- a/companiondevice/res/values/strings.xml
+++ b/companiondevice/res/values/strings.xml
@@ -28,6 +28,8 @@
<string name="add_associated_device_title">Connect to MyCompanion</string>
<!-- Instruction for adding associated device [CHAR LIMIT=100] -->
<string name="add_associated_device_subtitle">You can use your phone as a companion device to help manage your driving experience</string>
+ <!-- Title for a button for a user to associate a new device [CHAR LIMIT=50]-->
+ <string name="add_associated_device_button" translatable="false">Add new device</string>
<!-- Instruction for opening Companion App [CHAR LIMIT=100] -->
<string name="associated_device_install_app">Make sure you have <b>Companion App</b> installed on your phone</string>
<!-- Instruction for opening Companion App [CHAR LIMIT=100] -->
@@ -110,12 +112,14 @@
<!-- Skip button text [CHAR LIMIT=20] -->
<string name="skip">Skip</string>
- <!-- Connected status text [CHAR LIMIT=20]-->
+ <!-- Connected with secure channel status text [CHAR LIMIT=20]-->
<string name="connected">Connected</string>
<!-- Not detected status text [CHAR LIMIT=20]-->
<string name="notDetected">Not detected</string>
<!-- Disconnected status text [CHAR LIMIT=20]-->
<string name="disconnected">Disconnected</string>
+ <!-- Connected without secure channel status text [CHAR LIMIT=20]-->
+ <string name="detected">Detected</string>
<!-- Unknown status text [CHAR LIMIT=20]-->
<string name="unknown">Unknown</string>
@@ -199,4 +203,14 @@
<!-- 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>
+ <!-- Setting passenger strings' translatable to false until reference UI has been defined. -->
+ <!-- Text for a claim button on a device that has been claimed by the current user. [CHAR LIMIT=30] -->
+ <string name="claimed_device" translatable="false">This is my device</string>
+ <!-- Text for a claim button on a device that has not been claimed by the current user. [CHAR LIMIT=30] -->
+ <string name="unclaimed_device" translatable="false">This is not my device</string>
+ <!-- Text for a label on a driver's device in the list. [CHAR LIMIT=30] -->
+ <string name="driver_device" translatable="false">My device</string>
+ <!-- Text for a label on a passenger's device in the list. [CHAR LIMIT=30] -->
+ <string name="passenger_device" translatable="false">Passenger\'s device</string>
+
</resources>
diff --git a/companiondevice/src/com/google/android/companiondevicesupport/AssociatedDeviceDetailFragment.java b/companiondevice/src/com/google/android/companiondevicesupport/AssociatedDeviceDetailFragment.java
index 63d31ab..85d07bc 100644
--- a/companiondevice/src/com/google/android/companiondevicesupport/AssociatedDeviceDetailFragment.java
+++ b/companiondevice/src/com/google/android/companiondevicesupport/AssociatedDeviceDetailFragment.java
@@ -16,8 +16,11 @@
package com.google.android.companiondevicesupport;
+import static com.google.android.connecteddevice.util.SafeLog.logd;
+
import android.app.AlertDialog;
import android.app.Dialog;
+import android.content.Context;
import android.content.DialogInterface;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
@@ -34,10 +37,10 @@
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.ViewModelProvider;
-import androidx.lifecycle.ViewModelStoreOwner;
-import com.google.android.connecteddevice.model.AssociatedDeviceDetails;
import com.google.android.connecteddevice.model.TransportProtocols;
import com.google.android.connecteddevice.trust.TrustedDeviceConstants;
+import com.google.android.connecteddevice.ui.AssociatedDeviceDetails;
+import com.google.android.connecteddevice.ui.AssociatedDeviceDetails.ConnectionState;
import com.google.android.connecteddevice.ui.AssociatedDeviceViewModel;
import com.google.android.connecteddevice.ui.AssociatedDeviceViewModelFactory;
import java.util.Arrays;
@@ -47,12 +50,31 @@
public class AssociatedDeviceDetailFragment extends Fragment {
private static final String TAG = "AssociatedDeviceDetailFragment";
private static final String REMOVE_DEVICE_DIALOG_TAG = "RemoveDeviceDialog";
+ private static final String ASSOCIATED_DEVICE_DETAILS_KEY = "AssociatedDeviceDetailsKey";
+
+ private AssociatedDeviceDetails deviceDetails;
private TextView deviceName;
private TextView connectionStatusText;
private ImageView connectionStatusIndicator;
private TextView connectionText;
private ImageView connectionIcon;
private AssociatedDeviceViewModel model;
+ private TextView claimText;
+ private ImageView claimIcon;
+
+ /**
+ * Returns an instance of the {@link AssociatedDeviceDetailFragment} that will display the details
+ * of the given {@link AssociatedDeviceDetails}.
+ */
+ @NonNull
+ public static AssociatedDeviceDetailFragment newInstance(AssociatedDeviceDetails deviceDetails) {
+ Bundle arguments = new Bundle();
+ arguments.putParcelable(ASSOCIATED_DEVICE_DETAILS_KEY, deviceDetails);
+
+ AssociatedDeviceDetailFragment fragment = new AssociatedDeviceDetailFragment();
+ fragment.setArguments(arguments);
+ return fragment;
+ }
@Override
public View onCreateView(
@@ -67,64 +89,100 @@
connectionText = view.findViewById(R.id.connection_button_text);
connectionStatusText = view.findViewById(R.id.connection_status_text);
connectionStatusIndicator = view.findViewById(R.id.connection_status_indicator);
+ claimText = view.findViewById(R.id.claim_button_text);
+ claimIcon = view.findViewById(R.id.claim_button_icon);
List<String> transportProtocols =
Arrays.asList(getResources().getStringArray(R.array.transport_protocols));
model =
new ViewModelProvider(
- (ViewModelStoreOwner) requireActivity(),
+ requireActivity(),
new AssociatedDeviceViewModelFactory(
requireActivity().getApplication(),
transportProtocols.contains(TransportProtocols.PROTOCOL_SPP),
- getResources().getString(R.string.ble_device_name_prefix)))
+ getResources().getString(R.string.ble_device_name_prefix),
+ getResources().getBoolean(R.bool.enable_passenger)))
.get(AssociatedDeviceViewModel.class);
- model.getCurrentDeviceDetails().observe(this, this::setDeviceDetails);
+ model.getAssociatedDevicesDetails().observe(this, this::setDeviceDetails);
view.findViewById(R.id.connection_button)
.setOnClickListener(
- l -> {
- model.toggleConnectionStatusForCurrentDevice();
- });
+ l -> model.toggleConnectionStatusForDevice(deviceDetails.getAssociatedDevice()));
view.findViewById(R.id.forget_button).setOnClickListener(v -> showRemoveDeviceDialog());
- view.findViewById(R.id.trusted_device_feature_button)
- .setOnClickListener(
- v ->
- model.startFeatureActivityForCurrentDevice(
- TrustedDeviceConstants.INTENT_ACTION_TRUSTED_DEVICE_SETTING));
+ view.findViewById(R.id.trusted_device_feature_button).setOnClickListener(
+ v ->
+ model.startFeatureActivityForDevice(
+ TrustedDeviceConstants.INTENT_ACTION_TRUSTED_DEVICE_SETTING,
+ deviceDetails.getAssociatedDevice()));
+ View claimButton = view.findViewById(R.id.claim_button);
+ if (getResources().getBoolean(R.bool.enable_passenger)) {
+ claimButton.setOnClickListener(v -> toggleClaim());
+ } else {
+ claimButton.setVisibility(View.GONE);
+ }
}
@Override
public void onCreate(@Nullable Bundle bundle) {
super.onCreate(bundle);
+
+ Bundle arguments = getArguments();
+ arguments.setClassLoader(AssociatedDeviceDetails.class.getClassLoader());
+ deviceDetails = arguments.getParcelable(ASSOCIATED_DEVICE_DETAILS_KEY);
+
if (bundle != null) {
resumeRemoveDeviceDialog();
}
}
- private void setDeviceDetails(AssociatedDeviceDetails deviceDetails) {
- if (deviceDetails == null) {
+ private void setDeviceDetails(List<AssociatedDeviceDetails> associatedDevicesDetails) {
+ int index = associatedDevicesDetails.indexOf(deviceDetails);
+ if (index == -1) {
+ logd(TAG, "Device details updated for non-matching device. Ignoring.");
+ return;
+ }
+ deviceDetails = associatedDevicesDetails.get(index);
+
+ Context context = getContext();
+ if (context == null) {
return;
}
deviceName.setText(deviceDetails.getDeviceName());
if (!deviceDetails.isConnectionEnabled()) {
setConnectionStatus(
- ContextCompat.getColor(getContext(), R.color.connection_color_disconnected),
+ ContextCompat.getColor(context, R.color.connection_color_disconnected),
getString(R.string.disconnected),
- ContextCompat.getDrawable(getContext(), R.drawable.ic_phonelink_ring_24dp),
+ ContextCompat.getDrawable(context, R.drawable.ic_phonelink_ring_24dp),
getString(R.string.connect));
- } else if (deviceDetails.isConnected()) {
+ } else if (deviceDetails.getConnectionState() == ConnectionState.CONNECTED) {
setConnectionStatus(
- ContextCompat.getColor(getContext(), R.color.connection_color_connected),
+ ContextCompat.getColor(context, R.color.connection_color_connected),
getString(R.string.connected),
+ ContextCompat.getDrawable(context, R.drawable.ic_phonelink_erase_24dp),
+ getString(R.string.disconnect));
+ } else if (deviceDetails.getConnectionState() == ConnectionState.DETECTED) {
+ setConnectionStatus(
+ ContextCompat.getColor(getContext(), R.color.connection_color_detected),
+ getString(R.string.detected),
ContextCompat.getDrawable(getContext(), R.drawable.ic_phonelink_erase_24dp),
getString(R.string.disconnect));
} else {
setConnectionStatus(
- ContextCompat.getColor(getContext(), R.color.connection_color_not_detected),
+ ContextCompat.getColor(context, R.color.connection_color_not_detected),
getString(R.string.notDetected),
- ContextCompat.getDrawable(getContext(), R.drawable.ic_phonelink_erase_24dp),
+ ContextCompat.getDrawable(context, R.drawable.ic_phonelink_erase_24dp),
getString(R.string.disconnect));
}
+
+ if (deviceDetails.belongsToDriver()) {
+ claimText.setText(getString(R.string.claimed_device));
+ claimIcon.setImageDrawable(
+ ContextCompat.getDrawable(context, R.drawable.ic_baseline_star_24));
+ } else {
+ claimText.setText(getString(R.string.unclaimed_device));
+ claimIcon.setImageDrawable(
+ ContextCompat.getDrawable(context, R.drawable.ic_baseline_star_border_24));
+ }
}
private void setConnectionStatus(
@@ -139,9 +197,10 @@
}
private void showRemoveDeviceDialog() {
- String deviceName = model.getCurrentDeviceDetails().getValue().getDeviceName();
+ String deviceName = deviceDetails.getDeviceName();
RemoveDeviceDialogFragment dialogFragment =
- RemoveDeviceDialogFragment.newInstance(deviceName, (d, w) -> model.removeCurrentDevice());
+ RemoveDeviceDialogFragment.newInstance(
+ deviceName, (d, w) -> model.removeDevice(deviceDetails.getAssociatedDevice()));
dialogFragment.show(getParentFragmentManager(), REMOVE_DEVICE_DIALOG_TAG);
}
@@ -150,7 +209,16 @@
(RemoveDeviceDialogFragment)
getParentFragmentManager().findFragmentByTag(REMOVE_DEVICE_DIALOG_TAG);
if (removeDeviceDialogFragment != null) {
- removeDeviceDialogFragment.setOnConfirmListener((d, w) -> model.removeCurrentDevice());
+ removeDeviceDialogFragment.setOnConfirmListener(
+ (d, w) -> model.removeDevice(deviceDetails.getAssociatedDevice()));
+ }
+ }
+
+ private void toggleClaim() {
+ if (deviceDetails.belongsToDriver()) {
+ model.removeClaimOnDevice(deviceDetails.getAssociatedDevice());
+ } else {
+ model.claimDevice(deviceDetails.getAssociatedDevice());
}
}
diff --git a/companiondevice/src/com/google/android/companiondevicesupport/AssociatedDevicesListFragment.kt b/companiondevice/src/com/google/android/companiondevicesupport/AssociatedDevicesListFragment.kt
new file mode 100644
index 0000000..754bcab
--- /dev/null
+++ b/companiondevice/src/com/google/android/companiondevicesupport/AssociatedDevicesListFragment.kt
@@ -0,0 +1,159 @@
+/*
+ * 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.
+ */
+
+package com.google.android.companiondevicesupport
+
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.ColorRes
+import androidx.core.graphics.drawable.DrawableCompat
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.ViewModelStoreOwner
+import com.android.car.ui.recyclerview.CarUiContentListItem
+import com.android.car.ui.recyclerview.CarUiContentListItem.Action
+import com.android.car.ui.recyclerview.CarUiListItem
+import com.android.car.ui.recyclerview.CarUiListItemAdapter
+import com.android.car.ui.recyclerview.CarUiRecyclerView
+import com.google.android.connecteddevice.model.TransportProtocols
+import com.google.android.connecteddevice.ui.AssociatedDeviceDetails
+import com.google.android.connecteddevice.ui.AssociatedDeviceDetails.ConnectionState
+import com.google.android.connecteddevice.ui.AssociatedDeviceDetails.ConnectionState.CONNECTED
+import com.google.android.connecteddevice.ui.AssociatedDeviceDetails.ConnectionState.DETECTED
+import com.google.android.connecteddevice.ui.AssociatedDeviceDetails.ConnectionState.NOT_DETECTED
+import com.google.android.connecteddevice.ui.AssociatedDeviceViewModel
+import com.google.android.connecteddevice.ui.AssociatedDeviceViewModelFactory
+import com.google.android.connecteddevice.util.SafeLog.logd
+
+/** Fragment that shows the a list of associated devices and their connection status. */
+class AssociatedDevicesListFragment : Fragment() {
+ private lateinit var deviceListView: CarUiRecyclerView
+ private lateinit var model: AssociatedDeviceViewModel
+ private lateinit var adapter: CarUiListItemAdapter
+
+ /** The list of devices that are set within the [adapter]. */
+ private val listItems = mutableListOf<CarUiListItem>()
+
+ /** A listener for clicks on items within the list shown by this fragment. */
+ interface OnListItemClickListener {
+ /**
+ * Invoked when an item in the list has been clicked. This method is passed the device that is
+ * being shown by the item.
+ */
+ fun onListItemClicked(associatedDeviceDetails: AssociatedDeviceDetails)
+ }
+
+ public var onListItemClickListener: OnListItemClickListener? = null
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View =
+ inflater.inflate(
+ R.layout.associated_devices_list_fragment,
+ container,
+ /* attachToRoot= */ false
+ )
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ deviceListView = view.findViewById(R.id.associated_devices_list) as CarUiRecyclerView
+ deviceListView.adapter = CarUiListItemAdapter(listItems).also { adapter = it }
+
+ model = obtainAssociatedDeviceViewModel()
+ model.getAssociatedDevicesDetails().observe(this, ::refreshDeviceList)
+ }
+
+ private fun obtainAssociatedDeviceViewModel(): AssociatedDeviceViewModel {
+ val activity = requireActivity()
+ val transportProtocols = resources.getStringArray(R.array.transport_protocols)
+
+ return ViewModelProvider(
+ activity as ViewModelStoreOwner,
+ AssociatedDeviceViewModelFactory(
+ activity.getApplication(),
+ transportProtocols.contains(TransportProtocols.PROTOCOL_SPP),
+ resources.getString(R.string.ble_device_name_prefix),
+ resources.getBoolean(R.bool.enable_passenger)
+ )
+ )
+ .get(AssociatedDeviceViewModel::class.java)
+ }
+
+ private fun refreshDeviceList(associatedDeviceDetails: List<AssociatedDeviceDetails>) {
+ logd(TAG, "Change detected in associated devices. Refreshing list.")
+
+ // The adapter retains a reference to this list. So modify it in-place to ensure the adapter
+ // has the up-to-date contents.
+ listItems.clear()
+ listItems.addAll(associatedDeviceDetails.map { it.toCarUiContentListItem() })
+
+ adapter.notifyDataSetChanged()
+ }
+
+ /**
+ * Converts this [AssociatedDeviceDetails] to its analogous [CarUiContentListItem].
+ *
+ * When the list item is clicked, any [onListItemClickListener] set on this fragment will be
+ * invoked.
+ */
+ private fun AssociatedDeviceDetails.toCarUiContentListItem(): CarUiContentListItem {
+ val context = requireContext()
+
+ return CarUiContentListItem(Action.CHEVRON).apply {
+ setTitle(deviceName)
+ setBody(
+ context.getString(
+ if (belongsToDriver()) R.string.driver_device else R.string.passenger_device
+ )
+ )
+ context.getDrawable(R.drawable.ic_connection_indicator)?.mutate()?.let {
+ icon =
+ DrawableCompat.wrap(it).apply {
+ setTint(context.getColor(connectionState.toColorRes(isConnectionEnabled)))
+ }
+ }
+
+ setOnItemClickedListener {
+ onListItemClickListener?.onListItemClicked(this@toCarUiContentListItem)
+ }
+ }
+ }
+
+ @ColorRes
+ private fun ConnectionState.toColorRes(isConnectionEnabled: Boolean): Int =
+ if (isConnectionEnabled) {
+ when (this) {
+ NOT_DETECTED -> {
+ R.color.connection_color_not_detected
+ }
+ DETECTED -> {
+ R.color.connection_color_detected
+ }
+ CONNECTED -> {
+ R.color.connection_color_connected
+ }
+ }
+ } else {
+ R.color.connection_color_disconnected
+ }
+
+ companion object {
+ private const val TAG = "AssociatedDevicesListFragment"
+ }
+}
diff --git a/companiondevice/src/com/google/android/companiondevicesupport/AssociationActivity.java b/companiondevice/src/com/google/android/companiondevicesupport/AssociationActivity.java
index 5d2e314..996c223 100644
--- a/companiondevice/src/com/google/android/companiondevicesupport/AssociationActivity.java
+++ b/companiondevice/src/com/google/android/companiondevicesupport/AssociationActivity.java
@@ -29,8 +29,10 @@
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
import android.view.View;
import android.widget.Toast;
+import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelStoreOwner;
import com.android.car.setupwizardlib.CarSetupWizardCompatLayout;
@@ -39,6 +41,7 @@
import com.google.android.connecteddevice.api.RemoteFeature;
import com.google.android.connecteddevice.model.AssociatedDevice;
import com.google.android.connecteddevice.model.TransportProtocols;
+import com.google.android.connecteddevice.ui.AssociatedDeviceDetails;
import com.google.android.connecteddevice.ui.AssociatedDeviceViewModel;
import com.google.android.connecteddevice.ui.AssociatedDeviceViewModelFactory;
import java.util.Arrays;
@@ -50,6 +53,7 @@
private static final String TAG = "CompanionAssociationActivity";
private static final String COMPANION_LANDING_FRAGMENT_TAG = "CompanionLandingFragment";
private static final String DEVICE_DETAIL_FRAGMENT_TAG = "AssociatedDeviceDetailFragment";
+ private static final String DEVICES_LIST_FRAGMENT_TAG = "AssociatedDevicesListFragment";
private static final String PAIRING_CODE_FRAGMENT_TAG = "ConfirmPairingCodeFragment";
private static final String TURN_ON_BLUETOOTH_FRAGMENT_TAG = "TurnOnBluetoothFragment";
private static final String ASSOCIATION_ERROR_FRAGMENT_TAG = "AssociationErrorFragment";
@@ -58,25 +62,52 @@
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";
+ private static final String ASSOCIATED_DEVICE_DETAILS_BACKSTACK_NAME =
+ "AssociatedDeviceDetailsBackstack";
private ToolbarController toolbar;
private CarSetupWizardCompatLayout carSetupWizardLayout;
private AssociatedDeviceViewModel model;
+
+ private boolean isPassengerEnabled = false;
private boolean isStartedForSuw = false;
private boolean isStartedForSetupProfile = false;
private boolean isImmersive = false;
private boolean hideSkipButton = false;
@Override
- public void onCreate(Bundle saveInstanceState) {
+ public void onCreate(Bundle savedInstanceState) {
+ isPassengerEnabled = getResources().getBoolean(R.bool.enable_passenger);
+
resolveIntent();
+
// Set theme before calling super.onCreate(bundle) to avoid recreating activity.
setAssociationTheme();
- super.onCreate(saveInstanceState);
+ super.onCreate(savedInstanceState);
prepareLayout();
+
+ // Only need to attach the click listener if we are recreating this activity due to a
+ // configuration change.
+ if (isPassengerEnabled && savedInstanceState != null) {
+ maybeAttachItemClickListener();
+ }
+
observeViewModel();
}
+ /**
+ * Attempts to attach a click listener to a {@link AssociatedDevicesListFragment} if it is
+ * currently visible.
+ */
+ private void maybeAttachItemClickListener() {
+ Fragment fragment = getSupportFragmentManager().findFragmentByTag(DEVICES_LIST_FRAGMENT_TAG);
+ if (fragment == null) {
+ return;
+ }
+ ((AssociatedDevicesListFragment) fragment).setOnListItemClickListener(
+ this::showAssociatedDeviceDetailFragment);
+ }
+
@Override
protected void onStart() {
super.onStart();
@@ -155,7 +186,8 @@
new AssociatedDeviceViewModelFactory(
getApplication(),
transportProtocols.contains(TransportProtocols.PROTOCOL_SPP),
- getResources().getString(R.string.ble_device_name_prefix)))
+ getResources().getString(R.string.ble_device_name_prefix),
+ getResources().getBoolean(R.bool.enable_passenger)))
.get(AssociatedDeviceViewModel.class);
model
@@ -198,15 +230,7 @@
model
.getBluetoothState()
- .observe(
- this,
- state -> {
- if (state != BluetoothAdapter.STATE_ON
- && getSupportFragmentManager().findFragmentByTag(DEVICE_DETAIL_FRAGMENT_TAG)
- != null) {
- runOnUiThread(this::showTurnOnBluetoothDialog);
- }
- });
+ .observe(this, this::handleBluetoothStateChange);
model
.getPairingCode()
.observe(
@@ -217,22 +241,8 @@
}
});
model
- .getCurrentDeviceDetails()
- .observe(
- this,
- deviceDetails -> {
- if (deviceDetails == null) {
- return;
- }
- AssociatedDevice device = deviceDetails.getAssociatedDevice();
- if (isStartedByFeature()) {
- // Features always expect activity result when they start AssociationActivity.
- setDeviceToReturn(device);
- finish();
- } else {
- runOnUiThread(this::showAssociatedDeviceDetailFragment);
- }
- });
+ .getAssociatedDevicesDetails()
+ .observe(this, this::handleAssociatedDevicesDetailsChange);
model
.getRemovedDevice()
.observe(
@@ -240,7 +250,6 @@
device -> {
if (device != null) {
runOnUiThread(() -> showDeviceRemovedToast(device.getDeviceName()));
- showCompanionLandingFragment();
}
});
model
@@ -258,6 +267,80 @@
});
}
+ private void handleBluetoothStateChange(int state) {
+ FragmentManager fragmentManager = getSupportFragmentManager();
+
+ // A warning dialog for Bluetooth should only be shown during association.
+ if (state != BluetoothAdapter.STATE_ON
+ && fragmentManager.findFragmentByTag(DEVICE_DETAIL_FRAGMENT_TAG) != null
+ && fragmentManager.findFragmentByTag(DEVICES_LIST_FRAGMENT_TAG) != null) {
+ runOnUiThread(this::showTurnOnBluetoothDialog);
+ }
+ }
+
+ private void handleAssociatedDevicesDetailsChange(List<AssociatedDeviceDetails> devicesDetails) {
+ // An empty list means that there are no more associated devices.
+ if (devicesDetails.isEmpty()) {
+ handleEmptyDeviceList();
+ return;
+ }
+
+ if (isPassengerEnabled) {
+ showAssociatedDevicesList(devicesDetails);
+ } else {
+ // Otherwise, there will only be one associated device.
+ showAssociatedDeviceDetails(devicesDetails.get(0));
+ }
+ }
+
+ private void showAssociatedDeviceDetails(AssociatedDeviceDetails deviceDetails) {
+ if (isStartedByFeature()) {
+ // Features always expect activity result when they start AssociationActivity.
+ setDeviceToReturn(deviceDetails.getAssociatedDevice());
+ finish();
+ return;
+ }
+
+ runOnUiThread(() -> showAssociatedDeviceDetailFragment(deviceDetails));
+ }
+
+ /** Shows a list of associated devices. The provided list should be non-empty. */
+ private void showAssociatedDevicesList(List<AssociatedDeviceDetails> devicesDetails) {
+ if (isStartedByFeature()) {
+ setDeviceToReturn(findFirstDriverDevice(devicesDetails));
+ finish();
+ return;
+ }
+
+ // If the device details are showing, then the details fragment will handle any UI changes.
+ if (isAssociatedDeviceDetailsShowing()) {
+ return;
+ }
+
+ runOnUiThread(this::showAssociatedDevicesListFragment);
+ }
+
+ @Nullable
+ private AssociatedDevice findFirstDriverDevice(List<AssociatedDeviceDetails> devicesDetails) {
+ for (AssociatedDeviceDetails deviceDetails : devicesDetails) {
+ if (deviceDetails.belongsToDriver()) {
+ return deviceDetails.getAssociatedDevice();
+ }
+ }
+ return null;
+ }
+
+ private boolean isAssociatedDeviceDetailsShowing() {
+ FragmentManager fragmentManager = getSupportFragmentManager();
+ for (int i = 0; i < fragmentManager.getBackStackEntryCount(); i++) {
+ if (ASSOCIATED_DEVICE_DETAILS_BACKSTACK_NAME.equals(
+ fragmentManager.getBackStackEntryAt(i).getName())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private void showDeviceRemovedToast(String deviceName) {
Toast.makeText(
getApplicationContext(),
@@ -276,7 +359,23 @@
showProgressBar();
}
+ private void handleEmptyDeviceList() {
+ Fragment fragment =
+ getSupportFragmentManager().findFragmentByTag(COMPANION_LANDING_FRAGMENT_TAG);
+
+ if (fragment != null) {
+ logd(TAG,
+ "Device list is empty, but landing fragment already showing. Nothing more to be done.");
+ return;
+ }
+
+ showCompanionLandingFragment();
+ }
+
private void showCompanionLandingFragment() {
+ maybeClearDetailsFragmentFromBackstack();
+ dismissButtons();
+
if (getResources().getBoolean(R.bool.enable_qr_code)) {
logd(TAG, "Showing LandingFragment with QR code.");
showCompanionQrCodeLandingFragment();
@@ -297,6 +396,7 @@
(CompanionQrCodeLandingFragment)
getSupportFragmentManager().findFragmentByTag(COMPANION_LANDING_FRAGMENT_TAG);
if (fragment != null && fragment.isVisible()) {
+ logd(TAG, "Attempted to show QR code, but fragment is visible already. Ignoring");
return;
}
fragment =
@@ -328,11 +428,37 @@
launchFragment(fragment, ASSOCIATION_ERROR_FRAGMENT_TAG);
}
- private void showAssociatedDeviceDetailFragment() {
- dismissButtons();
+ private void showAssociatedDeviceDetailFragment(AssociatedDeviceDetails deviceDetails) {
+ // If passenger mode is enabled, then the device list fragment is shown first and will take
+ // care of adjusting the toolbar buttons.
+ if (!isPassengerEnabled) {
+ dismissButtons();
+ }
+
hideProgressBar();
- AssociatedDeviceDetailFragment fragment = new AssociatedDeviceDetailFragment();
- launchFragment(fragment, DEVICE_DETAIL_FRAGMENT_TAG);
+
+ AssociatedDeviceDetailFragment fragment =
+ AssociatedDeviceDetailFragment.newInstance(deviceDetails);
+
+ // If passenger mode is enabled, the details should be pushed on top of the list of devices.
+ // Otherwise, it will be the root view.
+ if (isPassengerEnabled) {
+ addFragment(fragment, DEVICE_DETAIL_FRAGMENT_TAG);
+ } else {
+ launchFragment(fragment, DEVICE_DETAIL_FRAGMENT_TAG);
+ }
+
+ showTurnOnBluetoothDialog();
+ }
+
+ private void showAssociatedDevicesListFragment() {
+ showAssociateButton();
+ hideProgressBar();
+
+ AssociatedDevicesListFragment fragment = new AssociatedDevicesListFragment();
+ fragment.setOnListItemClickListener(this::showAssociatedDeviceDetailFragment);
+
+ launchFragment(fragment, DEVICES_LIST_FRAGMENT_TAG);
showTurnOnBluetoothDialog();
}
@@ -389,6 +515,23 @@
});
}
+ private void showAssociateButton() {
+ if (isStartedForSuw) {
+ return;
+ }
+
+ MenuItem associationButton =
+ MenuItem.builder(this)
+ .setTitle(R.string.add_associated_device_button)
+ .setOnClickListener(v -> {
+ dismissButtons();
+ model.startAssociation();
+ })
+ .build();
+
+ toolbar.setMenuItems(Arrays.asList(associationButton));
+ }
+
private void showProgressBar() {
if (isStartedForSuw) {
carSetupWizardLayout.setProgressBarVisible(true);
@@ -412,6 +555,23 @@
}
}
+ /** Checks for the details page being shown and clears it from the Fragment backstack. */
+ private void maybeClearDetailsFragmentFromBackstack() {
+ // The details page is only added to the backstack if passenger mode is enabled. Also, no need
+ // to clear if the fragment isn't there.
+ if (!isPassengerEnabled
+ || getSupportFragmentManager().findFragmentByTag(DEVICE_DETAIL_FRAGMENT_TAG) == null) {
+ return;
+ }
+
+ // If the details fragment is showing, then it will be on the backstack.
+ FragmentManager fragmentManager = getSupportFragmentManager();
+ for (int i = 0; i < fragmentManager.getBackStackEntryCount(); i++) {
+ fragmentManager.popBackStack();
+ }
+ }
+
+ /** Displays the given {@code fragment} and replaces the current content. */
private void launchFragment(Fragment fragment, String tag) {
getSupportFragmentManager()
.beginTransaction()
@@ -419,6 +579,21 @@
.commit();
}
+ /** Displays the given {@code fragment} and adds it to the backstack. */
+ private void addFragment(Fragment fragment, String tag) {
+ getSupportFragmentManager()
+ .beginTransaction()
+ .setCustomAnimations(
+ R.anim.fade_in,
+ R.anim.fade_out,
+ R.anim.fade_in,
+ R.anim.fade_out
+ )
+ .replace(R.id.fragment_container, fragment, tag)
+ .addToBackStack(ASSOCIATED_DEVICE_DETAILS_BACKSTACK_NAME)
+ .commit();
+ }
+
private void retryAssociation() {
dismissButtons();
showProgressBar();
diff --git a/companiondevice/src/com/google/android/companiondevicesupport/AssociationErrorFragment.java b/companiondevice/src/com/google/android/companiondevicesupport/AssociationErrorFragment.java
index 1c815bc..fe005bd 100644
--- a/companiondevice/src/com/google/android/companiondevicesupport/AssociationErrorFragment.java
+++ b/companiondevice/src/com/google/android/companiondevicesupport/AssociationErrorFragment.java
@@ -50,7 +50,8 @@
new AssociatedDeviceViewModelFactory(
requireActivity().getApplication(),
transportProtocols.contains(TransportProtocols.PROTOCOL_SPP),
- getResources().getString(R.string.ble_device_name_prefix)))
+ getResources().getString(R.string.ble_device_name_prefix),
+ getResources().getBoolean(R.bool.enable_passenger)))
.get(AssociatedDeviceViewModel.class);
model.retryAssociation();
});
diff --git a/companiondevice/src/com/google/android/companiondevicesupport/CompanionLandingFragment.java b/companiondevice/src/com/google/android/companiondevicesupport/CompanionLandingFragment.java
index f7a6f9e..ea7bd21 100644
--- a/companiondevice/src/com/google/android/companiondevicesupport/CompanionLandingFragment.java
+++ b/companiondevice/src/com/google/android/companiondevicesupport/CompanionLandingFragment.java
@@ -75,7 +75,8 @@
new AssociatedDeviceViewModelFactory(
requireActivity().getApplication(),
transportProtocols.contains(TransportProtocols.PROTOCOL_SPP),
- getResources().getString(R.string.ble_device_name_prefix)))
+ getResources().getString(R.string.ble_device_name_prefix),
+ getResources().getBoolean(R.bool.enable_passenger)))
.get(AssociatedDeviceViewModel.class);
TextView connectToCarTextView = view.findViewById(R.id.connect_to_car_text);
model
diff --git a/companiondevice/src/com/google/android/companiondevicesupport/CompanionQrCodeLandingFragment.java b/companiondevice/src/com/google/android/companiondevicesupport/CompanionQrCodeLandingFragment.java
index 200c8e9..b0cf579 100644
--- a/companiondevice/src/com/google/android/companiondevicesupport/CompanionQrCodeLandingFragment.java
+++ b/companiondevice/src/com/google/android/companiondevicesupport/CompanionQrCodeLandingFragment.java
@@ -99,7 +99,8 @@
new AssociatedDeviceViewModelFactory(
requireActivity().getApplication(),
transportProtocols.contains(TransportProtocols.PROTOCOL_SPP),
- getResources().getString(R.string.ble_device_name_prefix)))
+ getResources().getString(R.string.ble_device_name_prefix),
+ getResources().getBoolean(R.bool.enable_passenger)))
.get(AssociatedDeviceViewModel.class);
TextView connectToCarTextView = view.findViewById(R.id.connect_to_car_instruction);
diff --git a/libs/connecteddevice/build.gradle b/libs/connecteddevice/build.gradle
index fb665bd..6105fad 100644
--- a/libs/connecteddevice/build.gradle
+++ b/libs/connecteddevice/build.gradle
@@ -1,6 +1,7 @@
apply plugin: 'com.android.library'
apply plugin: 'com.google.protobuf'
apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-parcelize'
buildscript {
repositories {
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/ConnectedDeviceManager.java b/libs/connecteddevice/src/com/google/android/connecteddevice/ConnectedDeviceManager.java
deleted file mode 100644
index 6cf5668..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/ConnectedDeviceManager.java
+++ /dev/null
@@ -1,922 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice;
-
-import static com.google.android.connecteddevice.util.SafeLog.logd;
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-import static com.google.android.connecteddevice.util.SafeLog.logw;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-import com.google.android.connecteddevice.connection.AssociationCallback;
-import com.google.android.connecteddevice.connection.CarBluetoothManager;
-import com.google.android.connecteddevice.model.AssociatedDevice;
-import com.google.android.connecteddevice.model.ConnectedDevice;
-import com.google.android.connecteddevice.model.DeviceMessage;
-import com.google.android.connecteddevice.model.Errors;
-import com.google.android.connecteddevice.model.StartAssociationResponse;
-import com.google.android.connecteddevice.oob.OobChannel;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage.AssociatedDeviceCallback;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage.OnAssociatedDevicesRetrievedListener;
-import com.google.android.connecteddevice.util.ByteUtils;
-import com.google.android.connecteddevice.util.EventLog;
-import com.google.android.connecteddevice.util.SafeConsumer;
-import com.google.android.connecteddevice.util.ThreadSafeCallbacks;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CopyOnWriteArraySet;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/** Manager of devices connected to the car. */
-@SuppressWarnings("AndroidConcurrentHashMap") // Targeting sdk version 29
-public class ConnectedDeviceManager {
-
- private static final String TAG = "ConnectedDeviceManager";
-
- // Device name length is limited by available bytes in BLE advertisement data packet.
- //
- // BLE advertisement limits data packet length to 31
- // Currently we send:
- // - 18 bytes for 16 chars UUID: 16 bytes + 2 bytes for header;
- // - 3 bytes for advertisement being connectable;
- // which leaves 10 bytes.
- // Subtracting 2 bytes used by header, we have 8 bytes for device name.
- // The device name length defined here should be smaller than the limit 8.
- private static final int DEVICE_NAME_LENGTH = 2;
-
- private final ConnectedDeviceStorage storage;
-
- private final CarBluetoothManager carBluetoothManager;
-
- /**
- * The {@link Executor} that will handle tasks linked with connecting to a remote device; this
- * includes advertising for association and reconnection with an associated device.
- */
- private final Executor connectionExecutor;
-
- private final Executor storageExecutor;
-
- private final ThreadSafeCallbacks<DeviceAssociationCallback> deviceAssociationCallbacks =
- new ThreadSafeCallbacks<>();
-
- private final ThreadSafeCallbacks<ConnectionCallback> activeUserConnectionCallbacks =
- new ThreadSafeCallbacks<>();
-
- private final ThreadSafeCallbacks<ConnectionCallback> allUserConnectionCallbacks =
- new ThreadSafeCallbacks<>();
-
- // deviceId -> (recipientId -> callbacks)
- private final Map<String, Map<UUID, ThreadSafeCallbacks<DeviceCallback>>> deviceCallbacks =
- new ConcurrentHashMap<>();
-
- // deviceId -> device
- private final Map<String, ConnectedDevice> connectedDevices = new ConcurrentHashMap<>();
-
- // recipientId -> (deviceId -> message bytes)
- private final Map<UUID, Map<String, List<DeviceMessage>>> recipientMissedMessages =
- new ConcurrentHashMap<>();
-
- // Recipient ids that received multiple callback registrations indicate that the recipient id
- // has been compromised. Another party now has access the messages intended for that recipient.
- // As a safeguard, that recipient id will be added to this list and blocked from further
- // callback notifications.
- private final Set<UUID> blockedRecipients = new CopyOnWriteArraySet<>();
-
- private final AtomicBoolean isConnectingToUserDevice = new AtomicBoolean(false);
-
- private final AtomicBoolean hasStarted = new AtomicBoolean(false);
-
- private byte[] nameForAssociation;
-
- private AssociationCallback associationCallback;
-
- private MessageDeliveryDelegate messageDeliveryDelegate;
-
- private OobChannel oobChannel;
-
- public ConnectedDeviceManager(
- @NonNull CarBluetoothManager carBluetoothManager, @NonNull ConnectedDeviceStorage storage) {
- this(
- carBluetoothManager,
- storage,
- Executors.newCachedThreadPool(),
- Executors.newSingleThreadExecutor(),
- Executors.newSingleThreadExecutor());
- }
-
- @VisibleForTesting
- ConnectedDeviceManager(
- @NonNull CarBluetoothManager carBluetoothManager,
- @NonNull ConnectedDeviceStorage storage,
- @NonNull Executor connectionExecutor,
- @NonNull Executor callbackExecutor,
- @NonNull Executor storageExecutor) {
- this.storage = storage;
- this.carBluetoothManager = carBluetoothManager;
- this.connectionExecutor = connectionExecutor;
- this.carBluetoothManager.registerCallback(generateCarManagerCallback(), callbackExecutor);
- this.storage.registerAssociatedDeviceCallback(associatedDeviceCallback);
- this.storageExecutor = storageExecutor;
- }
-
- /**
- * Start internal processes and begin discovering devices. Must be called before any connections
- * can be made using {@link #connectToActiveUserDevice()}.
- */
- public void start() {
- if (hasStarted.getAndSet(true)) {
- reset();
- } else {
- logd(TAG, "Starting ConnectedDeviceManager.");
- EventLog.onConnectedDeviceManagerStarted();
- }
- carBluetoothManager.start();
- connectToActiveUserDevice();
- }
-
- /** Reset internal processes and disconnect any active connections. */
- public void reset() {
- logd(TAG, "Resetting ConnectedDeviceManager.");
- for (ConnectedDevice device : connectedDevices.values()) {
- removeConnectedDevice(device.getDeviceId());
- }
- carBluetoothManager.stop();
- isConnectingToUserDevice.set(false);
- if (oobChannel != null) {
- oobChannel.interrupt();
- oobChannel = null;
- }
- associationCallback = null;
- }
-
- /** Returns {@link List<ConnectedDevice>} of devices currently connected. */
- @NonNull
- public List<ConnectedDevice> getActiveUserConnectedDevices() {
- List<ConnectedDevice> activeUserConnectedDevices = new ArrayList<>();
- for (ConnectedDevice device : connectedDevices.values()) {
- if (device.isAssociatedWithDriver()) {
- activeUserConnectedDevices.add(device);
- }
- }
- logd(TAG, "Returned " + activeUserConnectedDevices.size() + " active user devices.");
- return activeUserConnectedDevices;
- }
-
- /**
- * Register a callback for triggered associated device related events.
- *
- * @param callback {@link DeviceAssociationCallback} to register.
- * @param executor {@link Executor} to execute triggers on.
- */
- public void registerDeviceAssociationCallback(
- @NonNull DeviceAssociationCallback callback, @NonNull Executor executor) {
- deviceAssociationCallbacks.add(callback, executor);
- }
-
- /**
- * Unregister a device association callback.
- *
- * @param callback {@link DeviceAssociationCallback} to unregister.
- */
- public void unregisterDeviceAssociationCallback(@NonNull DeviceAssociationCallback callback) {
- deviceAssociationCallbacks.remove(callback);
- }
-
- /**
- * Register a callback for manager triggered connection events for only the currently active
- * user's devices.
- *
- * @param callback {@link ConnectionCallback} to register.
- * @param executor {@link Executor} to execute triggers on.
- */
- public void registerActiveUserConnectionCallback(
- @NonNull ConnectionCallback callback, @NonNull Executor executor) {
- activeUserConnectionCallbacks.add(callback, executor);
- logd(TAG, "ActiveUserConnectionCallback registered.");
- }
-
- /**
- * Unregister a connection callback from manager.
- *
- * @param callback {@link ConnectionCallback} to unregister.
- */
- public void unregisterConnectionCallback(ConnectionCallback callback) {
- activeUserConnectionCallbacks.remove(callback);
- allUserConnectionCallbacks.remove(callback);
- }
-
- /** Connect to a device for the active user if available. */
- @VisibleForTesting
- void connectToActiveUserDevice() {
- connectionExecutor.execute(
- () -> {
- logd(TAG, "Received request to connect to active user's device.");
- connectToActiveUserDeviceInternal();
- });
- }
-
- private void connectToActiveUserDeviceInternal() {
- boolean isLockAcquired = isConnectingToUserDevice.compareAndSet(false, true);
- if (!isLockAcquired) {
- logd(
- TAG,
- "A request has already been made to connect to this user's device. "
- + "Ignoring redundant request.");
- return;
- }
- List<AssociatedDevice> userDevices = storage.getDriverAssociatedDevices();
- if (userDevices.isEmpty()) {
- logw(TAG, "No devices associated with active user. Ignoring.");
- isConnectingToUserDevice.set(false);
- return;
- }
-
- // Only currently support one device per user for fast association, so take the
- // first one.
- AssociatedDevice userDevice = userDevices.get(0);
- if (!userDevice.isConnectionEnabled()) {
- logd(TAG, "Connection is disabled on device " + userDevice + ".");
- isConnectingToUserDevice.set(false);
- return;
- }
- if (connectedDevices.containsKey(userDevice.getDeviceId())) {
- logd(TAG, "Device has already been connected. No need to attempt connection " + "again.");
- isConnectingToUserDevice.set(false);
- return;
- }
- EventLog.onStartDeviceSearchStarted();
- carBluetoothManager.connectToDevice(UUID.fromString(userDevice.getDeviceId()));
- }
-
- /**
- * Start the association with a new device.
- *
- * @param callback Callback for association events.
- */
- public void startAssociation(@NonNull AssociationCallback callback) {
- associationCallback = callback;
- connectionExecutor.execute(
- () -> {
- logd(TAG, "Received request to start association.");
- carBluetoothManager.startAssociation(
- getNameForAssociation(), internalAssociationCallback);
- });
- }
-
- /** Stop the association with any device. */
- public void stopAssociation(@NonNull AssociationCallback callback) {
- if (associationCallback != callback) {
- logd(TAG, "Stop association called with unrecognized callback. Ignoring.");
- return;
- }
- logd(TAG, "Stopping association.");
- associationCallback = null;
- carBluetoothManager.stopAssociation();
- if (oobChannel != null) {
- oobChannel.interrupt();
- }
- oobChannel = null;
- }
-
- /**
- * Retrieves devices associated with the active user and notifies the given {@link
- * OnAssociatedDevicesRetrievedListener}.
- */
- public void retrieveActiveUserAssociatedDevices(OnAssociatedDevicesRetrievedListener listener) {
- storageExecutor.execute(
- () -> listener.onAssociatedDevicesRetrieved(storage.getDriverAssociatedDevices()));
- }
-
- /** Notify that the user has accepted a pairing code or any out-of-band confirmation. */
- public void notifyOutOfBandAccepted() {
- carBluetoothManager.notifyOutOfBandAccepted();
- }
-
- /**
- * Remove the associated device with the given device identifier for the current user.
- *
- * @param deviceId Device identifier.
- */
- public void removeActiveUserAssociatedDevice(@NonNull String deviceId) {
- storageExecutor.execute(() -> storage.removeAssociatedDevice(deviceId));
- disconnectDevice(deviceId);
- }
-
- /**
- * Enable connection on an associated device.
- *
- * @param deviceId Device identifier.
- */
- public void enableAssociatedDeviceConnection(@NonNull String deviceId) {
- logd(TAG, "enableAssociatedDeviceConnection() called on " + deviceId);
- storageExecutor.execute(
- () -> {
- storage.updateAssociatedDeviceConnectionEnabled(
- deviceId, /* isConnectionEnabled= */ true);
- connectToActiveUserDevice();
- });
- }
-
- /**
- * Disable connection on an associated device.
- *
- * @param deviceId Device identifier.
- */
- public void disableAssociatedDeviceConnection(@NonNull String deviceId) {
- logd(TAG, "disableAssociatedDeviceConnection() called on " + deviceId);
- storageExecutor.execute(
- () -> {
- storage.updateAssociatedDeviceConnectionEnabled(
- deviceId, /* isConnectionEnabled= */ false);
- disconnectDevice(deviceId);
- isConnectingToUserDevice.set(false);
- });
- }
-
- private void disconnectDevice(String deviceId) {
- ConnectedDevice device = connectedDevices.get(deviceId);
- if (device != null) {
- carBluetoothManager.disconnectDevice(deviceId);
- removeConnectedDevice(deviceId);
- }
- }
-
- /**
- * Register a callback for a specific device and recipient.
- *
- * @param device {@link ConnectedDevice} to register triggers on.
- * @param recipientId {@link UUID} to register as recipient of.
- * @param callback {@link DeviceCallback} to register.
- * @param executor {@link Executor} on which to execute callback.
- */
- public void registerDeviceCallback(
- @NonNull ConnectedDevice device,
- @NonNull UUID recipientId,
- @NonNull DeviceCallback callback,
- @NonNull Executor executor) {
- if (isRecipientDenyListed(recipientId)) {
- notifyOfBlocking(device, recipientId, callback, executor);
- return;
- }
- logd(
- TAG,
- "New callback registered on device "
- + device.getDeviceId()
- + " for recipient "
- + recipientId);
- String deviceId = device.getDeviceId();
- Map<UUID, ThreadSafeCallbacks<DeviceCallback>> recipientCallbacks =
- deviceCallbacks.computeIfAbsent(deviceId, key -> new ConcurrentHashMap<>());
-
- // Device already has a callback registered with this recipient UUID. For the
- // protection of the user, this UUID is now deny listed from future subscriptions
- // and the original subscription is notified and removed.
- if (recipientCallbacks.containsKey(recipientId)) {
- denyListRecipient(deviceId, recipientId);
- notifyOfBlocking(device, recipientId, callback, executor);
- return;
- }
-
- ThreadSafeCallbacks<DeviceCallback> newCallbacks = new ThreadSafeCallbacks<>();
- newCallbacks.add(callback, executor);
- recipientCallbacks.put(recipientId, newCallbacks);
-
- List<DeviceMessage> messages = popMissedMessages(recipientId, device.getDeviceId());
- if (messages != null) {
- for (DeviceMessage message : messages) {
- newCallbacks.invoke(deviceCallback -> deviceCallback.onMessageReceived(device, message));
- }
- }
- }
-
- /**
- * Set the delegate for message delivery operations.
- *
- * @param delegate The {@link MessageDeliveryDelegate} to set. {@code null} to unset.
- */
- public void setMessageDeliveryDelegate(@Nullable MessageDeliveryDelegate delegate) {
- messageDeliveryDelegate = delegate;
- }
-
- private static void notifyOfBlocking(
- @NonNull ConnectedDevice device,
- @NonNull UUID recipientId,
- @NonNull DeviceCallback callback,
- @NonNull Executor executor) {
- loge(
- TAG,
- "Multiple callbacks registered for recipient "
- + recipientId
- + "! Your recipient id is no longer secure and has been blocked from future use.");
- executor.execute(
- () -> callback.onDeviceError(device, Errors.DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED));
- }
-
- private void saveMissedMessage(@NonNull String deviceId, @NonNull DeviceMessage message) {
- // Store last message in case recipient registers callbacks in the future.
- UUID recipientId = message.getRecipient();
- logd(
- TAG,
- "No recipient registered for device "
- + deviceId
- + " and recipient "
- + recipientId
- + " combination. Saving message.");
- recipientMissedMessages
- .computeIfAbsent(recipientId, __ -> new HashMap<>())
- .computeIfAbsent(deviceId, __ -> new ArrayList<>())
- .add(message);
- }
-
- /**
- * Remove all messages sent for this device prior to a {@link DeviceCallback} being registered.
- *
- * @param recipientId Recipient's id
- * @param deviceId Device id
- * @return The missed {@code DeviceMessage}s, or {@code null} if no messages were missed.
- */
- @Nullable
- private List<DeviceMessage> popMissedMessages(
- @NonNull UUID recipientId,
- @NonNull String deviceId) {
- Map<String, List<DeviceMessage>> missedMessages = recipientMissedMessages.get(recipientId);
- if (missedMessages == null) {
- return null;
- }
-
- return missedMessages.remove(deviceId);
- }
-
- /**
- * Unregister callback from device events.
- *
- * @param device {@link ConnectedDevice} callback was registered on.
- * @param recipientId {@link UUID} callback was registered under.
- * @param callback {@link DeviceCallback} to unregister.
- */
- public void unregisterDeviceCallback(
- @NonNull ConnectedDevice device,
- @NonNull UUID recipientId,
- @NonNull DeviceCallback callback) {
- logd(
- TAG,
- "Device callback unregistered on device "
- + device.getDeviceId()
- + " for "
- + "recipient "
- + recipientId
- + ".");
-
- Map<UUID, ThreadSafeCallbacks<DeviceCallback>> recipientCallbacks =
- deviceCallbacks.get(device.getDeviceId());
- if (recipientCallbacks == null) {
- return;
- }
- ThreadSafeCallbacks<DeviceCallback> callbacks = recipientCallbacks.get(recipientId);
- if (callbacks == null) {
- return;
- }
-
- callbacks.remove(callback);
- if (callbacks.size() == 0) {
- recipientCallbacks.remove(recipientId);
- }
- }
-
- /**
- * Securely send message to a device.
- *
- * @param device {@link ConnectedDevice} to send the message to.
- * @param deviceMessage Message to send to device.
- * @throws IllegalStateException Secure channel has not been established.
- */
- public void sendMessage(@NonNull ConnectedDevice device, @NonNull DeviceMessage deviceMessage) {
- String deviceId = device.getDeviceId();
- boolean isEncrypted = deviceMessage.isMessageEncrypted();
- logd(
- TAG,
- "Sending new message to device "
- + deviceId
- + " for "
- + deviceMessage.getRecipient()
- + " containing "
- + deviceMessage.getMessage().length
- + ". Message will be sent securely: "
- + isEncrypted
- + ".");
-
- ConnectedDevice connectedDevice = connectedDevices.get(deviceId);
- if (connectedDevice == null) {
- loge(TAG, "Attempted to send message to unknown device " + deviceId + ". Ignoring.");
- return;
- }
-
- if (isEncrypted && !connectedDevice.hasSecureChannel()) {
- throw new IllegalStateException(
- "Cannot send a message securely to device that has not "
- + "established a secure channel.");
- }
-
- carBluetoothManager.sendMessage(deviceId, deviceMessage);
- }
-
- private boolean isRecipientDenyListed(UUID recipientId) {
- return blockedRecipients.contains(recipientId);
- }
-
- private void denyListRecipient(@NonNull String deviceId, @NonNull UUID recipientId) {
- Map<UUID, ThreadSafeCallbacks<DeviceCallback>> recipientCallbacks =
- deviceCallbacks.get(deviceId);
- if (recipientCallbacks == null) {
- // Should never happen, but null-safety check.
- return;
- }
-
- ThreadSafeCallbacks<DeviceCallback> existingCallback = recipientCallbacks.get(recipientId);
- if (existingCallback == null) {
- // Should never happen, but null-safety check.
- return;
- }
-
- ConnectedDevice connectedDevice = connectedDevices.get(deviceId);
- if (connectedDevice != null) {
- recipientCallbacks
- .get(recipientId)
- .invoke(
- callback ->
- callback.onDeviceError(
- connectedDevice, Errors.DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED));
- }
-
- recipientCallbacks.remove(recipientId);
- blockedRecipients.add(recipientId);
- }
-
- @VisibleForTesting
- void addConnectedDevice(@NonNull String deviceId) {
- if (connectedDevices.containsKey(deviceId)) {
- // Device already connected. No-op until secure channel established.
- return;
- }
- logd(TAG, "New device with id " + deviceId + " connected.");
- ConnectedDevice connectedDevice =
- new ConnectedDevice(
- deviceId,
- /* deviceName= */ null,
- storage.getDriverAssociatedDeviceIds().contains(deviceId),
- /* hasSecureChannel= */ false);
-
- connectedDevices.put(deviceId, connectedDevice);
- invokeConnectionCallbacks(
- connectedDevice.isAssociatedWithDriver(),
- callback -> callback.onDeviceConnected(connectedDevice));
- }
-
- @VisibleForTesting
- void removeConnectedDevice(@NonNull String deviceId) {
- logd(TAG, "Device " + deviceId + " disconnected.");
- ConnectedDevice connectedDevice = connectedDevices.get(deviceId);
- isConnectingToUserDevice.set(false);
- boolean isAssociated = false;
- if (connectedDevice != null) {
- connectedDevices.remove(deviceId);
- isAssociated = connectedDevice.isAssociatedWithDriver();
- invokeConnectionCallbacks(
- isAssociated, callback -> callback.onDeviceDisconnected(connectedDevice));
- }
-
- if (isAssociated || connectedDevices.isEmpty()) {
- // Try to regain connection to active user's device.
- connectToActiveUserDevice();
- }
- }
-
- @VisibleForTesting
- void onSecureChannelEstablished(@NonNull String deviceId) {
- if (connectedDevices.get(deviceId) == null) {
- loge(TAG, "Secure channel established on unknown device " + deviceId + ".");
- return;
- }
- ConnectedDevice connectedDevice = connectedDevices.get(deviceId);
- ConnectedDevice updatedConnectedDevice =
- new ConnectedDevice(
- connectedDevice.getDeviceId(),
- getConnectedDeviceName(deviceId),
- connectedDevice.isAssociatedWithDriver(),
- /* hasSecureChannel= */ true);
-
- boolean notifyCallbacks = connectedDevices.get(deviceId) != null;
-
- // TODO (b/143088482) Implement interrupt
- // Ignore if central already holds the active device connection and interrupt the
- // connection.
-
- connectedDevices.put(deviceId, updatedConnectedDevice);
- logd(
- TAG,
- "Secure channel established to "
- + deviceId
- + " . Notifying callbacks: "
- + notifyCallbacks
- + ".");
- if (notifyCallbacks) {
- notifyAllDeviceCallbacks(
- deviceId, callback -> callback.onSecureChannelEstablished(updatedConnectedDevice));
- }
- }
-
- @VisibleForTesting
- void onMessageReceived(@NonNull String deviceId, @NonNull DeviceMessage message) {
- logd(
- TAG,
- "New message received from device "
- + deviceId
- + " intended for "
- + message.getRecipient()
- + " containing "
- + message.getMessage().length
- + " bytes.");
-
- ConnectedDevice connectedDevice = connectedDevices.get(deviceId);
- if (connectedDevice == null) {
- logw(
- TAG,
- "Received message from unknown device "
- + deviceId
- + "or to unknown "
- + "recipient "
- + message.getRecipient()
- + ".");
- return;
- }
-
- if (messageDeliveryDelegate != null
- && !messageDeliveryDelegate.shouldDeliverMessageForDevice(connectedDevice)) {
- logw(
- TAG,
- "The message delegate has rejected this message. It will not be "
- + "delivered to the intended recipient.");
- return;
- }
-
- UUID recipientId = message.getRecipient();
- Map<UUID, ThreadSafeCallbacks<DeviceCallback>> deviceCallbacks =
- this.deviceCallbacks.get(deviceId);
- if (deviceCallbacks == null) {
- saveMissedMessage(deviceId, message);
- return;
- }
- ThreadSafeCallbacks<DeviceCallback> recipientCallbacks = deviceCallbacks.get(recipientId);
- if (recipientCallbacks == null) {
- saveMissedMessage(deviceId, message);
- return;
- }
-
- recipientCallbacks.invoke(
- callback -> callback.onMessageReceived(connectedDevice, message));
- }
-
- @VisibleForTesting
- void deviceErrorOccurred(@NonNull String deviceId) {
- ConnectedDevice connectedDevice = connectedDevices.get(deviceId);
- if (connectedDevice == null) {
- logw(TAG, "Failed to establish secure channel on unknown device " + deviceId + ".");
- return;
- }
-
- notifyAllDeviceCallbacks(
- deviceId,
- callback ->
- callback.onDeviceError(connectedDevice, Errors.DEVICE_ERROR_INVALID_SECURITY_KEY));
- }
-
- @VisibleForTesting
- void onAssociationCompleted(@NonNull String deviceId) {
- ConnectedDevice connectedDevice = connectedDevices.get(deviceId);
- if (connectedDevice == null) {
- return;
- }
-
- // The previous device is now obsolete and should be replaced with a new one properly
- // reflecting the state of belonging to the active user and notify features.
- if (connectedDevice.isAssociatedWithDriver()) {
- // Device was already marked as belonging to active user. No need to reissue callbacks.
- return;
- }
- removeConnectedDevice(deviceId);
- addConnectedDevice(deviceId);
- }
-
- @Nullable
- private String getConnectedDeviceName(@NonNull String deviceId) {
- ConnectedDevice device = connectedDevices.get(deviceId);
- if (device == null) {
- return null;
- }
- String deviceName = device.getDeviceName();
- if (deviceName != null) {
- return deviceName;
- }
- AssociatedDevice associatedDevice = storage.getAssociatedDevice(deviceId);
- if (associatedDevice == null) {
- return null;
- }
- return associatedDevice.getDeviceName();
- }
-
- private void invokeConnectionCallbacks(
- boolean belongsToActiveUser, @NonNull SafeConsumer<ConnectionCallback> notification) {
- logd(
- TAG,
- "Notifying connection callbacks for device belonging to active user "
- + belongsToActiveUser
- + ".");
- if (belongsToActiveUser) {
- activeUserConnectionCallbacks.invoke(notification);
- }
- allUserConnectionCallbacks.invoke(notification);
- }
-
- private void notifyAllDeviceCallbacks(
- @NonNull String deviceId, @NonNull SafeConsumer<DeviceCallback> notification) {
- logd(TAG, "Notifying all device callbacks for device " + deviceId + ".");
- Map<UUID, ThreadSafeCallbacks<DeviceCallback>> deviceCallbacks =
- this.deviceCallbacks.get(deviceId);
- if (deviceCallbacks == null) {
- return;
- }
-
- for (ThreadSafeCallbacks<DeviceCallback> callbacks : deviceCallbacks.values()) {
- callbacks.invoke(notification);
- }
- }
-
- /**
- * Returns the name that should be used for the device during association.
- *
- * <p>The returned name will be a randomized byte array.
- */
- @NonNull
- private byte[] getNameForAssociation() {
- if (nameForAssociation == null) {
- nameForAssociation = ByteUtils.randomBytes(DEVICE_NAME_LENGTH);
- }
- return nameForAssociation;
- }
-
- @NonNull
- private CarBluetoothManager.Callback generateCarManagerCallback() {
- return new CarBluetoothManager.Callback() {
- @Override
- public void onDeviceConnected(String deviceId) {
- addConnectedDevice(deviceId);
- }
-
- @Override
- public void onDeviceDisconnected(String deviceId) {
- removeConnectedDevice(deviceId);
- }
-
- @Override
- public void onSecureChannelEstablished(String deviceId) {
- EventLog.onSecureChannelEstablished();
- ConnectedDeviceManager.this.onSecureChannelEstablished(deviceId);
- }
-
- @Override
- public void onMessageReceived(String deviceId, DeviceMessage message) {
- ConnectedDeviceManager.this.onMessageReceived(deviceId, message);
- }
-
- @Override
- public void onSecureChannelError(String deviceId) {
- deviceErrorOccurred(deviceId);
- }
- };
- }
-
- private final AssociationCallback internalAssociationCallback =
- new AssociationCallback() {
- @Override
- public void onAssociationStartSuccess(StartAssociationResponse response) {
- if (associationCallback != null) {
- associationCallback.onAssociationStartSuccess(response);
- }
- }
-
- @Override
- public void onAssociationStartFailure() {
- if (associationCallback != null) {
- associationCallback.onAssociationStartFailure();
- }
- }
-
- @Override
- public void onAssociationError(int error) {
- if (associationCallback != null) {
- associationCallback.onAssociationError(error);
- }
- }
-
- @Override
- public void onVerificationCodeAvailable(String code) {
- if (associationCallback != null) {
- associationCallback.onVerificationCodeAvailable(code);
- }
- }
-
- @Override
- public void onAssociationCompleted(String deviceId) {
- if (associationCallback != null) {
- associationCallback.onAssociationCompleted(deviceId);
- }
- ConnectedDeviceManager.this.onAssociationCompleted(deviceId);
- }
- };
-
- private final AssociatedDeviceCallback associatedDeviceCallback =
- new AssociatedDeviceCallback() {
- @Override
- public void onAssociatedDeviceAdded(AssociatedDevice device) {
- deviceAssociationCallbacks.invoke(callback -> callback.onAssociatedDeviceAdded(device));
- }
-
- @Override
- public void onAssociatedDeviceRemoved(AssociatedDevice device) {
- deviceAssociationCallbacks.invoke(callback -> callback.onAssociatedDeviceRemoved(device));
- logd(TAG, "Successfully removed associated device " + device + ".");
- }
-
- @Override
- public void onAssociatedDeviceUpdated(AssociatedDevice device) {
- deviceAssociationCallbacks.invoke(callback -> callback.onAssociatedDeviceUpdated(device));
- }
- };
-
- /** Callback for triggered connection events from {@link ConnectedDeviceManager}. */
- public interface ConnectionCallback {
- /** Triggered when a new device has connected. */
- void onDeviceConnected(@NonNull ConnectedDevice device);
-
- /** Triggered when a device has disconnected. */
- void onDeviceDisconnected(@NonNull ConnectedDevice device);
- }
-
- /** Triggered device events for a connected device from {@link ConnectedDeviceManager}. */
- public interface DeviceCallback {
- /**
- * Triggered when secure channel has been established on a device. Encrypted messaging now
- * available.
- */
- void onSecureChannelEstablished(@NonNull ConnectedDevice device);
-
- /** Triggered when a new message is received from a device. */
- void onMessageReceived(@NonNull ConnectedDevice device, @NonNull DeviceMessage message);
-
- /** Triggered when an error has occurred for a device. */
- void onDeviceError(@NonNull ConnectedDevice device, @Errors.DeviceError int error);
- }
-
- /** Callback for association device related events. */
- public interface DeviceAssociationCallback {
-
- /** Triggered when an associated device has been added. */
- void onAssociatedDeviceAdded(@NonNull AssociatedDevice device);
-
- /** Triggered when an associated device has been removed. */
- void onAssociatedDeviceRemoved(@NonNull AssociatedDevice device);
-
- /** Triggered when the name of an associated device has been updated. */
- void onAssociatedDeviceUpdated(@NonNull AssociatedDevice device);
- }
-
- /** Delegate for message delivery operations. */
- public interface MessageDeliveryDelegate {
-
- /** Indicate whether a message should be delivered for the specified device. */
- boolean shouldDeliverMessageForDevice(@NonNull ConnectedDevice device);
- }
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/api/CompanionConnector.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/api/CompanionConnector.kt
index c38218b..fec1907 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/api/CompanionConnector.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/api/CompanionConnector.kt
@@ -31,11 +31,8 @@
import com.google.android.companionprotos.SystemQuery
import com.google.android.companionprotos.SystemQueryType
import com.google.android.connecteddevice.api.Connector.AppNameCallback
-import com.google.android.connecteddevice.api.Connector.Companion.ACTION_BIND_ASSOCIATION
import com.google.android.connecteddevice.api.Connector.Companion.ACTION_BIND_FEATURE_COORDINATOR
import com.google.android.connecteddevice.api.Connector.Companion.ACTION_BIND_FEATURE_COORDINATOR_FG
-import com.google.android.connecteddevice.api.Connector.Companion.ACTION_BIND_REMOTE_FEATURE
-import com.google.android.connecteddevice.api.Connector.Companion.ACTION_BIND_REMOTE_FEATURE_FG
import com.google.android.connecteddevice.api.Connector.Companion.SYSTEM_FEATURE_ID
import com.google.android.connecteddevice.api.Connector.Companion.USER_TYPE_ALL
import com.google.android.connecteddevice.api.Connector.Companion.USER_TYPE_DRIVER
@@ -74,12 +71,12 @@
private val isForegroundProcess: Boolean = false,
private val userType: @UserType Int = USER_TYPE_DRIVER
) : Connector {
- private val isLegacyOnly = AtomicBoolean(false)
-
private val retryHandler = Handler(Looper.getMainLooper())
private val loggerId = Logger.getLogger().loggerId
+ private val isPlatformInitialized = AtomicBoolean(false)
+
private val featureCoordinatorAction =
if (isForegroundProcess) {
ACTION_BIND_FEATURE_COORDINATOR_FG
@@ -87,13 +84,6 @@
ACTION_BIND_FEATURE_COORDINATOR
}
- private val connectedDeviceManagerAction =
- if (isForegroundProcess) {
- ACTION_BIND_REMOTE_FEATURE_FG
- } else {
- ACTION_BIND_REMOTE_FEATURE
- }
-
private val featureCoordinatorConnection =
object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
@@ -101,6 +91,7 @@
featureCoordinator =
IFeatureCoordinator.Stub.asInterface(service)
?: throw IllegalStateException("Cannot create wrapper of a null feature coordinator.")
+ logd(TAG, "Feature coordinator is alive: ${featureCoordinator?.asBinder()?.isBinderAlive}")
this@CompanionConnector.onServiceConnected()
}
@@ -114,42 +105,6 @@
}
}
- private val connectedDeviceManagerConnection =
- object : ServiceConnection {
- override fun onServiceConnected(name: ComponentName, service: IBinder) {
- logd(TAG, "ConnectedDeviceManager binding has connected successfully.")
- connectedDeviceManager = IConnectedDeviceManager.Stub.asInterface(service)
- this@CompanionConnector.onServiceConnected()
- }
-
- override fun onServiceDisconnected(name: ComponentName) {
- this@CompanionConnector.onServiceDisconnected()
- }
-
- override fun onNullBinding(name: ComponentName) {
- loge(TAG, "Received a null binding for ConnectedDeviceManager.")
- this@CompanionConnector.onNullBinding()
- }
- }
-
- private val associatedDeviceManagerConnection =
- object : ServiceConnection {
- override fun onServiceConnected(name: ComponentName, service: IBinder) {
- logd(TAG, "AssociatedDeviceManager binding has connected successfully.")
- associatedDeviceManager = IAssociatedDeviceManager.Stub.asInterface(service)
- this@CompanionConnector.onServiceConnected()
- }
-
- override fun onServiceDisconnected(name: ComponentName) {
- this@CompanionConnector.onServiceDisconnected()
- }
-
- override fun onNullBinding(name: ComponentName) {
- loge(TAG, "Received a null binding for AssociatedDeviceManager.")
- this@CompanionConnector.onNullBinding()
- }
- }
-
private val connectionCallback =
object : IConnectionCallback.Stub() {
override fun onDeviceConnected(device: ConnectedDevice) {
@@ -164,7 +119,6 @@
// Only call method once. If featureCoordinator is not null, connectedDeviceManager is a
// wrapper and will result in a double call on featureCoordinator.
aliveFeatureCoordinator?.registerDeviceCallback(device, featureId, deviceCallback)
- ?: aliveConnectedDeviceManager?.registerDeviceCallback(device, featureId, deviceCallback)
}
override fun onDeviceDisconnected(device: ConnectedDevice) {
@@ -176,11 +130,6 @@
// Only call method once. If featureCoordinator is not null, connectedDeviceManager is a
// wrapper and will result in a double call on featureCoordinator.
aliveFeatureCoordinator?.unregisterDeviceCallback(device, featureId, deviceCallback)
- ?: aliveConnectedDeviceManager?.unregisterDeviceCallback(
- device,
- featureId,
- deviceCallback
- )
}
}
@@ -225,7 +174,6 @@
val loggerBytes = Logger.getLogger().toByteArray()
try {
featureCoordinator?.processLogRecords(loggerId, loggerBytes)
- ?: connectedDeviceManager?.processLogRecords(loggerId, loggerBytes)
} catch (e: RemoteException) {
loge(TAG, "Failed to send log records for logger $loggerId.", e)
}
@@ -242,30 +190,7 @@
private var bindAttempts = 0
- /**
- * When bound to a [featureCoordinator], this connector will wrap [connectedDeviceManager] and
- * [associatedDeviceManager] around the coordinator. Otherwise, a legacy binding will be made
- * for both [connectedDeviceManager] and [associatedDeviceManager] separately.
- *
- * Note: legacy support is a temporary state and will be removed once all clients have
- * successfully moved to the [featureCoordinator] binding version.
- */
- @VisibleForTesting
- internal var featureCoordinator: IFeatureCoordinator? = null
- set(value) {
- field = value
- if (value == null) {
- connectedDeviceManager = null
- associatedDeviceManager = null
- return
- }
- connectedDeviceManager = createConnectedDeviceManagerWrapper(value)
- associatedDeviceManager = createAssociatedDeviceManagerWrapper(value)
- }
-
- @VisibleForTesting internal var connectedDeviceManager: IConnectedDeviceManager? = null
-
- @VisibleForTesting internal var associatedDeviceManager: IAssociatedDeviceManager? = null
+ @VisibleForTesting internal var featureCoordinator: IFeatureCoordinator? = null
override var featureId: ParcelUuid? = null
@@ -283,51 +208,32 @@
}
return when (userType) {
USER_TYPE_DRIVER -> aliveFeatureCoordinator?.connectedDevicesForDriver
- ?: aliveConnectedDeviceManager?.activeUserConnectedDevices
USER_TYPE_PASSENGER -> aliveFeatureCoordinator?.connectedDevicesForPassengers
USER_TYPE_ALL -> aliveFeatureCoordinator?.allConnectedDevices
- ?: aliveConnectedDeviceManager?.activeUserConnectedDevices
else -> null
}
?: emptyList()
}
override val isConnected: Boolean
- get() =
- aliveFeatureCoordinator != null ||
- (aliveConnectedDeviceManager != null && aliveAssociatedDeviceManager != null)
+ get() = aliveFeatureCoordinator != null
private val isBound: Boolean
- get() =
- featureCoordinator != null ||
- (connectedDeviceManager != null && associatedDeviceManager != null)
+ get() = featureCoordinator != null
private val aliveFeatureCoordinator
get() = featureCoordinator?.aliveOrNull()
- private val aliveConnectedDeviceManager
- get() = connectedDeviceManager?.aliveOrNull()
-
- private val aliveAssociatedDeviceManager
- get() = associatedDeviceManager?.aliveOrNull()
-
override fun connect() {
logd(TAG, "Initiating connection to companion platform.")
if (isConnected) {
logd(TAG, "Platform is already connected. Skipping binding.")
initializePlatform()
- callback?.onConnected()
return
}
logd(TAG, "Platform is not currently connected. Initiating binding.")
- val intent =
- if (isLegacyOnly.get()) {
- resolveIntent(connectedDeviceManagerAction)
- } else {
- resolveIntent(featureCoordinatorAction)
- ?: resolveIntent(connectedDeviceManagerAction).also { isLegacyOnly.set(true) }
- }
+ val intent = resolveIntent(featureCoordinatorAction)
if (intent == null) {
loge(TAG, "No services found supporting companion device. Aborting.")
@@ -335,15 +241,7 @@
return
}
- val success =
- if (isLegacyOnly.get()) {
- val associationIntent =
- Intent(ACTION_BIND_ASSOCIATION).apply { component = intent.component }
- context.bindService(intent, connectedDeviceManagerConnection, /* flag= */ 0) &&
- context.bindService(associationIntent, associatedDeviceManagerConnection, /* flag= */ 0)
- } else {
- context.bindService(intent, featureCoordinatorConnection, /* flag= */ 0)
- }
+ val success = context.bindService(intent, featureCoordinatorConnection, /* flag= */ 0)
if (success) {
logd(TAG, "Successfully started binding with ${intent.action}.")
return
@@ -364,18 +262,13 @@
val wasConnected = isBound
logd(TAG, "FeatureCoordinator is null: ${featureCoordinator == null} isBound: $isBound")
cleanUpFeatureCoordinator()
- cleanUpConnectedDeviceManager()
- associatedDeviceManager = null
retryHandler.removeCallbacksAndMessages(/* token= */ null)
try {
context.unbindService(featureCoordinatorConnection)
- context.unbindService(connectedDeviceManagerConnection)
- context.unbindService(associatedDeviceManagerConnection)
} catch (e: IllegalArgumentException) {
logw(TAG, "Attempted to unbind an already unbound service.")
}
bindAttempts = 0
- isLegacyOnly.set(false)
if (wasConnected) {
callback?.onDisconnected()
}
@@ -404,35 +297,13 @@
if (featureCoordinator != null) {
featureCoordinator = null
}
- }
-
- private fun cleanUpConnectedDeviceManager() {
- logd(TAG, "Cleaning up ConnectedDeviceManager.")
- aliveConnectedDeviceManager?.let {
- try {
- it.unregisterConnectionCallback(connectionCallback)
- val featureId = featureId
- if (featureId != null) {
- for (device in it.activeUserConnectedDevices) {
- it.unregisterDeviceCallback(device, featureId, deviceCallback)
- }
- }
- it.unregisterDeviceAssociationCallback(deviceAssociationCallback)
- it.unregisterOnLogRequestedListener(loggerId, logRequestedListener)
- } catch (e: RemoteException) {
- loge(TAG, "Error while cleaning up ConnectedDeviceManager.", e)
- }
- }
- connectedDeviceManager = null
+ isPlatformInitialized.set(false)
}
override fun binderForAction(action: String): IBinder? {
return when (action) {
ACTION_BIND_FEATURE_COORDINATOR, ACTION_BIND_FEATURE_COORDINATOR_FG ->
featureCoordinator?.asBinder()
- ACTION_BIND_REMOTE_FEATURE, ACTION_BIND_REMOTE_FEATURE_FG ->
- connectedDeviceManager?.asBinder()
- ACTION_BIND_ASSOCIATION -> associatedDeviceManager?.asBinder()
else -> null
}
}
@@ -596,7 +467,6 @@
val connectedDevices =
try {
aliveFeatureCoordinator?.allConnectedDevices
- ?: aliveConnectedDeviceManager?.activeUserConnectedDevices
} catch (e: RemoteException) {
loge(TAG, "Exception while retrieving connected devices.", e)
null
@@ -642,49 +512,38 @@
override fun startAssociation(callback: IAssociationCallback) {
aliveFeatureCoordinator?.startAssociation(callback)
- ?: aliveAssociatedDeviceManager?.startAssociation(callback)
}
override fun startAssociation(identifier: ParcelUuid, callback: IAssociationCallback) {
- if (isLegacyOnly.get()) {
- loge(TAG, "Called unsupported startAssociationWithIdentifier while in legacy mode.")
- return
- }
aliveFeatureCoordinator?.startAssociationWithIdentifier(callback, identifier)
}
override fun stopAssociation() {
- aliveFeatureCoordinator?.stopAssociation() ?: aliveAssociatedDeviceManager?.stopAssociation()
+ aliveFeatureCoordinator?.stopAssociation()
}
override fun acceptVerification() {
aliveFeatureCoordinator?.acceptVerification()
- ?: aliveAssociatedDeviceManager?.acceptVerification()
}
override fun removeAssociatedDevice(deviceId: String) {
aliveFeatureCoordinator?.removeAssociatedDevice(deviceId)
- ?: aliveAssociatedDeviceManager?.removeAssociatedDevice(deviceId)
}
override fun enableAssociatedDeviceConnection(deviceId: String) {
aliveFeatureCoordinator?.enableAssociatedDeviceConnection(deviceId)
- ?: aliveAssociatedDeviceManager?.enableAssociatedDeviceConnection(deviceId)
}
override fun disableAssociatedDeviceConnection(deviceId: String) {
aliveFeatureCoordinator?.disableAssociatedDeviceConnection(deviceId)
- ?: aliveAssociatedDeviceManager?.disableAssociatedDeviceConnection(deviceId)
}
override fun retrieveAssociatedDevices(listener: IOnAssociatedDevicesRetrievedListener) {
aliveFeatureCoordinator?.retrieveAssociatedDevices(listener)
- ?: aliveAssociatedDeviceManager?.retrievedActiveUserAssociatedDevices(listener)
}
override fun retrieveAssociatedDevicesForDriver(listener: IOnAssociatedDevicesRetrievedListener) {
aliveFeatureCoordinator?.retrieveAssociatedDevicesForDriver(listener)
- ?: aliveAssociatedDeviceManager?.retrievedActiveUserAssociatedDevices(listener)
}
override fun retrieveAssociatedDevicesForPassengers(
@@ -694,15 +553,20 @@
?: listener.onAssociatedDevicesRetrieved(emptyList())
}
+ override fun claimAssociatedDevice(deviceId: String) {
+ aliveFeatureCoordinator?.claimAssociatedDevice(deviceId)
+ }
+
+ override fun removeAssociatedDeviceClaim(deviceId: String) {
+ aliveFeatureCoordinator?.removeAssociatedDeviceClaim(deviceId)
+ }
+
private fun sendMessageInternal(device: ConnectedDevice, message: DeviceMessage) {
aliveFeatureCoordinator?.sendMessage(device, message)
- ?: aliveConnectedDeviceManager?.sendMessage(device, message)
}
private fun onServiceConnected() {
- if (featureCoordinator == null &&
- (connectedDeviceManager == null || associatedDeviceManager == null)
- ) {
+ if (!isConnected) {
logd(
TAG,
"Initialization criteria have not been met yet. Waiting for further service connections " +
@@ -711,15 +575,13 @@
return
}
initializePlatform()
- callback?.onConnected()
}
private fun initializePlatform() {
- initializeFeatureCoordinator()
- initializeConnectedDeviceManager()
- }
-
- private fun initializeFeatureCoordinator() {
+ if (!isPlatformInitialized.compareAndSet(false, true)) {
+ logw(TAG, "Platform is already initialized. Ignoring.")
+ return
+ }
logd(TAG, "Initializing FeatureCoordinator.")
val featureCoordinator = featureCoordinator
if (featureCoordinator == null) {
@@ -763,40 +625,7 @@
} catch (e: RemoteException) {
loge(TAG, "Error while initializing FeatureCoordinator.", e)
}
- }
-
- private fun initializeConnectedDeviceManager() {
- logd(TAG, "Initializing ConnectedDeviceManager.")
- if (featureCoordinator != null) {
- logd(TAG, "FeatureCoordinator has already registered callbacks.")
- return
- }
- val connectedDeviceManager =
- connectedDeviceManager
- ?: throw IllegalStateException(
- "Attempted to initialize the platform with a null ConnectedDeviceManager! This should " +
- "not happen and is indicative of a serious logic flaw."
- )
- try {
- connectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback)
- connectedDeviceManager.registerDeviceAssociationCallback(deviceAssociationCallback)
- connectedDeviceManager.registerOnLogRequestedListener(loggerId, logRequestedListener)
- val activeUserConnectedDevices = connectedDeviceManager.activeUserConnectedDevices
- val featureId = featureId
- for (device in activeUserConnectedDevices) {
- callback?.onDeviceConnected(device)
- if (device.hasSecureChannel()) {
- callback?.onSecureChannelEstablished(device)
- }
- if (featureId == null) {
- logd(TAG, "There is currently no feature id set. Skipping device registration.")
- continue
- }
- connectedDeviceManager.registerDeviceCallback(device, featureId, deviceCallback)
- }
- } catch (e: RemoteException) {
- loge(TAG, "Error while initializing ConnectedDeviceManager.", e)
- }
+ callback?.onConnected()
}
private fun onServiceDisconnected() {
@@ -805,17 +634,8 @@
}
private fun onNullBinding() {
- if (!isLegacyOnly.compareAndSet(false, true)) {
- logd(TAG, "Issuing onFailedToConnect callback after legacy null binding.")
- callback?.onFailedToConnect()
- return
- }
- logd(
- TAG,
- "Service does not support feature coordinator despite defining the action. " +
- "Attempting to bind again in legacy mode."
- )
- connect()
+ logd(TAG, "Issuing onFailedToConnect callback after null binding.")
+ callback?.onFailedToConnect()
}
private fun resolveIntent(action: String): Intent? {
@@ -908,160 +728,6 @@
this.featureCoordinator = featureCoordinator
}
- @JvmStatic
- /** Create a [CompanionConnector] instance with a [connectedDeviceManager] already populated. */
- fun createLocalConnector(
- context: Context,
- userType: @UserType Int,
- connectedDeviceManager: IConnectedDeviceManager,
- associatedDeviceManager: IAssociatedDeviceManager
- ): CompanionConnector =
- CompanionConnector(context, userType = userType).apply {
- this.connectedDeviceManager = connectedDeviceManager
- this.associatedDeviceManager = associatedDeviceManager
- }
-
- /**
- * Generate an [IConnectedDeviceManager] wrapper around the [fromFeatureCoordinator] to support
- * legacy functionality.
- */
- @VisibleForTesting
- internal fun createConnectedDeviceManagerWrapper(
- fromFeatureCoordinator: IFeatureCoordinator
- ): IConnectedDeviceManager {
-
- return object : IConnectedDeviceManager.Stub() {
- @Throws(RemoteException::class)
- override fun getActiveUserConnectedDevices(): List<ConnectedDevice> {
- return fromFeatureCoordinator.connectedDevicesForDriver
- }
-
- @Throws(RemoteException::class)
- override fun registerActiveUserConnectionCallback(callback: IConnectionCallback) {
- fromFeatureCoordinator.registerDriverConnectionCallback(callback)
- }
-
- @Throws(RemoteException::class)
- override fun unregisterConnectionCallback(callback: IConnectionCallback) {
- fromFeatureCoordinator.unregisterConnectionCallback(callback)
- }
-
- @Throws(RemoteException::class)
- override fun registerDeviceCallback(
- connectedDevice: ConnectedDevice,
- recipientId: ParcelUuid,
- callback: IDeviceCallback
- ) {
- fromFeatureCoordinator.registerDeviceCallback(connectedDevice, recipientId, callback)
- }
-
- @Throws(RemoteException::class)
- override fun unregisterDeviceCallback(
- connectedDevice: ConnectedDevice,
- recipientId: ParcelUuid,
- callback: IDeviceCallback
- ) {
- fromFeatureCoordinator.unregisterDeviceCallback(connectedDevice, recipientId, callback)
- }
-
- @Throws(RemoteException::class)
- override fun sendMessage(
- connectedDevice: ConnectedDevice,
- message: DeviceMessage
- ): Boolean {
- return fromFeatureCoordinator.sendMessage(connectedDevice, message)
- }
-
- @Throws(RemoteException::class)
- override fun registerDeviceAssociationCallback(callback: IDeviceAssociationCallback) {
- fromFeatureCoordinator.registerDeviceAssociationCallback(callback)
- }
-
- @Throws(RemoteException::class)
- override fun unregisterDeviceAssociationCallback(callback: IDeviceAssociationCallback) {
- fromFeatureCoordinator.unregisterDeviceAssociationCallback(callback)
- }
-
- @Throws(RemoteException::class)
- override fun registerOnLogRequestedListener(
- loggerId: Int,
- listener: IOnLogRequestedListener
- ) {
- fromFeatureCoordinator.registerOnLogRequestedListener(loggerId, listener)
- }
-
- @Throws(RemoteException::class)
- override fun unregisterOnLogRequestedListener(
- loggerId: Int,
- listener: IOnLogRequestedListener
- ) {
- fromFeatureCoordinator.unregisterOnLogRequestedListener(loggerId, listener)
- }
-
- @Throws(RemoteException::class)
- override fun processLogRecords(loggerId: Int, logRecords: ByteArray) {
- fromFeatureCoordinator.processLogRecords(loggerId, logRecords)
- }
- }
- }
-
- @VisibleForTesting
- internal fun createAssociatedDeviceManagerWrapper(
- fromFeatureCoordinator: IFeatureCoordinator
- ): IAssociatedDeviceManager.Stub {
- return object : IAssociatedDeviceManager.Stub() {
- override fun startAssociation(callback: IAssociationCallback) {
- fromFeatureCoordinator.startAssociation(callback)
- }
-
- override fun stopAssociation() {
- fromFeatureCoordinator.stopAssociation()
- }
-
- override fun retrievedActiveUserAssociatedDevices(
- listener: IOnAssociatedDevicesRetrievedListener
- ) {
- fromFeatureCoordinator.retrieveAssociatedDevicesForDriver(listener)
- }
-
- override fun acceptVerification() {
- fromFeatureCoordinator.acceptVerification()
- }
-
- override fun removeAssociatedDevice(deviceId: String) {
- fromFeatureCoordinator.removeAssociatedDevice(deviceId)
- }
-
- override fun registerDeviceAssociationCallback(callback: IDeviceAssociationCallback) {
- fromFeatureCoordinator.registerDeviceAssociationCallback(callback)
- }
-
- override fun unregisterDeviceAssociationCallback(callback: IDeviceAssociationCallback) {
- fromFeatureCoordinator.unregisterDeviceAssociationCallback(callback)
- }
-
- override fun getActiveUserConnectedDevices(): List<ConnectedDevice> {
- return fromFeatureCoordinator.connectedDevicesForDriver
- }
-
- override fun registerConnectionCallback(callback: IConnectionCallback) {
- fromFeatureCoordinator.registerDriverConnectionCallback(callback)
- }
-
- override fun unregisterConnectionCallback(callback: IConnectionCallback) {
- fromFeatureCoordinator.unregisterConnectionCallback(callback)
- }
-
- override fun enableAssociatedDeviceConnection(deviceId: String) {
- fromFeatureCoordinator.enableAssociatedDeviceConnection(deviceId)
- }
-
- override fun disableAssociatedDeviceConnection(deviceId: String) {
- fromFeatureCoordinator.disableAssociatedDeviceConnection(deviceId)
- }
- }
- }
-
/** A generator of unique IDs for queries. */
private class QueryIdGenerator {
private val messageId = AtomicInteger(0)
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/api/Connector.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/api/Connector.kt
index dc7ff1b..1263ed9 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/api/Connector.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/api/Connector.kt
@@ -135,6 +135,12 @@
*/
fun retrieveAssociatedDevicesForPassengers(listener: IOnAssociatedDevicesRetrievedListener)
+ /** Claim an associated device as belonging to the current user. */
+ fun claimAssociatedDevice(deviceId: String)
+
+ /** Remove the claim on the identified associated device. */
+ fun removeAssociatedDeviceClaim(deviceId: String)
+
/** Callbacks invoked on connection events. */
interface Callback {
/** Invoked when a connection has been successfully established. */
@@ -215,21 +221,6 @@
companion object {
/**
- * When a client calls [Context.bindService] to get the [IConnectedDeviceManager], this action
- * is required in the param [Intent].
- */
- const val ACTION_BIND_REMOTE_FEATURE =
- "com.google.android.connecteddevice.api.BIND_REMOTE_FEATURE"
-
- /**
- * When a client calls [Context.bindService] to get the [IConnectedDeviceManager] from a service
- * running in the foreground user. Any process that resides outside of the service host
- * application must use this action in its [Intent].
- */
- const val ACTION_BIND_REMOTE_FEATURE_FG =
- "com.google.android.connecteddevice.api.BIND_REMOTE_FEATURE_FG"
-
- /**
* When a client calls [Context.bindService] to get the [IFeatureCoordinator], this action is
* required in the param [Intent].
*/
@@ -244,12 +235,6 @@
const val ACTION_BIND_FEATURE_COORDINATOR_FG =
"com.google.android.connecteddevice.api.BIND_FEATURE_COORDINATOR_FG"
- /**
- * When a client calls [Context.bindService] to get the [IAssociatedDeviceManager], this action
- * is required in the param [Intent].
- */
- const val ACTION_BIND_ASSOCIATION = "com.google.android.connecteddevice.BIND_ASSOCIATION"
-
/** Type associated with a driver's device. */
const val USER_TYPE_DRIVER = 1 shl 0
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/api/IAssociatedDeviceManager.aidl b/libs/connecteddevice/src/com/google/android/connecteddevice/api/IAssociatedDeviceManager.aidl
deleted file mode 100644
index 34f9866..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/api/IAssociatedDeviceManager.aidl
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice.api;
-
-import com.google.android.connecteddevice.api.IAssociationCallback;
-import com.google.android.connecteddevice.api.IConnectionCallback;
-import com.google.android.connecteddevice.api.IDeviceAssociationCallback;
-import com.google.android.connecteddevice.api.IOnAssociatedDevicesRetrievedListener;
-import com.google.android.connecteddevice.model.AssociatedDevice;
-import com.google.android.connecteddevice.model.ConnectedDevice;
-import com.google.android.connecteddevice.model.OobEligibleDevice;
-
-/** Manager of devices associated with the car. */
-interface IAssociatedDeviceManager {
- /**
- * Starts the association with a new device.
- *
- * @param callback {@link IAssociationCallback} to be notified for association event.
- */
- void startAssociation(in IAssociationCallback callback);
-
- /** Stops the association with current device. */
- void stopAssociation();
-
- /**
- * Retrieves the devices associated with the active user from the database.
- *
- * @param listener {@link IOnAssociatedDevicesRetrievedListener} that will
- * be notified when the associated devices are retrieved.
- */
- void retrievedActiveUserAssociatedDevices(in IOnAssociatedDevicesRetrievedListener listener);
-
- /** Confirms the paring code. */
- void acceptVerification();
-
- /** Remove the associated device of the given identifier for the active user. */
- void removeAssociatedDevice(in String deviceId);
-
- /**
- * Register a callback for associated device related events.
- *
- * @param callback {@link IDeviceAssociationCallback} to register.
- */
- void registerDeviceAssociationCallback(in IDeviceAssociationCallback callback);
-
- /** Unregister the device association callback from manager.
- *
- * @param callback {@link IDeviceAssociationCallback} to unregister.
- */
- void unregisterDeviceAssociationCallback(in IDeviceAssociationCallback callback);
-
- /** Returns {@link List<ConnectedDevice>} of devices currently connected. */
- List<ConnectedDevice> getActiveUserConnectedDevices();
-
- /**
- * Register a callback for connection events for only the currently active user's devices.
- *
- * @param callback {@link IConnectionCallback} to register.
- */
- void registerConnectionCallback(in IConnectionCallback callback);
-
- /**
- * Unregister the connection callback from manager.
- *
- * @param callback {@link IConnectionCallback} to unregister.
- */
- void unregisterConnectionCallback(in IConnectionCallback callback);
-
- /** Enable connection on the associated device with the given identifier. */
- void enableAssociatedDeviceConnection(in String deviceId);
-
- /** Disable connection on the associated device with the given identifier. */
- void disableAssociatedDeviceConnection(in String deviceId);
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/api/IConnectedDeviceManager.aidl b/libs/connecteddevice/src/com/google/android/connecteddevice/api/IConnectedDeviceManager.aidl
deleted file mode 100644
index ef0606f..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/api/IConnectedDeviceManager.aidl
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice.api;
-
-import android.os.ParcelUuid;
-
-import com.google.android.connecteddevice.api.IConnectionCallback;
-import com.google.android.connecteddevice.api.IDeviceAssociationCallback;
-import com.google.android.connecteddevice.api.IDeviceCallback;
-import com.google.android.connecteddevice.api.IOnLogRequestedListener;
-import com.google.android.connecteddevice.model.ConnectedDevice;
-import com.google.android.connecteddevice.model.DeviceMessage;
-
-/** Manager of devices connected to the car. */
-interface IConnectedDeviceManager {
-
- /** Returns {@link List<ConnectedDevice>} of devices currently connected. */
- List<ConnectedDevice> getActiveUserConnectedDevices();
-
- /**
- * Register a callback for manager triggered connection events for only the currently active
- * user's devices.
- *
- * @param callback {@link IConnectionCallback} to register.
- */
- void registerActiveUserConnectionCallback(in IConnectionCallback callback);
-
- /**
- * Unregister a connection callback from manager.
- *
- * @param callback {@link IConnectionCallback} to unregister.
- */
- void unregisterConnectionCallback(in IConnectionCallback callback);
-
- /**
- * Register a callback for a specific connectedDevice and recipient.
- *
- * @param connectedDevice {@link ConnectedDevice} to register triggers on.
- * @param recipientId {@link ParcelUuid} to register as recipient of.
- * @param callback {@link IDeviceCallback} to register.
- */
- void registerDeviceCallback(in ConnectedDevice connectedDevice, in ParcelUuid recipientId,
- in IDeviceCallback callback);
-
- /**
- * Unregister callback from connectedDevice events.
- *
- * @param connectedDevice {@link ConnectedDevice} callback was registered on.
- * @param recipientId {@link ParcelUuid} callback was registered under.
- * @param callback {@link IDeviceCallback} to unregister.
- */
- void unregisterDeviceCallback(in ConnectedDevice connectedDevice, in ParcelUuid recipientId,
- in IDeviceCallback callback);
-
- /**
- * Send a message to a connected device.
- *
- * @param connectedDevice {@link ConnectedDevice} to send the message to.
- * @param message Message to send.
- */
- boolean sendMessage(in ConnectedDevice connectedDevice, in DeviceMessage message);
-
-
- /**
- * Register a callback for associated device related events.
- *
- * @param callback {@link IDeviceAssociationCallback} to register.
- */
- void registerDeviceAssociationCallback(in IDeviceAssociationCallback callback);
-
- /**
- * Unregister a device association callback from manager.
- *
- * @param callback {@link IDeviceAssociationCallback} to unregister.
- */
- void unregisterDeviceAssociationCallback(in IDeviceAssociationCallback callback);
-
- /**
- * Register listener for the log request with the given logger identifier.
- *
- * @param listener {@link IOnLogRequestedListener} to register.
- */
- void registerOnLogRequestedListener(in int loggerId, in IOnLogRequestedListener listener);
-
- /**
- * Unregister listener from the log request.
- *
- * @param listener {@link IOnLogRequestedListener} to unregister.
- */
- void unregisterOnLogRequestedListener(in int loggerId, in IOnLogRequestedListener listener);
-
- /**
- * Process log records in the logger with given id so it can be combined with log records
- * from other loggers.
- *
- * @param loggerId of the logger.
- * @param logRecords to process.
- */
- void processLogRecords(in int loggerId, in byte[] logRecords);
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/AssociationSecureChannel.java b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/AssociationSecureChannel.java
deleted file mode 100644
index 38be547..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/AssociationSecureChannel.java
+++ /dev/null
@@ -1,224 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice.connection;
-
-import static com.google.android.connecteddevice.util.SafeLog.logd;
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-import static com.google.android.encryptionrunner.EncryptionRunnerFactory.newRunner;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
-import com.google.android.connecteddevice.util.ByteUtils;
-import com.google.android.encryptionrunner.EncryptionRunner;
-import com.google.android.encryptionrunner.EncryptionRunnerFactory.EncryptionRunnerType;
-import com.google.android.encryptionrunner.HandshakeException;
-import com.google.android.encryptionrunner.HandshakeMessage;
-import com.google.android.encryptionrunner.HandshakeMessage.HandshakeState;
-import com.google.android.encryptionrunner.Key;
-import java.security.InvalidParameterException;
-import java.util.Arrays;
-import java.util.UUID;
-
-/** A secure channel established with the association flow. */
-public class AssociationSecureChannel extends SecureChannel {
-
- private static final String TAG = "AssociationSecureChannel";
-
- private static final int DEVICE_ID_BYTES = 16;
-
- private final ConnectedDeviceStorage storage;
-
- private ShowVerificationCodeListener showVerificationCodeListener;
-
- @HandshakeState private int state = HandshakeState.UNKNOWN;
-
- private Key pendingKey;
-
- private String deviceId;
-
- public AssociationSecureChannel(DeviceMessageStream stream, ConnectedDeviceStorage storage) {
- this(stream, storage, newRunner(EncryptionRunnerType.UKEY2));
- }
-
- AssociationSecureChannel(
- DeviceMessageStream stream,
- ConnectedDeviceStorage storage,
- EncryptionRunner encryptionRunner) {
- super(stream, encryptionRunner);
- encryptionRunner.setIsReconnect(false);
- this.storage = storage;
- }
-
- @Override
- void processHandshake(@NonNull byte[] message) throws HandshakeException {
- switch (state) {
- case HandshakeState.UNKNOWN:
- processHandshakeUnknown(message);
- break;
- case HandshakeState.IN_PROGRESS:
- processHandshakeInProgress(message);
- break;
- case HandshakeState.FINISHED:
- processHandshakeDeviceIdAndSecret(message);
- break;
- default:
- loge(TAG, "Encountered unexpected handshake state: " + state + ".");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
- }
- }
-
- private void processHandshakeUnknown(@NonNull byte[] message) throws HandshakeException {
- logd(TAG, "Responding to handshake init request.");
- HandshakeMessage handshakeMessage = getEncryptionRunner().respondToInitRequest(message);
- state = handshakeMessage.getHandshakeState();
- sendHandshakeMessage(handshakeMessage.getNextMessage(), /* isEncrypted= */ false);
- }
-
- private void processHandshakeInProgress(@NonNull byte[] message) throws HandshakeException {
- logd(TAG, "Continuing handshake.");
- HandshakeMessage handshakeMessage = getEncryptionRunner().continueHandshake(message);
- state = handshakeMessage.getHandshakeState();
- if (state != HandshakeState.VERIFICATION_NEEDED) {
- loge(
- TAG,
- "processHandshakeInProgress: Encountered unexpected handshake state: " + state + ".");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
- return;
- }
-
- String code = handshakeMessage.getVerificationCode();
- if (code == null) {
- loge(TAG, "Unable to get verification code.");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_VERIFICATION);
- return;
- }
- processVerificationCode(code);
- }
-
- private void processVerificationCode(@NonNull String code) {
- if (showVerificationCodeListener == null) {
- loge(
- TAG,
- "No verification code listener has been set. Unable to display verification code to "
- + "user.");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
- return;
- }
-
- logd(TAG, "Showing pairing code: " + code);
- showVerificationCodeListener.showVerificationCode(code);
- }
-
- private void processHandshakeDeviceIdAndSecret(@NonNull byte[] message) {
- UUID deviceId = ByteUtils.bytesToUUID(Arrays.copyOf(message, DEVICE_ID_BYTES));
- if (deviceId == null) {
- loge(TAG, "Received invalid device id. Aborting.");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_DEVICE_ID);
- return;
- }
- this.deviceId = deviceId.toString();
- notifyCallback(callback -> callback.onDeviceIdReceived(this.deviceId));
-
- storage.saveEncryptionKey(this.deviceId, pendingKey.asBytes());
- pendingKey = null;
- try {
- storage.saveChallengeSecret(
- this.deviceId, Arrays.copyOfRange(message, DEVICE_ID_BYTES, message.length));
- } catch (InvalidParameterException e) {
- loge(TAG, "Error saving challenge secret.", e);
- notifySecureChannelFailure(CHANNEL_ERROR_STORAGE_ERROR);
- return;
- }
-
- notifyCallback(Callback::onSecureChannelEstablished);
- }
-
- /** Set the listener that notifies to show verification code. {@code null} to clear. */
- public void setShowVerificationCodeListener(@Nullable ShowVerificationCodeListener listener) {
- showVerificationCodeListener = listener;
- }
-
- @VisibleForTesting
- @Nullable
- public ShowVerificationCodeListener getShowVerificationCodeListener() {
- return showVerificationCodeListener;
- }
-
- /**
- * Called by the client to notify that the user has accepted a pairing code or any out-of-band
- * confirmation, and send confirmation signals to remote bluetooth device.
- */
- public void notifyOutOfBandAccepted() {
- HandshakeMessage message;
- try {
- message = getEncryptionRunner().notifyPinVerified();
- } catch (HandshakeException e) {
- loge(TAG, "Error during PIN verification", e);
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_VERIFICATION);
- return;
- }
- if (message.getHandshakeState() != HandshakeState.FINISHED) {
- loge(
- TAG,
- "Handshake not finished after calling verify PIN. Instead got "
- + "state: "
- + message.getHandshakeState()
- + ".");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
- return;
- }
-
- Key localKey = message.getKey();
- if (localKey == null) {
- loge(TAG, "Unable to finish association, generated key is null.");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_ENCRYPTION_KEY);
- return;
- }
- state = message.getHandshakeState();
- setEncryptionKey(localKey);
- pendingKey = localKey;
- logd(TAG, "Pairing code successfully verified.");
- sendUniqueIdToClient();
- }
-
- private void sendUniqueIdToClient() {
- UUID uniqueId = storage.getUniqueId();
- logd(TAG, "Sending car's device id of " + uniqueId + " to device.");
- sendHandshakeMessage(ByteUtils.uuidToBytes(uniqueId), /* isEncrypted= */ true);
- }
-
- @HandshakeState
- int getState() {
- return state;
- }
-
- void setState(@HandshakeState int state) {
- this.state = state;
- }
-
- /** Listener that will be invoked to display verification code. */
- public interface ShowVerificationCodeListener {
- /**
- * Invoke when a verification need to be displayed during device association.
- *
- * @param code The verification code to show.
- */
- void showVerificationCode(@NonNull String code);
- }
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/CarBluetoothManager.java b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/CarBluetoothManager.java
deleted file mode 100644
index f5b8613..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/CarBluetoothManager.java
+++ /dev/null
@@ -1,605 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice.connection;
-
-import static com.google.android.companionprotos.CapabilitiesExchangeProto.CapabilitiesExchange.OobChannelType.BT_RFCOMM;
-import static com.google.android.connecteddevice.model.Errors.DEVICE_ERROR_INVALID_HANDSHAKE;
-import static com.google.android.connecteddevice.util.SafeLog.logd;
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-import static com.google.android.connecteddevice.util.SafeLog.logw;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGatt;
-import androidx.annotation.CallSuper;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-import com.google.android.companionprotos.CapabilitiesExchangeProto.CapabilitiesExchange.OobChannelType;
-import com.google.android.connecteddevice.model.AssociatedDevice;
-import com.google.android.connecteddevice.model.DeviceMessage;
-import com.google.android.connecteddevice.model.OobEligibleDevice;
-import com.google.android.connecteddevice.oob.OobChannel;
-import com.google.android.connecteddevice.oob.OobConnectionManager;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
-import com.google.android.connecteddevice.util.ThreadSafeCallbacks;
-import com.google.common.collect.ImmutableList;
-import java.util.List;
-import java.util.Objects;
-import java.util.UUID;
-import java.util.concurrent.CopyOnWriteArraySet;
-import java.util.concurrent.Executor;
-import java.util.stream.Collectors;
-
-/**
- * Generic manager for a car that keeps track of connected devices and their associated callbacks.
- */
-public abstract class CarBluetoothManager {
-
- private static final String TAG = "CarBluetoothManager";
-
- private static final ImmutableList<OobChannelType> SUPPORTED_OOB_CAPABILITIES =
- ImmutableList.of(BT_RFCOMM);
-
- protected final ConnectedDeviceStorage storage;
-
- protected final CopyOnWriteArraySet<ConnectedRemoteDevice> connectedDevices =
- new CopyOnWriteArraySet<>();
-
- protected final ThreadSafeCallbacks<Callback> callbacks = new ThreadSafeCallbacks<>();
-
- protected final OobChannel oobChannel;
-
- private final boolean isCompressionEnabled;
-
- private final OobConnectionManager oobConnectionManager;
-
- private final boolean isCapabilitiesEligible;
-
- private String clientDeviceName;
-
- private String clientDeviceAddress;
-
- private AssociationCallback associationCallback;
-
- protected CarBluetoothManager(
- @NonNull ConnectedDeviceStorage connectedDeviceStorage,
- boolean enableCompression,
- @Nullable OobChannel oobChannel,
- boolean isCapabilitiesEligible) {
- storage = connectedDeviceStorage;
- isCompressionEnabled = enableCompression;
- oobConnectionManager = new OobConnectionManager();
- this.oobChannel = oobChannel;
- this.isCapabilitiesEligible = isCapabilitiesEligible;
- }
-
- protected CarBluetoothManager(
- @NonNull ConnectedDeviceStorage connectedDeviceStorage,
- boolean enableCompression,
- boolean isCapabilitiesEligible) {
- this(connectedDeviceStorage, enableCompression, /* oobChannel= */ null, isCapabilitiesEligible);
- }
-
- /** Attempt to connect to device with provided id. */
- public void connectToDevice(@NonNull UUID deviceId) {
- for (ConnectedRemoteDevice device : connectedDevices) {
- if (UUID.fromString(device.deviceId).equals(deviceId)) {
- logd(TAG, "Already connected to device " + deviceId + ".");
- // Already connected to this device. Ignore requests to connect again.
- return;
- }
- }
- // Clear any previous session before starting a new one.
- reset();
- initiateConnectionToDevice(deviceId);
- }
-
- /** Start to connect to associated devices */
- public abstract void initiateConnectionToDevice(@NonNull UUID deviceId);
-
- /** Start the association with a new device */
- public abstract void startAssociation(
- @NonNull byte[] nameForAssociation, @NonNull AssociationCallback callback);
-
- /** Disconnect the provided device from this manager. */
- public abstract void disconnectDevice(@NonNull String deviceId);
-
- /** Get current {@link OobConnectionManager}. */
- @Nullable
- @VisibleForTesting
- public OobConnectionManager getOobConnectionManager() {
- return oobConnectionManager;
- }
-
- /** Get current {@link AssociationCallback}. */
- @Nullable
- public AssociationCallback getAssociationCallback() {
- return associationCallback;
- }
-
- /** Set current {@link AssociationCallback}. */
- public void setAssociationCallback(@Nullable AssociationCallback callback) {
- this.associationCallback = callback;
- }
-
- /** Set the value of the client device name */
- public void setClientDeviceName(String deviceName) {
- clientDeviceName = deviceName;
- }
-
- /** Set the value of client device's mac address */
- public void setClientDeviceAddress(String macAddress) {
- clientDeviceAddress = macAddress;
- }
-
- /** Initialize and start the manager. */
- @CallSuper
- public void start() {}
-
- /** Stop the manager and clean up. */
- public void stop() {
- for (ConnectedRemoteDevice device : connectedDevices) {
- if (device.gatt != null) {
- device.gatt.close();
- }
- }
- connectedDevices.clear();
- }
-
- /** Stop the association process with any device. */
- public void stopAssociation() {
- if (!isAssociating()) {
- return;
- }
- reset();
- }
-
- /** Register a {@link Callback} to be notified on the {@link Executor}. */
- public void registerCallback(@NonNull Callback callback, @NonNull Executor executor) {
- callbacks.add(callback, executor);
- }
-
- /**
- * Unregister a callback.
- *
- * @param callback The {@link Callback} to unregister.
- */
- public void unregisterCallback(@NonNull Callback callback) {
- callbacks.remove(callback);
- }
-
- /**
- * Send a message to a connected device.
- *
- * @param deviceId Id of connected device.
- * @param message {@link DeviceMessage} to send.
- */
- public void sendMessage(@NonNull String deviceId, @NonNull DeviceMessage message) {
- ConnectedRemoteDevice device = getConnectedDevice(deviceId);
- if (device == null) {
- logw(TAG, "Attempted to send message to unknown device $deviceId. Ignored.");
- return;
- }
-
- sendMessage(device, message);
- }
-
- /**
- * Send a message to a connected device.
- *
- * @param device The connected {@link ConnectedRemoteDevice}.
- * @param message {@link DeviceMessage} to send.
- */
- public void sendMessage(@NonNull ConnectedRemoteDevice device, @NonNull DeviceMessage message) {
- String deviceId = device.deviceId;
- if (deviceId == null) {
- deviceId = "Unidentified device";
- }
-
- logd(TAG, "Writing " + message.getMessage().length + " bytes to " + deviceId + ".");
- device.secureChannel.sendClientMessage(message);
- }
-
- /** Clean manager status and callbacks. */
- @CallSuper
- public void reset() {
- // Clean up state prior to issuing callbacks to avoid race conditions.
- List<String> callbackIds =
- connectedDevices
- .stream()
- .map((device) -> device.deviceId)
- .filter(Objects::nonNull)
- .collect(Collectors.toList());
- clientDeviceAddress = null;
- clientDeviceName = null;
- connectedDevices.clear();
- oobConnectionManager.reset();
- associationCallback = null;
- for (String deviceId : callbackIds) {
- callbacks.invoke(callback -> callback.onDeviceDisconnected(deviceId));
- }
- }
-
- /** Notify that the user has accepted a pairing code or other out-of-band confirmation. */
- public void notifyOutOfBandAccepted() {
- if (getConnectedDevice() == null) {
- disconnectWithError(
- "Null connected device found when out-of-band confirmation " + "received.");
- return;
- }
-
- AssociationSecureChannel secureChannel =
- (AssociationSecureChannel) getConnectedDevice().secureChannel;
- if (secureChannel == null) {
- disconnectWithError(
- "Null SecureBleChannel found for the current connected device "
- + "when out-of-band confirmation received.");
- return;
- }
-
- secureChannel.notifyOutOfBandAccepted();
- }
-
- /** Returns the secure channel of current connected device. */
- @Nullable
- public SecureChannel getConnectedDeviceChannel() {
- ConnectedRemoteDevice connectedDevice = getConnectedDevice();
- if (connectedDevice == null) {
- return null;
- }
-
- return connectedDevice.secureChannel;
- }
-
- /** Return the current connected device. */
- @Nullable
- protected final ConnectedRemoteDevice getConnectedDevice() {
- if (connectedDevices.isEmpty()) {
- return null;
- }
- // Directly return the next because there will only be one device connected at one time.
- return connectedDevices.iterator().next();
- }
-
- /**
- * Get the {@link ConnectedRemoteDevice} with matching {@link BluetoothGatt} if available. Returns
- * {@code null} if no matches are found.
- */
- @Nullable
- protected final ConnectedRemoteDevice getConnectedDevice(@NonNull BluetoothGatt gatt) {
- for (ConnectedRemoteDevice device : connectedDevices) {
- if (device.gatt == gatt) {
- return device;
- }
- }
-
- return null;
- }
-
- /**
- * Get the {@link ConnectedRemoteDevice} with matching {@link BluetoothDevice} if available.
- * Returns {@code null} if no matches are found.
- */
- @Nullable
- protected final ConnectedRemoteDevice getConnectedDevice(@NonNull BluetoothDevice device) {
- for (ConnectedRemoteDevice connectedDevice : connectedDevices) {
- if (device.equals(connectedDevice.device)) {
- return connectedDevice;
- }
- }
-
- return null;
- }
-
- /**
- * Get the {@link ConnectedRemoteDevice} with matching device id if available. Returns {@code
- * null} if no matches are found.
- */
- @Nullable
- protected final ConnectedRemoteDevice getConnectedDevice(@NonNull String deviceId) {
- for (ConnectedRemoteDevice device : connectedDevices) {
- if (deviceId.equals(device.deviceId)) {
- return device;
- }
- }
-
- return null;
- }
-
- protected final void onDeviceConnectedForAssociation(
- DeviceMessageStream secureStream, ConnectedRemoteDevice connectedDevice) {
-
- BluetoothDevice device = connectedDevice.device;
- ImmutableList<OobChannelType> supportedOobCapabilities = SUPPORTED_OOB_CAPABILITIES;
- if (!isCapabilitiesEligible) {
- logd(TAG, "Capabilities exchange is not supported.");
- supportedOobCapabilities = ImmutableList.of();
- }
-
- // TODO(b/180743873) Hardcoding to true until ConnectionResolver is capable of resolving all
- // the way to a SecureChannel. At that time we will introduce another flag for whether we
- // are reconnecting and move this logic internal to the resolver itself.
- ConnectionResolver connectionResolver =
- new ConnectionResolver(
- secureStream,
- /* isCapabilitiesEligible= */ true,
- supportedOobCapabilities);
-
- connectionResolver.resolveConnection(
- resolvedConnection -> {
- ImmutableList<OobChannelType> oobChannels = resolvedConnection.oobChannelTypes();
-
- if (oobChannel == null || oobChannels == null || !oobChannels.contains(BT_RFCOMM)) {
- logd(TAG, "Oob channels list are empty, fallback to normal association " + "flow.");
- addConnectedDeviceWithAssociationSecureChannel(
- connectedDevice, new AssociationSecureChannel(secureStream, storage));
- return;
- }
-
- logd(TAG, "Completing out of band data exchange.");
- oobChannel.completeOobDataExchange(
- new OobEligibleDevice(device.getAddress(), OobEligibleDevice.OOB_TYPE_BLUETOOTH),
- new OobChannel.Callback() {
- @Override
- public void onOobExchangeSuccess() {
- logd(
- TAG,
- "Out of band exchange succeeded. Proceeding to association with"
- + " device.");
- if (getOobConnectionManager().startOobExchange(oobChannel)) {
- addConnectedDeviceWithAssociationSecureChannel(
- connectedDevice,
- new OobAssociationSecureChannel(
- secureStream, storage, getOobConnectionManager()));
- } else {
- logw(
- TAG,
- "Out of band exchange failed, falling back to Numeric" + " Comparison.");
- addConnectedDeviceWithAssociationSecureChannel(
- connectedDevice, new AssociationSecureChannel(secureStream, storage));
- }
- }
-
- @Override
- public void onOobExchangeFailure() {
- logw(
- TAG, "Out of band exchange failed, falling back to Numeric" + " Comparison.");
- addConnectedDeviceWithAssociationSecureChannel(
- connectedDevice, new AssociationSecureChannel(secureStream, storage));
- }
- });
- });
- }
-
- protected void onVerificationCodeAvailable(String code) {
- if (!isAssociating()) {
- loge(TAG, "No valid callback for association.");
- return;
- }
- getAssociationCallback().onVerificationCodeAvailable(code);
- }
- /** Add the {@link ConnectedRemoteDevice} that has connected. */
- protected final void addConnectedDevice(@NonNull ConnectedRemoteDevice device) {
- device.secureChannel.setCompressionEnabled(isCompressionEnabled);
- connectedDevices.add(device);
- }
-
- /** Return the number of devices currently connected. */
- protected final int getConnectedDevicesCount() {
- return connectedDevices.size();
- }
-
- /** Remove [@link BleDevice} that has been disconnected. */
- protected final void removeConnectedDevice(@NonNull ConnectedRemoteDevice device) {
- connectedDevices.remove(device);
- }
-
- /** Return [@code true} if the manager is currently in an association process. */
- protected final boolean isAssociating() {
- return getAssociationCallback() != null;
- }
-
- /**
- * Set the device id of {@link ConnectedRemoteDevice} and then notify device connected callback.
- *
- * @param deviceId The device id received from remote device.
- */
- protected final void setDeviceIdAndNotifyCallbacks(@NonNull String deviceId) {
- logd(TAG, "Setting device id: " + deviceId);
- ConnectedRemoteDevice connectedDevice = getConnectedDevice();
- if (connectedDevice == null) {
- disconnectWithError("Null connected device found when device id received.");
- return;
- }
-
- connectedDevice.deviceId = deviceId;
- callbacks.invoke(callback -> callback.onDeviceConnected(deviceId));
- }
-
- /** Log error which causes the disconnect with {@link Exception} and notify callbacks. */
- protected final void disconnectWithError(@NonNull String errorMessage, @Nullable Exception e) {
- loge(TAG, errorMessage, e);
- if (isAssociating()) {
- associationCallback.onAssociationError(DEVICE_ERROR_INVALID_HANDSHAKE);
- }
- reset();
- }
-
- /** Log error which cause the disconnection and notify callbacks. */
- protected final void disconnectWithError(@NonNull String errorMessage) {
- disconnectWithError(errorMessage, null);
- }
-
- private void addConnectedDeviceWithAssociationSecureChannel(
- ConnectedRemoteDevice connectedDevice,
- AssociationSecureChannel secureChannel) {
- secureChannel.setShowVerificationCodeListener(this::onVerificationCodeAvailable);
- secureChannel.registerCallback(secureChannelCallback);
- connectedDevice.secureChannel = secureChannel;
- addConnectedDevice(connectedDevice);
- }
-
- protected final SecureChannel.Callback secureChannelCallback =
- new SecureChannel.Callback() {
- @Override
- public void onSecureChannelEstablished() {
- ConnectedRemoteDevice connectedDevice = getConnectedDevice();
- if (connectedDevice == null || connectedDevice.deviceId == null) {
- disconnectWithError("Null device id found when secure channel " + "established.");
- return;
- }
- String deviceId = connectedDevice.deviceId;
- if (clientDeviceAddress == null) {
- disconnectWithError("Null device address found when secure channel " + "established.");
- return;
- }
-
- if (isAssociating()) {
- logd(
- TAG,
- "Secure channel established for un-associated device. Saving "
- + "association of that device for current user.");
- storage.addAssociatedDeviceForDriver(
- new AssociatedDevice(
- deviceId,
- clientDeviceAddress,
- clientDeviceName,
- /* isConnectionEnabled= */ true));
- if (associationCallback != null) {
- associationCallback.onAssociationCompleted(deviceId);
- oobConnectionManager.reset();
- setAssociationCallback(null);
- }
- }
- callbacks.invoke(callback -> callback.onSecureChannelEstablished(deviceId));
- }
-
- @Override
- public void onEstablishSecureChannelFailure(int error) {
- ConnectedRemoteDevice connectedDevice = getConnectedDevice();
- if (connectedDevice == null || connectedDevice.deviceId == null) {
- disconnectWithError(
- "Null device id found when secure channel " + "failed to establish.");
- return;
- }
- String deviceId = connectedDevice.deviceId;
- callbacks.invoke(callback -> callback.onSecureChannelError(deviceId));
-
- if (isAssociating()) {
- associationCallback.onAssociationError(error);
- oobConnectionManager.reset();
- }
-
- disconnectWithError("Error while establishing secure connection.");
- }
-
- @Override
- public void onMessageReceived(DeviceMessage deviceMessage) {
- ConnectedRemoteDevice connectedDevice = getConnectedDevice();
- if (connectedDevice == null || connectedDevice.deviceId == null) {
- disconnectWithError("Null device id found when message received.");
- return;
- }
-
- logd(
- TAG,
- "Received new message from "
- + connectedDevice.deviceId
- + " with "
- + deviceMessage.getMessage().length
- + " bytes in its "
- + "payload. Notifying "
- + callbacks.size()
- + " callbacks.");
- callbacks.invoke(
- callback -> callback.onMessageReceived(connectedDevice.deviceId, deviceMessage));
- }
-
- @Override
- public void onMessageReceivedError(Exception exception) {
- disconnectWithError("Error while receiving message.");
- }
-
- @Override
- public void onDeviceIdReceived(String deviceId) {
- setDeviceIdAndNotifyCallbacks(deviceId);
- }
- };
-
- /** State for a connected device. */
- public enum ConnectedDeviceState {
- CONNECTING,
- PENDING_VERIFICATION,
- CONNECTED,
- UNKNOWN
- }
-
- /** Container class to hold information about a connected device. */
- public static class ConnectedRemoteDevice {
- @NonNull public BluetoothDevice device;
- @Nullable public BluetoothGatt gatt;
- @NonNull public ConnectedDeviceState state;
- @Nullable public String deviceId;
- @Nullable public SecureChannel secureChannel;
-
- public ConnectedRemoteDevice(@NonNull BluetoothDevice device, @Nullable BluetoothGatt gatt) {
- this.device = device;
- this.gatt = gatt;
- state = ConnectedDeviceState.UNKNOWN;
- }
- }
-
- /** Callback for triggered events from {@link CarBluetoothManager}. */
- public interface Callback {
- /**
- * Triggered when device is connected and device id retrieved. Device is now ready to receive
- * messages.
- *
- * @param deviceId Id of device that has connected.
- */
- void onDeviceConnected(@NonNull String deviceId);
-
- /**
- * Triggered when device is disconnected.
- *
- * @param deviceId Id of device that has disconnected.
- */
- void onDeviceDisconnected(@NonNull String deviceId);
-
- /**
- * Triggered when device has established encryption for secure communication.
- *
- * @param deviceId Id of device that has established encryption.
- */
- void onSecureChannelEstablished(@NonNull String deviceId);
-
- /**
- * Triggered when a new message is received.
- *
- * @param deviceId Id of the device that sent the message.
- * @param message {@link DeviceMessage} received.
- */
- void onMessageReceived(@NonNull String deviceId, @NonNull DeviceMessage message);
-
- /**
- * Triggered when an error when establishing the secure channel.
- *
- * @param deviceId Id of the device that experienced the error.
- */
- void onSecureChannelError(@NonNull String deviceId);
- }
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ChannelResolver.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ChannelResolver.kt
index 28660a9..309dddc 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ChannelResolver.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ChannelResolver.kt
@@ -15,8 +15,6 @@
*/
package com.google.android.connecteddevice.connection
-import com.google.android.companionprotos.CapabilitiesExchangeProto.CapabilitiesExchange
-import com.google.android.companionprotos.CapabilitiesExchangeProto.CapabilitiesExchange.OobChannelType
import com.google.android.companionprotos.VersionExchangeProto.VersionExchange
import com.google.android.connecteddevice.model.DeviceMessage
import com.google.android.connecteddevice.model.DeviceMessage.OperationType.ENCRYPTION_HANDSHAKE
@@ -24,22 +22,18 @@
import com.google.android.connecteddevice.oob.OobRunner
import com.google.android.connecteddevice.storage.ConnectedDeviceStorage
import com.google.android.connecteddevice.transport.ConnectionProtocol
-import com.google.android.connecteddevice.transport.ConnectionProtocol.DataReceivedListener
+import com.google.android.connecteddevice.transport.IDataReceivedListener
import com.google.android.connecteddevice.transport.ProtocolDevice
import com.google.android.connecteddevice.util.SafeLog.logd
import com.google.android.connecteddevice.util.SafeLog.loge
-import com.google.android.connecteddevice.util.SafeLog.logw
import com.google.android.encryptionrunner.EncryptionRunner
import com.google.android.encryptionrunner.EncryptionRunnerFactory.EncryptionRunnerType
import com.google.android.encryptionrunner.EncryptionRunnerFactory.newRunner
-import com.google.common.util.concurrent.MoreExecutors.directExecutor
import com.google.protobuf.ExtensionRegistryLite
import com.google.protobuf.InvalidProtocolBufferException
import java.lang.Integer.min
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
-import java.util.concurrent.Executor
-import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
import kotlin.properties.Delegates
@@ -53,10 +47,8 @@
*
* Channel resolving for association:
* 1. Process the first received data as version exchange message, send back car versions.
- * 2. If capability exchange is supported, process the second received data as capability exchange
- * message and send back car capabilities.
- * 3. Create a [ProtocolStream] with the current [ConnectionProtocol].
- * 4. Create a [MultiProtocolSecureChannel], add [ProtocolStream]s for all connected
+ * 2. Create a [ProtocolStream] with the current [ConnectionProtocol].
+ * 3. Create a [MultiProtocolSecureChannel], add [ProtocolStream]s for all connected
* [ConnectionProtocol]s and notify that the channel has been resolved.
*
* Channel resolving for reconnect:
@@ -77,16 +69,13 @@
) {
private val currentDevice: AtomicReference<ProtocolDevice?> =
AtomicReference<ProtocolDevice?>(null)
- private val capabilityExchanged = AtomicBoolean(false)
- private val executor: Executor = directExecutor()
// protocol device -> listeners. This map tracks listeners for channel resolving on connected
// protocols.
- private val protocolDevices = ConcurrentHashMap<ProtocolDevice, DataReceivedListener>()
+ private val protocolDevices = ConcurrentHashMap<ProtocolDevice, IDataReceivedListener>()
private var isReconnect by Delegates.notNull<Boolean>()
private var deviceId: UUID? = null
private var challenge: ByteArray? = null
private var oobRunner: OobRunner? = null
- private var resolvedSecurityVersion: Int = 0
/** Resolves the [MultiProtocolSecureChannel] for a reconnect with [deviceId] and [challenge]. */
fun resolveReconnect(deviceId: UUID, challenge: ByteArray) {
@@ -114,24 +103,20 @@
fun addProtocolDevice(device: ProtocolDevice) {
logd(TAG, "Registering listener for received data received.")
val dataReceivedListener =
- object : DataReceivedListener {
+ object : IDataReceivedListener.Stub() {
override fun onDataReceived(protocolId: String, data: ByteArray) {
if (currentDevice.compareAndSet(null, device)) {
processVersionMessage(data, device)
return
}
- if (isReconnect || !capabilityExchanged.compareAndSet(false, true)) {
- logd(
- TAG,
- "Channel already resolving with connection ${currentDevice.get()?.protocolId}. " +
- "Ignoring data received from connection $protocolId."
- )
- return
- }
- processCapabilityMessage(data, device)
+ logd(
+ TAG,
+ "Channel already resolving with connection ${currentDevice.get()?.protocolId}. " +
+ "Ignoring data received from connection $protocolId."
+ )
}
}
- device.protocol.registerDataReceivedListener(device.protocolId, dataReceivedListener, executor)
+ device.protocol.registerDataReceivedListener(device.protocolId, dataReceivedListener)
protocolDevices[device] = dataReceivedListener
}
@@ -157,7 +142,7 @@
}
val resolvedMessageVersion = min(MAX_MESSAGING_VERSION, version.maxSupportedMessagingVersion)
- resolvedSecurityVersion = min(MAX_SECURITY_VERSION, version.maxSupportedSecurityVersion)
+ val resolvedSecurityVersion = min(MAX_SECURITY_VERSION, version.maxSupportedSecurityVersion)
logd(
TAG,
"Resolved to messaging version $resolvedMessageVersion and security version " +
@@ -170,87 +155,21 @@
.setMaxSupportedSecurityVersion(MAX_SECURITY_VERSION)
.setMinSupportedSecurityVersion(MIN_SECURITY_VERSION)
.build()
- device.protocol.sendData(device.protocolId, carVersion.toByteArray())
+ device.protocol.sendData(device.protocolId, carVersion.toByteArray(), callback = null)
- if (!isReconnect && resolvedSecurityVersion == SECURITY_VERSION_FOR_CAPABILITIES_EXCHANGE) {
- logd(TAG, "Waiting for capabilities exchange message.")
- return
+ if (!isReconnect) {
+ logd(TAG, "Send OOB data to remote device $device.")
+ oobRunner?.sendOobData(device)
}
- logd(TAG, "Capabilities exchange not supported, resolving stream.")
- if (!isReconnect && resolvedSecurityVersion > SECURITY_VERSION_FOR_CAPABILITIES_EXCHANGE) {
- logd(TAG, "Start OOB data exchange.")
- val supportedChannels =
- oobRunner?.supportedTypes?.map { it.asOobChannelType() } ?: emptyList<OobChannelType>()
- oobRunner?.startOobDataExchange(device, supportedChannels, resolvedSecurityVersion)
- }
resolveStream(device)
}
- private fun processCapabilityMessage(message: ByteArray, device: ProtocolDevice) {
- val oobRunner = this.oobRunner
- if (oobRunner == null) {
- logw(TAG, "OOB runner not set during capabilities exchange. Sending empty list.")
- sendCapabilities(emptyList<OobChannelType>(), device)
- resolveStream(device)
- return
- }
-
- val capabilities =
- try {
- CapabilitiesExchange.parseFrom(message, ExtensionRegistryLite.getEmptyRegistry())
- } catch (e: InvalidProtocolBufferException) {
- onError("Received a malformed capabilities exchange message. Disconnecting.", e)
- return
- }
- val sharedOobChannels = capabilities.supportedOobChannelsList.toMutableList()
- logd(TAG, "Received ${sharedOobChannels.size} OOB channels from device.")
- val onDeviceChannels = oobRunner.supportedTypes.map { it.asOobChannelType() }
- logd(TAG, "Car supported ${onDeviceChannels.size} OOB channels.")
- sharedOobChannels.retainAll(onDeviceChannels.toMutableList())
- sendCapabilities(sharedOobChannels, device)
-
- if (!oobRunner.startOobDataExchange(
- device,
- sharedOobChannels,
- resolvedSecurityVersion,
- generateOobRunnerCallback(device)
- )
- ) {
- logw(TAG, "Failed to start OOB data exchange, fallback to numeric comparison.")
- resolveStream(device)
- }
- }
-
- private fun generateOobRunnerCallback(device: ProtocolDevice) =
- object : OobRunner.Callback {
- override fun onOobDataExchangeSuccess() {
- logd(TAG, "OOB data exchange completed successfully.")
- encryptionRunner = newRunner(EncryptionRunnerType.OOB_UKEY2)
- resolveStream(device)
- }
-
- override fun onOobDataExchangeFailure() {
- logw(TAG, "Failed to exchange OOB data, fallback to numeric comparison.")
- resolveStream(device)
- }
- }
- private fun sendCapabilities(sharedOobChannels: List<OobChannelType>, device: ProtocolDevice) {
- logd(TAG, "Found ${sharedOobChannels.size} common OOB channels.")
- val carCapabilities =
- CapabilitiesExchange.newBuilder().addAllSupportedOobChannels(sharedOobChannels).build()
- device.protocol.sendData(device.protocolId, carCapabilities.toByteArray())
- }
-
- private fun String.asOobChannelType(): OobChannelType =
- OobChannelType.values().firstOrNull { it.name.equals(this) }
- ?: OobChannelType.OOB_CHANNEL_UNKNOWN
-
private fun resolveStream(device: ProtocolDevice) {
protocolDevices.remove(device)?.let { listener ->
device.protocol.unregisterDataReceivedListener(device.protocolId, listener)
}
- val stream = streamFactory.createProtocolStream(device, executor)
+ val stream = streamFactory.createProtocolStream(device)
if (isReconnect && device.protocol.isDeviceVerificationRequired) {
verifyDevice(stream)
return
@@ -315,22 +234,7 @@
private fun resolveChannel(stream: ProtocolStream) {
encryptionRunner.setIsReconnect(isReconnect)
val channel =
- if (resolvedSecurityVersion > SECURITY_VERSION_FOR_CAPABILITIES_EXCHANGE)
- MultiProtocolSecureChannelV4(
- stream,
- storage,
- encryptionRunner,
- oobRunner,
- deviceId?.toString()
- )
- else
- MultiProtocolSecureChannelPreV4(
- stream,
- storage,
- encryptionRunner,
- oobRunner,
- deviceId?.toString()
- )
+ MultiProtocolSecureChannel(stream, storage, encryptionRunner, oobRunner, deviceId?.toString())
protocolDevices.keys.forEach { channel.addStream(ProtocolStream(it)) }
clearDataReceivedListeners()
callback.onChannelResolved(channel)
@@ -361,9 +265,8 @@
companion object {
internal const val MIN_MESSAGING_VERSION = 3
internal const val MAX_MESSAGING_VERSION = 3
- internal const val MIN_SECURITY_VERSION = 2
+ internal const val MIN_SECURITY_VERSION = 4
internal const val MAX_SECURITY_VERSION = 4
- internal const val SECURITY_VERSION_FOR_CAPABILITIES_EXCHANGE = 3
private const val TAG = "ChannelResolver"
}
}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ConnectionResolver.java b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ConnectionResolver.java
deleted file mode 100644
index b9c4c99..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ConnectionResolver.java
+++ /dev/null
@@ -1,271 +0,0 @@
-/*
- * 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.
- */
-
-package com.google.android.connecteddevice.connection;
-
-import static com.google.android.connecteddevice.util.SafeLog.logd;
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-import static java.lang.Math.min;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-import com.google.android.companionprotos.CapabilitiesExchangeProto.CapabilitiesExchange;
-import com.google.android.companionprotos.CapabilitiesExchangeProto.CapabilitiesExchange.OobChannelType;
-import com.google.android.companionprotos.VersionExchangeProto.VersionExchange;
-import com.google.auto.value.AutoValue;
-import com.google.common.collect.ImmutableList;
-import com.google.protobuf.ExtensionRegistryLite;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Manages the version and capabilities exchange that must be completed in order to establish a
- * secure channel with a remote device.
- *
- * <p>The {@link ResolvedConnection} result is used to determine which {@code SecureChannel} to use
- * for the association flow.
- */
-public final class ConnectionResolver {
- private static final String TAG = "ConnectionResolver";
-
- public static final int MIN_SECURITY_VERSION_FOR_CAPABILITIES_EXCHANGE = 3;
- // Messaging and security versions are dictated by this device.
- @VisibleForTesting public static final int MESSAGING_VERSION = 3;
- @VisibleForTesting public static final int MIN_SECURITY_VERSION = 2;
- @VisibleForTesting public static final int MAX_SECURITY_VERSION = 3;
- private final ImmutableList<OobChannelType> supportedOobChannels;
-
- private enum State {
- UNKNOWN,
- WAITING_FOR_VERSION_EXCHANGE,
- WAITING_FOR_CAPABILITIES_EXCHANGE,
- DONE
- };
-
- private ErrorListener errorListener;
- private State state = State.WAITING_FOR_VERSION_EXCHANGE;
- private int messagingVersion;
- private int securityVersion;
-
- private final List<ConnectionResolvedListener> listeners;
- private final DeviceMessageStream deviceMessageStream;
- private final boolean isCapabilitiesEligible;
-
- public ConnectionResolver(
- DeviceMessageStream deviceMessageStream,
- boolean isCapabilitiesEligible) {
- this(deviceMessageStream, isCapabilitiesEligible, ImmutableList.of(OobChannelType.BT_RFCOMM));
- }
-
- public ConnectionResolver(
- DeviceMessageStream deviceMessageStream,
- boolean isCapabilitiesEligible,
- ImmutableList<OobChannelType> supportedOobChannels) {
- listeners = new ArrayList<>();
- this.deviceMessageStream = deviceMessageStream;
- this.isCapabilitiesEligible = isCapabilitiesEligible;
- this.supportedOobChannels = supportedOobChannels;
- }
-
- private void initializeListeners() {
- setErrorListener(deviceMessageStream::notifyDataReceivedErrorListener);
- registerListener((resolvedConnection) -> deviceMessageStream.setConnectionResolved(true));
-
- logd(TAG, "Setting data received listener");
- deviceMessageStream.setDataReceivedListener(this::onMessageReceived);
- }
-
- public void resolveConnection(@Nullable ConnectionResolvedListener listener) {
- initializeListeners();
-
- if (listener != null) {
- registerListener(listener);
- }
- }
-
- public void registerListener(ConnectionResolvedListener listener) {
- listeners.add(listener);
- }
-
- public void unregisterListener(ConnectionResolvedListener listener) {
- listeners.remove(listener);
- }
-
- public void setErrorListener(@Nullable ErrorListener errorListener) {
- this.errorListener = errorListener;
- }
-
- public void onMessageReceived(byte[] message) {
- logd(TAG, "Message received for state " + state);
- switch (state) {
- case WAITING_FOR_VERSION_EXCHANGE:
- exchangeVersion(message);
- break;
- case WAITING_FOR_CAPABILITIES_EXCHANGE:
- exchangeCapabilities(message);
- break;
- case DONE:
- case UNKNOWN:
- notifyError(null);
- }
- }
-
- private void exchangeVersion(byte[] message) {
- logd(TAG, "Exchanging version.");
- VersionExchange versionExchange;
- try {
- versionExchange =
- VersionExchange.parseFrom(message, ExtensionRegistryLite.getEmptyRegistry());
- } catch (IOException e) {
- loge(TAG, "Could not parse version exchange message", e);
- notifyError(e);
-
- return;
- }
- int minMessagingVersion = versionExchange.getMinSupportedMessagingVersion();
- int maxMessagingVersion = versionExchange.getMaxSupportedMessagingVersion();
- int minSecurityVersion = versionExchange.getMinSupportedSecurityVersion();
- int maxSecurityVersion = versionExchange.getMaxSupportedSecurityVersion();
- if (minMessagingVersion > MESSAGING_VERSION
- || maxMessagingVersion < MESSAGING_VERSION
- || minSecurityVersion > MAX_SECURITY_VERSION
- || maxSecurityVersion < MIN_SECURITY_VERSION) {
- loge(
- TAG,
- "Unsupported message version for min "
- + minMessagingVersion
- + " and max "
- + maxMessagingVersion
- + " or security version for "
- + minSecurityVersion
- + " and max "
- + maxSecurityVersion
- + ".");
- notifyError(new IllegalStateException("Unsupported version."));
- return;
- }
-
- VersionExchange headunitVersion =
- VersionExchange.newBuilder()
- .setMinSupportedMessagingVersion(MESSAGING_VERSION)
- .setMaxSupportedMessagingVersion(MESSAGING_VERSION)
- .setMinSupportedSecurityVersion(MIN_SECURITY_VERSION)
- .setMaxSupportedSecurityVersion(MAX_SECURITY_VERSION)
- .build();
- deviceMessageStream.writeRawBytes(headunitVersion.toByteArray());
- logd(TAG, "Sent supported versions to the phone.");
-
- int maxSharedSecurityVersion = min(maxSecurityVersion, MAX_SECURITY_VERSION);
- messagingVersion = MESSAGING_VERSION;
- securityVersion = maxSharedSecurityVersion;
- logd(
- TAG,
- "Resolved to messaging v"
- + MESSAGING_VERSION + " and security v"
- + maxSharedSecurityVersion
- + ".");
-
- if (shouldDoCapabilitiesExchange()) {
- state = State.WAITING_FOR_CAPABILITIES_EXCHANGE;
- } else {
- notifyConnectionResolved(
- ResolvedConnection.create(
- messagingVersion, securityVersion, /* oobChannelTypes= */ null));
- }
- }
-
- private boolean shouldDoCapabilitiesExchange() {
- return isCapabilitiesEligible
- && securityVersion >= MIN_SECURITY_VERSION_FOR_CAPABILITIES_EXCHANGE;
- }
-
- private void exchangeCapabilities(byte[] message) {
- CapabilitiesExchange capabilitiesExchange;
- try {
- capabilitiesExchange =
- CapabilitiesExchange.parseFrom(message, ExtensionRegistryLite.getEmptyRegistry());
- } catch (IOException e) {
- loge(TAG, "Could not parse capabilities exchange message", e);
- notifyError(e);
- return;
- }
-
- // Must wrap in an ArrayList to prevent retainAll from throwing an
- // UnsupportedOperationException.
- List<OobChannelType> sharedSupportedOobChannels =
- new ArrayList<>(capabilitiesExchange.getSupportedOobChannelsList());
- logd(
- TAG,
- "Received " + sharedSupportedOobChannels.size() + " supported OOB channels from device.");
- sharedSupportedOobChannels.retainAll(supportedOobChannels);
- logd(TAG, "Found " + sharedSupportedOobChannels.size() + " common OOB channels.");
- CapabilitiesExchange headunitCapabilities = CapabilitiesExchange.newBuilder()
- .addAllSupportedOobChannels(sharedSupportedOobChannels)
- .build();
-
- deviceMessageStream.writeRawBytes(headunitCapabilities.toByteArray());
-
- logd(TAG, "Sent supported capabilities to the phone.");
-
- notifyConnectionResolved(
- ResolvedConnection.create(
- messagingVersion, securityVersion, ImmutableList.copyOf(sharedSupportedOobChannels)));
- }
-
- private void notifyConnectionResolved(ResolvedConnection resolvedConnection) {
- state = State.DONE;
-
- for (ConnectionResolvedListener listener : listeners) {
- listener.onConnectionResolved(resolvedConnection);
- }
- }
-
- private void notifyError(@Nullable Exception e) {
- if (errorListener != null) {
- errorListener.onError(e);
- }
- }
-
- /** Listener to be invoked when version and capabilities exchange is complete. */
- public interface ConnectionResolvedListener {
- void onConnectionResolved(ResolvedConnection resolvedConnection);
- }
-
- /** Listener to be invoked when there is an error with the version or capabilities exchange. */
- public interface ErrorListener {
- void onError(Exception e);
- }
-
- /** Holds the values of the version and capabilities exchange. */
- @AutoValue
- public abstract static class ResolvedConnection {
- public static ResolvedConnection create(
- int messagingVersion,
- int securityVersion,
- @Nullable ImmutableList<OobChannelType> oobChannelTypes) {
- return new AutoValue_ConnectionResolver_ResolvedConnection(
- messagingVersion, securityVersion, oobChannelTypes);
- }
-
- public abstract int messagingVersion();
-
- public abstract int securityVersion();
-
- @Nullable
- public abstract ImmutableList<OobChannelType> oobChannelTypes();
- }
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/DeviceMessageStream.java b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/DeviceMessageStream.java
deleted file mode 100644
index 9bed0c5..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/DeviceMessageStream.java
+++ /dev/null
@@ -1,374 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice.connection;
-
-import static com.google.android.connecteddevice.util.SafeLog.logd;
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-import static com.google.android.connecteddevice.util.SafeLog.logw;
-
-import androidx.annotation.IntRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import com.google.android.companionprotos.DeviceMessageProto.Message;
-import com.google.android.companionprotos.OperationProto.OperationType;
-import com.google.android.companionprotos.PacketProto.Packet;
-import com.google.android.connecteddevice.model.DeviceMessage;
-import com.google.android.connecteddevice.util.ByteUtils;
-import com.google.android.connecteddevice.util.EventLog;
-import com.google.protobuf.ByteString;
-import com.google.protobuf.ExtensionRegistryLite;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.util.ArrayDeque;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/** Abstract class which includes common logic of different types of {@link DeviceMessageStream}. */
-public abstract class DeviceMessageStream {
-
- private static final String TAG = "DeviceMessageStream";
-
- private final ArrayDeque<Packet> packetQueue = new ArrayDeque<>();
-
- private final Map<Integer, ByteArrayOutputStream> pendingData = new HashMap<>();
-
- // messageId -> nextExpectedPacketNumber
- private final Map<Integer, Integer> pendingPacketNumber = new HashMap<>();
-
- private final MessageIdGenerator messageIdGenerator = new MessageIdGenerator();
-
- private final AtomicBoolean isSendingInProgress = new AtomicBoolean(false);
-
- // TODO(b/169869570): Remove when Protocol interface is available
- private final AtomicBoolean isConnectionResolved = new AtomicBoolean(false);
-
- private int maxWriteSize;
-
- /** Listener which will be notified when there is new {@link DeviceMessage} received. */
- private MessageReceivedListener messageReceivedListener;
-
- private DataReceivedListener dataReceivedListener;
-
- /** Listener which will be notified when there is error parsing the received message. */
- private MessageReceivedErrorListener messageReceivedErrorListener;
-
- protected DeviceMessageStream(int defaultMaxWriteSize) {
- maxWriteSize = defaultMaxWriteSize;
- }
-
- /**
- * Send data to the connected device. Note: {@link #sendCompleted()} must be called when the bytes
- * have successfully been sent to indicate the stream is ready to send more data.
- */
- protected abstract void send(byte[] data);
-
- /**
- * Set the given listener to be notified when a new message was received from the client. If
- * listener is {@code null}, clear.
- */
- public final void setMessageReceivedListener(@Nullable MessageReceivedListener listener) {
- messageReceivedListener = listener;
- }
-
- public final void setDataReceivedListener(@Nullable DataReceivedListener listener) {
- logd(TAG, "Setting dataReceivedListener");
- dataReceivedListener = listener;
- }
-
- /**
- * Set the given listener to be notified when there was an error during receiving message from the
- * client. If listener is {@code null}, clear.
- */
- public final void setMessageReceivedErrorListener(
- @Nullable MessageReceivedErrorListener listener) {
- messageReceivedErrorListener = listener;
- }
-
- public final void setConnectionResolved(boolean isResolved) {
- isConnectionResolved.set(isResolved);
- }
-
- /**
- * Notify the {@code messageReceivedListener} about the message received if it is not {@code
- * null}.
- *
- * @param deviceMessage The message received.
- */
- protected final void notifyMessageReceivedListener(@NonNull DeviceMessage deviceMessage) {
- if (messageReceivedListener != null) {
- messageReceivedListener.onMessageReceived(deviceMessage);
- }
- }
-
- // TODO(b/169869570): Remove when Protocol interface is available
- public final void notifyDataReceivedErrorListener(Exception e) {
- notifyMessageReceivedErrorListener(e);
- }
-
- private void notifyDataReceivedListener(byte[] data) {
- logd(TAG, "Notifying dataReceivedListener");
- if (dataReceivedListener != null) {
- dataReceivedListener.onDataReceived(data);
- }
- }
-
- /**
- * Notify the {@code messageReceivedErrorListener} about the message received if it is not {@code
- * null}.
- *
- * @param e The exception happened when parsing the received message.
- */
- protected final void notifyMessageReceivedErrorListener(Exception e) {
- if (messageReceivedErrorListener != null) {
- messageReceivedErrorListener.onMessageReceivedError(e);
- }
- }
-
- /**
- * Send {@link DeviceMessage} to remote connected devices.
- *
- * @param deviceMessage The message which need to be sent
- */
- public void writeMessage(@NonNull DeviceMessage deviceMessage) {
- Message.Builder builder =
- Message.newBuilder()
- .setOperation(OperationType.forNumber(deviceMessage.getOperationType().getValue()))
- .setIsPayloadEncrypted(deviceMessage.isMessageEncrypted())
- .setPayload(ByteString.copyFrom(deviceMessage.getMessage()))
- .setOriginalSize(deviceMessage.getOriginalMessageSize());
-
- UUID recipient = deviceMessage.getRecipient();
- if (recipient != null) {
- builder.setRecipient(ByteString.copyFrom(ByteUtils.uuidToBytes(recipient)));
- }
-
- Message message = builder.build();
- byte[] rawBytes = message.toByteArray();
- List<Packet> packets;
- try {
- packets = PacketFactory.makePackets(rawBytes, messageIdGenerator.next(), maxWriteSize);
- } catch (PacketFactoryException e) {
- loge(TAG, "Error while creating message packets.", e);
- return;
- }
- packetQueue.addAll(packets);
- writeNextMessageInQueue();
- }
-
- private void writeNextMessageInQueue() {
- if (packetQueue.isEmpty()) {
- logd(TAG, "No more packets to send.");
- return;
- }
- boolean isLockAcquired = isSendingInProgress.compareAndSet(false, true);
- if (!isLockAcquired) {
- logd(TAG, "Unable to send packet at this time.");
- return;
- }
-
- Packet packet = packetQueue.remove();
- logd(
- TAG,
- "Writing packet "
- + packet.getPacketNumber()
- + " of "
- + packet.getTotalPackets()
- + " for "
- + packet.getMessageId()
- + ".");
- send(packet.toByteArray());
- }
-
- /**
- * Send raw data to remote connected devices.
- *
- * @param data message to send
- * @throws IllegalArgumentException if {@code data} is larger than {@code maxWriteSize}
- */
- // TODO(b/169869570): Move to Protocol interface when available
- public void writeRawBytes(@NonNull byte[] data) {
- if (data.length > maxWriteSize) {
- throw new IllegalArgumentException(
- "Length of data is greater than max write size, send as a Message instead.");
- }
-
- send(data);
- }
-
- /** Process incoming data from stream. */
- protected final synchronized void onDataReceived(byte[] data) {
- logd(TAG, "Data was received, isConnectionResolved = " + isConnectionResolved.get());
- if (isConnectionResolved.get()) {
- Packet packet;
- try {
- packet = Packet.parseFrom(data, ExtensionRegistryLite.getEmptyRegistry());
- } catch (IOException e) {
- loge(TAG, "Can not parse packet from client.", e);
- notifyMessageReceivedErrorListener(e);
- return;
- }
- processPacket(packet);
- return;
- }
-
- // The version and capabilities exchange use raw bytes rather than device messages, so notify
- // raw data listener rather than message listener.
- notifyDataReceivedListener(data);
- }
-
- protected final void processPacket(@NonNull Packet packet) {
- int messageId = packet.getMessageId();
- int packetNumber = packet.getPacketNumber();
- int expectedPacket = pendingPacketNumber.getOrDefault(messageId, 1);
- if (packetNumber == expectedPacket - 1) {
- logw(
- TAG,
- "Received duplicate packet "
- + packet.getPacketNumber()
- + " for message "
- + messageId
- + ". Ignoring.");
- return;
- }
- if (packetNumber != expectedPacket) {
- loge(TAG, "Received unexpected packet " + packetNumber + " for message " + messageId + ".");
- notifyMessageReceivedErrorListener(
- new IllegalStateException("Packet received out of order."));
- return;
- }
- pendingPacketNumber.put(messageId, packetNumber + 1);
-
- ByteArrayOutputStream currentPayloadStream =
- pendingData.getOrDefault(messageId, new ByteArrayOutputStream());
- pendingData.putIfAbsent(messageId, currentPayloadStream);
-
- byte[] payload = packet.getPayload().toByteArray();
- try {
- currentPayloadStream.write(payload);
- } catch (IOException e) {
- loge(TAG, "Error writing packet to stream.", e);
- notifyMessageReceivedErrorListener(e);
- return;
- }
- logd(
- TAG,
- "Parsed packet "
- + packet.getPacketNumber()
- + " of "
- + packet.getTotalPackets()
- + " for message "
- + messageId
- + ". Writing "
- + payload.length
- + ".");
-
- if (packetNumber == 1) {
- EventLog.onMessageStarted(messageId);
- }
-
- if (packet.getPacketNumber() != packet.getTotalPackets()) {
- return;
- }
-
- byte[] messageBytes = currentPayloadStream.toByteArray();
- EventLog.onMessageFullyReceived(messageId, messageBytes.length);
- pendingData.remove(messageId);
-
- logd(
- TAG,
- "Received complete device message " + messageId + " of " + messageBytes.length + " bytes.");
- Message message;
- try {
- message = Message.parseFrom(messageBytes, ExtensionRegistryLite.getEmptyRegistry());
- } catch (IOException e) {
- loge(TAG, "Cannot parse device message from client.", e);
- notifyMessageReceivedErrorListener(e);
- return;
- }
-
- DeviceMessage deviceMessage =
- DeviceMessage.createIncomingMessage(
- ByteUtils.bytesToUUID(message.getRecipient().toByteArray()),
- message.getIsPayloadEncrypted(),
- DeviceMessage.OperationType.fromValue(message.getOperation().getNumber()),
- message.getPayload().toByteArray(),
- message.getOriginalSize());
- notifyMessageReceivedListener(deviceMessage);
- }
-
- /** The maximum amount of bytes that can be written in a single packet. */
- public final void setMaxWriteSize(@IntRange(from = 1) int maxWriteSize) {
- if (maxWriteSize <= 0) {
- return;
- }
- this.maxWriteSize = maxWriteSize;
- }
-
- /** Indicate current send operation has completed. */
- public void sendCompleted() {
- isSendingInProgress.set(false);
- writeNextMessageInQueue();
- }
-
- /** A generator of unique IDs for messages. */
- private static class MessageIdGenerator {
- private final AtomicInteger messageId = new AtomicInteger(0);
-
- int next() {
- int current = messageId.getAndIncrement();
- messageId.compareAndSet(Integer.MAX_VALUE, 0);
- return current;
- }
- }
-
- /** Listener to be invoked when a complete message is received from the client. */
- public interface MessageReceivedListener {
-
- /**
- * Called when a complete message is received from the client.
- *
- * @param deviceMessage The message received from the client.
- */
- void onMessageReceived(@NonNull DeviceMessage deviceMessage);
- }
-
- /** Listener to be invoked when a message of raw data is received from the client. */
- // TODO(b/169869570): Move to Protocol interface when available
- public interface DataReceivedListener {
- /**
- * Called when a message of raw data is received from the client.
- *
- * @param data the data received from the client
- */
- void onDataReceived(@NonNull byte[] data);
- }
-
- /** Listener to be invoked when there was an error during receiving message from the client. */
- public interface MessageReceivedErrorListener {
- /**
- * Called when there was an error during receiving message from the client.
- *
- * @param exception The error.
- */
- void onMessageReceivedError(@NonNull Exception exception);
- }
-
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannel.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannel.kt
index b5b9836..e908d5c 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannel.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannel.kt
@@ -16,6 +16,8 @@
package com.google.android.connecteddevice.connection
import androidx.annotation.VisibleForTesting
+import com.google.android.companionprotos.VerificationCode
+import com.google.android.companionprotos.VerificationCodeState
import com.google.android.connecteddevice.model.DeviceMessage
import com.google.android.connecteddevice.model.DeviceMessage.OperationType
import com.google.android.connecteddevice.oob.OobRunner
@@ -27,6 +29,7 @@
import com.google.android.encryptionrunner.HandshakeException
import com.google.android.encryptionrunner.HandshakeMessage.HandshakeState
import com.google.android.encryptionrunner.Key
+import com.google.protobuf.ByteString
import java.security.SignatureException
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
@@ -40,7 +43,7 @@
* Establishes a secure channel with [EncryptionRunner] over [ProtocolStream]s as server side, sends
* and receives messages securely after the secure channel has been established.
*/
-abstract class MultiProtocolSecureChannel(
+open class MultiProtocolSecureChannel(
stream: ProtocolStream,
private val storage: ConnectedDeviceStorage,
private val encryptionRunner: EncryptionRunner,
@@ -94,7 +97,7 @@
* The out of band verification code get from the [EncryptionRunner], will be set during out of
* band association.
*/
- protected var oobCode: ByteArray? = null
+ private var oobCode: ByteArray? = null
@HandshakeState private var state: Int = HandshakeState.UNKNOWN
@@ -112,7 +115,6 @@
HandshakeState.UNKNOWN -> processHandshakeInitialization(message)
HandshakeState.IN_PROGRESS -> processHandshakeInProgress(message)
HandshakeState.VERIFICATION_NEEDED -> processVerificationCodeMessage(message)
- HandshakeState.OOB_VERIFICATION_NEEDED -> confirmOobVerificationCode(message)
HandshakeState.FINISHED ->
// Should not reach this line.
loge(TAG, "Received handshake message after handshake is completed. Ignored.")
@@ -152,27 +154,12 @@
visualVerificationCode = handshakeMessage.verificationCode
val fullVerificationCode = handshakeMessage.fullVerificationCode
if (fullVerificationCode != null) {
- processOobVerificationCode(fullVerificationCode)
+ this.oobCode = fullVerificationCode
} else {
loge(TAG, "Full verification is null. Notify channel error")
notifySecureChannelFailure(ChannelError.CHANNEL_ERROR_INVALID_STATE)
}
}
- HandshakeState.OOB_VERIFICATION_NEEDED -> {
- val oobCode = handshakeMessage.fullVerificationCode
- // Unreachable error, the [oobCode] will always be nonnull if the status is
- // [OOB_VERIFICATION_NEEDED].
- if (oobCode == null) {
- loge(
- TAG,
- "Handshake state is OOB_VERIFICATION_NEEDED, but handshake message does " +
- "not contain out of band verification code. Notifying callback of failure."
- )
- notifySecureChannelFailure(ChannelError.CHANNEL_ERROR_INVALID_VERIFICATION)
- return
- }
- this.oobCode = oobCode
- }
else -> {
loge(
TAG,
@@ -184,11 +171,19 @@
}
}
- /** Will be called when OOB verification code is available from [EncryptionRunner]. */
- protected abstract fun processOobVerificationCode(oobCode: ByteArray)
-
/** Process the message received from remote device during verification stage. */
- protected abstract fun processVerificationCodeMessage(message: ByteArray)
+ private fun processVerificationCodeMessage(message: ByteArray) {
+ val verificationMessage = VerificationCode.parseFrom(message)
+ when (verificationMessage.state) {
+ VerificationCodeState.OOB_VERIFICATION ->
+ confirmOobVerificationCode(verificationMessage.payload.toByteArray())
+ VerificationCodeState.VISUAL_VERIFICATION -> invokeVerificationCodeListener()
+ else -> {
+ loge(TAG, "Unexpected verification message received, issue error callback.")
+ notifySecureChannelFailure(ChannelError.CHANNEL_ERROR_INVALID_STATE)
+ }
+ }
+ }
/** Should be called when OOB verification code is received from remote device. */
protected fun confirmOobVerificationCode(encryptedCode: ByteArray) {
@@ -229,7 +224,14 @@
}
/** Generate understandable OOB data response which will be sent to remote device. */
- protected abstract fun createOobResponse(code: ByteArray): ByteArray
+ private fun createOobResponse(code: ByteArray) =
+ VerificationCode.newBuilder()
+ .run {
+ setState(VerificationCodeState.OOB_VERIFICATION)
+ setPayload(ByteString.copyFrom(code))
+ build()
+ }
+ .toByteArray()
/** Communicate with other components that the visual verification code is available. */
protected fun invokeVerificationCodeListener() {
@@ -313,7 +315,12 @@
* confirmation, and send confirmation signals to remote bluetooth device.
*/
open fun notifyVerificationCodeAccepted() {
- processVisualVerificationConfirmed()
+ val confirmationMessage =
+ VerificationCode.newBuilder().run {
+ setState(VerificationCodeState.VISUAL_CONFIRMATION)
+ build()
+ }
+ sendHandshakeMessage(confirmationMessage.toByteArray())
verificationCodeAcceptedInternal()
}
@@ -348,9 +355,6 @@
notifyCallback { it.onSecureChannelEstablished() }
}
- /** A confirmation message should be sent to remote device according to the version. */
- protected abstract fun processVisualVerificationConfirmed()
-
/** Add a protocol stream to this channel. */
fun addStream(stream: ProtocolStream) {
streams.add(stream)
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannelPreV4.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannelPreV4.kt
deleted file mode 100644
index 9d23d06..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannelPreV4.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * 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.
- */
-package com.google.android.connecteddevice.connection
-
-import com.google.android.connecteddevice.oob.OobRunner
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage
-import com.google.android.connecteddevice.util.SafeLog.loge
-import com.google.android.encryptionrunner.EncryptionRunner
-import java.util.zip.Inflater
-
-/** Handles specific handshake logic before security V4. */
-open class MultiProtocolSecureChannelPreV4(
- stream: ProtocolStream,
- storage: ConnectedDeviceStorage,
- encryptionRunner: EncryptionRunner,
- oobRunner: OobRunner? = null,
- deviceId: String? = null,
- inflater: Inflater = Inflater(),
-) : MultiProtocolSecureChannel(stream, storage, encryptionRunner, oobRunner, deviceId, inflater) {
-
- override fun processOobVerificationCode(oobCode: ByteArray) {
- invokeVerificationCodeListener()
- }
-
- override fun processVerificationCodeMessage(message: ByteArray) {
- loge(
- TAG,
- "Verification code message will only be received under security version 4. notify callback" +
- " of failure."
- )
- notifySecureChannelFailure(ChannelError.CHANNEL_ERROR_INVALID_STATE)
- }
-
- override fun createOobResponse(code: ByteArray): ByteArray {
- // Raw bytes of the OOB code is expected by remote device before V4.
- return code
- }
-
- override fun processVisualVerificationConfirmed() {
- // Verification confirmation message is not needed before V4.
- }
-
- companion object {
- private const val TAG = "MultiProtocolSecureChannelPreV4"
- }
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannelV4.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannelV4.kt
deleted file mode 100644
index f2f69e4..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannelV4.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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.
- */
-package com.google.android.connecteddevice.connection
-
-import com.google.android.companionprotos.VerificationCode
-import com.google.android.companionprotos.VerificationCodeState
-import com.google.android.connecteddevice.oob.OobRunner
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage
-import com.google.android.connecteddevice.util.SafeLog.logd
-import com.google.android.connecteddevice.util.SafeLog.loge
-import com.google.android.encryptionrunner.EncryptionRunner
-import com.google.protobuf.ByteString
-
-/** Handles specific handshake logic for security V4 and plus. */
-class MultiProtocolSecureChannelV4(
- stream: ProtocolStream,
- storage: ConnectedDeviceStorage,
- encryptionRunner: EncryptionRunner,
- oobRunner: OobRunner? = null,
- deviceId: String? = null,
-) : MultiProtocolSecureChannel(stream, storage, encryptionRunner, oobRunner, deviceId) {
- override fun processVerificationCodeMessage(message: ByteArray) {
- val verificationMessage = VerificationCode.parseFrom(message)
- when (verificationMessage.state) {
- VerificationCodeState.OOB_VERIFICATION ->
- confirmOobVerificationCode(verificationMessage.payload.toByteArray())
- VerificationCodeState.VISUAL_VERIFICATION -> invokeVerificationCodeListener()
- else -> {
- loge(TAG, "Unexpected verification message received, issue error callback.")
- notifySecureChannelFailure(ChannelError.CHANNEL_ERROR_INVALID_STATE)
- }
- }
- }
-
- override fun createOobResponse(code: ByteArray) =
- VerificationCode.newBuilder()
- .run {
- setState(VerificationCodeState.OOB_VERIFICATION)
- setPayload(ByteString.copyFrom(code))
- build()
- }
- .toByteArray()
-
- override fun processVisualVerificationConfirmed() {
- val confirmationMessage =
- VerificationCode.newBuilder().run {
- setState(VerificationCodeState.VISUAL_CONFIRMATION)
- build()
- }
- sendHandshakeMessage(confirmationMessage.toByteArray())
- }
-
- override fun processOobVerificationCode(oobCode: ByteArray) {
- this.oobCode = oobCode
- logd(TAG, "Waiting for verification code message.")
- }
-
- companion object {
- private const val TAG = "MultiProtocolSecureChannelV4"
- }
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/OobAssociationSecureChannel.java b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/OobAssociationSecureChannel.java
deleted file mode 100644
index 957c3e1..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/OobAssociationSecureChannel.java
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice.connection;
-
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-
-import androidx.annotation.NonNull;
-import com.google.android.connecteddevice.oob.OobConnectionManager;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
-import com.google.android.encryptionrunner.EncryptionRunner;
-import com.google.android.encryptionrunner.EncryptionRunnerFactory;
-import com.google.android.encryptionrunner.HandshakeException;
-import com.google.android.encryptionrunner.HandshakeMessage;
-import com.google.android.encryptionrunner.HandshakeMessage.HandshakeState;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.util.Arrays;
-import javax.crypto.BadPaddingException;
-import javax.crypto.IllegalBlockSizeException;
-
-/** A secure channel established with the association flow with an out-of-band verification. */
-public class OobAssociationSecureChannel extends AssociationSecureChannel {
-
- private static final String TAG = "OobAssociationSecureChannel";
-
- private final OobConnectionManager oobConnectionManager;
-
- private byte[] oobCode;
-
- public OobAssociationSecureChannel(
- DeviceMessageStream stream,
- ConnectedDeviceStorage storage,
- OobConnectionManager oobConnectionManager) {
- this(
- stream,
- storage,
- oobConnectionManager,
- EncryptionRunnerFactory.newRunner(EncryptionRunnerFactory.EncryptionRunnerType.OOB_UKEY2));
- }
-
- OobAssociationSecureChannel(
- DeviceMessageStream stream,
- ConnectedDeviceStorage storage,
- OobConnectionManager oobConnectionManager,
- EncryptionRunner encryptionRunner) {
- super(stream, storage, encryptionRunner);
- this.oobConnectionManager = oobConnectionManager;
- }
-
- @Override
- void processHandshake(@NonNull byte[] message) throws HandshakeException {
- switch (getState()) {
- case HandshakeState.IN_PROGRESS:
- processHandshakeInProgress(message);
- break;
- case HandshakeState.OOB_VERIFICATION_NEEDED:
- processHandshakeOobVerificationNeeded(message);
- break;
- default:
- super.processHandshake(message);
- }
- }
-
- private void processHandshakeInProgress(@NonNull byte[] message) throws HandshakeException {
- HandshakeMessage handshakeMessage = getEncryptionRunner().continueHandshake(message);
- setState(handshakeMessage.getHandshakeState());
- int state = getState();
- if (state != HandshakeState.OOB_VERIFICATION_NEEDED) {
- loge(
- TAG,
- "processHandshakeInProgress: Encountered unexpected handshake state: " + state + ".");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
- return;
- }
-
- oobCode = handshakeMessage.getFullVerificationCode();
- if (oobCode == null) {
- loge(TAG, "Unable to get out of band verification code.");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_VERIFICATION);
- return;
- }
- }
-
- private void processHandshakeOobVerificationNeeded(@NonNull byte[] message) {
- byte[] decryptedCode;
- try {
- decryptedCode = oobConnectionManager.decryptVerificationCode(message);
- } catch (InvalidKeyException
- | InvalidAlgorithmParameterException
- | IllegalBlockSizeException
- | BadPaddingException e) {
- loge(TAG, "Decryption failed for verification code exchange", e);
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_HANDSHAKE);
- return;
- }
-
- if (oobCode == null || !Arrays.equals(oobCode, decryptedCode)) {
- loge(TAG, "Exchanged verification codes do not match. Aborting secure channel.");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_VERIFICATION);
- return;
- }
-
- byte[] encryptedCode;
- try {
- encryptedCode = oobConnectionManager.encryptVerificationCode(oobCode);
- } catch (InvalidKeyException
- | InvalidAlgorithmParameterException
- | IllegalBlockSizeException
- | BadPaddingException e) {
- loge(TAG, "Encryption failed for verification code exchange.", e);
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_HANDSHAKE);
- return;
- }
-
- loge(TAG, "OOB accespted send encrpted code.");
- sendHandshakeMessage(encryptedCode, /* isEncrypted= */ false);
-
- notifyOutOfBandAccepted();
- }
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ProtocolStream.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ProtocolStream.kt
index b489d35..349e9a8 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ProtocolStream.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ProtocolStream.kt
@@ -19,7 +19,10 @@
import com.google.android.companionprotos.OperationProto.OperationType
import com.google.android.companionprotos.PacketProto.Packet
import com.google.android.connecteddevice.model.DeviceMessage
-import com.google.android.connecteddevice.transport.ConnectionProtocol
+import com.google.android.connecteddevice.transport.IDataReceivedListener
+import com.google.android.connecteddevice.transport.IDataSendCallback
+import com.google.android.connecteddevice.transport.IDeviceDisconnectedListener
+import com.google.android.connecteddevice.transport.IDeviceMaxDataSizeChangedListener
import com.google.android.connecteddevice.transport.ProtocolDevice
import com.google.android.connecteddevice.util.ByteUtils
import com.google.android.connecteddevice.util.EventLog.onMessageFullyReceived
@@ -32,18 +35,11 @@
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.util.ArrayDeque
-import java.util.concurrent.Executor
-import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
-/**
- * Data stream for a specified [protocol] and its corresponding device identified with [protocolId].
- */
-open class ProtocolStream(
- private val device: ProtocolDevice,
- callbackExecutor: Executor = Executors.newSingleThreadExecutor()
-) {
+/** Data stream for a specified [device]. */
+open class ProtocolStream(private val device: ProtocolDevice) {
/** Listener which will be notified when there is new [DeviceMessage] received. */
var messageReceivedListener: MessageReceivedListener? = null
@@ -69,31 +65,28 @@
logd(TAG, "Creating new ProtocolStream for protocol device ${device.protocolId}.")
device.protocol.registerDataReceivedListener(
device.protocolId,
- object : ConnectionProtocol.DataReceivedListener {
+ object : IDataReceivedListener.Stub() {
override fun onDataReceived(protocolId: String, data: ByteArray) {
onDataReceived(data)
}
- },
- callbackExecutor
+ }
)
device.protocol.registerDeviceDisconnectedListener(
device.protocolId,
- object : ConnectionProtocol.DeviceDisconnectedListener {
+ object : IDeviceDisconnectedListener.Stub() {
override fun onDeviceDisconnected(protocolId: String) {
isConnected.set(false)
protocolDisconnectListener?.onProtocolDisconnected()
}
- },
- callbackExecutor
+ }
)
device.protocol.registerDeviceMaxDataSizeChangedListener(
device.protocolId,
- object : ConnectionProtocol.DeviceMaxDataSizeChangedListener {
+ object : IDeviceMaxDataSizeChangedListener.Stub() {
override fun onDeviceMaxDataSizeChanged(protocolId: String, maxBytes: Int) {
maxWriteSize = maxBytes
}
- },
- callbackExecutor
+ }
)
maxWriteSize = device.protocol.getMaxWriteSize(device.protocolId)
}
@@ -107,7 +100,7 @@
device.protocol.sendData(
device.protocolId,
data,
- object : ConnectionProtocol.DataSendCallback {
+ object : IDataSendCallback.Stub() {
override fun onDataSentSuccessfully() {
logd(TAG, "Data sent successfully. Sending next message in queue.")
isSendingInProgress.set(false)
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ProtocolStreamFactory.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ProtocolStreamFactory.kt
index 738ce09..9bdadfa 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ProtocolStreamFactory.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ProtocolStreamFactory.kt
@@ -16,18 +16,16 @@
package com.google.android.connecteddevice.connection
import com.google.android.connecteddevice.transport.ProtocolDevice
-import java.util.concurrent.Executor
/** Factory for creating [ProtocolStream]. */
interface ProtocolStreamFactory {
/** Creates a [ProtocolStream] with provided [device]. */
- fun createProtocolStream(device: ProtocolDevice, executor: Executor): ProtocolStream
+ fun createProtocolStream(device: ProtocolDevice): ProtocolStream
}
/** Implementation of [ProtocolStreamFactory]. */
class ProtocolStreamFactoryImpl : ProtocolStreamFactory {
- override fun createProtocolStream(device: ProtocolDevice, executor: Executor): ProtocolStream =
- ProtocolStream(device, executor)
+ override fun createProtocolStream(device: ProtocolDevice): ProtocolStream = ProtocolStream(device)
}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ReconnectSecureChannel.java b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ReconnectSecureChannel.java
deleted file mode 100644
index abf7b2d..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ReconnectSecureChannel.java
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice.connection;
-
-import static com.google.android.connecteddevice.util.SafeLog.logd;
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
-import com.google.android.connecteddevice.util.ByteUtils;
-import com.google.android.connecteddevice.util.EventLog;
-import com.google.android.encryptionrunner.EncryptionRunner;
-import com.google.android.encryptionrunner.EncryptionRunnerFactory;
-import com.google.android.encryptionrunner.EncryptionRunnerFactory.EncryptionRunnerType;
-import com.google.android.encryptionrunner.HandshakeException;
-import com.google.android.encryptionrunner.HandshakeMessage;
-import com.google.android.encryptionrunner.HandshakeMessage.HandshakeState;
-import com.google.android.encryptionrunner.Key;
-import java.util.Arrays;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/** A secure channel established with the reconnection flow. */
-public class ReconnectSecureChannel extends SecureChannel {
-
- private static final String TAG = "ReconnectSecureChannel";
-
- private final ConnectedDeviceStorage storage;
-
- private final String deviceId;
-
- private final byte[] expectedChallengeResponse;
-
- private final AtomicBoolean hasVerifiedDevice = new AtomicBoolean(false);
-
- @HandshakeState private int state = HandshakeState.UNKNOWN;
-
-
- /**
- * Create a new secure reconnection channel.
- *
- * @param stream The {@link DeviceMessageStream} for communication with the device.
- * @param storage {@link ConnectedDeviceStorage} for secure storage.
- * @param deviceId Id of the device being reconnected.
- * @param expectedChallengeResponse Expected response to challenge issued in reconnect. Should
- * pass {@code null} when device verification is not needed during the reconnection process.
- */
- public ReconnectSecureChannel(
- @NonNull DeviceMessageStream stream,
- @NonNull ConnectedDeviceStorage storage,
- @NonNull String deviceId,
- @Nullable byte[] expectedChallengeResponse) {
- super(stream, newReconnectRunner());
- this.storage = storage;
- this.deviceId = deviceId;
- if (expectedChallengeResponse == null) {
- // Skip the device verification step for spp reconnection
- hasVerifiedDevice.set(true);
- }
- this.expectedChallengeResponse = expectedChallengeResponse;
- }
-
- private static EncryptionRunner newReconnectRunner() {
- EncryptionRunner encryptionRunner =
- EncryptionRunnerFactory.newRunner(EncryptionRunnerType.UKEY2);
- encryptionRunner.setIsReconnect(true);
- return encryptionRunner;
- }
-
- @Override
- void processHandshake(byte[] message) throws HandshakeException {
- switch (state) {
- case HandshakeState.UNKNOWN:
- if (!hasVerifiedDevice.get()) {
- processHandshakeDeviceVerification(message);
- } else {
- processHandshakeInitialization(message);
- }
- break;
- case HandshakeState.IN_PROGRESS:
- processHandshakeInProgress(message);
- break;
- case HandshakeState.RESUMING_SESSION:
- processHandshakeResumingSession(message);
- break;
- default:
- loge(TAG, "Encountered unexpected handshake state: " + state + ".");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
- }
- }
-
- private void processHandshakeDeviceVerification(byte[] message) {
- EventLog.onDeviceChallengeReceived();
- byte[] challengeResponse = Arrays.copyOf(message, expectedChallengeResponse.length);
- byte[] deviceChallenge =
- Arrays.copyOfRange(message, expectedChallengeResponse.length, message.length);
- if (!Arrays.equals(expectedChallengeResponse, challengeResponse)) {
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_ENCRYPTION_KEY);
- return;
- }
- logd(TAG, "Responding to challenge " + ByteUtils.byteArrayToHexString(deviceChallenge) + ".");
- byte[] deviceChallengeResponse = storage.hashWithChallengeSecret(deviceId, deviceChallenge);
- if (deviceChallengeResponse == null) {
- notifySecureChannelFailure(CHANNEL_ERROR_STORAGE_ERROR);
- }
- sendHandshakeMessage(deviceChallengeResponse, /* isEncrypted= */ false);
- hasVerifiedDevice.set(true);
- }
-
- private void processHandshakeInitialization(byte[] message) throws HandshakeException {
- logd(TAG, "Responding to handshake init request.");
- HandshakeMessage handshakeMessage = getEncryptionRunner().respondToInitRequest(message);
- state = handshakeMessage.getHandshakeState();
- sendHandshakeMessage(handshakeMessage.getNextMessage(), /* isEncrypted= */ false);
- }
-
- private void processHandshakeInProgress(@NonNull byte[] message) throws HandshakeException {
- logd(TAG, "Continuing handshake.");
- HandshakeMessage handshakeMessage = getEncryptionRunner().continueHandshake(message);
- state = handshakeMessage.getHandshakeState();
- }
-
- private void processHandshakeResumingSession(@NonNull byte[] message) throws HandshakeException {
- logd(TAG, "Start reconnection authentication.");
-
- byte[] previousKey = storage.getEncryptionKey(deviceId);
- if (previousKey == null) {
- loge(TAG, "Unable to resume session, previous key is null.");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_ENCRYPTION_KEY);
- return;
- }
-
- HandshakeMessage handshakeMessage =
- getEncryptionRunner().authenticateReconnection(message, previousKey);
- state = handshakeMessage.getHandshakeState();
- if (state != HandshakeState.FINISHED) {
- loge(TAG, "Unable to resume session, unexpected next handshake state: " + state + ".");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
- return;
- }
-
- Key newKey = handshakeMessage.getKey();
- if (newKey == null) {
- loge(TAG, "Unable to resume session, new key is null.");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_ENCRYPTION_KEY);
- return;
- }
-
- storage.saveEncryptionKey(deviceId, newKey.asBytes());
- logd(TAG, "Saved new key for reconnection.");
- setEncryptionKey(newKey);
- sendServerAuthToClient(handshakeMessage.getNextMessage());
- notifyCallback(Callback::onSecureChannelEstablished);
- }
-
- private void sendServerAuthToClient(@Nullable byte[] message) {
- if (message == null) {
- loge(TAG, "Unable to send server authentication message to client, message is null.");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_MSG);
- return;
- }
-
- sendHandshakeMessage(message, /* isEncrypted= */ false);
- }
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/SecureChannel.java b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/SecureChannel.java
deleted file mode 100644
index deff7f9..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/SecureChannel.java
+++ /dev/null
@@ -1,380 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice.connection;
-
-import static com.google.android.connecteddevice.model.DeviceMessage.OperationType.ENCRYPTION_HANDSHAKE;
-import static com.google.android.connecteddevice.util.SafeLog.logd;
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-import com.google.android.connecteddevice.model.DeviceMessage;
-import com.google.android.connecteddevice.model.DeviceMessage.OperationType;
-import com.google.android.connecteddevice.util.SafeConsumer;
-import com.google.android.encryptionrunner.EncryptionRunner;
-import com.google.android.encryptionrunner.HandshakeException;
-import com.google.android.encryptionrunner.Key;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.security.SignatureException;
-import java.util.Arrays;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.zip.DataFormatException;
-import java.util.zip.Deflater;
-import java.util.zip.Inflater;
-
-/**
- * Establishes a secure channel with {@link EncryptionRunner} over {@link DeviceMessageStream} as
- * server side, sends and receives messages securely after the secure channel has been established.
- */
-public abstract class SecureChannel {
-
- private static final String TAG = "SecureChannel";
-
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({
- CHANNEL_ERROR_INVALID_HANDSHAKE,
- CHANNEL_ERROR_INVALID_MSG,
- CHANNEL_ERROR_INVALID_DEVICE_ID,
- CHANNEL_ERROR_INVALID_VERIFICATION,
- CHANNEL_ERROR_INVALID_STATE,
- CHANNEL_ERROR_INVALID_ENCRYPTION_KEY,
- CHANNEL_ERROR_STORAGE_ERROR
- })
- @interface ChannelError {}
-
- /** Indicates an error during a Handshake of EncryptionRunner. */
- static final int CHANNEL_ERROR_INVALID_HANDSHAKE = 0;
- /** Received an invalid handshake message or has an invalid handshake message to send. */
- static final int CHANNEL_ERROR_INVALID_MSG = 1;
- /** Unable to retrieve a valid id. */
- static final int CHANNEL_ERROR_INVALID_DEVICE_ID = 2;
- /** Unable to get verification code or there's a error during pin verification. */
- static final int CHANNEL_ERROR_INVALID_VERIFICATION = 3;
- /** Encountered an unexpected handshake state. */
- static final int CHANNEL_ERROR_INVALID_STATE = 4;
- /** Failed to get a valid previous/new encryption key. */
- static final int CHANNEL_ERROR_INVALID_ENCRYPTION_KEY = 5;
- /** Failed to save or retrieve security keys. */
- static final int CHANNEL_ERROR_STORAGE_ERROR = 6;
-
- private final DeviceMessageStream stream;
-
- private final EncryptionRunner encryptionRunner;
-
- private final AtomicReference<Key> encryptionKey = new AtomicReference<>();
-
- private final Inflater inflater;
-
- private final Deflater deflater;
-
- private boolean isCompressionEnabled = true;
-
- private Callback callback;
-
- SecureChannel(@NonNull DeviceMessageStream stream, @NonNull EncryptionRunner encryptionRunner) {
- this(stream, encryptionRunner, new Inflater(), new Deflater(Deflater.BEST_COMPRESSION));
- }
-
- SecureChannel(
- @NonNull DeviceMessageStream stream,
- @NonNull EncryptionRunner encryptionRunner,
- @NonNull Inflater inflater,
- @NonNull Deflater deflater) {
- this.stream = stream;
- this.encryptionRunner = encryptionRunner;
- this.stream.setMessageReceivedListener(this::onMessageReceived);
- this.inflater = inflater;
- this.deflater = deflater;
- }
-
- /** Logic for processing a handshake message from device. */
- abstract void processHandshake(byte[] message) throws HandshakeException;
-
- /**
- * Sets whether outgoing messages will attempt to be compressed prior to sending. Default value is
- * {@code true}.
- */
- public void setCompressionEnabled(boolean enabled) {
- isCompressionEnabled = enabled;
- }
-
- void sendHandshakeMessage(@Nullable byte[] message, boolean isEncrypted) {
- if (message == null) {
- loge(TAG, "Unable to send next handshake message, message is null.");
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_MSG);
- return;
- }
-
- logd(TAG, "Sending handshake message.");
- DeviceMessage deviceMessage =
- DeviceMessage.createOutgoingMessage(
- /* recipient= */ null, isEncrypted, ENCRYPTION_HANDSHAKE, message);
- sendMessage(deviceMessage);
- }
-
- /** Set the encryption key that secures this channel. */
- void setEncryptionKey(@Nullable Key encryptionKey) {
- this.encryptionKey.set(encryptionKey);
- }
-
- /**
- * Send a client message.
- *
- * <p>Note: This should be called with an encrypted message only after the secure channel has been
- * established.
- *
- * @param deviceMessage The {@link DeviceMessage} to send.
- */
- public void sendClientMessage(@NonNull DeviceMessage deviceMessage) {
- sendMessage(deviceMessage);
- }
-
- private void sendMessage(@NonNull DeviceMessage deviceMessage) {
- if (isCompressionEnabled) {
- compressMessage(deviceMessage);
- }
- if (deviceMessage.isMessageEncrypted()) {
- encryptMessage(deviceMessage);
- }
- stream.writeMessage(deviceMessage);
- }
-
- private void encryptMessage(@NonNull DeviceMessage deviceMessage) {
- Key key = encryptionKey.get();
- if (key == null) {
- throw new IllegalStateException("Secure channel has not been established.");
- }
-
- byte[] encryptedMessage = key.encryptData(deviceMessage.getMessage());
- deviceMessage.setMessage(encryptedMessage);
- }
-
- /** Get the BLE stream backing this channel. */
- @NonNull
- public DeviceMessageStream getStream() {
- return stream;
- }
-
- /** Register a callback that notifies secure channel events. */
- public void registerCallback(Callback callback) {
- this.callback = callback;
- }
-
- /** Unregister a callback. */
- void unregisterCallback(Callback callback) {
- if (callback == this.callback) {
- this.callback = null;
- }
- }
-
- @VisibleForTesting
- @Nullable
- public Callback getCallback() {
- return callback;
- }
-
- void notifyCallback(@NonNull SafeConsumer<Callback> notification) {
- if (callback != null) {
- notification.accept(callback);
- }
- }
-
- /** Notify callbacks that an error has occurred. */
- void notifySecureChannelFailure(@ChannelError int error) {
- loge(TAG, "Secure channel error: " + error);
- notifyCallback(callback -> callback.onEstablishSecureChannelFailure(error));
- }
-
- /** Return the {@link EncryptionRunner} for this channel. */
- @NonNull
- EncryptionRunner getEncryptionRunner() {
- return encryptionRunner;
- }
-
- /**
- * Process the inner message and replace with decrypted value if necessary. If an error occurs the
- * inner message will be replaced with {@code null} and call {@link
- * Callback#onMessageReceivedError(Exception)} on the registered callback.
- *
- * @param deviceMessage The message to process.
- * @return {@code true} if message was successfully processed. {@code false} if an error occurred.
- */
- @VisibleForTesting
- boolean processMessage(@NonNull DeviceMessage deviceMessage) {
- if (!deviceMessage.isMessageEncrypted()) {
- logd(TAG, "Message was not encrypted. No further action necessary.");
- return true;
- }
- Key key = encryptionKey.get();
- if (key == null) {
- loge(TAG, "Received encrypted message before secure channel has been established.");
- notifyCallback(callback -> callback.onMessageReceivedError(null));
- deviceMessage.setMessage(null);
- return false;
- }
- try {
- byte[] decryptedMessage = key.decryptData(deviceMessage.getMessage());
- deviceMessage.setMessage(decryptedMessage);
- logd(TAG, "Decrypted secure message.");
- return true;
- } catch (SignatureException e) {
- loge(TAG, "Could not decrypt secure message.", e);
- notifyCallback(callback -> callback.onMessageReceivedError(e));
- deviceMessage.setMessage(null);
-
- return false;
- }
- }
-
- @VisibleForTesting
- void onMessageReceived(@NonNull DeviceMessage deviceMessage) {
- boolean success = processMessage(deviceMessage);
- if (success) {
- success = decompressMessage(deviceMessage);
- }
- OperationType operationType = deviceMessage.getOperationType();
- switch (operationType) {
- case ENCRYPTION_HANDSHAKE:
- if (!success) {
- notifyCallback(
- callback ->
- callback.onEstablishSecureChannelFailure(CHANNEL_ERROR_INVALID_HANDSHAKE));
- break;
- }
- logd(TAG, "Received handshake message.");
- try {
- processHandshake(deviceMessage.getMessage());
- } catch (HandshakeException e) {
- loge(TAG, "Handshake failed.", e);
- notifyCallback(
- callback ->
- callback.onEstablishSecureChannelFailure(CHANNEL_ERROR_INVALID_HANDSHAKE));
- }
- break;
- case CLIENT_MESSAGE:
- case QUERY:
- case QUERY_RESPONSE:
- if (!success || deviceMessage.getMessage() == null) {
- break;
- }
- logd(TAG, "Received client message.");
- notifyCallback(callback -> callback.onMessageReceived(deviceMessage));
- break;
- default:
- loge(TAG, "Received unexpected operation type: " + operationType.name() + ".");
- }
- }
-
- @VisibleForTesting
- void compressMessage(@NonNull DeviceMessage deviceMessage) {
- byte[] originalMessage = deviceMessage.getMessage();
- byte[] compressedMessage = new byte[originalMessage.length];
- deflater.reset();
- deflater.setInput(originalMessage);
- deflater.finish();
- int compressedSize = deflater.deflate(compressedMessage);
- if (compressedSize >= originalMessage.length) {
- logd(TAG, "Message compression resulted in no savings. Sending original message.");
- deviceMessage.setOriginalMessageSize(0);
- return;
- }
- deviceMessage.setOriginalMessageSize(originalMessage.length);
- deviceMessage.setMessage(Arrays.copyOf(compressedMessage, compressedSize));
- long compressionSavings =
- Math.round((originalMessage.length - compressedSize) / (double) originalMessage.length)
- * 100L;
- logd(
- TAG,
- "Message compressed from "
- + originalMessage.length
- + " to "
- + compressedSize
- + " bytes saving "
- + compressionSavings
- + "%.");
- }
-
- @VisibleForTesting
- boolean decompressMessage(@NonNull DeviceMessage deviceMessage) {
- byte[] message = deviceMessage.getMessage();
- int originalMessageSize = deviceMessage.getOriginalMessageSize();
- if (originalMessageSize == 0) {
- logd(TAG, "Incoming message was not compressed. No further action necessary.");
- return true;
- }
- inflater.reset();
- inflater.setInput(message);
- byte[] decompressedMessage = new byte[originalMessageSize];
- try {
- inflater.inflate(decompressedMessage);
- deviceMessage.setMessage(decompressedMessage);
- } catch (DataFormatException e) {
- loge(TAG, "An error occurred while decompressing the message.", e);
- notifySecureChannelFailure(CHANNEL_ERROR_INVALID_MSG);
- return false;
- }
- logd(
- TAG,
- "Message successfully decompressed from "
- + message.length
- + " to "
- + originalMessageSize
- + " bytes.");
- return true;
- }
-
- /**
- * Callbacks that will be invoked during establishing secure channel, sending and receiving
- * messages securely.
- */
- public interface Callback {
- /** Invoked when secure channel has been established successfully. */
- default void onSecureChannelEstablished() {}
-
- /**
- * Invoked when a {@link ChannelError} has been encountered in attempting to establish a secure
- * channel.
- *
- * @param error The failure indication.
- */
- default void onEstablishSecureChannelFailure(@SecureChannel.ChannelError int error) {}
-
- /**
- * Invoked when a complete message is received securely from the client and decrypted.
- *
- * @param deviceMessage The {@link DeviceMessage} with decrypted message.
- */
- default void onMessageReceived(@NonNull DeviceMessage deviceMessage) {}
-
- /**
- * Invoked when there was an error during a processing or decrypting of a client message.
- *
- * @param exception The error.
- */
- default void onMessageReceivedError(@Nullable Exception exception) {}
-
- /**
- * Invoked when the device id was received from the client.
- *
- * @param deviceId The unique device id of client.
- */
- default void onDeviceIdReceived(@NonNull String deviceId) {}
- }
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ble/BleCentralManager.java b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ble/BleCentralManager.java
deleted file mode 100644
index 5b78381..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ble/BleCentralManager.java
+++ /dev/null
@@ -1,198 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice.connection.ble;
-
-import static com.google.android.connecteddevice.util.SafeLog.logd;
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-import static com.google.android.connecteddevice.util.SafeLog.logw;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothManager;
-import android.bluetooth.le.BluetoothLeScanner;
-import android.bluetooth.le.ScanCallback;
-import android.bluetooth.le.ScanFilter;
-import android.bluetooth.le.ScanResult;
-import android.bluetooth.le.ScanSettings;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.os.Handler;
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/** Class that manages BLE scanning operations. */
-public class BleCentralManager {
-
- private static final String TAG = "BleCentralManager";
-
- private static final int RETRY_LIMIT = 5;
-
- private static final int RETRY_INTERVAL_MS = 1000;
-
- private final Context context;
-
- private final Handler handler;
-
- private final AtomicInteger scannerState = new AtomicInteger(STOPPED);
-
- private final BluetoothManager bluetoothManager;
-
- private List<ScanFilter> scanFilters;
-
- private ScanSettings scanSettings;
-
- private ScanCallback scanCallback;
-
- private BluetoothLeScanner scanner;
-
- private int scannerStartCount = 0;
-
-
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({STOPPED, STARTED, SCANNING})
- private @interface ScannerState {}
-
- private static final int STOPPED = 0;
- private static final int STARTED = 1;
- private static final int SCANNING = 2;
-
- public BleCentralManager(@NonNull Context context) {
- this.context = context;
- handler = new Handler(context.getMainLooper());
- bluetoothManager = context.getSystemService(BluetoothManager.class);
- }
-
- /**
- * Start the BLE scanning process.
- *
- * @param filters Optional list of {@link ScanFilter}s to apply to scan results.
- * @param settings {@link ScanSettings} to apply to scanner.
- * @param callback {@link ScanCallback} for scan events.
- */
- public void startScanning(
- @Nullable List<ScanFilter> filters,
- @NonNull ScanSettings settings,
- @NonNull ScanCallback callback) {
- if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
- loge(TAG, "Attempted start scanning, but system does not support BLE. Ignoring");
- return;
- }
- logd(TAG, "Request received to start scanning.");
- scannerStartCount = 0;
- scanFilters = filters;
- scanSettings = settings;
- scanCallback = callback;
- updateScannerState(STARTED);
- startScanningInternally();
- }
-
- /** Stop the scanner */
- public void stopScanning() {
- logd(TAG, "Attempting to stop scanning");
- if (scanner != null) {
- scanner.stopScan(internalScanCallback);
- }
- scanCallback = null;
- updateScannerState(STOPPED);
- }
-
- /** Returns {@code true} if currently scanning, {@code false} otherwise. */
- public boolean isScanning() {
- return scannerState.get() == SCANNING;
- }
-
- /** Clean up the scanning process. */
- public void cleanup() {
- if (isScanning()) {
- stopScanning();
- }
- }
-
- private void startScanningInternally() {
- logd(TAG, "Attempting to start scanning");
- BluetoothAdapter adapter = bluetoothManager.getAdapter();
- if (scanner == null && adapter != null) {
- scanner = adapter.getBluetoothLeScanner();
- }
- if (scanner != null) {
- scanner.startScan(scanFilters, scanSettings, internalScanCallback);
- updateScannerState(SCANNING);
- } else {
- handler.postDelayed(
- () -> {
- // Keep trying
- logd(TAG, "Scanner unavailable. Trying again.");
- startScanningInternally();
- },
- RETRY_INTERVAL_MS);
- }
- }
-
- private void updateScannerState(@ScannerState int newState) {
- scannerState.set(newState);
- }
-
- private final ScanCallback internalScanCallback =
- new ScanCallback() {
- @Override
- public void onScanResult(int callbackType, ScanResult result) {
- if (scanCallback != null) {
- scanCallback.onScanResult(callbackType, result);
- }
- }
-
- @Override
- public void onBatchScanResults(List<ScanResult> results) {
- logd(TAG, "Batch scan found " + results.size() + " results.");
- if (scanCallback != null) {
- scanCallback.onBatchScanResults(results);
- }
- }
-
- @Override
- public void onScanFailed(int errorCode) {
- if (scannerStartCount >= RETRY_LIMIT) {
- loge(TAG, "Cannot start BLE Scanner. Scanning Retry count: " + scannerStartCount);
- if (scanCallback != null) {
- scanCallback.onScanFailed(errorCode);
- }
- return;
- }
-
- scannerStartCount++;
- logw(
- TAG,
- "BLE Scanner failed to start. Error: " + errorCode + " Retry: " + scannerStartCount);
- switch (errorCode) {
- case SCAN_FAILED_ALREADY_STARTED:
- // Scanner already started. Do nothing.
- break;
- case SCAN_FAILED_APPLICATION_REGISTRATION_FAILED:
- case SCAN_FAILED_INTERNAL_ERROR:
- handler.postDelayed(
- BleCentralManager.this::startScanningInternally, RETRY_INTERVAL_MS);
- break;
- default:
- // Ignore other codes.
- }
- }
- };
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ble/BleDeviceMessageStream.java b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ble/BleDeviceMessageStream.java
deleted file mode 100644
index 6b10448..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ble/BleDeviceMessageStream.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice.connection.ble;
-
-import static com.google.android.connecteddevice.util.SafeLog.logd;
-import static com.google.android.connecteddevice.util.SafeLog.logw;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGattCharacteristic;
-import androidx.annotation.NonNull;
-import com.google.android.connecteddevice.connection.DeviceMessageStream;
-import com.google.android.connecteddevice.transport.ble.BlePeripheralManager;
-
-/** BLE message stream to a device. */
-public class BleDeviceMessageStream extends DeviceMessageStream {
-
- private static final String TAG = "BleDeviceMessageStream";
-
- private final BlePeripheralManager blePeripheralManager;
-
- private final BluetoothDevice device;
-
- private final BluetoothGattCharacteristic writeCharacteristic;
-
- private final BluetoothGattCharacteristic readCharacteristic;
-
- BleDeviceMessageStream(
- @NonNull BlePeripheralManager blePeripheralManager,
- @NonNull BluetoothDevice device,
- @NonNull BluetoothGattCharacteristic writeCharacteristic,
- @NonNull BluetoothGattCharacteristic readCharacteristic,
- int defaultMaxWriteSize) {
- super(defaultMaxWriteSize);
- this.blePeripheralManager = blePeripheralManager;
- this.device = device;
- this.writeCharacteristic = writeCharacteristic;
- this.readCharacteristic = readCharacteristic;
- this.blePeripheralManager.addOnCharacteristicWriteListener(this::onCharacteristicWrite);
- this.blePeripheralManager.addOnCharacteristicReadListener(this::onCharacteristicRead);
- }
-
- @Override
- protected void send(byte[] data) {
- writeCharacteristic.setValue(data);
- blePeripheralManager.notifyCharacteristicChanged(
- device, writeCharacteristic, /* confirm= */ false);
- }
-
- private void onCharacteristicRead(@NonNull BluetoothDevice device) {
- if (!this.device.equals(device)) {
- logw(
- TAG,
- "Received a read notification from a device ("
- + device.getAddress()
- + ") that is not the expected device ("
- + this.device.getAddress()
- + ") registered "
- + "to this stream. Ignoring.");
- return;
- }
-
- logd(TAG, "Releasing lock on characteristic.");
- sendCompleted();
- }
-
- private void onCharacteristicWrite(
- @NonNull BluetoothDevice device,
- @NonNull BluetoothGattCharacteristic characteristic,
- @NonNull byte[] value) {
- logd(TAG, "Received a message from a device (" + device.getAddress() + ").");
- if (!this.device.equals(device)) {
- logw(
- TAG,
- "Received a message from a device ("
- + device.getAddress()
- + ") that is not "
- + "the expected device ("
- + this.device.getAddress()
- + ") registered to this "
- + "stream. Ignoring.");
- return;
- }
-
- if (!characteristic.getUuid().equals(readCharacteristic.getUuid())) {
- logw(
- TAG,
- "Received a write to a characteristic ("
- + characteristic.getUuid()
- + ") that"
- + " is not the expected UUID ("
- + readCharacteristic.getUuid()
- + "). "
- + "Ignoring.");
- return;
- }
- onDataReceived(value);
- }
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ble/CarBleCentralManager.java b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ble/CarBleCentralManager.java
deleted file mode 100644
index 488eb0d..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ble/CarBleCentralManager.java
+++ /dev/null
@@ -1,382 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice.connection.ble;
-
-import static com.google.android.connecteddevice.util.SafeLog.logd;
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-import static com.google.android.connecteddevice.util.SafeLog.logw;
-import static com.google.android.connecteddevice.util.ScanDataAnalyzer.containsUuidsInOverflow;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.BluetoothGattCallback;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattDescriptor;
-import android.bluetooth.BluetoothGattService;
-import android.bluetooth.BluetoothProfile;
-import android.bluetooth.le.ScanCallback;
-import android.bluetooth.le.ScanRecord;
-import android.bluetooth.le.ScanResult;
-import android.bluetooth.le.ScanSettings;
-import android.content.Context;
-import android.os.ParcelUuid;
-import androidx.annotation.NonNull;
-import com.google.android.connecteddevice.connection.AssociationCallback;
-import com.google.android.connecteddevice.connection.CarBluetoothManager;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
-import java.math.BigInteger;
-import java.util.List;
-import java.util.UUID;
-import java.util.concurrent.CopyOnWriteArraySet;
-
-/**
- * Communication manager for a car that maintains continuous connections with all devices in the car
- * for the duration of a drive.
- */
-public class CarBleCentralManager extends CarBluetoothManager {
-
- private static final String TAG = "CarBleCentralManager";
-
- // system/bt/internal_include/bt_target.h#GATT_MAX_PHY_CHANNEL
- private static final int MAX_CONNECTIONS = 7;
-
- private static final UUID CHARACTERISTIC_CONFIG =
- UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
-
- private static final int STATUS_FORCED_DISCONNECT = -1;
-
- private final ScanSettings scanSettings =
- new ScanSettings.Builder()
- .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
- .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
- .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
- .build();
-
- private final CopyOnWriteArraySet<ConnectedRemoteDevice> ignoredDevices =
- new CopyOnWriteArraySet<>();
-
- private final Context context;
-
- private final BleCentralManager bleCentralManager;
-
- private final UUID serviceUuid;
-
- private final UUID writeCharacteristicUuid;
-
- private final UUID readCharacteristicUuid;
-
- private final BigInteger parsedBgServiceBitMask;
-
- /**
- * Create a new manager.
- *
- * @param context The caller's [Context].
- * @param bleCentralManager [BleCentralManager] for establishing connections.
- * @param connectedDeviceStorage Shared [ConnectedDeviceStorage] for companion features.
- * @param serviceUuid [UUID] of peripheral's service.
- * @param bgServiceMask iOS overflow bit mask for service UUID.
- * @param writeCharacteristicUuid [UUID] of characteristic the car will write to.
- * @param readCharacteristicUuid [UUID] of characteristic the device will write to.
- * @param enableCompression Enable compression on outgoing messages.
- * @param isCapabilitiesEligible Association should attempt a capabilities exchange.
- */
- public CarBleCentralManager(
- @NonNull Context context,
- @NonNull BleCentralManager bleCentralManager,
- @NonNull ConnectedDeviceStorage connectedDeviceStorage,
- @NonNull UUID serviceUuid,
- @NonNull String bgServiceMask,
- @NonNull UUID writeCharacteristicUuid,
- @NonNull UUID readCharacteristicUuid,
- boolean enableCompression,
- boolean isCapabilitiesEligible) {
- super(connectedDeviceStorage, enableCompression, isCapabilitiesEligible);
- this.context = context;
- this.bleCentralManager = bleCentralManager;
- this.serviceUuid = serviceUuid;
- this.writeCharacteristicUuid = writeCharacteristicUuid;
- this.readCharacteristicUuid = readCharacteristicUuid;
- parsedBgServiceBitMask = new BigInteger(bgServiceMask, 16);
- }
-
- @Override
- public void start() {
- super.start();
- bleCentralManager.startScanning(/* filters= */ null, scanSettings, scanCallback);
- }
-
- @Override
- public void stop() {
- super.stop();
- bleCentralManager.stopScanning();
- }
-
- @Override
- public void disconnectDevice(String deviceId) {
- logd(TAG, "Request to disconnect from device " + deviceId + ".");
- ConnectedRemoteDevice device = getConnectedDevice(deviceId);
- if (device == null) {
- return;
- }
-
- deviceDisconnected(device, STATUS_FORCED_DISCONNECT);
- }
-
- // TODO(b/141312136): Support car central role
- @Override
- public AssociationCallback getAssociationCallback() {
- return null;
- }
-
- @Override
- public void setAssociationCallback(AssociationCallback callback) {}
-
- @Override
- public void connectToDevice(UUID deviceId) {}
-
- @Override
- public void initiateConnectionToDevice(UUID deviceId) {}
-
- @Override
- public void startAssociation(byte[] nameForAssociation, AssociationCallback callback) {}
-
- private void ignoreDevice(@NonNull ConnectedRemoteDevice device) {
- ignoredDevices.add(device);
- }
-
- private boolean isDeviceIgnored(@NonNull BluetoothDevice device) {
- for (ConnectedRemoteDevice connectedDevice : ignoredDevices) {
- if (device.equals(connectedDevice.device)) {
- return true;
- }
- }
- return false;
- }
-
- private boolean shouldAttemptConnection(@NonNull ScanResult result) {
- // Ignore any results that are not connectable.
- if (!result.isConnectable()) {
- return false;
- }
-
- // Do not attempt to connect if we have already hit our max. This should rarely happen
- // and is protecting against a race condition of scanning stopped and new results coming in.
- if (getConnectedDevicesCount() >= MAX_CONNECTIONS) {
- return false;
- }
-
- BluetoothDevice device = result.getDevice();
-
- // Do not connect if device has already been ignored.
- if (isDeviceIgnored(device)) {
- return false;
- }
-
- // Check if already attempting to connect to this device.
- if (getConnectedDevice(device) != null) {
- return false;
- }
-
- // Ignore any device without a scan record.
- ScanRecord scanRecord = result.getScanRecord();
- if (scanRecord == null) {
- return false;
- }
-
- // Connect to any device that is advertising our service UUID.
- List<ParcelUuid> serviceUuids = scanRecord.getServiceUuids();
- if (serviceUuids != null) {
- for (ParcelUuid serviceUuid : serviceUuids) {
- if (serviceUuid.getUuid().equals(this.serviceUuid)) {
- return true;
- }
- }
- }
- if (containsUuidsInOverflow(scanRecord.getBytes(), parsedBgServiceBitMask)) {
- return true;
- }
-
- // Can safely ignore devices advertising unrecognized service uuids.
- if (serviceUuids != null && !serviceUuids.isEmpty()) {
- return false;
- }
-
- // TODO(b/139066293): Current implementation quickly exhausts connections resulting in
- // greatly reduced performance for connecting to devices we know we want to connect to.
- // Return true once fixed.
- return false;
- }
-
- private void startDeviceConnection(@NonNull BluetoothDevice device) {
- BluetoothGatt gatt =
- device.connectGatt(
- context, /* autoConnect= */ false, connectionCallback, BluetoothDevice.TRANSPORT_LE);
- if (gatt == null) {
- return;
- }
-
- ConnectedRemoteDevice bleDevice = new ConnectedRemoteDevice(device, gatt);
- bleDevice.state = ConnectedDeviceState.CONNECTING;
- addConnectedDevice(bleDevice);
-
- // Stop scanning if we have reached the maximum number of connections.
- if (getConnectedDevicesCount() >= MAX_CONNECTIONS) {
- bleCentralManager.stopScanning();
- }
- }
-
- private void deviceConnected(@NonNull ConnectedRemoteDevice device) {
- if (device.gatt == null) {
- loge(TAG, "Device connected with null gatt. Disconnecting.");
- deviceDisconnected(device, BluetoothProfile.STATE_DISCONNECTED);
- return;
- }
- device.state = ConnectedDeviceState.PENDING_VERIFICATION;
- device.gatt.discoverServices();
- logd(
- TAG,
- "New device connected: "
- + device.gatt.getDevice().getAddress()
- + ". Active connections: "
- + getConnectedDevicesCount()
- + ".");
- }
-
- private void deviceDisconnected(@NonNull ConnectedRemoteDevice device, int status) {
- removeConnectedDevice(device);
- if (device.gatt != null) {
- device.gatt.close();
- }
- if (device.deviceId != null) {
- callbacks.invoke(callback -> callback.onDeviceDisconnected(device.deviceId));
- }
- logd(
- TAG,
- "Device with id "
- + device.deviceId
- + " disconnected with state "
- + status
- + ". Remaining active connections: "
- + getConnectedDevicesCount()
- + ".");
- }
-
- private final ScanCallback scanCallback =
- new ScanCallback() {
- @Override
- public void onScanResult(int callbackType, ScanResult result) {
- super.onScanResult(callbackType, result);
- if (shouldAttemptConnection(result)) {
- startDeviceConnection(result.getDevice());
- }
- }
-
- @Override
- public void onScanFailed(int errorCode) {
- super.onScanFailed(errorCode);
- loge(TAG, "BLE scanning failed with error code: " + errorCode);
- }
- };
-
- private final BluetoothGattCallback connectionCallback =
- new BluetoothGattCallback() {
- @Override
- public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
- super.onConnectionStateChange(gatt, status, newState);
- if (gatt == null) {
- logw(TAG, "Null gatt passed to onConnectionStateChange. Ignoring.");
- return;
- }
-
- ConnectedRemoteDevice connectedDevice = getConnectedDevice(gatt);
- if (connectedDevice == null) {
- return;
- }
-
- switch (newState) {
- case BluetoothProfile.STATE_CONNECTED:
- deviceConnected(connectedDevice);
- break;
- case BluetoothProfile.STATE_DISCONNECTED:
- deviceDisconnected(connectedDevice, status);
- break;
- default:
- logd(TAG, "Connection state changed. New state: " + newState + " status: " + status);
- }
- }
-
- @Override
- public void onServicesDiscovered(BluetoothGatt gatt, int status) {
- super.onServicesDiscovered(gatt, status);
- if (gatt == null) {
- logw(TAG, "Null gatt passed to onServicesDiscovered. Ignoring.");
- return;
- }
-
- ConnectedRemoteDevice connectedDevice = getConnectedDevice(gatt);
- if (connectedDevice == null) {
- return;
- }
- BluetoothGattService service = gatt.getService(serviceUuid);
- if (service == null) {
- ignoreDevice(connectedDevice);
- gatt.disconnect();
- return;
- }
-
- connectedDevice.state = ConnectedDeviceState.CONNECTED;
- BluetoothGattCharacteristic writeCharacteristic =
- service.getCharacteristic(writeCharacteristicUuid);
- BluetoothGattCharacteristic readCharacteristic =
- service.getCharacteristic(readCharacteristicUuid);
- if (writeCharacteristic == null || readCharacteristic == null) {
- logw(TAG, "Unable to find expected characteristics on peripheral.");
- gatt.disconnect();
- return;
- }
-
- // Turn on notifications for read characteristic.
- BluetoothGattDescriptor descriptor =
- readCharacteristic.getDescriptor(CHARACTERISTIC_CONFIG);
- descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
- if (!gatt.writeDescriptor(descriptor)) {
- loge(TAG, "Write descriptor to read characteristic failed.");
- gatt.disconnect();
- return;
- }
-
- if (!gatt.setCharacteristicNotification(readCharacteristic, /* enable= */ true)) {
- loge(TAG, "Set notifications to read characteristic failed.");
- gatt.disconnect();
- return;
- }
-
- logd(TAG, "Service and characteristics successfully discovered.");
- }
-
- @Override
- public void onDescriptorWrite(
- BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
- super.onDescriptorWrite(gatt, descriptor, status);
- if (gatt == null) {
- logw(TAG, "Null gatt passed to onDescriptorWrite. Ignoring.");
- return;
- }
- // TODO(b/141312136): Create SecureBleChannel and assign to connectedDevice.
- }
- };
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ble/CarBlePeripheralManager.java b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ble/CarBlePeripheralManager.java
deleted file mode 100644
index e0fc747..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ble/CarBlePeripheralManager.java
+++ /dev/null
@@ -1,523 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice.connection.ble;
-
-import static com.google.android.connecteddevice.model.Errors.DEVICE_ERROR_UNEXPECTED_DISCONNECTION;
-import static com.google.android.connecteddevice.util.SafeLog.logd;
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-import static com.google.android.connecteddevice.util.SafeLog.logw;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattDescriptor;
-import android.bluetooth.BluetoothGattService;
-import android.bluetooth.le.AdvertiseCallback;
-import android.bluetooth.le.AdvertiseData;
-import android.bluetooth.le.AdvertiseSettings;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.ParcelUuid;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-import com.google.android.connecteddevice.connection.AssociationCallback;
-import com.google.android.connecteddevice.connection.AssociationSecureChannel;
-import com.google.android.connecteddevice.connection.CarBluetoothManager;
-import com.google.android.connecteddevice.connection.ConnectionResolver;
-import com.google.android.connecteddevice.connection.DeviceMessageStream;
-import com.google.android.connecteddevice.connection.ReconnectSecureChannel;
-import com.google.android.connecteddevice.model.OobData;
-import com.google.android.connecteddevice.model.StartAssociationResponse;
-import com.google.android.connecteddevice.oob.OobChannel;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
-import com.google.android.connecteddevice.transport.ble.BlePeripheralManager;
-import com.google.android.connecteddevice.util.ByteUtils;
-import com.google.android.connecteddevice.util.EventLog;
-import java.time.Duration;
-import java.util.Arrays;
-import java.util.UUID;
-import java.util.concurrent.Future;
-
-/** Communication manager that allows for targeted connections to a specific device in the car. */
-public class CarBlePeripheralManager extends CarBluetoothManager {
-
- private static final String TAG = "CarBlePeripheralManager";
-
- // Attribute protocol bytes attached to message. Available write size is MTU size minus att
- // bytes.
- private static final int ATT_PROTOCOL_BYTES = 3;
-
- private static final UUID CLIENT_CHARACTERISTIC_CONFIG =
- UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
-
- private static final int SALT_BYTES = 8;
-
- private static final int TOTAL_AD_DATA_BYTES = 16;
-
- private static final int TRUNCATED_BYTES = 3;
-
- private static final String TIMEOUT_HANDLER_THREAD_NAME = "peripheralThread";
-
- private final BlePeripheralManager blePeripheralManager;
-
- private final UUID associationServiceUuid;
-
- private final UUID reconnectServiceUuid;
-
- private final UUID reconnectDataUuid;
-
- private final BluetoothGattCharacteristic writeCharacteristic;
-
- @VisibleForTesting final BluetoothGattCharacteristic readCharacteristic;
-
- private final BluetoothGattCharacteristic advertiseDataCharacteristic;
-
- private HandlerThread timeoutHandlerThread;
-
- private Handler timeoutHandler;
-
- private final Duration maxReconnectAdvertisementDuration;
-
- private final int defaultMtuSize;
-
- private String reconnectDeviceId;
-
- private byte[] reconnectChallenge;
-
- private AdvertiseCallback advertiseCallback;
-
- private Future<?> bluetoothNameTask;
-
- public CarBlePeripheralManager(
- @NonNull BlePeripheralManager blePeripheralManager,
- @NonNull ConnectedDeviceStorage connectedDeviceStorage,
- @NonNull UUID associationServiceUuid,
- @NonNull UUID reconnectServiceUuid,
- @NonNull UUID reconnectDataUuid,
- @NonNull UUID advertiseDataCharacteristicUuid,
- @NonNull UUID writeCharacteristicUuid,
- @NonNull UUID readCharacteristicUuid,
- @NonNull Duration maxReconnectAdvertisementDuration,
- int defaultMtuSize,
- boolean enableCompression,
- boolean isCapabilitiesEligible) {
- this(
- blePeripheralManager,
- connectedDeviceStorage,
- associationServiceUuid,
- reconnectServiceUuid,
- reconnectDataUuid,
- advertiseDataCharacteristicUuid,
- writeCharacteristicUuid,
- readCharacteristicUuid,
- maxReconnectAdvertisementDuration,
- defaultMtuSize,
- enableCompression,
- /* oobChannel= */ null,
- isCapabilitiesEligible);
- }
-
- /**
- * Initialize a new instance of manager.
- *
- * @param blePeripheralManager {@link BlePeripheralManager} for establishing connection.
- * @param connectedDeviceStorage Shared {@link ConnectedDeviceStorage} for companion features.
- * @param associationServiceUuid {@link UUID} of association service.
- * @param reconnectServiceUuid {@link UUID} of reconnect service.
- * @param reconnectDataUuid {@link UUID} key of reconnect advertisement data.
- * @param advertiseDataCharacteristicUuid {@link UUID} of characteristic that contains advertise
- * data.
- * @param writeCharacteristicUuid {@link UUID} of characteristic the car will write to.
- * @param readCharacteristicUuid {@link UUID} of characteristic the device will write to.
- * @param maxReconnectAdvertisementDuration Maximum duration to advertise for reconnect before
- * restarting.
- * @param defaultMtuSize Default MTU size for new channels.
- * @param enableCompression Enable compression on outgoing messages.
- * @param oobChannel Channel for exchanging out of band verification.
- * @param isCapabilitiesEligible Association should attempt a capabilities exchange.
- */
- public CarBlePeripheralManager(
- @NonNull BlePeripheralManager blePeripheralManager,
- @NonNull ConnectedDeviceStorage connectedDeviceStorage,
- @NonNull UUID associationServiceUuid,
- @NonNull UUID reconnectServiceUuid,
- @NonNull UUID reconnectDataUuid,
- @NonNull UUID advertiseDataCharacteristicUuid,
- @NonNull UUID writeCharacteristicUuid,
- @NonNull UUID readCharacteristicUuid,
- @NonNull Duration maxReconnectAdvertisementDuration,
- int defaultMtuSize,
- boolean enableCompression,
- @Nullable OobChannel oobChannel,
- boolean isCapabilitiesEligible) {
- super(connectedDeviceStorage, enableCompression, oobChannel, isCapabilitiesEligible);
- this.blePeripheralManager = blePeripheralManager;
- this.associationServiceUuid = associationServiceUuid;
- this.reconnectServiceUuid = reconnectServiceUuid;
- this.reconnectDataUuid = reconnectDataUuid;
- writeCharacteristic =
- new BluetoothGattCharacteristic(
- writeCharacteristicUuid,
- BluetoothGattCharacteristic.PROPERTY_NOTIFY | BluetoothGattCharacteristic.PROPERTY_READ,
- BluetoothGattCharacteristic.PERMISSION_READ);
- writeCharacteristic.addDescriptor(createBluetoothGattDescriptor());
-
- readCharacteristic =
- new BluetoothGattCharacteristic(
- readCharacteristicUuid,
- BluetoothGattCharacteristic.PROPERTY_WRITE
- | BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE,
- BluetoothGattCharacteristic.PERMISSION_WRITE);
- readCharacteristic.addDescriptor(createBluetoothGattDescriptor());
-
- advertiseDataCharacteristic =
- new BluetoothGattCharacteristic(
- advertiseDataCharacteristicUuid,
- BluetoothGattCharacteristic.PROPERTY_NOTIFY | BluetoothGattCharacteristic.PROPERTY_READ,
- BluetoothGattCharacteristic.PERMISSION_READ);
- advertiseDataCharacteristic.addDescriptor(createBluetoothGattDescriptor());
-
- this.maxReconnectAdvertisementDuration = maxReconnectAdvertisementDuration;
- this.defaultMtuSize = defaultMtuSize;
- }
-
- private BluetoothGattDescriptor createBluetoothGattDescriptor() {
- BluetoothGattDescriptor descriptor =
- new BluetoothGattDescriptor(
- CLIENT_CHARACTERISTIC_CONFIG,
- BluetoothGattDescriptor.PERMISSION_READ | BluetoothGattDescriptor.PERMISSION_WRITE);
- descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
- return descriptor;
- }
-
- @Override
- public void start() {
- super.start();
- timeoutHandlerThread = new HandlerThread(TIMEOUT_HANDLER_THREAD_NAME);
- timeoutHandlerThread.start();
- timeoutHandler = new Handler(timeoutHandlerThread.getLooper());
- }
-
- @Override
- public void stop() {
- super.stop();
- if (timeoutHandlerThread != null) {
- timeoutHandlerThread.quit();
- }
- reset();
- }
-
- @Override
- public void disconnectDevice(@NonNull String deviceId) {
- if (deviceId.equals(reconnectDeviceId)) {
- logd(TAG, "Reconnection canceled for device " + deviceId + ".");
- reset();
- return;
- }
- ConnectedRemoteDevice connectedDevice = getConnectedDevice();
- if (connectedDevice == null || !deviceId.equals(connectedDevice.deviceId)) {
- return;
- }
- reset();
- }
-
- @Override
- public void reset() {
- super.reset();
- logd(TAG, "Resetting state.");
- if (timeoutHandler != null) {
- timeoutHandler.removeCallbacks(timeoutRunnable);
- }
- blePeripheralManager.cleanup();
- reconnectDeviceId = null;
- reconnectChallenge = null;
- if (bluetoothNameTask != null) {
- bluetoothNameTask.cancel(true);
- }
- bluetoothNameTask = null;
- }
-
- @Override
- public void initiateConnectionToDevice(@NonNull UUID deviceId) {
- reconnectDeviceId = deviceId.toString();
- advertiseCallback =
- new AdvertiseCallback() {
- @Override
- public void onStartSuccess(AdvertiseSettings settingsInEffect) {
- super.onStartSuccess(settingsInEffect);
- timeoutHandler.postDelayed(
- timeoutRunnable, maxReconnectAdvertisementDuration.toMillis());
- logd(TAG, "Successfully started advertising for device " + deviceId + ".");
- }
- };
- blePeripheralManager.unregisterCallback(associationPeripheralCallback);
- blePeripheralManager.registerCallback(reconnectPeripheralCallback);
- timeoutHandler.removeCallbacks(timeoutRunnable);
- byte[] advertiseData = createReconnectData(reconnectDeviceId);
- if (advertiseData == null) {
- loge(TAG, "Unable to create advertisement data. Aborting reconnect.");
- return;
- }
- startAdvertising(
- reconnectServiceUuid,
- advertiseCallback,
- advertiseData,
- reconnectDataUuid,
- /* scanResponse= */ null,
- /* scanResponseUuid= */ null);
- }
-
- /**
- * Create data for reconnection advertisement.
- *
- * <p>
- *
- * <p>Process:
- *
- * <ol>
- * <li>Generate random {@value SALT_BYTES} byte salt and zero-pad to {@value
- * TOTAL_AD_DATA_BYTES} bytes.
- * <li>Hash with stored challenge secret and truncate to {@value TRUNCATED_BYTES} bytes.
- * <li>Concatenate hashed {@value TRUNCATED_BYTES} bytes with salt and return.
- * </ol>
- */
- @Nullable
- private byte[] createReconnectData(String deviceId) {
- byte[] salt = ByteUtils.randomBytes(SALT_BYTES);
- byte[] zeroPadded =
- ByteUtils.concatByteArrays(salt, new byte[TOTAL_AD_DATA_BYTES - SALT_BYTES]);
- reconnectChallenge = storage.hashWithChallengeSecret(deviceId, zeroPadded);
- if (reconnectChallenge == null) {
- return null;
- }
- return ByteUtils.concatByteArrays(Arrays.copyOf(reconnectChallenge, TRUNCATED_BYTES), salt);
- }
-
- @Override
- public void startAssociation(
- @NonNull byte[] nameForAssociation, @NonNull AssociationCallback callback) {
- reset();
- setAssociationCallback(callback);
- blePeripheralManager.unregisterCallback(reconnectPeripheralCallback);
- blePeripheralManager.registerCallback(associationPeripheralCallback);
- advertiseCallback =
- new AdvertiseCallback() {
- @Override
- public void onStartSuccess(AdvertiseSettings settingsInEffect) {
- super.onStartSuccess(settingsInEffect);
- callback.onAssociationStartSuccess(
- new StartAssociationResponse(
- /* oobData= */ new OobData(new byte[0], new byte[0], new byte[0]),
- nameForAssociation,
- ByteUtils.byteArrayToHexString(nameForAssociation)));
- logd(TAG, "Successfully started advertising for association.");
- }
-
- @Override
- public void onStartFailure(int errorCode) {
- super.onStartFailure(errorCode);
- callback.onAssociationStartFailure();
- logd(TAG, "Failed to start advertising for association. Error code: " + errorCode);
- }
- };
-
- startAdvertising(
- associationServiceUuid,
- advertiseCallback,
- /* advertiseData= */ null,
- /* advertiseDataUuid= */ null,
- nameForAssociation,
- reconnectDataUuid);
- }
-
- /** Set the timeout handler for testing. This should be called after {@link #start()}. */
- @VisibleForTesting
- void setTimeoutHandler(Handler handler) {
- timeoutHandler = handler;
- }
-
- private void startAdvertising(
- @NonNull UUID serviceUuid,
- @NonNull AdvertiseCallback callback,
- @Nullable byte[] advertiseData,
- @Nullable UUID advertiseDataUuid,
- @Nullable byte[] scanResponse,
- @Nullable UUID scanResponseUuid) {
- BluetoothGattService gattService =
- new BluetoothGattService(serviceUuid, BluetoothGattService.SERVICE_TYPE_PRIMARY);
- gattService.addCharacteristic(writeCharacteristic);
- gattService.addCharacteristic(readCharacteristic);
-
- AdvertiseData.Builder advertisementBuilder = new AdvertiseData.Builder();
- ParcelUuid uuid = new ParcelUuid(serviceUuid);
- advertisementBuilder.addServiceUuid(uuid);
- if (advertiseData != null) {
- ParcelUuid dataUuid = uuid;
- if (advertiseDataUuid != null) {
- dataUuid = new ParcelUuid(advertiseDataUuid);
- }
- advertisementBuilder.addServiceData(dataUuid, advertiseData);
-
- // Also embed the advertise data into a fixed GATT service characteristic.
- advertiseDataCharacteristic.setValue(advertiseData);
- gattService.addCharacteristic(advertiseDataCharacteristic);
- }
-
- AdvertiseData.Builder scanResponseBuilder = new AdvertiseData.Builder();
- if (scanResponse != null && scanResponseUuid != null) {
- ParcelUuid scanResponseParcelUuid = new ParcelUuid(scanResponseUuid);
- scanResponseBuilder.addServiceData(scanResponseParcelUuid, scanResponse);
- }
-
- blePeripheralManager.startAdvertising(
- gattService, advertisementBuilder.build(), scanResponseBuilder.build(), callback);
- }
-
- private void addConnectedDevice(BluetoothDevice device, boolean isReconnect) {
- EventLog.onDeviceConnected();
- blePeripheralManager.stopAdvertising(advertiseCallback);
- if (timeoutHandler != null) {
- timeoutHandler.removeCallbacks(timeoutRunnable);
- }
-
- setClientDeviceAddress(device.getAddress());
-
- DeviceMessageStream secureStream =
- new BleDeviceMessageStream(
- blePeripheralManager,
- device,
- writeCharacteristic,
- readCharacteristic,
- defaultMtuSize - ATT_PROTOCOL_BYTES);
-
- secureStream.setMessageReceivedErrorListener(
- exception ->
- disconnectWithError("Error occurred in stream: " + exception.getMessage(), exception));
-
- ConnectedRemoteDevice connectedDevice = new ConnectedRemoteDevice(device, /* gatt= */ null);
-
- if (isReconnect) {
- // Capabilities are only exchanged during association so isCapabilitiesEligible is always
- // false here.
- ConnectionResolver connectionResolver =
- new ConnectionResolver(secureStream, /* isCapabilitiesEligible= */ false);
- connectionResolver.resolveConnection(
- (resolvedConnection) -> {
- ReconnectSecureChannel secureChannel =
- new ReconnectSecureChannel(
- secureStream, storage, reconnectDeviceId, reconnectChallenge);
- secureChannel.registerCallback(secureChannelCallback);
- connectedDevice.secureChannel = secureChannel;
- addConnectedDevice(connectedDevice);
- setDeviceIdAndNotifyCallbacks(reconnectDeviceId);
- reconnectDeviceId = null;
- reconnectChallenge = null;
- });
- } else {
- onDeviceConnectedForAssociation(secureStream, connectedDevice);
- }
- }
-
- private void setMtuSize(int mtuSize) {
- ConnectedRemoteDevice connectedDevice = getConnectedDevice();
- if (connectedDevice != null
- && connectedDevice.secureChannel != null) {
- connectedDevice.secureChannel.getStream().setMaxWriteSize(mtuSize - ATT_PROTOCOL_BYTES);
- }
- }
-
- private final BlePeripheralManager.Callback reconnectPeripheralCallback =
- new BlePeripheralManager.Callback() {
- @Override
- public void onMtuSizeChanged(int size) {
- setMtuSize(size);
- }
-
- @Override
- public void onRemoteDeviceConnected(BluetoothDevice device) {
- addConnectedDevice(device, /* isReconnect= */ true);
- }
-
- @Override
- public void onRemoteDeviceDisconnected(BluetoothDevice device) {
- String deviceId = reconnectDeviceId;
- ConnectedRemoteDevice connectedDevice = getConnectedDevice(device);
- // Reset before invoking callbacks to avoid a race condition with reconnect
- // logic.
- reset();
- if (connectedDevice != null) {
- deviceId = connectedDevice.deviceId;
- }
- final String finalDeviceId = deviceId;
- if (finalDeviceId == null) {
- logw(
- TAG,
- "Callbacks were not issued for disconnect because the device id " + "was null.");
- return;
- }
- logd(TAG, "Connected device " + finalDeviceId + " disconnected.");
- callbacks.invoke(callback -> callback.onDeviceDisconnected(finalDeviceId));
- }
- };
-
- private final BlePeripheralManager.Callback associationPeripheralCallback =
- new BlePeripheralManager.Callback() {
- @Override
- public void onMtuSizeChanged(int size) {
- setMtuSize(size);
- }
-
- @Override
- public void onRemoteDeviceConnected(BluetoothDevice device) {
- addConnectedDevice(device, /* isReconnect= */ false);
- ConnectedRemoteDevice connectedDevice = getConnectedDevice();
- if (connectedDevice == null || connectedDevice.secureChannel == null) {
- return;
- }
- ((AssociationSecureChannel) connectedDevice.secureChannel)
- .setShowVerificationCodeListener(
- CarBlePeripheralManager.this::onVerificationCodeAvailable);
- }
-
- @Override
- public void onRemoteDeviceDisconnected(BluetoothDevice device) {
- logd(TAG, "Remote device disconnected.");
- ConnectedRemoteDevice connectedDevice = getConnectedDevice(device);
- if (isAssociating()) {
- getAssociationCallback().onAssociationError(DEVICE_ERROR_UNEXPECTED_DISCONNECTION);
- }
- // Reset before invoking callbacks to avoid a race condition with reconnect
- // logic.
- reset();
- if (connectedDevice == null || connectedDevice.deviceId == null) {
- logw(TAG, "Callbacks were not issued for disconnect.");
- return;
- }
- callbacks.invoke(callback -> callback.onDeviceDisconnected(connectedDevice.deviceId));
- }
- };
-
- private final Runnable timeoutRunnable =
- new Runnable() {
- @Override
- public void run() {
- logd(TAG, "Timeout period expired without a connection. Restarting advertisement.");
- blePeripheralManager.stopAdvertising(advertiseCallback);
- connectToDevice(UUID.fromString(reconnectDeviceId));
- }
- };
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/spp/CarSppManager.java b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/spp/CarSppManager.java
deleted file mode 100644
index 322b2ff..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/spp/CarSppManager.java
+++ /dev/null
@@ -1,305 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice.connection.spp;
-
-import static com.google.android.connecteddevice.model.Errors.DEVICE_ERROR_UNEXPECTED_DISCONNECTION;
-import static com.google.android.connecteddevice.util.SafeLog.logd;
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-
-import android.bluetooth.BluetoothDevice;
-import android.os.ParcelUuid;
-import android.os.RemoteException;
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-import com.google.android.connecteddevice.connection.AssociationCallback;
-import com.google.android.connecteddevice.connection.AssociationSecureChannel;
-import com.google.android.connecteddevice.connection.CarBluetoothManager;
-import com.google.android.connecteddevice.connection.ConnectionResolver;
-import com.google.android.connecteddevice.connection.DeviceMessageStream;
-import com.google.android.connecteddevice.connection.ReconnectSecureChannel;
-import com.google.android.connecteddevice.model.OobData;
-import com.google.android.connecteddevice.model.StartAssociationResponse;
-import com.google.android.connecteddevice.oob.BluetoothRfcommChannel;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
-import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder;
-import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder.OnErrorListener;
-import com.google.android.connecteddevice.transport.spp.Connection;
-import com.google.android.connecteddevice.transport.spp.PendingConnection;
-import com.google.android.connecteddevice.transport.spp.PendingConnection.OnConnectedListener;
-import com.google.android.connecteddevice.transport.spp.PendingConnection.OnConnectionErrorListener;
-import com.google.android.connecteddevice.util.EventLog;
-import com.google.common.base.Objects;
-import java.util.UUID;
-
-/**
- * Communication manager that allows for targeted connections to a specific device from the car
- * using {@link ConnectedDeviceSppDelegateBinder} .
- */
-public class CarSppManager extends CarBluetoothManager {
-
- private static final String TAG = "CarSppManager";
-
- private final ConnectedDeviceSppDelegateBinder sppServiceBinder;
-
- private final UUID associationServiceUuid;
-
- private final int packetMaxBytes;
-
- private String reconnectDeviceId;
-
- @VisibleForTesting Connection currentConnection;
-
- private PendingConnection currentPendingConnection;
-
- /**
- * Initialize a new instance of manager.
- *
- * @param sppBinder {@link ConnectedDeviceSppDelegateBinder} for establishing connection.
- * @param connectedDeviceStorage Shared {@link ConnectedDeviceStorage} for companion features.
- * @param packetMaxBytes Maximum size in bytes to write in one packet.
- * @param enableCompression Enable compression on outgoing messages.
- * @param isCapabilitiesEligible Association should attempt a capabilities exchange.
- */
- public CarSppManager(
- @NonNull ConnectedDeviceSppDelegateBinder sppBinder,
- @NonNull ConnectedDeviceStorage connectedDeviceStorage,
- @NonNull UUID associationServiceUuid,
- int packetMaxBytes,
- boolean enableCompression,
- boolean isCapabilitiesEligible) {
- super(
- connectedDeviceStorage,
- enableCompression,
- new BluetoothRfcommChannel(sppBinder),
- isCapabilitiesEligible);
- this.sppServiceBinder = sppBinder;
- this.associationServiceUuid = associationServiceUuid;
- this.packetMaxBytes = packetMaxBytes;
- }
-
- @Override
- public void stop() {
- super.stop();
- reset();
- }
-
- @Override
- public void disconnectDevice(@NonNull String deviceId) {
- ConnectedRemoteDevice connectedDevice = getConnectedDevice();
- if (connectedDevice == null || !deviceId.equals(connectedDevice.deviceId)) {
- return;
- }
- reset();
- }
-
- @Override
- public void initiateConnectionToDevice(@NonNull UUID deviceId) {
- logd(TAG, "Start spp reconnection listening for device with id: " + deviceId);
- currentConnection = null;
- reconnectDeviceId = deviceId.toString();
-
- sppServiceBinder.unregisterConnectionCallback(associationServiceUuid);
- sppServiceBinder.registerConnectionCallback(deviceId, reconnectOnErrorListener);
- try {
- PendingConnection pendingConnection =
- sppServiceBinder.connectAsServer(deviceId, /* isSecure= */ true);
-
- if (pendingConnection == null) {
- return;
- }
- currentPendingConnection = pendingConnection;
- pendingConnection
- .setOnConnectedListener(reconnectOnConnectedListener)
- .setOnConnectionErrorListener(onConnectionErrorListener);
- } catch (RemoteException e) {
- loge(TAG, "Error when start connection with remote device as server.", e);
- }
- }
-
- @Override
- public void reset() {
- super.reset();
- reconnectDeviceId = null;
- if (currentConnection != null) {
- try {
- sppServiceBinder.disconnect(currentConnection);
- } catch (RemoteException e) {
- loge(TAG, "Error when try to disconnect with remote device.", e);
- }
- currentConnection = null;
- }
-
- if (currentPendingConnection != null) {
- try {
- sppServiceBinder.cancelConnectionAttempt(currentPendingConnection);
- } catch (RemoteException e) {
- loge(TAG, "Error when try to disconnect with remote device.", e);
- }
- currentPendingConnection = null;
- }
- }
-
- /** Start the association by listening to incoming connect request. */
- @Override
- public void startAssociation(
- @NonNull byte[] nameForAssociation, @NonNull AssociationCallback callback) {
- if (reconnectDeviceId != null) {
- sppServiceBinder.unregisterConnectionCallback(UUID.fromString(reconnectDeviceId));
- }
- reset();
- setAssociationCallback(callback);
- sppServiceBinder.registerConnectionCallback(associationServiceUuid, associationOnErrorListener);
- try {
- PendingConnection pendingConnection =
- sppServiceBinder.connectAsServer(associationServiceUuid, /* isSecure= */ true);
- if (pendingConnection == null) {
- callback.onAssociationStartFailure();
- } else {
- currentPendingConnection = pendingConnection;
- pendingConnection
- .setOnConnectedListener(associationOnConnectedListener)
- .setOnConnectionErrorListener(onConnectionErrorListener);
- callback.onAssociationStartSuccess(
- new StartAssociationResponse(
- /* oobData= */ new OobData(new byte[0], new byte[0], new byte[0]),
- /* deviceIdentifier= */ new byte[0],
- /* deviceName= */ ""));
- }
- } catch (RemoteException e) {
- callback.onAssociationStartFailure();
- loge(TAG, "Error when try to start associate with remote device.", e);
- }
- }
-
- private void onDeviceConnected(Connection connection, boolean isReconnect) {
- currentConnection = connection;
- currentPendingConnection = null;
- EventLog.onDeviceConnected();
- BluetoothDevice device = connection.getRemoteDevice();
- setClientDeviceAddress(device.getAddress());
- setClientDeviceName(connection.getRemoteDeviceName());
- DeviceMessageStream secureStream =
- new SppDeviceMessageStream(sppServiceBinder, connection, packetMaxBytes);
- secureStream.setMessageReceivedErrorListener(
- exception ->
- disconnectWithError("Error occurred in stream: " + exception.getMessage(), exception));
-
- ConnectedRemoteDevice connectedDevice = new ConnectedRemoteDevice(device, /* gatt= */ null);
-
- if (isReconnect) {
- // Capabilities are only exchanged during association so isCapabilitiesEligible is always
- // false here.
- ConnectionResolver connectionResolver =
- new ConnectionResolver(secureStream, /* isCapabilitiesEligible= */ false);
- connectionResolver.resolveConnection(
- (resolvedConnection) -> {
- ReconnectSecureChannel secureChannel =
- new ReconnectSecureChannel(
- secureStream,
- storage,
- reconnectDeviceId,
- /* expectedChallengeResponse= */ null);
- secureChannel.registerCallback(secureChannelCallback);
- connectedDevice.secureChannel = secureChannel;
- addConnectedDevice(connectedDevice);
- setDeviceIdAndNotifyCallbacks(reconnectDeviceId);
- reconnectDeviceId = null;
- });
- } else {
- onDeviceConnectedForAssociation(secureStream, connectedDevice);
- }
- }
-
- private final OnConnectedListener reconnectOnConnectedListener =
- new OnConnectedListener() {
- @Override
- public void onConnected(
- UUID uuid, BluetoothDevice remoteDevice, boolean isSecure, String deviceName) {
- Connection connection =
- new Connection(new ParcelUuid(uuid), remoteDevice, isSecure, deviceName);
- onDeviceConnected(connection, /* isReconnect= */ true);
- }
- };
-
- @VisibleForTesting
- final OnErrorListener reconnectOnErrorListener =
- new OnErrorListener() {
-
- @Override
- public void onError(Connection connection) {
- if (!Objects.equal(currentConnection, connection)) {
- loge(TAG, "Receive connection error callback on a unrecognized connection, ignored.");
- return;
- }
- currentConnection = null;
- reset();
- }
- };
-
- private final OnConnectedListener associationOnConnectedListener =
- new OnConnectedListener() {
- @Override
- public void onConnected(
- UUID uuid, BluetoothDevice remoteDevice, boolean isSecure, String deviceName) {
- Connection connection =
- new Connection(new ParcelUuid(uuid), remoteDevice, isSecure, deviceName);
- onDeviceConnected(connection, /* isReconnect= */ false);
- ConnectedRemoteDevice connectedDevice = getConnectedDevice();
- if (connectedDevice == null || connectedDevice.secureChannel == null) {
- loge(TAG, "No connected device or secure channel found when trying to associate.");
- return;
- }
- ((AssociationSecureChannel) connectedDevice.secureChannel)
- .setShowVerificationCodeListener(
- code -> {
- if (!isAssociating()) {
- loge(TAG, "No valid callback for association.");
- return;
- }
- getAssociationCallback().onVerificationCodeAvailable(code);
- });
- }
- };
-
- final OnConnectionErrorListener onConnectionErrorListener =
- new OnConnectionErrorListener() {
- @Override
- public void onConnectionError() {
- loge(TAG, "Connection attempt failed when performing a server role.");
- }
- };
-
- @VisibleForTesting
- final OnErrorListener associationOnErrorListener =
- new OnErrorListener() {
-
- @Override
- public void onError(Connection connection) {
- if (!Objects.equal(currentConnection, connection)) {
- loge(TAG, "Receive connection error callback on a unrecognized connection, ignored.");
- return;
- }
- currentConnection = null;
- if (isAssociating()) {
- getAssociationCallback().onAssociationError(DEVICE_ERROR_UNEXPECTED_DISCONNECTION);
- } else {
- loge(TAG, "Encounter association error with no association callback registered.");
- }
- reset();
- }
- };
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/spp/SppDeviceMessageStream.java b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/spp/SppDeviceMessageStream.java
deleted file mode 100644
index e4a4d93..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/spp/SppDeviceMessageStream.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice.connection.spp;
-
-import static com.google.android.connecteddevice.util.SafeLog.logd;
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-
-import android.os.RemoteException;
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-import com.google.android.connecteddevice.connection.DeviceMessageStream;
-import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder;
-import com.google.android.connecteddevice.transport.spp.Connection;
-
-/** Spp message stream to a device. */
-class SppDeviceMessageStream extends DeviceMessageStream {
-
- private static final String TAG = "SppDeviceMessageStream";
-
- private final ConnectedDeviceSppDelegateBinder sppServiceBinder;
- private final Connection connection;
-
- SppDeviceMessageStream(
- @NonNull ConnectedDeviceSppDelegateBinder sppBinder,
- @NonNull Connection connection,
- int maxWriteSize) {
- super(maxWriteSize);
- this.sppServiceBinder = sppBinder;
- this.connection = connection;
- this.sppServiceBinder.setOnMessageReceivedListener(connection, this::onMessageReceived);
- }
-
- @Override
- protected void send(byte[] data) {
- try {
- sppServiceBinder.sendMessage(connection, data);
- } catch (RemoteException e) {
- loge(TAG, "Error when sending message to remote device");
- }
- sendCompleted();
- }
-
- @VisibleForTesting
- void onMessageReceived(@NonNull byte[] value) {
- logd(
- TAG,
- "Received a message from remote device("
- + connection.getRemoteDevice().getAddress()
- + ").");
- onDataReceived(value);
- }
-}
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 73c8d9b..7044d39 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/core/MultiProtocolDeviceController.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/core/MultiProtocolDeviceController.kt
@@ -16,6 +16,8 @@
package com.google.android.connecteddevice.core
import android.database.sqlite.SQLiteCantOpenDatabaseException
+import android.os.ParcelUuid
+import androidx.annotation.GuardedBy
import androidx.annotation.VisibleForTesting
import com.google.android.connecteddevice.api.IAssociationCallback
import com.google.android.connecteddevice.connection.ChannelResolver
@@ -32,9 +34,10 @@
import com.google.android.connecteddevice.model.StartAssociationResponse
import com.google.android.connecteddevice.oob.OobRunner
import com.google.android.connecteddevice.storage.ConnectedDeviceStorage
+import com.google.android.connecteddevice.transport.ConnectChallenge
import com.google.android.connecteddevice.transport.ConnectionProtocol
-import com.google.android.connecteddevice.transport.ConnectionProtocol.ConnectChallenge
-import com.google.android.connecteddevice.transport.ConnectionProtocol.DiscoveryCallback
+import com.google.android.connecteddevice.transport.IDeviceDisconnectedListener
+import com.google.android.connecteddevice.transport.IDiscoveryCallback
import com.google.android.connecteddevice.transport.ProtocolDevice
import com.google.android.connecteddevice.util.ByteUtils
import com.google.android.connecteddevice.util.SafeLog.logd
@@ -46,11 +49,12 @@
import java.util.Objects
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
-import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.CopyOnWriteArraySet
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicReference
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
/**
* The controller to manage all the connected devices and connected protocols of each connected
@@ -84,8 +88,10 @@
@VisibleForTesting internal val associationPendingDeviceId = AtomicReference<UUID?>(null)
- private val associatedDevices = CopyOnWriteArrayList<AssociatedDevice>()
- private val driverDevices = CopyOnWriteArrayList<AssociatedDevice>()
+ private val lock = ReentrantLock()
+
+ @GuardedBy("lock") private val associatedDevices = mutableListOf<AssociatedDevice>()
+ @GuardedBy("lock") private val driverDevices = mutableListOf<AssociatedDevice>()
private val storageCallback =
object : ConnectedDeviceStorage.AssociatedDeviceCallback {
@@ -106,31 +112,33 @@
override val connectedDevices: List<ConnectedDevice>
get() {
- val devices = mutableListOf<ConnectedDevice>()
- for (device in connectedRemoteDevices.values) {
- val associatedDevice =
- associatedDevices.firstOrNull { it.deviceId == device.deviceId.toString() }
- if (associatedDevice == null) {
- logd(
- TAG,
- "Unable to find a device with id ${device.deviceId} in associated devices. Skipped " +
- "mapping."
+ lock.withLock {
+ val devices = mutableListOf<ConnectedDevice>()
+ for (device in connectedRemoteDevices.values) {
+ val associatedDevice =
+ associatedDevices.firstOrNull { it.deviceId == device.deviceId.toString() }
+ if (associatedDevice == null) {
+ logd(
+ TAG,
+ "Unable to find a device with id ${device.deviceId} in associated devices. Skipped " +
+ "mapping."
+ )
+ continue
+ }
+ val belongsToDriver = driverDevices.any { it.deviceId == associatedDevice.deviceId }
+ val hasSecureChannel = device.secureChannel != null
+ devices.add(
+ ConnectedDevice(
+ associatedDevice.deviceId,
+ associatedDevice.deviceName,
+ belongsToDriver,
+ hasSecureChannel
+ )
)
- continue
}
- val belongsToDriver = driverDevices.any { it.deviceId == associatedDevice.deviceId }
- val hasSecureChannel = device.secureChannel != null
- devices.add(
- ConnectedDevice(
- associatedDevice.deviceId,
- associatedDevice.deviceName,
- belongsToDriver,
- hasSecureChannel
- )
- )
- }
- return devices
+ return devices
+ }
}
init {
@@ -181,7 +189,7 @@
}
for (protocol in protocols) {
val discoveryCallback = generateConnectionDiscoveryCallback(deviceId, protocol, challenge)
- protocol.startConnectionDiscovery(deviceId, challenge, discoveryCallback)
+ protocol.startConnectionDiscovery(ParcelUuid(deviceId), challenge, discoveryCallback)
}
}
@@ -206,7 +214,11 @@
for (protocol in protocols) {
val discoveryCallback =
generateAssociationDiscoveryCallback(protocol, callback, startAssociationResponse)
- protocol.startAssociationDiscovery(nameForAssociation, discoveryCallback, associationUuid)
+ protocol.startAssociationDiscovery(
+ nameForAssociation,
+ ParcelUuid(associationUuid),
+ discoveryCallback
+ )
}
}
@@ -262,7 +274,7 @@
override fun disconnectDevice(deviceId: UUID) {
logd(TAG, "Disconnecting device with id $deviceId.")
for (protocol in protocols) {
- protocol.stopConnectionDiscovery(deviceId)
+ protocol.stopConnectionDiscovery(ParcelUuid(deviceId))
}
val device = connectedRemoteDevices.remove(deviceId)
if (device == null) {
@@ -319,10 +331,16 @@
while (true) {
try {
logd(TAG, "Populating associated devices from storage.")
- associatedDevices.clear()
- driverDevices.clear()
- associatedDevices.addAll(storage.allAssociatedDevices)
- driverDevices.addAll(storage.driverAssociatedDevices)
+
+ // Fetch devices prior to applying lock to reduce lock time.
+ val driverOnlyDevices = storage.driverAssociatedDevices
+ val allDevices = storage.allAssociatedDevices
+ lock.withLock {
+ associatedDevices.clear()
+ associatedDevices.addAll(allDevices)
+ driverDevices.clear()
+ driverDevices.addAll(driverOnlyDevices)
+ }
logd(TAG, "Devices populated successfully.")
break
} catch (sqliteException: SQLiteCantOpenDatabaseException) {
@@ -368,7 +386,7 @@
protocol: ConnectionProtocol,
challenge: ConnectChallenge
) =
- object : DiscoveryCallback {
+ object : IDiscoveryCallback.Stub() {
override fun onDeviceConnected(protocolId: String) {
logd(
TAG,
@@ -376,8 +394,7 @@
)
protocol.registerDeviceDisconnectedListener(
protocolId,
- generateDeviceDisconnectedListener(deviceId, protocol),
- callbackExecutor
+ generateDeviceDisconnectedListener(deviceId, protocol)
)
val protocolDevice = ProtocolDevice(protocol, protocolId)
val device =
@@ -424,7 +441,7 @@
associationCallback: IAssociationCallback,
response: StartAssociationResponse
) =
- object : DiscoveryCallback {
+ object : IDiscoveryCallback.Stub() {
override fun onDeviceConnected(protocolId: String) {
logd(TAG, "New connection protocol connected, id: $protocolId, protocol: $protocol")
val protocolDevice = ProtocolDevice(protocol, protocolId)
@@ -440,8 +457,7 @@
}
protocol.registerDeviceDisconnectedListener(
protocolId,
- generateDeviceDisconnectedListener(pendingId, protocol),
- callbackExecutor
+ generateDeviceDisconnectedListener(pendingId, protocol)
)
// The channel only needs to be resolved once for all protocols connected to one remote
// device.
@@ -545,7 +561,7 @@
deviceId: UUID,
protocol: ConnectionProtocol,
) =
- object : ConnectionProtocol.DeviceDisconnectedListener {
+ object : IDeviceDisconnectedListener.Stub() {
override fun onDeviceDisconnected(protocolId: String) {
logd(TAG, "Remote connect protocol disconnected, id: $protocolId, protocol: $protocol")
connectedRemoteDevices.compute(deviceId) { deviceId, device ->
@@ -727,8 +743,7 @@
for (protocolDevice in newDevice.protocolDevices) {
protocolDevice.protocol.registerDeviceDisconnectedListener(
protocolDevice.protocolId,
- generateDeviceDisconnectedListener(deviceId, protocolDevice.protocol),
- callbackExecutor
+ generateDeviceDisconnectedListener(deviceId, protocolDevice.protocol)
)
}
return newDevice
@@ -742,15 +757,17 @@
/* deviceName= */ null,
/* isConnectionEnabled= */ true
)
- 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)
+ lock.withLock {
+ 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)
}
- associatedDevices.add(associatedDevice)
}
/**
@@ -761,7 +778,7 @@
device: ConnectedRemoteDevice,
onCallback: (ConnectedDevice, Callback) -> Unit
) {
- val connectedDevice = device.toConnectedDevice(driverDevices)
+ val connectedDevice = lock.withLock { device.toConnectedDevice(driverDevices) }
callbacks.invoke { onCallback(connectedDevice, it) }
}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/model/AssociatedDeviceDetails.java b/libs/connecteddevice/src/com/google/android/connecteddevice/model/AssociatedDeviceDetails.java
deleted file mode 100644
index 2061543..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/model/AssociatedDeviceDetails.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice.model;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-/** Class that contains the details of an associated device. */
-public class AssociatedDeviceDetails {
- private final AssociatedDevice device;
-
- private final boolean isConnected;
-
- public AssociatedDeviceDetails(@NonNull AssociatedDevice device, boolean isConnected) {
- this.device = device;
- this.isConnected = isConnected;
- }
-
- /** Get the device id. */
- @NonNull
- public String getDeviceId() {
- return device.getDeviceId();
- }
-
- /** Get the name of the associated device. */
- @Nullable
- public String getDeviceName() {
- return device.getDeviceName();
- }
-
- /** Get the device address. */
- @NonNull
- public String getDeviceAddress() {
- return device.getDeviceAddress();
- }
-
- /** {@code true} if the connection is enabled for the device. */
- public boolean isConnectionEnabled() {
- return device.isConnectionEnabled();
- }
-
- /** {@code true} if the device is connected. */
- public boolean isConnected() {
- return isConnected;
- }
-
- /** Returns the claiming user id, or {@value AssociatedDevice#UNCLAIMED_USER_ID} if unclaimed. */
- public int getUserId() {
- return device.getUserId();
- }
-
- /** Get {@link AssociatedDevice}. */
- @NonNull
- public AssociatedDevice getAssociatedDevice() {
- return device;
- }
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.java b/libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.java
deleted file mode 100644
index 3f21931..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice.model;
-
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-import android.os.Parcel;
-import android.os.Parcelable;
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import java.lang.annotation.Retention;
-import java.util.Objects;
-
-/** Device that may be used for an out-of-band channel. */
-public class OobEligibleDevice implements Parcelable {
-
- /** Device type */
- @Retention(SOURCE)
- @IntDef(value = {OOB_TYPE_BLUETOOTH})
- public @interface OobType {}
-
- public static final int OOB_TYPE_BLUETOOTH = 0;
-
- private final String deviceAddress;
-
- @OobType private final int oobType;
-
- public OobEligibleDevice(@NonNull String deviceAddress, @OobType int oobType) {
- this.deviceAddress = deviceAddress;
- this.oobType = oobType;
- }
-
- private OobEligibleDevice(Parcel in) {
- this(in.readString(), in.readInt());
- }
-
- @NonNull
- public String getDeviceAddress() {
- return deviceAddress;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeString(deviceAddress);
- dest.writeInt(oobType);
- }
-
- @OobType
- public int getOobType() {
- return oobType;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (obj == this) {
- return true;
- }
- if (!(obj instanceof OobEligibleDevice)) {
- return false;
- }
- OobEligibleDevice device = (OobEligibleDevice) obj;
- return Objects.equals(device.deviceAddress, deviceAddress) && device.oobType == oobType;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(deviceAddress, oobType);
- }
-
- public static final Parcelable.Creator<OobEligibleDevice> CREATOR =
- new Parcelable.Creator<OobEligibleDevice>() {
- @Override
- public OobEligibleDevice createFromParcel(Parcel source) {
- return new OobEligibleDevice(source);
- }
-
- @Override
- public OobEligibleDevice[] newArray(int size) {
- return new OobEligibleDevice[size];
- }
- };
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/oob/BluetoothRfcommChannel.java b/libs/connecteddevice/src/com/google/android/connecteddevice/oob/BluetoothRfcommChannel.java
index 51251bb..8f219bc 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/oob/BluetoothRfcommChannel.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/oob/BluetoothRfcommChannel.java
@@ -29,7 +29,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
-import com.google.android.connecteddevice.model.OobEligibleDevice;
import com.google.android.connecteddevice.transport.BluetoothDeviceProvider;
import com.google.android.connecteddevice.transport.ConnectionProtocol;
import com.google.android.connecteddevice.transport.ProtocolDevice;
@@ -70,26 +69,16 @@
BluetoothDevice remoteDevice =
((BluetoothDeviceProvider) protocol).getBluetoothDeviceById(device.getProtocolId());
completeOobDataExchange(
- new OobEligibleDevice(remoteDevice.getAddress(), OobEligibleDevice.OOB_TYPE_BLUETOOTH),
- callback,
- () -> BluetoothAdapter.getDefaultAdapter().getBondedDevices());
+ remoteDevice, callback, () -> BluetoothAdapter.getDefaultAdapter().getBondedDevices());
return true;
}
- @Override
- public void completeOobDataExchange(
- @NonNull OobEligibleDevice device, @NonNull Callback callback) {
- completeOobDataExchange(
- device, callback, () -> BluetoothAdapter.getDefaultAdapter().getBondedDevices());
- }
-
@VisibleForTesting
void completeOobDataExchange(
- OobEligibleDevice device, Callback callback, BondedDevicesResolver bondedDevicesResolver) {
+ BluetoothDevice remoteDevice,
+ Callback callback,
+ BondedDevicesResolver bondedDevicesResolver) {
this.callback = callback;
-
- BluetoothDevice remoteDevice =
- BluetoothAdapter.getDefaultAdapter().getRemoteDevice(device.getDeviceAddress());
Set<BluetoothDevice> bondedDevices = bondedDevicesResolver.getBondedDevices();
if (bondedDevices == null || !bondedDevices.contains(remoteDevice)) {
notifyFailure(
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/oob/OobChannel.java b/libs/connecteddevice/src/com/google/android/connecteddevice/oob/OobChannel.java
index 1bf8a1a..51b867b 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/oob/OobChannel.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/oob/OobChannel.java
@@ -17,7 +17,6 @@
package com.google.android.connecteddevice.oob;
import androidx.annotation.NonNull;
-import com.google.android.connecteddevice.model.OobEligibleDevice;
import com.google.android.connecteddevice.transport.ProtocolDevice;
/**
@@ -44,14 +43,6 @@
@NonNull ProtocolDevice protocolDevice, @NonNull Callback callback);
/**
- * Exchange out of band data with a remote device. This must be done prior to the start of the
- * association with that device.
- *
- * @param device The remote device to exchange out of band data with
- */
- void completeOobDataExchange(@NonNull OobEligibleDevice device, @NonNull Callback callback);
-
- /**
* Send raw data over the out of band channel
*
* @param oobData to be sent
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/oob/OobConnectionManager.java b/libs/connecteddevice/src/com/google/android/connecteddevice/oob/OobConnectionManager.java
deleted file mode 100644
index 5d51221..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/oob/OobConnectionManager.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice.oob;
-
-import static com.google.android.connecteddevice.util.SafeLog.logd;
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-
-import android.security.keystore.KeyProperties;
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-import com.google.common.primitives.Bytes;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.util.Arrays;
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.KeyGenerator;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-/**
- * This is a class that manages a token--{@link OobConnectionManager#encryptionKey}-- passed via an
- * out of band {@link OobChannel} that is distinct from the channel that is currently being secured.
- *
- * <p>Intended usage:
- *
- * <pre>{@code
- * OobConnectionManager oobConncetionManager = new OobConnectionManager();
- * oobConnectionManager.startOobExchange(channel);
- *
- * }</pre>
- *
- * <pre>{@code When a message is received:
- * verificationCode = OobConnectionManager#decryptVerificationCode(byte[])
- * check that verification code is valid
- * if it is:
- * encryptedMessage = OobConnectionManager#encryptVerificationCode(byte[])
- * send encryptedMessage
- * verify handshake
- * otherwise:
- * fail handshake
- * }</pre>
- *
- * <pre>{@code
- * when oobData is received via the out of band channel:
- * OobConnectionManager#setOobData(byte[])
- *
- * encryptedMessage = OobConnectionManager#encryptVerificationCode(byte[])
- * sendMessage
- * when a message is received:
- * verificationCode = OobConnectionManager#decryptVerificationCode(byte[])
- * check that verification code is valid
- * if it is:
- * verify handshake
- * otherwise:
- * fail handshake
- * }</pre>
- */
-public class OobConnectionManager {
- private static final String TAG = "OobConnectionManager";
- private static final String ALGORITHM = "AES/GCM/NoPadding";
- // The nonce length is chosen to be consistent with the standard specification:
- // Section 8.2 of https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
- @VisibleForTesting static final int NONCE_LENGTH_BYTES = 12;
-
- private final Cipher cipher;
- @VisibleForTesting byte[] encryptionIv = new byte[NONCE_LENGTH_BYTES];
- @VisibleForTesting byte[] decryptionIv = new byte[NONCE_LENGTH_BYTES];
- @VisibleForTesting SecretKey encryptionKey;
-
- public OobConnectionManager() {
- try {
- cipher = Cipher.getInstance(ALGORITHM);
- } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
- loge(TAG, "Unable to create cipher with " + ALGORITHM + ".", e);
- throw new IllegalStateException(e);
- }
- }
-
- /** Encrypts {@code verificationCode} using {@link OobConnectionManager#encryptionKey} */
- @NonNull
- public byte[] encryptVerificationCode(@NonNull byte[] verificationCode)
- throws InvalidAlgorithmParameterException, BadPaddingException, InvalidKeyException,
- IllegalBlockSizeException {
- cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, new IvParameterSpec(encryptionIv));
- return cipher.doFinal(verificationCode);
- }
-
- /** Decrypts {@code encryptedMessage} using {@link OobConnectionManager#encryptionKey} */
- @NonNull
- public byte[] decryptVerificationCode(@NonNull byte[] encryptedMessage)
- throws InvalidAlgorithmParameterException, BadPaddingException, InvalidKeyException,
- IllegalBlockSizeException {
- cipher.init(Cipher.DECRYPT_MODE, encryptionKey, new IvParameterSpec(decryptionIv));
- return cipher.doFinal(encryptedMessage);
- }
-
- void setOobData(@NonNull byte[] oobData) {
- encryptionIv = Arrays.copyOf(oobData, NONCE_LENGTH_BYTES);
- decryptionIv = Arrays.copyOfRange(oobData, NONCE_LENGTH_BYTES, NONCE_LENGTH_BYTES * 2);
- encryptionKey =
- new SecretKeySpec(
- Arrays.copyOfRange(oobData, NONCE_LENGTH_BYTES * 2, oobData.length),
- KeyProperties.KEY_ALGORITHM_AES);
- }
-
- /**
- * Start the out of band exchange with a given {@link OobChannel}.
- *
- * @param oobChannel Channel to be used for exchange.
- * @return {@code true} if exchange started successfully. {@code false} if an error occurred.
- */
- public boolean startOobExchange(@NonNull OobChannel oobChannel) {
- KeyGenerator keyGenerator = null;
- try {
- keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES);
- } catch (NoSuchAlgorithmException e) {
- loge(TAG, "Unable to get AES key generator.", e);
- return false;
- }
- encryptionKey = keyGenerator.generateKey();
-
- SecureRandom secureRandom = new SecureRandom();
- secureRandom.nextBytes(encryptionIv);
- secureRandom.nextBytes(decryptionIv);
-
- oobChannel.sendOobData(Bytes.concat(decryptionIv, encryptionIv, encryptionKey.getEncoded()));
- return true;
- }
-
- /** Clears all stored encryption keys. */
- public void reset() {
- logd(TAG, "Resetting encryption keys.");
- encryptionIv = new byte[NONCE_LENGTH_BYTES];
- decryptionIv = new byte[NONCE_LENGTH_BYTES];
- encryptionKey = null;
- }
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/oob/OobRunner.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/oob/OobRunner.kt
index fcfd44b..7a38881 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/oob/OobRunner.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/oob/OobRunner.kt
@@ -24,7 +24,6 @@
import com.google.android.connecteddevice.transport.ProtocolDevice
import com.google.android.connecteddevice.util.SafeLog.logd
import com.google.android.connecteddevice.util.SafeLog.loge
-import com.google.android.connecteddevice.util.SafeLog.logw
import com.google.protobuf.ByteString
import java.security.NoSuchAlgorithmException
import java.security.SecureRandom
@@ -81,30 +80,16 @@
}
/**
- * Iterate through all available OOB channels, establish OOB channel and exchange OOB key with
- * remote device, result will be returned through [callback].
+ * Iterate through all available OOB channels, establish OOB channels and send OOB data to remote
+ * device.
*/
- open fun startOobDataExchange(
+ open fun sendOobData(
protocolDevice: ProtocolDevice,
- commonTypes: List<OobChannelType>,
- securityVersion: Int,
- callback: Callback? = null
): Boolean {
- for (oobType in commonTypes) {
- if (oobType.name !in supportedTypes) {
- logw(TAG, "Unsupported OOB channel ${oobType.name}. Ignored.")
- continue
- }
+ for (oobType in supportedTypes.map { it.asOobChannelType() }) {
logd(TAG, "Establish OOB channel with ${oobType.name}.")
val oobChannel = oobChannelFactory.createOobChannel(oobType)
- if (oobChannel.completeOobDataExchange(
- protocolDevice,
- generateOobChannelCallback(
- oobChannel,
- callback,
- securityVersion >= MIN_SECURITY_VERSION_FOR_OOB_PROTO
- )
- )
+ if (oobChannel.completeOobDataExchange(protocolDevice, generateOobChannelCallback(oobChannel))
) {
currentOobChannel = oobChannel
return true
@@ -113,11 +98,11 @@
return false
}
- private fun generateOobChannelCallback(
- oobChannel: OobChannel,
- callback: Callback?,
- isProtoApplied: Boolean
- ) =
+ private fun String.asOobChannelType(): OobChannelType =
+ OobChannelType.values().firstOrNull { it.name.equals(this) }
+ ?: OobChannelType.OOB_CHANNEL_UNKNOWN
+
+ private fun generateOobChannelCallback(oobChannel: OobChannel) =
object : OobChannel.Callback {
override fun onOobExchangeSuccess() {
val data = oobData
@@ -127,14 +112,12 @@
"OOB channel established successfully with invalid OOB data, issue failure " +
"callback."
)
- callback?.onOobDataExchangeFailure()
return
}
- oobChannel.sendOobData(if (isProtoApplied) toOobProto(data) else data.toRawBytes())
- callback?.onOobDataExchangeSuccess()
+ oobChannel.sendOobData(toOobProto(data))
}
override fun onOobExchangeFailure() {
- callback?.onOobDataExchangeFailure()
+ loge(TAG, "Failed to start OOB data exchange.")
}
}
@@ -182,21 +165,9 @@
.toByteArray()
}
- private fun OobData.toRawBytes() = mobileIv + ihuIv + encryptionKey
-
- /** Callbacks for [OobRunner.startOobDataExchange] */
- interface Callback {
- /** Called when [OobRunner.startOobDataExchange] finishes successfully. */
- fun onOobDataExchangeSuccess()
-
- /** Called when OOB data exchange fails. */
- fun onOobDataExchangeFailure()
- }
-
companion object {
private const val TAG = "OobRunner"
private const val ALGORITHM = "AES/GCM/NoPadding"
- @VisibleForTesting internal const val MIN_SECURITY_VERSION_FOR_OOB_PROTO = 4
// The nonce length is chosen to be consistent with the standard specification:
// Section 8.2 of https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/oob/PassThroughChannel.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/oob/PassThroughChannel.kt
index 70fb708..7ee2512 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/oob/PassThroughChannel.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/oob/PassThroughChannel.kt
@@ -33,11 +33,6 @@
return true
}
- override fun completeOobDataExchange(
- device: com.google.android.connecteddevice.model.OobEligibleDevice,
- callback: OobChannel.Callback
- ) {}
-
override fun sendOobData(oobData: ByteArray) {}
override fun interrupt() {}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/service/AssociationBinder.java b/libs/connecteddevice/src/com/google/android/connecteddevice/service/AssociationBinder.java
deleted file mode 100644
index e59e48f..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/service/AssociationBinder.java
+++ /dev/null
@@ -1,369 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice.service;
-
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-
-import android.os.IBinder;
-import android.os.RemoteException;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import com.google.android.connecteddevice.ConnectedDeviceManager;
-import com.google.android.connecteddevice.ConnectedDeviceManager.ConnectionCallback;
-import com.google.android.connecteddevice.ConnectedDeviceManager.DeviceAssociationCallback;
-import com.google.android.connecteddevice.api.IAssociatedDeviceManager;
-import com.google.android.connecteddevice.api.IAssociationCallback;
-import com.google.android.connecteddevice.api.IConnectionCallback;
-import com.google.android.connecteddevice.api.IDeviceAssociationCallback;
-import com.google.android.connecteddevice.api.IOnAssociatedDevicesRetrievedListener;
-import com.google.android.connecteddevice.connection.AssociationCallback;
-import com.google.android.connecteddevice.model.AssociatedDevice;
-import com.google.android.connecteddevice.model.ConnectedDevice;
-import com.google.android.connecteddevice.model.StartAssociationResponse;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage.OnAssociatedDevicesRetrievedListener;
-import com.google.android.connecteddevice.util.RemoteCallbackBinder;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CopyOnWriteArraySet;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-
-/** Binder for exposing associated device actions to internal features. */
-public class AssociationBinder extends IAssociatedDeviceManager.Stub {
-
- private static final String TAG = "AssociationBinder";
-
- private final ConnectedDeviceManager connectedDeviceManager;
-
- // Registered association callback
- private IAssociationCallback iAssociationCallback;
-
- // aidl callback binder -> device association callback
- // Need to maintain a mapping in order to support unregistering callbacks.
- private final Map<IBinder, DeviceAssociationCallback> deviceAssociationCallbacks =
- new ConcurrentHashMap<>();
-
- // aidl callback binder -> connection callback
- // Need to maintain a mapping in order to support unregistering callbacks.
- private final Map<IBinder, ConnectionCallback> connectionCallbacks = new ConcurrentHashMap<>();
-
- private final Executor executor = Executors.newSingleThreadExecutor();
- private final Set<RemoteCallbackBinder> callbackBinders = new CopyOnWriteArraySet<>();
-
- public AssociationBinder(ConnectedDeviceManager connectedDeviceManager) {
- this.connectedDeviceManager = connectedDeviceManager;
- }
-
- @Override
- public void startAssociation(IAssociationCallback callback) {
- RemoteCallbackBinder remoteBinder =
- new RemoteCallbackBinder(callback.asBinder(), iBinder -> stopAssociation());
- callbackBinders.add(remoteBinder);
- iAssociationCallback = callback;
- connectedDeviceManager.startAssociation(associationCallback);
- }
-
- @Override
- public void stopAssociation() {
- if (iAssociationCallback == null) {
- return;
- }
- RemoteCallbackBinder remoteBinder = findRemoteCallbackBinder(iAssociationCallback.asBinder());
- if (remoteBinder == null) {
- return;
- }
- connectedDeviceManager.stopAssociation(associationCallback);
- resetAssociationCallback();
- }
-
- @Override
- public void retrievedActiveUserAssociatedDevices(
- IOnAssociatedDevicesRetrievedListener remoteListener) {
- OnAssociatedDevicesRetrievedListener listener =
- devices -> {
- try {
- remoteListener.onAssociatedDevicesRetrieved(devices);
- } catch (RemoteException exception) {
- loge(TAG, "onAssociatedDevicesRetrieved failed.", exception);
- }
- };
- connectedDeviceManager.retrieveActiveUserAssociatedDevices(listener);
- }
-
- @Override
- public void acceptVerification() {
- connectedDeviceManager.notifyOutOfBandAccepted();
- }
-
- @Override
- public void removeAssociatedDevice(String deviceId) {
- connectedDeviceManager.removeActiveUserAssociatedDevice(deviceId);
- }
-
- @Override
- public void registerDeviceAssociationCallback(IDeviceAssociationCallback callback) {
- DeviceAssociationCallback deviceAssociationCallback =
- new DeviceAssociationCallback() {
- @Override
- public void onAssociatedDeviceAdded(AssociatedDevice device) {
- try {
- callback.onAssociatedDeviceAdded(device);
- } catch (RemoteException exception) {
- loge(TAG, "onAssociatedDeviceAdded failed.", exception);
- }
- }
-
- @Override
- public void onAssociatedDeviceRemoved(AssociatedDevice device) {
- try {
- callback.onAssociatedDeviceRemoved(device);
- } catch (RemoteException exception) {
- loge(TAG, "onAssociatedDeviceRemoved failed.", exception);
- }
- }
-
- @Override
- public void onAssociatedDeviceUpdated(AssociatedDevice device) {
- try {
- callback.onAssociatedDeviceUpdated(device);
- } catch (RemoteException exception) {
- loge(TAG, "onAssociatedDeviceUpdated failed.", exception);
- }
- }
- };
- connectedDeviceManager.registerDeviceAssociationCallback(deviceAssociationCallback, executor);
- RemoteCallbackBinder remoteBinder =
- new RemoteCallbackBinder(
- callback.asBinder(), iBinder -> unregisterDeviceAssociationCallback(callback));
- callbackBinders.add(remoteBinder);
- deviceAssociationCallbacks.put(callback.asBinder(), deviceAssociationCallback);
- }
-
- @Override
- public void unregisterDeviceAssociationCallback(IDeviceAssociationCallback callback) {
- RemoteCallbackBinder remoteBinder = findRemoteCallbackBinder(callback.asBinder());
- if (remoteBinder == null) {
- loge(
- TAG,
- "Failed to unregister DeviceAssociationCallback "
- + callback
- + ", it has not been registered.");
- return;
- }
- DeviceAssociationCallback deviceAssociationCallback =
- deviceAssociationCallbacks.remove(callback.asBinder());
- if (deviceAssociationCallback == null) {
- loge(
- TAG,
- "Failed to unregister DeviceAssociationCallback "
- + callback
- + ", it has not been registered.");
- return;
- }
- connectedDeviceManager.unregisterDeviceAssociationCallback(deviceAssociationCallback);
- remoteBinder.cleanUp();
- callbackBinders.remove(remoteBinder);
- }
-
- @Override
- public List<ConnectedDevice> getActiveUserConnectedDevices() {
- return connectedDeviceManager.getActiveUserConnectedDevices();
- }
-
- @Override
- public void registerConnectionCallback(IConnectionCallback callback) {
- ConnectionCallback connectionCallback =
- new ConnectedDeviceManager.ConnectionCallback() {
- @Override
- public void onDeviceConnected(ConnectedDevice device) {
- if (callback == null) {
- loge(TAG, "No IConnectionCallback has been set, ignoring " + "onDeviceConnected.");
- return;
- }
- try {
- callback.onDeviceConnected(device);
- } catch (RemoteException exception) {
- loge(TAG, "onDeviceConnected failed.", exception);
- }
- }
-
- @Override
- public void onDeviceDisconnected(ConnectedDevice device) {
- if (callback == null) {
- loge(TAG, "No IConnectionCallback has been set, ignoring " + "onDeviceConnected.");
- return;
- }
- try {
- callback.onDeviceDisconnected(device);
- } catch (RemoteException exception) {
- loge(TAG, "onDeviceDisconnected failed.", exception);
- }
- }
- };
- connectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback, executor);
- RemoteCallbackBinder remoteBinder =
- new RemoteCallbackBinder(
- callback.asBinder(), iBinder -> unregisterConnectionCallback(callback));
- callbackBinders.add(remoteBinder);
- connectionCallbacks.put(callback.asBinder(), connectionCallback);
- }
-
- @Override
- public void unregisterConnectionCallback(@NonNull IConnectionCallback callback) {
- RemoteCallbackBinder remoteBinder = findRemoteCallbackBinder(callback.asBinder());
- if (remoteBinder == null) {
- loge(
- TAG,
- "Failed to unregister ConnectionCallback "
- + callback.asBinder()
- + ", it was not been registered.");
- return;
- }
- ConnectionCallback connectionCallback = connectionCallbacks.remove(callback.asBinder());
- if (connectionCallback == null) {
- loge(
- TAG,
- "Failed to unregister ConnectionCallback "
- + callback.asBinder()
- + ", it was not been registered.");
- return;
- }
- connectedDeviceManager.unregisterConnectionCallback(connectionCallback);
- remoteBinder.cleanUp();
- callbackBinders.remove(remoteBinder);
- }
-
- @Override
- public void enableAssociatedDeviceConnection(String deviceId) {
- connectedDeviceManager.enableAssociatedDeviceConnection(deviceId);
- }
-
- @Override
- public void disableAssociatedDeviceConnection(String deviceId) {
- connectedDeviceManager.disableAssociatedDeviceConnection(deviceId);
- }
-
- @Nullable
- private RemoteCallbackBinder findRemoteCallbackBinder(@NonNull IBinder binder) {
- for (RemoteCallbackBinder remoteBinder : callbackBinders) {
- if (remoteBinder.getCallbackBinder().equals(binder)) {
- return remoteBinder;
- }
- }
- return null;
- }
-
- private void resetAssociationCallback() {
- RemoteCallbackBinder remoteBinder = findRemoteCallbackBinder(iAssociationCallback.asBinder());
- if (remoteBinder != null) {
- remoteBinder.cleanUp();
- callbackBinders.remove(remoteBinder);
- }
- iAssociationCallback = null;
- }
-
- private final AssociationCallback associationCallback =
- new AssociationCallback() {
-
- @Override
- public void onAssociationStartSuccess(StartAssociationResponse response) {
- if (iAssociationCallback == null) {
- loge(
- TAG,
- "No IAssociationCallback has been set, ignoring " + "onAssociationStartSuccess.");
- return;
- }
- try {
- iAssociationCallback.onAssociationStartSuccess(response);
- } catch (RemoteException exception) {
- loge(TAG, "onAssociationStartSuccess failed.", exception);
- }
- }
-
- @Override
- public void onAssociationStartFailure() {
- if (iAssociationCallback == null) {
- loge(
- TAG,
- "No IAssociationCallback has been set, ignoring " + "onAssociationStartFailure.");
- return;
- }
- try {
- iAssociationCallback.onAssociationStartFailure();
- } catch (RemoteException exception) {
- loge(TAG, "onAssociationStartFailure failed.", exception);
- }
- resetAssociationCallback();
- }
-
- @Override
- public void onAssociationError(int error) {
- if (iAssociationCallback == null) {
- loge(
- TAG,
- "No IAssociationCallback has been set, ignoring "
- + "onAssociationError: "
- + error
- + ".");
- return;
- }
- try {
- iAssociationCallback.onAssociationError(error);
- } catch (RemoteException exception) {
- loge(TAG, "onAssociationError failed. Error: " + error + "", exception);
- }
- resetAssociationCallback();
- }
-
- @Override
- public void onVerificationCodeAvailable(String code) {
- if (iAssociationCallback == null) {
- loge(
- TAG,
- "No IAssociationCallback has been set, ignoring "
- + "onVerificationCodeAvailable, code: "
- + code);
- return;
- }
- try {
- iAssociationCallback.onVerificationCodeAvailable(code);
- } catch (RemoteException exception) {
- loge(TAG, "onVerificationCodeAvailable failed. Code: " + code + "", exception);
- }
- }
-
- @Override
- public void onAssociationCompleted(String deviceId) {
- if (iAssociationCallback == null) {
- loge(
- TAG,
- "No IAssociationCallback has been set, ignoring "
- + "onAssociationCompleted, deviceId: "
- + deviceId);
- return;
- }
- try {
- iAssociationCallback.onAssociationCompleted();
- } catch (RemoteException exception) {
- loge(TAG, "onAssociationCompleted failed.", exception);
- }
- resetAssociationCallback();
- }
- };
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceFgUserService.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceFgUserService.kt
index 1fb95d0..5003a4f 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceFgUserService.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceFgUserService.kt
@@ -20,18 +20,19 @@
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
+import android.os.Handler
import android.os.IBinder
+import android.os.Looper
import android.os.UserManager
import com.google.android.connecteddevice.api.CompanionConnector
import com.google.android.connecteddevice.api.Connector
-import com.google.android.connecteddevice.api.IConnectedDeviceManager
import com.google.android.connecteddevice.util.SafeLog.logd
import com.google.android.connecteddevice.util.SafeLog.loge
+import java.time.Duration
/**
* Service responsible for starting services that must be run in the active foreground user.
- * External features running in the foreground user may bind with `ACTION_BIND_REMOTE_FEATURE` to
- * access the [IConnectedDeviceManager] instance.
+ * External features running in the foreground user must bind to this service.
*/
class ConnectedDeviceFgUserService : TrunkService() {
private lateinit var connector: CompanionConnector
@@ -41,17 +42,32 @@
@SuppressLint("UnprotectedReceiver") // Broadcasts are protected.
override fun onCreate() {
super.onCreate()
+ logd(TAG, "Creating service.")
connector = CompanionConnector(this)
connector.callback =
object : Connector.Callback {
+ override fun onConnected() {
+ logd(TAG, "Successfully connected to the companion platform.")
+ val userManager = getSystemService(UserManager::class.java)
+ if (userManager.isUserUnlocked) {
+ logd(TAG, "User was already unlocked. Starting unlock branch services.")
+ startBranchServices(META_UNLOCK_SERVICES)
+ }
+ }
+
override fun onDisconnected() {
- loge(TAG, "Lost connection to companion. Stopping service.")
+ loge(TAG, "Lost connection to the companion platform. Stopping service.")
stopSelf()
}
override fun onFailedToConnect() {
- loge(TAG, "Unable to establish connection with companion. Stopping service.")
- stopSelf()
+ loge(
+ TAG,
+ "Unable to establish connection with the companion platform. Retrying in " +
+ "${RETRY_WAIT.toMillis()} ms."
+ )
+ Handler(Looper.myLooper() ?: Looper.getMainLooper())
+ .postDelayed({ connector.connect() }, RETRY_WAIT.toMillis())
}
}
connector.connect()
@@ -62,12 +78,6 @@
// Listen for user going to the background so we can clean up.
registerReceiver(userBackgroundReceiver, IntentFilter(Intent.ACTION_USER_BACKGROUND))
receiversRegistered = true
-
- val userManager = getSystemService(UserManager::class.java)
- if (userManager.isUserUnlocked) {
- logd(TAG, "User was already unlocked on service start.")
- onUserUnlocked()
- }
}
override fun onDestroy() {
@@ -85,8 +95,11 @@
}
private fun onUserUnlocked() {
- logd(TAG, "Starting unlock branch services.")
- startBranchServices(META_UNLOCK_SERVICES)
+ logd(TAG, "User has been unlocked.")
+ if (connector.isConnected) {
+ logd(TAG, "Starting unlock branch services.")
+ startBranchServices(META_UNLOCK_SERVICES)
+ }
}
private fun unregisterReceivers() {
@@ -119,5 +132,7 @@
/** `string-array` List of services to start after the user has unlocked. */
private const val META_UNLOCK_SERVICES = "com.google.android.connecteddevice.unlock_services"
+
+ private val RETRY_WAIT = Duration.ofMillis(500)
}
}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceManagerBinder.java b/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceManagerBinder.java
deleted file mode 100644
index 0afc74c..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceManagerBinder.java
+++ /dev/null
@@ -1,351 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice.service;
-
-import static com.google.android.connecteddevice.util.SafeLog.loge;
-
-import android.os.IBinder;
-import android.os.ParcelUuid;
-import android.os.RemoteException;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import com.google.android.connecteddevice.ConnectedDeviceManager;
-import com.google.android.connecteddevice.ConnectedDeviceManager.ConnectionCallback;
-import com.google.android.connecteddevice.ConnectedDeviceManager.DeviceAssociationCallback;
-import com.google.android.connecteddevice.ConnectedDeviceManager.DeviceCallback;
-import com.google.android.connecteddevice.api.IConnectedDeviceManager;
-import com.google.android.connecteddevice.api.IConnectionCallback;
-import com.google.android.connecteddevice.api.IDeviceAssociationCallback;
-import com.google.android.connecteddevice.api.IDeviceCallback;
-import com.google.android.connecteddevice.api.IOnLogRequestedListener;
-import com.google.android.connecteddevice.logging.LoggingManager;
-import com.google.android.connecteddevice.logging.LoggingManager.OnLogRequestedListener;
-import com.google.android.connecteddevice.model.AssociatedDevice;
-import com.google.android.connecteddevice.model.ConnectedDevice;
-import com.google.android.connecteddevice.model.DeviceMessage;
-import com.google.android.connecteddevice.util.RemoteCallbackBinder;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CopyOnWriteArraySet;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-
-/** Binder for exposing ConnectedDeviceManager to features. */
-public class ConnectedDeviceManagerBinder extends IConnectedDeviceManager.Stub {
-
- private static final String TAG = "ConnectedDeviceManagerBinder";
-
- private final ConnectedDeviceManager connectedDeviceManager;
- private final LoggingManager loggingManager;
-
- // aidl callback binder -> connection callback
- // Need to maintain a mapping in order to support unregistering callbacks.
- private final ConcurrentHashMap<IBinder, ConnectionCallback> connectionCallbacks =
- new ConcurrentHashMap<>();
-
- // aidl callback binder -> device association callback
- // Need to maintain a mapping in order to support unregistering callbacks.
- private final ConcurrentHashMap<IBinder, DeviceAssociationCallback> associationCallbacks =
- new ConcurrentHashMap<>();
-
- // aidl callback binder -> device callback
- // Need to maintain a mapping in order to support unregistering callbacks.
- private final ConcurrentHashMap<IBinder, DeviceCallback> deviceCallbacks =
- new ConcurrentHashMap<>();
-
- // aidl listener binder -> log requested listener
- // Need to maintain a mapping in order to support unregistering callbacks.
- private final ConcurrentHashMap<IBinder, OnLogRequestedListener> logRequestedListeners =
- new ConcurrentHashMap<>();
-
- private final Executor callbackExecutor = Executors.newSingleThreadExecutor();
- private final Set<RemoteCallbackBinder> callbackBinders = new CopyOnWriteArraySet<>();
-
- public ConnectedDeviceManagerBinder(
- ConnectedDeviceManager connectedDeviceManager, LoggingManager loggingManager) {
- this.connectedDeviceManager = connectedDeviceManager;
- this.loggingManager = loggingManager;
- }
-
- @Override
- public List<ConnectedDevice> getActiveUserConnectedDevices() {
- return connectedDeviceManager.getActiveUserConnectedDevices();
- }
-
- @Override
- public void registerActiveUserConnectionCallback(@NonNull IConnectionCallback callback) {
- ConnectionCallback connectionCallback =
- new ConnectionCallback() {
- @Override
- public void onDeviceConnected(ConnectedDevice device) {
- try {
- callback.onDeviceConnected(device);
- } catch (RemoteException exception) {
- loge(TAG, "onDeviceConnected failed.", exception);
- }
- }
-
- @Override
- public void onDeviceDisconnected(ConnectedDevice device) {
- try {
- callback.onDeviceDisconnected(device);
- } catch (RemoteException exception) {
- loge(TAG, "onDeviceDisconnected failed.", exception);
- }
- }
- };
- connectedDeviceManager.registerActiveUserConnectionCallback(
- connectionCallback, callbackExecutor);
- RemoteCallbackBinder remoteBinder =
- new RemoteCallbackBinder(
- callback.asBinder(), iBinder -> unregisterConnectionCallback(callback));
- callbackBinders.add(remoteBinder);
- connectionCallbacks.put(callback.asBinder(), connectionCallback);
- }
-
- @Override
- public void unregisterConnectionCallback(@NonNull IConnectionCallback callback) {
- IBinder binder = callback.asBinder();
- RemoteCallbackBinder remoteBinder = findRemoteCallbackBinder(binder);
- if (remoteBinder == null) {
- loge(
- TAG,
- "RemoteCallbackBinder is null, IConnectionCallback was not " + "previously registered.");
- return;
- }
- ConnectionCallback connectionCallback = connectionCallbacks.get(binder);
- if (connectionCallback == null) {
- loge(TAG, "ConnectionCallback is null.");
- return;
- }
- connectedDeviceManager.unregisterConnectionCallback(connectionCallback);
- remoteBinder.cleanUp();
- callbackBinders.remove(remoteBinder);
- connectionCallbacks.remove(binder);
- }
-
- @Override
- public void registerDeviceCallback(
- @NonNull ConnectedDevice connectedDevice,
- @NonNull ParcelUuid recipientId,
- @NonNull IDeviceCallback callback) {
- DeviceCallback deviceCallback =
- new DeviceCallback() {
- @Override
- public void onSecureChannelEstablished(ConnectedDevice device) {
- try {
- callback.onSecureChannelEstablished(device);
- } catch (RemoteException exception) {
- loge(TAG, "onSecureChannelEstablished failed.", exception);
- }
- }
-
- @Override
- public void onMessageReceived(ConnectedDevice device, DeviceMessage message) {
- try {
- callback.onMessageReceived(device, message);
- } catch (RemoteException exception) {
- loge(TAG, "onMessageReceived failed.", exception);
- }
- }
-
- @Override
- public void onDeviceError(ConnectedDevice device, int error) {
- try {
- callback.onDeviceError(device, error);
- } catch (RemoteException exception) {
- loge(TAG, "onDeviceError failed.", exception);
- }
- }
- };
- connectedDeviceManager.registerDeviceCallback(
- connectedDevice,
- recipientId.getUuid(),
- deviceCallback,
- callbackExecutor);
- deviceCallbacks.put(callback.asBinder(), deviceCallback);
- RemoteCallbackBinder remoteBinder =
- new RemoteCallbackBinder(
- callback.asBinder(),
- iBinder -> unregisterDeviceCallback(connectedDevice, recipientId, callback));
- callbackBinders.add(remoteBinder);
- }
-
- @Override
- public void unregisterDeviceCallback(
- @NonNull ConnectedDevice connectedDevice,
- @NonNull ParcelUuid recipientId,
- @NonNull IDeviceCallback callback) {
- IBinder binder = callback.asBinder();
- RemoteCallbackBinder remoteBinder = findRemoteCallbackBinder(binder);
- if (remoteBinder == null) {
- loge(
- TAG,
- "RemoteCallbackBinder is null, IDeviceCallback was not previously "
- + "registered. Ignoring call to unregister.");
- return;
- }
- DeviceCallback deviceCallback = deviceCallbacks.remove(binder);
- if (deviceCallback == null) {
- loge(TAG, "No DeviceCallback associated with given callback. " + "Cannot unregister.");
- return;
- }
- connectedDeviceManager.unregisterDeviceCallback(
- connectedDevice, recipientId.getUuid(), deviceCallback);
- remoteBinder.cleanUp();
- callbackBinders.remove(remoteBinder);
- }
-
- @Override
- public boolean sendMessage(
- @NonNull ConnectedDevice connectedDevice,
- @NonNull DeviceMessage message) {
- try {
- connectedDeviceManager.sendMessage(connectedDevice, message);
- return true;
- } catch (IllegalStateException exception) {
- loge(TAG, "Attempted to send message prior to secure channel established.", exception);
- }
- return false;
- }
-
- @Override
- public void registerDeviceAssociationCallback(@NonNull IDeviceAssociationCallback callback) {
- DeviceAssociationCallback associationCallback =
- new DeviceAssociationCallback() {
- @Override
- public void onAssociatedDeviceAdded(AssociatedDevice device) {
- try {
- callback.onAssociatedDeviceAdded(device);
- } catch (RemoteException exception) {
- loge(TAG, "onAssociatedDeviceAdded failed.", exception);
- }
- }
-
- @Override
- public void onAssociatedDeviceRemoved(AssociatedDevice device) {
- try {
- callback.onAssociatedDeviceRemoved(device);
- } catch (RemoteException exception) {
- loge(TAG, "onAssociatedDeviceRemoved failed.", exception);
- }
- }
-
- @Override
- public void onAssociatedDeviceUpdated(AssociatedDevice device) {
- try {
- callback.onAssociatedDeviceUpdated(device);
- } catch (RemoteException exception) {
- loge(TAG, "onAssociatedDeviceUpdated failed.", exception);
- }
- }
- };
-
- connectedDeviceManager.registerDeviceAssociationCallback(
- associationCallback, callbackExecutor);
- RemoteCallbackBinder remoteBinder =
- new RemoteCallbackBinder(
- callback.asBinder(), iBinder -> unregisterDeviceAssociationCallback(callback));
- callbackBinders.add(remoteBinder);
- associationCallbacks.put(callback.asBinder(), associationCallback);
- }
-
- @Override
- public void unregisterDeviceAssociationCallback(@NonNull IDeviceAssociationCallback callback) {
- IBinder binder = callback.asBinder();
- RemoteCallbackBinder remoteBinder = findRemoteCallbackBinder(binder);
- if (remoteBinder == null) {
- loge(
- TAG,
- "RemoteCallbackBinder is null, IDeviceAssociationCallback was "
- + "not previously registered.");
- return;
- }
- DeviceAssociationCallback associationCallback = associationCallbacks.remove(binder);
- if (associationCallback == null) {
- loge(TAG, "DeviceAssociationCallback is null.");
- return;
- }
- connectedDeviceManager.unregisterDeviceAssociationCallback(associationCallback);
- remoteBinder.cleanUp();
- callbackBinders.remove(remoteBinder);
- }
-
- @Override
- public void registerOnLogRequestedListener(
- int loggerId, @NonNull IOnLogRequestedListener listener) {
- OnLogRequestedListener onLogRequestedListener =
- () -> {
- try {
- listener.onLogRecordsRequested();
- } catch (RemoteException exception) {
- loge(TAG, "Failed to notify log records requested.", exception);
- }
- };
-
- loggingManager.registerLogRequestedListener(loggerId, onLogRequestedListener, callbackExecutor);
- IBinder listenerBinder = listener.asBinder();
- RemoteCallbackBinder remoteBinder =
- new RemoteCallbackBinder(
- listenerBinder, iBinder -> unregisterOnLogRequestedListener(loggerId, listener));
- callbackBinders.add(remoteBinder);
- logRequestedListeners.put(listenerBinder, onLogRequestedListener);
- }
-
- @Override
- public void unregisterOnLogRequestedListener(
- int loggerId, @NonNull IOnLogRequestedListener listener) {
- IBinder listenerBinder = listener.asBinder();
- RemoteCallbackBinder remoteBinder = findRemoteCallbackBinder(listenerBinder);
- if (remoteBinder == null) {
- loge(
- TAG,
- "RemoteCallbackBinder is null, IOnLogRequestedListener "
- + listener
- + " is not previously registered.");
- return;
- }
- OnLogRequestedListener onLogRequestedListener = logRequestedListeners.remove(listenerBinder);
- if (onLogRequestedListener == null) {
- loge(
- TAG,
- "OnLogRequestedListener is null, IOnLogRequestedListener "
- + listener
- + " is not previously registered.");
- return;
- }
- loggingManager.unregisterLogRequestedListener(loggerId, onLogRequestedListener);
- remoteBinder.cleanUp();
- callbackBinders.remove(listenerBinder);
- }
-
- @Override
- public void processLogRecords(int loggerId, @NonNull byte[] logRecords) {
- loggingManager.prepareLocalLogRecords(loggerId, logRecords);
- }
-
- @Nullable
- private RemoteCallbackBinder findRemoteCallbackBinder(@NonNull IBinder binder) {
- for (RemoteCallbackBinder remoteBinder : callbackBinders) {
- if (remoteBinder.getCallbackBinder().equals(binder)) {
- return remoteBinder;
- }
- }
- return null;
- }
-}
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 3daa450..39cb491 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceService.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceService.java
@@ -28,32 +28,15 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.os.IBinder;
-import android.os.ParcelUuid;
import androidx.annotation.NonNull;
-import com.google.android.connecteddevice.ConnectedDeviceManager;
import com.google.android.connecteddevice.api.CompanionConnector;
import com.google.android.connecteddevice.api.Connector;
-import com.google.android.connecteddevice.api.IAssociatedDeviceManager;
-import com.google.android.connecteddevice.api.IAssociationCallback;
-import com.google.android.connecteddevice.api.IConnectedDeviceManager;
-import com.google.android.connecteddevice.api.IConnectionCallback;
-import com.google.android.connecteddevice.api.IDeviceAssociationCallback;
-import com.google.android.connecteddevice.api.IDeviceCallback;
-import com.google.android.connecteddevice.api.IFeatureCoordinator;
-import com.google.android.connecteddevice.api.IOnAssociatedDevicesRetrievedListener;
-import com.google.android.connecteddevice.api.IOnLogRequestedListener;
-import com.google.android.connecteddevice.connection.CarBluetoothManager;
-import com.google.android.connecteddevice.connection.ble.CarBlePeripheralManager;
-import com.google.android.connecteddevice.connection.spp.CarSppManager;
import com.google.android.connecteddevice.core.DeviceController;
import com.google.android.connecteddevice.core.FeatureCoordinator;
import com.google.android.connecteddevice.core.MultiProtocolDeviceController;
import com.google.android.connecteddevice.logging.LoggingFeature;
import com.google.android.connecteddevice.logging.LoggingManager;
-import com.google.android.connecteddevice.model.ConnectedDevice;
-import com.google.android.connecteddevice.model.DeviceMessage;
import com.google.android.connecteddevice.model.TransportProtocols;
-import com.google.android.connecteddevice.oob.BluetoothRfcommChannel;
import com.google.android.connecteddevice.oob.OobChannelFactory;
import com.google.android.connecteddevice.oob.OobRunner;
import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
@@ -78,10 +61,7 @@
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
-/**
- * Early start service that holds a {@link ConnectedDeviceManager} reference to support companion
- * device features.
- */
+/** Early start service that hosts the core companion platform. */
public final class ConnectedDeviceService extends TrunkService {
private static final String TAG = "ConnectedDeviceService";
@@ -90,11 +70,7 @@
// reconnect advertisement every 6 minutes to avoid crossing a rotation.
private static final Duration MAX_ADVERTISEMENT_DURATION = Duration.ofMinutes(6);
- /**
- * {@code string-array} Supported transport protocols. If the {@link FeatureCoordinator} is not
- * enabled, the first protocol in the list will be selected to initialize the
- * {@link ConnectedDeviceManager}.
- */
+ /** {@code string-array} Supported transport protocols. */
private static final String META_SUPPORTED_TRANSPORT_PROTOCOLS =
"com.google.android.connecteddevice.transport_protocols";
/** {@code String} UUID for association advertisement. */
@@ -120,19 +96,8 @@
/** {@code int} Maximum number of bytes each SPP packet can contain. */
private static final String META_SPP_PACKET_BYTES =
"com.google.android.connecteddevice.spp_packet_bytes";
- /** {@code boolean} Whether to compress outgoing messages. */
- private static final String META_COMPRESS_OUTGOING_MESSAGES =
- "com.google.android.connecteddevice.compress_outgoing_messages";
/** {@code boolean} Enable BLE proxy. */
private static final String META_ENABLE_PROXY = "com.google.android.connecteddevice.enable_proxy";
- /** {@code boolean} Enable a capabilities exchange during association. */
- private static final String META_ENABLE_CAPABILITIES_EXCHANGE =
- "com.google.android.connecteddevice.enable_capabilities_exchange";
- /**
- * {@code boolean} Use the {@link IFeatureCoordinator} instead of {@link IConnectedDeviceManager}
- */
- private static final String META_ENABLE_FEATURE_COORDINATOR =
- "com.google.android.connecteddevice.enable_feature_coordinator";
private static final String META_SUPPORTED_OOB_CHANNELS =
"com.google.android.connecteddevice.supported_oob_channels";
@@ -157,12 +122,6 @@
private static final int DEFAULT_SPP_PACKET_SIZE = 700;
- private static final boolean ENABLE_COMPRESSION_BY_DEFAULT = true;
-
- private static final boolean ENABLE_CAPABILITIES_EXCHANGE_BY_DEFAULT = false;
-
- 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 =
@@ -185,22 +144,14 @@
private final ScheduledExecutorService scheduledExecutorService =
Executors.newSingleThreadScheduledExecutor();
- private ConnectedDeviceManager connectedDeviceManager;
-
private LoggingManager loggingManager;
private FeatureCoordinator featureCoordinator;
private ConnectedDeviceStorage storage;
- private IConnectedDeviceManager.Stub connectedDeviceManagerBinder;
-
- private IAssociatedDeviceManager.Stub associationBinder;
-
private ConnectedDeviceSppDelegateBinder sppDelegateBinder;
- private boolean useFeatureCoordinator;
-
private List<String> supportedTransportProtocols;
private SystemFeature systemFeature;
@@ -232,17 +183,11 @@
+ "this service. Reverting to default values.");
supportedTransportProtocols = Arrays.asList(DEFAULT_TRANSPORT_PROTOCOLS);
}
- useFeatureCoordinator =
- getMetaBoolean(META_ENABLE_FEATURE_COORDINATOR, ENABLE_FEATURE_COORDINATOR_BY_DEFAULT);
loggingManager = new LoggingManager(this);
storage = new ConnectedDeviceStorage(this);
sppDelegateBinder = new ConnectedDeviceSppDelegateBinder(onRemoteCallbackSetListener);
- if (useFeatureCoordinator) {
- initializeFeatureCoordinator();
- } else {
- initializeConnectedDeviceManager();
- }
+ initializeFeatureCoordinator();
populateFeatures();
@@ -286,8 +231,6 @@
protocols, storage, oobRunner, associationUuid, enablePassenger);
featureCoordinator = new FeatureCoordinator(deviceController, storage, loggingManager);
logd(TAG, "Wrapping FeatureCoordinator in legacy binders for backwards compatibility.");
- connectedDeviceManagerBinder = createConnectedDeviceManagerWrapper();
- associationBinder = createAssociatedDeviceManagerWrapper();
}
private BlePeripheralProtocol createBlePeripheralProtocol() {
@@ -328,117 +271,20 @@
return new SppProtocol(sppDelegateBinder, maxSppPacketSize);
}
- private void initializeConnectedDeviceManager() {
- logd(TAG, "Initializing ConnectedDeviceManager version of the platform.");
- boolean isCompressionEnabled =
- getMetaBoolean(META_COMPRESS_OUTGOING_MESSAGES, ENABLE_COMPRESSION_BY_DEFAULT);
- boolean isCapabilitiesEligible =
- getMetaBoolean(META_ENABLE_CAPABILITIES_EXCHANGE, ENABLE_CAPABILITIES_EXCHANGE_BY_DEFAULT);
- CarBluetoothManager carBluetoothManager;
- String defaultProtocol = supportedTransportProtocols.get(0);
- logd(TAG, "Setting " + defaultProtocol + " as the transport.");
- switch (defaultProtocol) {
- case TransportProtocols.PROTOCOL_SPP:
- carBluetoothManager =
- createSppManager(storage, isCompressionEnabled, isCapabilitiesEligible);
- break;
- case TransportProtocols.PROTOCOL_BLE_PERIPHERAL:
- default:
- carBluetoothManager =
- createBleManager(storage, isCompressionEnabled, isCapabilitiesEligible);
- }
- connectedDeviceManager = new ConnectedDeviceManager(carBluetoothManager, storage);
- connectedDeviceManagerBinder =
- new ConnectedDeviceManagerBinder(connectedDeviceManager, loggingManager);
- associationBinder = new AssociationBinder(connectedDeviceManager);
- }
-
private void populateFeatures() {
logd(TAG, "Populating features.");
- if (featureCoordinator != null) {
- loggingFeature =
- new LoggingFeature(
- this,
- loggingManager,
- CompanionConnector.createLocalConnector(
- this, Connector.USER_TYPE_DRIVER, featureCoordinator));
- systemFeature =
- new SystemFeature(
- this,
- storage,
- CompanionConnector.createLocalConnector(
- this, Connector.USER_TYPE_DRIVER, featureCoordinator));
- } else {
- loggingFeature =
- new LoggingFeature(
- this,
- loggingManager,
- CompanionConnector.createLocalConnector(
- this, Connector.USER_TYPE_ALL, connectedDeviceManagerBinder, associationBinder));
- systemFeature =
- new SystemFeature(
- this,
- storage,
- CompanionConnector.createLocalConnector(
- this, Connector.USER_TYPE_ALL, connectedDeviceManagerBinder, associationBinder));
- }
- }
-
- private CarBluetoothManager createSppManager(
- @NonNull ConnectedDeviceStorage storage,
- boolean isCompressionEnabled,
- boolean isCapabilitiesEligible) {
- UUID sppServiceUuid = UUID.fromString(requireMetaString(META_ASSOCIATION_SERVICE_UUID));
- int maxSppPacketSize = getMetaInt(META_SPP_PACKET_BYTES, DEFAULT_SPP_PACKET_SIZE);
- return new CarSppManager(
- sppDelegateBinder,
- storage,
- sppServiceUuid,
- maxSppPacketSize,
- isCompressionEnabled,
- isCapabilitiesEligible);
- }
-
- private CarBluetoothManager createBleManager(
- @NonNull ConnectedDeviceStorage storage,
- boolean isCompressionEnabled,
- boolean isCapabilitiesEligible) {
- UUID associationUuid = UUID.fromString(requireMetaString(META_ASSOCIATION_SERVICE_UUID));
- UUID reconnectUuid =
- UUID.fromString(getMetaString(META_RECONNECT_SERVICE_UUID, DEFAULT_RECONNECT_UUID));
- UUID reconnectDataUuid =
- UUID.fromString(getMetaString(META_RECONNECT_DATA_UUID, DEFAULT_RECONNECT_DATA_UUID));
- UUID advertiseDataCharacteristicUuid =
- UUID.fromString(
- getMetaString(
- META_ADVERTISE_DATA_CHARACTERISTIC_UUID,
- DEFAULT_ADVERTISE_DATA_CHARACTERISTIC_UUID));
- UUID writeUuid = UUID.fromString(getMetaString(META_WRITE_UUID, DEFAULT_WRITE_UUID));
- UUID readUuid = UUID.fromString(getMetaString(META_READ_UUID, DEFAULT_READ_UUID));
- int defaultMtuSize = getMetaInt(META_DEFAULT_MTU_BYTES, DEFAULT_MTU_SIZE);
- boolean isProxyEnabled = getMetaBoolean(META_ENABLE_PROXY, PROXY_ENABLED_BY_DEFAULT);
- BlePeripheralManager blePeripheralManager;
- if (isProxyEnabled) {
- logi(TAG, "Initializing with ProxyBlePeripheralManager");
- blePeripheralManager =
- new ProxyBlePeripheralManager(new NetworkSocketFactory(this), scheduledExecutorService);
- } else {
- blePeripheralManager = new OnDeviceBlePeripheralManager(this);
- }
- return new CarBlePeripheralManager(
- blePeripheralManager,
- storage,
- associationUuid,
- reconnectUuid,
- reconnectDataUuid,
- advertiseDataCharacteristicUuid,
- writeUuid,
- readUuid,
- MAX_ADVERTISEMENT_DURATION,
- defaultMtuSize,
- isCompressionEnabled,
- new BluetoothRfcommChannel(sppDelegateBinder),
- isCapabilitiesEligible);
+ loggingFeature =
+ new LoggingFeature(
+ this,
+ loggingManager,
+ CompanionConnector.createLocalConnector(
+ this, Connector.USER_TYPE_DRIVER, featureCoordinator));
+ systemFeature =
+ new SystemFeature(
+ this,
+ storage,
+ CompanionConnector.createLocalConnector(
+ this, Connector.USER_TYPE_ALL, featureCoordinator));
}
@Override
@@ -449,10 +295,6 @@
logd(TAG, "Service bound. Action: " + intent.getAction());
String action = intent.getAction();
switch (action) {
- case CompanionConnector.ACTION_BIND_ASSOCIATION:
- return associationBinder;
- case CompanionConnector.ACTION_BIND_REMOTE_FEATURE:
- return connectedDeviceManagerBinder;
case ConnectedDeviceSppDelegateBinder.ACTION_BIND_SPP:
return sppDelegateBinder;
case CompanionConnector.ACTION_BIND_FEATURE_COORDINATOR:
@@ -498,11 +340,7 @@
logd(TAG, "Features are already cleaned up. No need to clean up again.");
return;
}
- if (useFeatureCoordinator) {
- featureCoordinator.reset();
- } else {
- connectedDeviceManager.reset();
- }
+ featureCoordinator.reset();
loggingManager.reset();
systemFeature.stop();
loggingFeature.stop();
@@ -519,11 +357,7 @@
() -> {
logd(TAG, "Initializing features.");
loggingManager.start();
- if (useFeatureCoordinator) {
- featureCoordinator.start();
- } else {
- connectedDeviceManager.start();
- }
+ featureCoordinator.start();
systemFeature.start();
loggingFeature.start();
})
@@ -533,131 +367,4 @@
private boolean isTransportSupported(@NonNull String protocol) {
return supportedTransportProtocols.contains(protocol);
}
-
- private IConnectedDeviceManager.Stub createConnectedDeviceManagerWrapper() {
- return new IConnectedDeviceManager.Stub() {
- @Override
- public List<ConnectedDevice> getActiveUserConnectedDevices() {
- return featureCoordinator.getConnectedDevicesForDriver();
- }
-
- @Override
- public void registerActiveUserConnectionCallback(IConnectionCallback callback) {
- featureCoordinator.registerDriverConnectionCallback(callback);
- }
-
- @Override
- public void unregisterConnectionCallback(IConnectionCallback callback) {
- featureCoordinator.unregisterConnectionCallback(callback);
- }
-
- @Override
- public void registerDeviceCallback(
- ConnectedDevice connectedDevice, ParcelUuid recipientId, IDeviceCallback callback) {
- featureCoordinator.registerDeviceCallback(connectedDevice, recipientId, callback);
- }
-
- @Override
- public void unregisterDeviceCallback(
- ConnectedDevice connectedDevice, ParcelUuid recipientId, IDeviceCallback callback) {
- featureCoordinator.unregisterDeviceCallback(connectedDevice, recipientId, callback);
- }
-
- @Override
- public boolean sendMessage(ConnectedDevice connectedDevice, DeviceMessage message) {
- return featureCoordinator.sendMessage(connectedDevice, message);
- }
-
- @Override
- public void registerDeviceAssociationCallback(IDeviceAssociationCallback callback) {
- featureCoordinator.registerDeviceAssociationCallback(callback);
- }
-
- @Override
- public void unregisterDeviceAssociationCallback(IDeviceAssociationCallback callback) {
- featureCoordinator.unregisterDeviceAssociationCallback(callback);
- }
-
- @Override
- public void registerOnLogRequestedListener(int loggerId, IOnLogRequestedListener listener) {
- featureCoordinator.registerOnLogRequestedListener(loggerId, listener);
- }
-
- @Override
- public void unregisterOnLogRequestedListener(int loggerId, IOnLogRequestedListener listener) {
- featureCoordinator.unregisterOnLogRequestedListener(loggerId, listener);
- }
-
- @Override
- public void processLogRecords(int loggerId, byte[] logRecords) {
- featureCoordinator.processLogRecords(loggerId, logRecords);
- }
- };
- }
-
- private IAssociatedDeviceManager.Stub createAssociatedDeviceManagerWrapper() {
- return new IAssociatedDeviceManager.Stub() {
-
- @Override
- public void startAssociation(IAssociationCallback callback) {
- featureCoordinator.startAssociation(callback);
- }
-
- @Override
- public void stopAssociation() {
- featureCoordinator.stopAssociation();
- }
-
- @Override
- public void retrievedActiveUserAssociatedDevices(
- IOnAssociatedDevicesRetrievedListener listener) {
- featureCoordinator.retrieveAssociatedDevicesForDriver(listener);
- }
-
- @Override
- public void acceptVerification() {
- featureCoordinator.acceptVerification();
- }
-
- @Override
- public void removeAssociatedDevice(String deviceId) {
- featureCoordinator.removeAssociatedDevice(deviceId);
- }
-
- @Override
- public void registerDeviceAssociationCallback(IDeviceAssociationCallback callback) {
- featureCoordinator.registerDeviceAssociationCallback(callback);
- }
-
- @Override
- public void unregisterDeviceAssociationCallback(IDeviceAssociationCallback callback) {
- featureCoordinator.unregisterDeviceAssociationCallback(callback);
- }
-
- @Override
- public List<ConnectedDevice> getActiveUserConnectedDevices() {
- return featureCoordinator.getConnectedDevicesForDriver();
- }
-
- @Override
- public void registerConnectionCallback(IConnectionCallback callback) {
- featureCoordinator.registerDriverConnectionCallback(callback);
- }
-
- @Override
- public void unregisterConnectionCallback(IConnectionCallback callback) {
- featureCoordinator.unregisterConnectionCallback(callback);
- }
-
- @Override
- public void enableAssociatedDeviceConnection(String deviceId) {
- featureCoordinator.enableAssociatedDeviceConnection(deviceId);
- }
-
- @Override
- public void disableAssociatedDeviceConnection(String deviceId) {
- featureCoordinator.disableAssociatedDeviceConnection(deviceId);
- }
- };
- }
}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/service/TrunkService.java b/libs/connecteddevice/src/com/google/android/connecteddevice/service/TrunkService.java
index 8736bf3..71ecd33 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/service/TrunkService.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/service/TrunkService.java
@@ -115,7 +115,7 @@
if (attempts > MAX_BIND_ATTEMPTS) {
loge(
TAG,
- "Failed to bind to " + flatComponentName + "after " + attempts + " attempts. Aborting.");
+ "Failed to bind to " + flatComponentName + " after " + attempts + " attempts. Aborting.");
return;
}
logw(TAG, "Unable to bind to " + flatComponentName + ". Trying again.");
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.aidl b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ConnectChallenge.aidl
similarity index 80%
rename from libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.aidl
rename to libs/connecteddevice/src/com/google/android/connecteddevice/transport/ConnectChallenge.aidl
index ca4f80b..a18116f 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.aidl
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ConnectChallenge.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2022 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.
@@ -14,6 +14,6 @@
* limitations under the License.
*/
-package com.google.android.connecteddevice.model;
+package com.google.android.connecteddevice.transport;
-parcelable OobEligibleDevice;
+parcelable ConnectChallenge;
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ConnectChallenge.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ConnectChallenge.kt
new file mode 100644
index 0000000..da2db6b
--- /dev/null
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ConnectChallenge.kt
@@ -0,0 +1,25 @@
+package com.google.android.connecteddevice.transport
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+/** Container class to hold the connect challenge the salt that generated the challenge. */
+@Parcelize
+data class ConnectChallenge(val challenge: ByteArray, val salt: ByteArray) : Parcelable {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+ if (other !is ConnectChallenge) {
+ return false
+ }
+
+ return challenge.contentEquals(other.challenge) && salt.contentEquals(other.salt)
+ }
+
+ override fun hashCode(): Int {
+ var result = challenge.contentHashCode()
+ result = 31 * result + salt.contentHashCode()
+ return result
+ }
+}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ConnectionProtocol.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ConnectionProtocol.kt
index 7fc6616..da7b0b4 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ConnectionProtocol.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ConnectionProtocol.kt
@@ -15,77 +15,80 @@
*/
package com.google.android.connecteddevice.transport
+import android.os.ParcelUuid
import androidx.annotation.CallSuper
+import com.google.android.connecteddevice.util.AidlThreadSafeCallbacks
import com.google.android.connecteddevice.util.SafeLog.logd
import com.google.android.connecteddevice.util.SafeLog.logw
-import com.google.android.connecteddevice.util.ThreadSafeCallbacks
-import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.Executor
+import java.util.concurrent.Executors
/**
* Representation of a communication protocol that provides actions and event notifications for
* interacting with devices.
*/
-abstract class ConnectionProtocol {
+abstract class ConnectionProtocol(
+ private val callbackExecutor: Executor = Executors.newCachedThreadPool()
+) : IConnectionProtocol.Stub() {
protected val dataReceivedListeners:
- MutableMap<String, ThreadSafeCallbacks<DataReceivedListener>> =
+ MutableMap<String, AidlThreadSafeCallbacks<IDataReceivedListener>> =
ConcurrentHashMap()
protected val deviceDisconnectedListeners:
- MutableMap<String, ThreadSafeCallbacks<DeviceDisconnectedListener>> =
+ MutableMap<String, AidlThreadSafeCallbacks<IDeviceDisconnectedListener>> =
ConcurrentHashMap()
protected val maxDataSizeChangedListeners:
- MutableMap<String, ThreadSafeCallbacks<DeviceMaxDataSizeChangedListener>> =
+ MutableMap<String, AidlThreadSafeCallbacks<IDeviceMaxDataSizeChangedListener>> =
ConcurrentHashMap()
- protected val missedData: MutableMap<String, MutableList<ByteArray>> = ConcurrentHashMap()
+ private val missedData: MutableMap<String, MutableList<ByteArray>> = ConcurrentHashMap()
/**
* `true` if challenge exchange is required to verify the remote device for establishing a secure
* channel over this [ConnectionProtocol].
*/
- abstract val isDeviceVerificationRequired: Boolean
+ abstract override fun isDeviceVerificationRequired(): Boolean
/**
* Begin the discovery process with [name] and [identifier] for a new device to associate with.
*/
- abstract fun startAssociationDiscovery(
+ abstract override fun startAssociationDiscovery(
name: String,
- callback: DiscoveryCallback,
- identifier: UUID
+ identifier: ParcelUuid,
+ callback: IDiscoveryCallback
)
/**
* Begin the discovery process for a device that will respond to the supplied [id] with
* [challenge].
*/
- abstract fun startConnectionDiscovery(
- id: UUID,
+ abstract override fun startConnectionDiscovery(
+ id: ParcelUuid,
challenge: ConnectChallenge,
- callback: DiscoveryCallback
+ callback: IDiscoveryCallback
)
/** Stop an ongoing association discovery. */
- abstract fun stopAssociationDiscovery()
+ abstract override fun stopAssociationDiscovery()
/** Stop an ongoing connection discovery for the provided device. */
- abstract fun stopConnectionDiscovery(id: UUID)
+ abstract override fun stopConnectionDiscovery(id: ParcelUuid)
/** Send data to a device. */
- abstract fun sendData(protocolId: String, data: ByteArray, callback: DataSendCallback? = null)
+ abstract override fun sendData(protocolId: String, data: ByteArray, callback: IDataSendCallback?)
/** Disconnect a specific device. */
- abstract fun disconnectDevice(protocolId: String)
+ abstract override fun disconnectDevice(protocolId: String)
/**
* Disconnect all active connections, cancel any discoveries in progress, and clean up to a
* neutral state.
*/
@CallSuper
- open fun reset() {
+ override fun reset() {
deviceDisconnectedListeners.clear()
dataReceivedListeners.clear()
maxDataSizeChangedListeners.clear()
@@ -96,49 +99,43 @@
* Returns the maximum number of bytes that can be written in a single message for the device
* matching the [protocolId].
*/
- abstract fun getMaxWriteSize(protocolId: String): Int
+ abstract override fun getMaxWriteSize(protocolId: String): Int
/** Register a listener to be notified when data has been received on protocol [protocolId]. */
- open fun registerDataReceivedListener(
- protocolId: String,
- listener: DataReceivedListener,
- executor: Executor
- ) {
+ override fun registerDataReceivedListener(protocolId: String, listener: IDataReceivedListener) {
logd(TAG, "Registering a new DataReceivedListener.")
- val listeners = dataReceivedListeners.computeIfAbsent(protocolId) { ThreadSafeCallbacks() }
- listeners.add(listener, executor)
+ val listeners = dataReceivedListeners.computeIfAbsent(protocolId) { AidlThreadSafeCallbacks() }
+ listeners.add(listener, callbackExecutor)
missedData.remove(protocolId)?.forEach { data ->
listeners.invoke { it.onDataReceived(protocolId, data) }
}
}
/** Register a listener to be notified when device has disconnected on protocol [protocolId]. */
- open fun registerDeviceDisconnectedListener(
+ override fun registerDeviceDisconnectedListener(
protocolId: String,
- listener: DeviceDisconnectedListener,
- executor: Executor
+ listener: IDeviceDisconnectedListener
) {
deviceDisconnectedListeners
- .computeIfAbsent(protocolId) { ThreadSafeCallbacks() }
- .add(listener, executor)
+ .computeIfAbsent(protocolId) { AidlThreadSafeCallbacks() }
+ .add(listener, callbackExecutor)
}
/**
* Register a listener to be notified when the protocol [protocolId] has negotiated a new maximum
* data size.
*/
- open fun registerDeviceMaxDataSizeChangedListener(
+ override fun registerDeviceMaxDataSizeChangedListener(
protocolId: String,
- listener: DeviceMaxDataSizeChangedListener,
- executor: Executor
+ listener: IDeviceMaxDataSizeChangedListener
) {
maxDataSizeChangedListeners
- .computeIfAbsent(protocolId) { ThreadSafeCallbacks() }
- .add(listener, executor)
+ .computeIfAbsent(protocolId) { AidlThreadSafeCallbacks() }
+ .add(listener, callbackExecutor)
}
- /** Unregister a previously registered [DataReceivedListener]. */
- open fun unregisterDataReceivedListener(protocolId: String, listener: DataReceivedListener) {
+ /** Unregister a previously registered [listener]. */
+ override fun unregisterDataReceivedListener(protocolId: String, listener: IDataReceivedListener) {
logd(TAG, "Removing data DataReceivedListener.")
val listeners =
dataReceivedListeners.getOrElse(protocolId) {
@@ -154,10 +151,10 @@
}
}
- /** Unregister a previously registered [DeviceDisconnectedListener]. */
- open fun unregisterDeviceDisconnectListener(
+ /** Unregister a previously registered [listener]. */
+ override fun unregisterDeviceDisconnectListener(
protocolId: String,
- listener: DeviceDisconnectedListener
+ listener: IDeviceDisconnectedListener
) {
val listeners =
deviceDisconnectedListeners.getOrElse(protocolId) {
@@ -173,10 +170,10 @@
}
}
- /** Unregister a previously registered [DeviceMaxDataSizeChangedListener]. */
- open fun unregisterDeviceMaxDataSizeChangedListener(
+ /** Unregister a previously registered [listener]. */
+ override fun unregisterDeviceMaxDataSizeChangedListener(
protocolId: String,
- listener: DeviceMaxDataSizeChangedListener
+ listener: IDeviceMaxDataSizeChangedListener
) {
val listeners =
maxDataSizeChangedListeners.getOrElse(protocolId) {
@@ -194,7 +191,7 @@
}
/** Removes registered listeners for connection [protocolId]. */
- fun removeListeners(protocolId: String) {
+ override fun removeListeners(protocolId: String) {
deviceDisconnectedListeners.remove(protocolId)
dataReceivedListeners.remove(protocolId)
maxDataSizeChangedListeners.remove(protocolId)
@@ -219,48 +216,6 @@
}
}
- /** Container class to hold the connect challenge the salt that generated the challenge. */
- data class ConnectChallenge(val challenge: ByteArray, val salt: ByteArray)
-
- /** Event notifications on the discovery process. */
- interface DiscoveryCallback {
- /** Invoked when discovery for a device has started successfully. */
- fun onDiscoveryStartedSuccessfully()
-
- /** Invoked when discovery for a device failed to start. */
- fun onDiscoveryFailedToStart()
-
- /** Invoked when a device connection is established in response to the discovery. */
- fun onDeviceConnected(protocolId: String)
- }
-
- /** Callback for the result of sending data. */
- interface DataSendCallback {
- /** Invoked when the data was successfully sent. */
- fun onDataSentSuccessfully()
-
- /** Invoked when the data failed to send. */
- fun onDataFailedToSend()
- }
-
- /** Listener to be invoked when a device has disconnected. */
- interface DeviceDisconnectedListener {
- /** Called when the device has disconnected on protocol [protocolId]. */
- fun onDeviceDisconnected(protocolId: String)
- }
-
- /** Listener to be invoked when data has been received from a device. */
- interface DataReceivedListener {
- /** Called when [data] is received from the remote device on protocol [protocolId]. */
- fun onDataReceived(protocolId: String, data: ByteArray)
- }
-
- /** Listener to be invoked when the protocol has negotiated a new maximum data size. */
- interface DeviceMaxDataSizeChangedListener {
- /** Called when the protocol [protocolId] has negotiated a new maximum data size [maxBytes]. */
- fun onDeviceMaxDataSizeChanged(protocolId: String, maxBytes: Int)
- }
-
companion object {
private const val TAG = "ConnectionProtocol"
}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IConnectionProtocol.aidl b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IConnectionProtocol.aidl
new file mode 100644
index 0000000..40047e6
--- /dev/null
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IConnectionProtocol.aidl
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+package com.google.android.connecteddevice.transport;
+
+import android.os.ParcelUuid;
+
+import com.google.android.connecteddevice.transport.ConnectChallenge;
+import com.google.android.connecteddevice.transport.IDataReceivedListener;
+import com.google.android.connecteddevice.transport.IDataSendCallback;
+import com.google.android.connecteddevice.transport.IDeviceDisconnectedListener;
+import com.google.android.connecteddevice.transport.IDeviceMaxDataSizeChangedListener;
+import com.google.android.connecteddevice.transport.IDiscoveryCallback;
+
+interface IConnectionProtocol {
+ /**
+ * Returns true if challenge exchange is required to verify the remote device for establishing a
+ * secure channel over this protocol.
+ */
+ boolean isDeviceVerificationRequired();
+
+ /**
+ * Begin the discovery process with the name and identifier for a new device to associate with.
+ */
+ void startAssociationDiscovery(
+ in String name,
+ in ParcelUuid identifier,
+ in IDiscoveryCallback callback);
+
+ /**
+ * Begin the discovery process for a device that will respond to the supplied id and challenge.
+ */
+ void startConnectionDiscovery(
+ in ParcelUuid id,
+ in ConnectChallenge challenge,
+ in IDiscoveryCallback callback);
+
+ /** Stop an ongoing association discovery. */
+ void stopAssociationDiscovery();
+
+ /** Stop an ongoing connection discovery for the provided device. */
+ void stopConnectionDiscovery(in ParcelUuid id);
+
+ /** Send data to a device. */
+ void sendData(in String protocolId, in byte[] data, in @nullable IDataSendCallback callback);
+
+ /** Disconnect a specific device. */
+ void disconnectDevice(in String protocolId);
+
+ /**
+ * Disconnect all active connections, cancel any discoveries in progress, and clean up to a
+ * neutral state.
+ */
+ void reset();
+
+ /**
+ * Returns the maximum number of bytes that can be written in a single message for the device
+ * matching the protocolId.
+ */
+ int getMaxWriteSize(in String protocolId);
+
+ /** Register a listener to be notified when data has been received on the specified device. */
+ void registerDataReceivedListener(in String protocolId, in IDataReceivedListener listener);
+
+ /** Register a listener to be notified when device has disconnected. */
+ void registerDeviceDisconnectedListener(
+ in String protocolId,
+ in IDeviceDisconnectedListener listener);
+
+ /**
+ * Register a listener to be notified when the specified device has negotiated a new maximum
+ * data size.
+ */
+ void registerDeviceMaxDataSizeChangedListener(
+ in String protocolId,
+ in IDeviceMaxDataSizeChangedListener listener);
+
+ /** Unregister a previously registered listener. */
+ void unregisterDataReceivedListener(in String protocolId, in IDataReceivedListener listener);
+
+ /** Unregister a previously registered listener. */
+ void unregisterDeviceDisconnectListener(
+ in String protocolId,
+ in IDeviceDisconnectedListener listener);
+
+ /** Unregister a previously registered listener. */
+ void unregisterDeviceMaxDataSizeChangedListener(
+ in String protocolId,
+ in IDeviceMaxDataSizeChangedListener listener);
+
+ /** Removes registered listeners for the specified device. */
+ void removeListeners(in String protocolId);
+}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.aidl b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDataReceivedListener.aidl
similarity index 60%
copy from libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.aidl
copy to libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDataReceivedListener.aidl
index ca4f80b..ab64f72 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.aidl
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDataReceivedListener.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2022 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.
@@ -14,6 +14,10 @@
* limitations under the License.
*/
-package com.google.android.connecteddevice.model;
+package com.google.android.connecteddevice.transport;
-parcelable OobEligibleDevice;
+/** Listener to be invoked when data has been received from a device. */
+oneway interface IDataReceivedListener {
+ /** Called when [data] is received from the remote device on protocol protocolId. */
+ void onDataReceived(in String protocolId, in byte[] data);
+}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.aidl b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDataSendCallback.aidl
similarity index 60%
copy from libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.aidl
copy to libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDataSendCallback.aidl
index ca4f80b..7add8ec 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.aidl
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDataSendCallback.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2022 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.
@@ -14,6 +14,13 @@
* limitations under the License.
*/
-package com.google.android.connecteddevice.model;
+package com.google.android.connecteddevice.transport;
-parcelable OobEligibleDevice;
+/** Callback for the result of sending data. */
+oneway interface IDataSendCallback {
+ /** Invoked when the data was successfully sent. */
+ void onDataSentSuccessfully();
+
+ /** Invoked when the data failed to send. */
+ void onDataFailedToSend();
+}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.aidl b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDeviceDisconnectedListener.aidl
similarity index 62%
copy from libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.aidl
copy to libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDeviceDisconnectedListener.aidl
index ca4f80b..239466a 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/model/OobEligibleDevice.aidl
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDeviceDisconnectedListener.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2022 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.
@@ -14,6 +14,10 @@
* limitations under the License.
*/
-package com.google.android.connecteddevice.model;
+package com.google.android.connecteddevice.transport;
-parcelable OobEligibleDevice;
+/** Listener to be invoked when a device has disconnected. */
+oneway interface IDeviceDisconnectedListener {
+ /** Called when the device has disconnected on protocol protocolId. */
+ void onDeviceDisconnected(in String protocolId);
+}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDeviceMaxDataSizeChangedListener.aidl b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDeviceMaxDataSizeChangedListener.aidl
new file mode 100644
index 0000000..8dce7f1
--- /dev/null
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDeviceMaxDataSizeChangedListener.aidl
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+package com.google.android.connecteddevice.transport;
+
+/** Listener to be invoked when the protocol has negotiated a new maximum data size. */
+oneway interface IDeviceMaxDataSizeChangedListener {
+ /** Called when the protocol [protocolId] has negotiated a new maximum data size [maxBytes]. */
+ void onDeviceMaxDataSizeChanged(in String protocolId, in int maxBytes);
+}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDiscoveryCallback.aidl b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDiscoveryCallback.aidl
new file mode 100644
index 0000000..b994385
--- /dev/null
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IDiscoveryCallback.aidl
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+package com.google.android.connecteddevice.transport;
+
+/** Event notifications on the discovery process. */
+oneway interface IDiscoveryCallback {
+ /** Invoked when discovery for a device has started successfully. */
+ void onDiscoveryStartedSuccessfully();
+
+ /** Invoked when discovery for a device failed to start. */
+ void onDiscoveryFailedToStart();
+
+ /** Invoked when a device connection is established in response to the discovery. */
+ void onDeviceConnected(in String protocolId);
+}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ble/BlePeripheralProtocol.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ble/BlePeripheralProtocol.kt
index 867191f..91d3ac9 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ble/BlePeripheralProtocol.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ble/BlePeripheralProtocol.kt
@@ -27,19 +27,26 @@
import android.os.ParcelUuid
import androidx.annotation.VisibleForTesting
import com.google.android.connecteddevice.transport.BluetoothDeviceProvider
+import com.google.android.connecteddevice.transport.ConnectChallenge
import com.google.android.connecteddevice.transport.ConnectionProtocol
+import com.google.android.connecteddevice.transport.IDataSendCallback
+import com.google.android.connecteddevice.transport.IDiscoveryCallback
import com.google.android.connecteddevice.util.ByteUtils
import com.google.android.connecteddevice.util.SafeLog.logd
import com.google.android.connecteddevice.util.SafeLog.loge
import com.google.android.connecteddevice.util.SafeLog.logw
import java.time.Duration
import java.util.UUID
+import java.util.concurrent.Executor
+import java.util.concurrent.Executors
/**
* A ble peripheral communication protocol that provides actions and event notifications for
* interacting with devices.
*/
-class BlePeripheralProtocol(
+class BlePeripheralProtocol
+@JvmOverloads
+constructor(
private val blePeripheralManager: BlePeripheralManager,
private val reconnectServiceUuid: UUID,
private val reconnectDataUuid: UUID,
@@ -48,9 +55,8 @@
readCharacteristicUuid: UUID,
private val maxReconnectAdvertisementDuration: Duration,
defaultMtuSize: Int,
-) : ConnectionProtocol(), BluetoothDeviceProvider {
- override val isDeviceVerificationRequired = true
-
+ callbackExecutor: Executor = Executors.newCachedThreadPool()
+) : ConnectionProtocol(callbackExecutor), BluetoothDeviceProvider {
private val writeCharacteristic =
BluetoothGattCharacteristic(
writeCharacteristicUuid,
@@ -123,7 +129,11 @@
val currentChallenge = connectChallenge
if (currentDeviceId != null && currentDiscoveryCallback != null && currentChallenge != null) {
reset()
- startConnectionDiscovery(currentDeviceId, currentChallenge, currentDiscoveryCallback)
+ startConnectionDiscovery(
+ ParcelUuid(currentDeviceId),
+ currentChallenge,
+ currentDiscoveryCallback
+ )
}
}
@@ -134,9 +144,9 @@
private var protocolId: String? = null
private var connectChallenge: ConnectChallenge? = null
private var advertiseCallback: AdvertiseCallback? = null
- private var discoveryCallback: DiscoveryCallback? = null
+ private var discoveryCallback: IDiscoveryCallback? = null
private var timeoutHandler: Handler? = null
- private var dataSendCallback: DataSendCallback? = null
+ private var dataSendCallback: IDataSendCallback? = null
init {
writeCharacteristic.addDescriptor(createBluetoothGattDescriptor())
@@ -148,8 +158,8 @@
override fun startAssociationDiscovery(
name: String,
- callback: DiscoveryCallback,
- identifier: UUID
+ identifier: ParcelUuid,
+ callback: IDiscoveryCallback,
) {
if (!isReadyToStartDiscovery()) {
return
@@ -173,7 +183,7 @@
}
advertiseCallback = associationAdvertiseCallback
startAdvertising(
- identifier,
+ identifier.uuid,
associationAdvertiseCallback,
scanResponse = ByteUtils.hexStringToByteArray(name),
scanResponseUuid = reconnectDataUuid
@@ -181,15 +191,15 @@
}
override fun startConnectionDiscovery(
- id: UUID,
+ id: ParcelUuid,
challenge: ConnectChallenge,
- callback: DiscoveryCallback
+ callback: IDiscoveryCallback
) {
if (!isReadyToStartDiscovery()) {
return
}
reset()
- deviceId = id
+ deviceId = id.uuid
discoveryCallback = callback
connectChallenge = challenge
blePeripheralManager.registerCallback(peripheralCallback)
@@ -230,15 +240,15 @@
reset()
}
- override fun stopConnectionDiscovery(id: UUID) {
- if (id != deviceId || advertiseCallback == null) {
+ override fun stopConnectionDiscovery(id: ParcelUuid) {
+ if (id.uuid != deviceId || advertiseCallback == null) {
logd(TAG, "No connection discovery is happening for device $id, ignoring.")
return
}
reset()
}
- override fun sendData(protocolId: String, data: ByteArray, callback: DataSendCallback?) {
+ override fun sendData(protocolId: String, data: ByteArray, callback: IDataSendCallback?) {
val device: BluetoothDevice? = bluetoothDevice
if (device == null) {
loge(TAG, "Failed to send data, no connected device.")
@@ -309,6 +319,8 @@
override fun getBluetoothDeviceById(protocolId: String) =
if (protocolId == this.protocolId) bluetoothDevice else null
+ override fun isDeviceVerificationRequired(): Boolean = true
+
private fun startAdvertising(
serviceUuid: UUID,
callback: AdvertiseCallback,
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/spp/ConnectedDeviceSppDelegateBinder.java b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/spp/ConnectedDeviceSppDelegateBinder.java
index 435a751..3a5f9d6 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/spp/ConnectedDeviceSppDelegateBinder.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/spp/ConnectedDeviceSppDelegateBinder.java
@@ -246,10 +246,12 @@
public void registerConnectionCallback(
@NonNull UUID serviceUuid, @Nullable OnErrorListener onErrorListener) {
+ logd(TAG, "Registering a new error listener for " + serviceUuid + ".");
connectionErrorListeners.put(serviceUuid, onErrorListener);
}
public void unregisterConnectionCallback(@NonNull UUID serviceUuid) {
+ logd(TAG, "Removing error listener for " + serviceUuid + ".");
connectionErrorListeners.remove(serviceUuid);
}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/spp/SppProtocol.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/spp/SppProtocol.kt
index 64714e6..f63e826 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/spp/SppProtocol.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/spp/SppProtocol.kt
@@ -19,13 +19,18 @@
import android.os.ParcelUuid
import android.os.RemoteException
import com.google.android.connecteddevice.transport.BluetoothDeviceProvider
+import com.google.android.connecteddevice.transport.ConnectChallenge
import com.google.android.connecteddevice.transport.ConnectionProtocol
+import com.google.android.connecteddevice.transport.IDataSendCallback
+import com.google.android.connecteddevice.transport.IDiscoveryCallback
import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder.OnErrorListener
import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder.OnMessageReceivedListener
import com.google.android.connecteddevice.util.SafeLog.logd
import com.google.android.connecteddevice.util.SafeLog.loge
import com.google.android.connecteddevice.util.SafeLog.logw
import java.util.UUID
+import java.util.concurrent.Executor
+import java.util.concurrent.Executors
/**
* Representation of a Serial Port Profile channel, which communicated with [SppService] via
@@ -34,12 +39,13 @@
* @property sppBinder [ConnectedDeviceSppDelegateBinder] for communication with [SppService].
* @property maxWriteSize Maximum size in bytes to write in one packet.
*/
-class SppProtocol(
+class SppProtocol
+@JvmOverloads
+constructor(
private val sppBinder: ConnectedDeviceSppDelegateBinder,
private val maxWriteSize: Int,
-) : ConnectionProtocol(), BluetoothDeviceProvider {
- override val isDeviceVerificationRequired = false
-
+ callbackExecutor: Executor = Executors.newCachedThreadPool()
+) : ConnectionProtocol(callbackExecutor), BluetoothDeviceProvider {
private val pendingConnections = mutableMapOf<UUID, PendingConnection>()
private val connections = mutableMapOf<UUID, Connection>()
private val connectedDevices = mutableMapOf<UUID, BluetoothDevice>()
@@ -47,24 +53,25 @@
override fun startAssociationDiscovery(
name: String,
- callback: DiscoveryCallback,
- identifier: UUID
+ identifier: ParcelUuid,
+ callback: IDiscoveryCallback
) {
- associationIdentifier = identifier
- logd(TAG, "Start association discovery for association with UUID $identifier")
- startConnection(identifier, callback)
+ val uuidIdentifier = identifier.uuid
+ associationIdentifier = uuidIdentifier
+ logd(TAG, "Start association discovery for association with UUID $uuidIdentifier.")
+ startConnection(uuidIdentifier, callback)
}
override fun startConnectionDiscovery(
- id: UUID,
+ id: ParcelUuid,
challenge: ConnectChallenge,
- callback: DiscoveryCallback
+ callback: IDiscoveryCallback
) {
logd(TAG, "Starting connection discovery for device $id")
- startConnection(id, callback)
+ startConnection(id.uuid, callback)
}
- private fun startConnection(id: UUID, callback: DiscoveryCallback) {
+ private fun startConnection(id: UUID, callback: IDiscoveryCallback) {
try {
val pendingConnection = sppBinder.connectAsServer(id, /* isSecure= */ true)
if (pendingConnection == null) {
@@ -80,7 +87,7 @@
}
}
- private fun generateOnConnectionListener(callback: DiscoveryCallback) =
+ private fun generateOnConnectionListener(callback: IDiscoveryCallback) =
PendingConnection.OnConnectedListener { uuid, remoteDevice, isSecure, deviceName ->
val protocolId = UUID.randomUUID()
val connection = Connection(ParcelUuid(uuid), remoteDevice, isSecure, deviceName)
@@ -118,10 +125,11 @@
val listeners = deviceDisconnectedListeners[protocolId.toString()]
listeners?.invoke { it.onDeviceDisconnected(protocolId.toString()) }
connectedDevices.remove(protocolId)
+ connections.remove(protocolId)
logd(
TAG,
- "Inform device connection error with connection $protocolId to " +
- "${listeners?.size()} listeners."
+ "Inform device connection error with connection $protocolId to ${listeners?.size()} " +
+ "listeners."
)
removeListeners(protocolId.toString())
}
@@ -138,9 +146,9 @@
associationIdentifier = null
}
- override fun stopConnectionDiscovery(id: UUID) {
- logd(TAG, "Stop connection discovery with UUID $id.")
- stopDiscovery(id)
+ override fun stopConnectionDiscovery(id: ParcelUuid) {
+ logd(TAG, "Stop connection discovery with UUID ${id.uuid}.")
+ stopDiscovery(id.uuid)
}
private fun stopDiscovery(id: UUID) {
@@ -152,7 +160,7 @@
}
}
- override fun sendData(protocolId: String, data: ByteArray, callback: DataSendCallback?) {
+ override fun sendData(protocolId: String, data: ByteArray, callback: IDataSendCallback?) {
val connection = connections[UUID.fromString(protocolId)]
if (connection == null) {
callback?.onDataFailedToSend()
@@ -219,6 +227,8 @@
override fun getMaxWriteSize(protocolId: String) = maxWriteSize
+ override fun isDeviceVerificationRequired() = false
+
companion object {
private const val TAG = "SppProtocol"
}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/trust/TrustedDeviceAgentService.java b/libs/connecteddevice/src/com/google/android/connecteddevice/trust/TrustedDeviceAgentService.java
index eee9729..01aca37 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/trust/TrustedDeviceAgentService.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/trust/TrustedDeviceAgentService.java
@@ -34,7 +34,9 @@
import android.os.IBinder;
import android.os.RemoteException;
import android.os.UserHandle;
+import android.os.UserManager;
import android.service.trust.TrustAgentService;
+import androidx.annotation.VisibleForTesting;
import com.google.android.connecteddevice.trust.api.ITrustedDeviceAgentDelegate;
import com.google.android.connecteddevice.trust.api.ITrustedDeviceManager;
import java.time.Duration;
@@ -60,15 +62,21 @@
private Handler retryHandler;
- private ITrustedDeviceManager trustedDeviceManager;
+ @VisibleForTesting ITrustedDeviceManager trustedDeviceManager;
private int retries;
+ private UserManager userManager;
+
+ private KeyguardManager keyguardManager;
+
@SuppressLint("UnprotectedReceiver") // Broadcast is protected.
@Override
public void onCreate() {
super.onCreate();
logd(TAG, "Starting trust agent service.");
+ userManager = (UserManager) getSystemService(Context.USER_SERVICE);
+ keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
TrustedDeviceEventLog.onTrustAgentStarted();
registerReceiver(userUnlockedReceiver, new IntentFilter(Intent.ACTION_USER_UNLOCKED));
retryThread = new HandlerThread(RETRY_HANDLER_THREAD_NAME);
@@ -130,20 +138,19 @@
}
}
- private void deviceUnlocked() {
+ @VisibleForTesting
+ void maybeDismissLockscreen() {
if (!isManagingTrust.compareAndSet(true, false)) {
- logd(
- TAG,
- "User was unlocked via an alternative mechanism than an escrow token. No further "
- + "action required.");
+ logd(TAG, "User was unlocked before receiving an escrow token.");
return;
}
- logd(TAG, "User successfully unlocked with an escrow token. Dismissing the lock screen.");
+ logd(TAG, "Dismissing the lockscreen.");
grantTrust(
"Granting trust from escrow token for user.",
TRUST_DURATION_MS,
FLAG_GRANT_TRUST_DISMISS_KEYGUARD);
TrustedDeviceEventLog.onUserUnlocked();
+ setManagingTrust(false);
if (trustedDeviceManager == null) {
loge(TAG, "Manager was null when device was unlocked. Ignoring.");
return;
@@ -155,22 +162,33 @@
}
}
- private void bindToService() {
+ @VisibleForTesting
+ boolean isUserUnlocked(int userId) {
+ return userManager.isUserUnlocked(UserHandle.of(userId));
+ }
+
+ @VisibleForTesting
+ void bindToService() {
Intent intent = new Intent(this, TrustedDeviceManagerService.class);
bindService(intent, serviceConnection, /* flags= */ 0);
}
+ @VisibleForTesting
+ final void setupManager() {
+ try {
+ trustedDeviceManager.setTrustedDeviceAgentDelegate(trustedDeviceAgentDelegate);
+ logd(TAG, "Successfully connected to TrustedDeviceManager.");
+ } catch (RemoteException e) {
+ loge(TAG, "Error while establishing connection to TrustedDeviceManager.", e);
+ }
+ }
+
private final ServiceConnection serviceConnection =
new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
trustedDeviceManager = ITrustedDeviceManager.Stub.asInterface(service);
- try {
- trustedDeviceManager.setTrustedDeviceAgentDelegate(trustedDeviceAgentDelegate);
- logd(TAG, "Successfully connected to TrustedDeviceManager.");
- } catch (RemoteException e) {
- loge(TAG, "Error while establishing connection to TrustedDeviceManager.", e);
- }
+ setupManager();
}
@Override
@@ -206,8 +224,19 @@
@Override
public void unlockUserWithToken(byte[] token, long handle, int userId) {
- setManagingTrust(true);
+ if (!keyguardManager.isDeviceLocked()) {
+ logd(TAG, "Received an escrow token while no lockscreen was visible. Ignoring.");
+ return;
+ }
+ logd(TAG, "Received an escrow token for user " + userId + ".");
isManagingTrust.set(true);
+ setManagingTrust(true);
+ if (isUserUnlocked(userId)) {
+ logd(TAG, "User was already unlocked when token was received.");
+ maybeDismissLockscreen();
+ return;
+ }
+ logd(TAG, "Unlocking user with token.");
TrustedDeviceAgentService.this.unlockUserWithToken(handle, token, UserHandle.of(userId));
}
@@ -222,7 +251,7 @@
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- deviceUnlocked();
+ maybeDismissLockscreen();
}
};
}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/trust/TrustedDeviceManager.java b/libs/connecteddevice/src/com/google/android/connecteddevice/trust/TrustedDeviceManager.java
index 5e5caf1..12b3ffa 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/trust/TrustedDeviceManager.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/trust/TrustedDeviceManager.java
@@ -51,13 +51,17 @@
import com.google.android.connecteddevice.trust.storage.TrustedDeviceDatabase;
import com.google.android.connecteddevice.trust.storage.TrustedDeviceDatabaseProvider;
import com.google.android.connecteddevice.trust.storage.TrustedDeviceEntity;
+import com.google.android.connecteddevice.trust.storage.TrustedDeviceTokenEntity;
import com.google.android.connecteddevice.util.ByteUtils;
import com.google.android.connecteddevice.util.SafeConsumer;
import com.google.protobuf.ByteString;
import com.google.protobuf.ExtensionRegistryLite;
import com.google.protobuf.InvalidProtocolBufferException;
+import java.security.MessageDigest;
+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 java.util.concurrent.atomic.AtomicBoolean;
@@ -72,6 +76,8 @@
/** Length of token generated on a trusted device. */
private static final int ESCROW_TOKEN_LENGTH = 8;
+ private static final String SHA256 = "SHA-256";
+
private final RemoteCallbackList<ITrustedDeviceCallback> remoteTrustedDeviceCallbacks =
new RemoteCallbackList<>();
@@ -189,7 +195,6 @@
@Override
public void onEscrowTokenAdded(int userId, long handle) {
logd(TAG, "Escrow token has been successfully added.");
- pendingToken = null;
if (remoteEnrollmentCallbacks.getRegisteredCallbackCount() == 0) {
isWaitingForCredentials.set(true);
@@ -214,6 +219,11 @@
loge(TAG, "Unable to complete device enrollment. Pending device was null.");
return;
}
+ byte[] hashedToken = hashToken(pendingToken, UUID.fromString(pendingDevice.getDeviceId()));
+ if (hashedToken == null) {
+ loge(TAG, "Unable to hash pending token. Aborting enrollment.");
+ return;
+ }
logd(
TAG,
@@ -225,13 +235,17 @@
String deviceId = pendingDevice.getDeviceId();
TrustedDeviceEntity entity = new TrustedDeviceEntity(deviceId, userId, handle);
+ TrustedDeviceTokenEntity tokenEntity =
+ new TrustedDeviceTokenEntity(deviceId, ByteUtils.byteArrayToHexString(hashedToken));
databaseExecutor.execute(
() -> {
database.removeFeatureState(deviceId);
database.addOrReplaceTrustedDevice(entity);
+ database.addOrReplaceTrustedDeviceHashedToken(tokenEntity);
});
pendingDevice = null;
+ pendingToken = null;
TrustedDevice trustedDevice = new TrustedDevice(deviceId, userId, handle);
notifyRemoteCallbackList(
@@ -274,7 +288,8 @@
logd(TAG, "Received request to remove trusted device");
databaseExecutor.execute(
() -> {
- TrustedDeviceEntity entity = database.getTrustedDevice(trustedDevice.getDeviceId());
+ TrustedDeviceEntity entity =
+ database.getTrustedDeviceIfValid(trustedDevice.getDeviceId());
if (entity == null) {
return;
}
@@ -356,8 +371,11 @@
// Remove invalid trusted devices.
databaseExecutor.execute(
() -> {
+ byte[] stateMessage = createDisabledStateSyncMessage();
List<TrustedDeviceEntity> entities = database.getInvalidTrustedDevicesForUser(userId);
for (TrustedDeviceEntity entity : entities) {
+ FeatureStateEntity stateEntity = new FeatureStateEntity(entity.id, stateMessage);
+ databaseExecutor.execute(() -> database.addOrReplaceFeatureState(stateEntity));
removeTrustedDeviceInternal(entity);
}
});
@@ -419,6 +437,8 @@
logd(TAG, "Marking device " + entity.id + " as invalid.");
entity.isValid = false;
database.addOrReplaceTrustedDevice(entity);
+ logd(TAG, "Removing hashed token for device " + entity.id + ".");
+ database.removeTrustedDeviceHashedToken(entity.id);
sendStateDisabledMessage(entity.id);
notifyRemoteCallbackList(
remoteTrustedDeviceCallbacks,
@@ -467,8 +487,38 @@
return trustedDevices;
}
- private static boolean areCredentialsValid(@Nullable PhoneCredentials credentials) {
- return credentials != null;
+ @WorkerThread
+ private boolean areCredentialsValid(@Nullable PhoneCredentials credentials, String deviceId) {
+ if (credentials == null) {
+ return false;
+ }
+ TrustedDeviceTokenEntity entity = database.getTrustedDeviceHashedToken(deviceId);
+ if (entity == null) {
+ loge(TAG, "Unable to find hashed token for device " + deviceId + ".");
+ return false;
+ }
+ byte[] hashedToken =
+ hashToken(credentials.getEscrowToken().toByteArray(), UUID.fromString(deviceId));
+ if (hashedToken == null) {
+ return false;
+ }
+ return MessageDigest.isEqual(
+ hashedToken, ByteUtils.hexStringToByteArray(entity.getHashedToken()));
+ }
+
+ @Nullable
+ private static byte[] hashToken(byte[] token, UUID deviceId) {
+ MessageDigest digest;
+ try {
+ digest = MessageDigest.getInstance(SHA256);
+ } catch (NoSuchAlgorithmException e) {
+ loge(TAG, "Unable to find " + SHA256 + " algorithm. Token hash could not be generated.");
+ return null;
+ }
+
+ digest.update(token);
+ digest.update(ByteUtils.uuidToBytes(deviceId));
+ return digest.digest();
}
private void sendStateDisabledMessage(@NonNull String deviceId) {
@@ -513,7 +563,7 @@
}
byte[] message = payload.toByteArray();
- PhoneCredentials credentials = null;
+ PhoneCredentials credentials;
try {
credentials = PhoneCredentials.parseFrom(message, ExtensionRegistryLite.getEmptyRegistry());
} catch (InvalidProtocolBufferException e) {
@@ -521,12 +571,12 @@
return;
}
- if (!areCredentialsValid(credentials)) {
+ if (!areCredentialsValid(credentials, device.getDeviceId())) {
loge(TAG, "Received invalid credentials from device. Not unlocking head unit.");
return;
}
- TrustedDeviceEntity entity = database.getTrustedDevice(device.getDeviceId());
+ TrustedDeviceEntity entity = database.getTrustedDeviceIfValid(device.getDeviceId());
if (entity == null) {
logw(TAG, "Received unlock request from an untrusted device.");
@@ -559,7 +609,7 @@
private void processStatusSyncMessage(
@NonNull ConnectedDevice device,
@NonNull ByteString payload) {
- TrustedDeviceState state = null;
+ TrustedDeviceState state;
try {
state = TrustedDeviceState.parseFrom(payload, ExtensionRegistryLite.getEmptyRegistry());
} catch (InvalidProtocolBufferException e) {
@@ -578,7 +628,7 @@
return;
}
- TrustedDeviceEntity entity = database.getTrustedDevice(device.getDeviceId());
+ TrustedDeviceEntity entity = database.getTrustedDeviceIfValid(device.getDeviceId());
if (entity == null) {
logw(TAG, "Received state sync message from an untrusted device.");
@@ -598,7 +648,7 @@
logw(TAG, "Received error message with null payload. Ignoring.");
return;
}
- TrustedDeviceError trustedDeviceError = null;
+ TrustedDeviceError trustedDeviceError;
try {
trustedDeviceError =
TrustedDeviceError.parseFrom(payload, ExtensionRegistryLite.getEmptyRegistry());
@@ -705,7 +755,7 @@
new TrustedDeviceFeature.Callback() {
@Override
public void onMessageReceived(ConnectedDevice device, byte[] message) {
- TrustedDeviceMessage trustedDeviceMessage = null;
+ TrustedDeviceMessage trustedDeviceMessage;
try {
trustedDeviceMessage =
TrustedDeviceMessage.parseFrom(message, ExtensionRegistryLite.getEmptyRegistry());
@@ -770,7 +820,7 @@
@Override
public void onAssociatedDeviceRemoved(AssociatedDevice device) {
List<TrustedDevice> devices = getTrustedDevicesForActiveUser();
- if (devices == null || devices.isEmpty()) {
+ if (devices.isEmpty()) {
return;
}
TrustedDevice deviceToRemove = null;
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceDao.java b/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceDao.java
index 77314dd..ec8cdf6 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceDao.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceDao.java
@@ -12,9 +12,13 @@
public interface TrustedDeviceDao {
/** Get a {@link TrustedDeviceEntity} based on device id. */
- @Query("SELECT * FROM trusted_devices WHERE id LIKE :deviceId AND isValid = 1 LIMIT 1")
+ @Query("SELECT * FROM trusted_devices WHERE id LIKE :deviceId LIMIT 1")
TrustedDeviceEntity getTrustedDevice(String deviceId);
+ /** Get a {@link TrustedDeviceEntity} based on device id only if it is valid. */
+ @Query("SELECT * FROM trusted_devices WHERE id LIKE :deviceId AND isValid = 1 LIMIT 1")
+ TrustedDeviceEntity getTrustedDeviceIfValid(String deviceId);
+
/** Get a {@link FeatureStateEntity} based on device id. */
@Query("SELECT * FROM feature_state WHERE id = :deviceId")
FeatureStateEntity getFeatureState(String deviceId);
@@ -47,4 +51,19 @@
/** Remove any stored feature statue for a car with the given {@code deviceId}. */
@Query("DELETE FROM feature_state WHERE id = :deviceId")
void removeFeatureState(String deviceId);
+
+ /** Get a {@link TrustedDeviceTokenEntity} based on device id. */
+ @Query("SELECT * FROM trusted_device_tokens WHERE id = :deviceId LIMIT 1")
+ TrustedDeviceTokenEntity getTrustedDeviceHashedToken(String deviceId);
+
+ /**
+ * Add a {@link TrustedDeviceTokenEntity}. Replaces any previously stored hashed token with a
+ * matching device id.
+ */
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ void addOrReplaceTrustedDeviceHashedToken(TrustedDeviceTokenEntity hashedToken);
+
+ /** Remove a {@link TrustedDeviceTokenEntity} belonging with the given {@code deviceId}. */
+ @Query("DELETE FROM trusted_device_tokens WHERE id = :deviceId")
+ void removeTrustedDeviceHashedToken(String deviceId);
}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceDatabase.java b/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceDatabase.java
index f9bba20..fcb829e 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceDatabase.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceDatabase.java
@@ -5,8 +5,12 @@
/** Database for trusted device feature. */
@Database(
- entities = {TrustedDeviceEntity.class, FeatureStateEntity.class},
- version = 3,
+ entities = {
+ TrustedDeviceEntity.class,
+ FeatureStateEntity.class,
+ TrustedDeviceTokenEntity.class
+ },
+ version = 4,
exportSchema = true)
public abstract class TrustedDeviceDatabase extends RoomDatabase {
/** Return the DAO for the trusted device table. */
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceDatabaseProvider.java b/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceDatabaseProvider.java
index 7dda690..475ab3e 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceDatabaseProvider.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceDatabaseProvider.java
@@ -4,12 +4,13 @@
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.room.Room;
+import androidx.room.RoomDatabase;
import androidx.room.migration.Migration;
import androidx.sqlite.db.SupportSQLiteDatabase;
/** Provider of the common database within this library. */
public class TrustedDeviceDatabaseProvider {
- private static final String DATABASE_NAME = "trusted-device-database";
+ @VisibleForTesting static final String DATABASE_NAME = "trusted-device-database";
/**
* Performs the database migration from version 1 to 2.
@@ -41,25 +42,48 @@
}
};
+ /**
+ * Performs the database migration from 3 to 4.
+ *
+ * <p>This migration creates a new table for hashed tokens and invalidates any existing trusted
+ * device entities for removal on next startup.
+ */
+ @VisibleForTesting
+ static final Migration MIGRATION_3_4 =
+ new Migration(3, 4) {
+ @Override
+ public void migrate(SupportSQLiteDatabase database) {
+ database.execSQL(
+ "CREATE TABLE IF NOT EXISTS trusted_device_tokens "
+ + "(id TEXT NOT NULL, hashed_token TEXT NOT NULL, PRIMARY KEY(id))");
+ database.execSQL("UPDATE trusted_devices SET isValid = 0");
+ }
+ };
+
private static TrustedDeviceDatabaseProvider instance;
- private final TrustedDeviceDatabase database;
+ @VisibleForTesting final TrustedDeviceDatabase database;
- private TrustedDeviceDatabaseProvider(@NonNull Context context) {
- database =
+ @VisibleForTesting
+ TrustedDeviceDatabaseProvider(@NonNull Context context, boolean allowMainThreadQueries) {
+ RoomDatabase.Builder<TrustedDeviceDatabase> builder =
Room.databaseBuilder(context, TrustedDeviceDatabase.class, DATABASE_NAME)
- .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
- .enableMultiInstanceInvalidation()
- .fallbackToDestructiveMigration()
- .build();
+ .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)
+ .enableMultiInstanceInvalidation();
+ if (allowMainThreadQueries) {
+ builder.allowMainThreadQueries();
+ }
+ database = builder.build();
}
/** Returns an instance of the {@link TrustedDeviceDatabase} to run queries off of. */
@NonNull
public static synchronized TrustedDeviceDatabase get(@NonNull Context context) {
if (instance == null) {
- instance = new TrustedDeviceDatabaseProvider(
- context.getApplicationContext().createDeviceProtectedStorageContext());
+ instance =
+ new TrustedDeviceDatabaseProvider(
+ context.getApplicationContext().createDeviceProtectedStorageContext(),
+ /* allowMainThreadQueries= */ false);
}
return instance.database;
}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceTokenEntity.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceTokenEntity.kt
new file mode 100644
index 0000000..e700e05
--- /dev/null
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/trust/storage/TrustedDeviceTokenEntity.kt
@@ -0,0 +1,14 @@
+package com.google.android.connecteddevice.trust.storage
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+/** Table entity representing the hashed escrow token for a trusted device. */
+@Entity(tableName = "trusted_device_tokens")
+data class TrustedDeviceTokenEntity(
+ /** Device id of the trusted device. */
+ @PrimaryKey val id: String,
+ /** Base64 encoding of hashed token. */
+ @ColumnInfo(name = "hashed_token") val hashedToken: String
+)
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/ui/AssociatedDeviceDetails.java b/libs/connecteddevice/src/com/google/android/connecteddevice/ui/AssociatedDeviceDetails.java
new file mode 100644
index 0000000..bf41dfa
--- /dev/null
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/ui/AssociatedDeviceDetails.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+package com.google.android.connecteddevice.ui;
+
+import android.app.ActivityManager;
+import android.os.Parcel;
+import android.os.Parcelable;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.google.android.connecteddevice.model.AssociatedDevice;
+import java.util.Objects;
+
+/**
+ * Class that contains the details of an associated device.
+ *
+ * <p>Two {@link AssociatedDeviceDetails} are considered equal if their backing
+ * {@link AssociatedDevice} are equal. The connection state is not taken into account.
+ */
+public class AssociatedDeviceDetails implements Parcelable {
+
+ /** States of a device connection. */
+ public enum ConnectionState {
+ NOT_DETECTED,
+ DETECTED,
+ CONNECTED
+ }
+
+ private final AssociatedDevice device;
+
+ private final ConnectionState state;
+
+ public AssociatedDeviceDetails(@NonNull AssociatedDevice device, ConnectionState state) {
+ this.device = device;
+ this.state = state;
+ }
+
+ private AssociatedDeviceDetails(Parcel in) {
+ this(
+ in.readParcelable(AssociatedDevice.class.getClassLoader()),
+ ConnectionState.values()[in.readInt()]);
+ }
+
+ /** Get the device id. */
+ @NonNull
+ public String getDeviceId() {
+ return device.getDeviceId();
+ }
+
+ /** Get the name of the associated device. */
+ @Nullable
+ public String getDeviceName() {
+ return device.getDeviceName();
+ }
+
+ /** Get the device address. */
+ @NonNull
+ public String getDeviceAddress() {
+ return device.getDeviceAddress();
+ }
+
+ /** {@code true} if the connection is enabled for the device. */
+ public boolean isConnectionEnabled() {
+ return device.isConnectionEnabled();
+ }
+
+ /** Returns the current state of the device's connection. */
+ @NonNull
+ public ConnectionState getConnectionState() {
+ return state;
+ }
+
+ /** Returns the claiming user id, or {@value AssociatedDevice#UNCLAIMED_USER_ID} if unclaimed. */
+ public int getUserId() {
+ return device.getUserId();
+ }
+
+ /** Returns whether this device belongs to the current driver. */
+ public boolean belongsToDriver() {
+ return device.getUserId() == ActivityManager.getCurrentUser();
+ }
+
+ /** Get {@link AssociatedDevice}. */
+ @NonNull
+ public AssociatedDevice getAssociatedDevice() {
+ return device;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof AssociatedDeviceDetails)) {
+ return false;
+ }
+
+ // The connection state does not factor into equality.
+ AssociatedDeviceDetails details = (AssociatedDeviceDetails) obj;
+ return Objects.equals(device, details.device);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(device);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(device, /* flags= */ 0);
+ dest.writeInt(state.ordinal());
+ }
+
+ // Explicitly specifying the type within <> to support building with Java 8.
+ public static final Parcelable.Creator<AssociatedDeviceDetails> CREATOR =
+ new Parcelable.Creator<AssociatedDeviceDetails>() {
+ @Override
+ public AssociatedDeviceDetails createFromParcel(Parcel source) {
+ return new AssociatedDeviceDetails(source);
+ }
+
+ @Override
+ public AssociatedDeviceDetails[] newArray(int size) {
+ return new AssociatedDeviceDetails[size];
+ }
+ };
+}
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 171ba8c..4b09061 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/ui/AssociatedDeviceViewModel.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/ui/AssociatedDeviceViewModel.java
@@ -30,7 +30,6 @@
import android.content.IntentFilter;
import android.os.ParcelUuid;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
@@ -41,11 +40,13 @@
import com.google.android.connecteddevice.api.IAssociationCallback;
import com.google.android.connecteddevice.api.IOnAssociatedDevicesRetrievedListener;
import com.google.android.connecteddevice.model.AssociatedDevice;
-import com.google.android.connecteddevice.model.AssociatedDeviceDetails;
import com.google.android.connecteddevice.model.ConnectedDevice;
import com.google.android.connecteddevice.model.StartAssociationResponse;
+import com.google.android.connecteddevice.ui.AssociatedDeviceDetails.ConnectionState;
import java.time.Duration;
+import java.util.ArrayList;
import java.util.List;
+import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
/**
@@ -69,10 +70,9 @@
}
private final List<AssociatedDevice> associatedDevices = new CopyOnWriteArrayList<>();
- private final List<ConnectedDevice> connectedDevices = new CopyOnWriteArrayList<>();
- private final MutableLiveData<AssociatedDeviceDetails> currentDeviceDetails =
- new MutableLiveData<>(null);
+ private final MutableLiveData<List<AssociatedDeviceDetails>> associatedDevicesDetails =
+ new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<String> advertisedCarName = new MutableLiveData<>(null);
private final MutableLiveData<StartAssociationResponse> associationResponse =
new MutableLiveData<>(null);
@@ -84,6 +84,7 @@
private final MutableLiveData<AssociatedDevice> removedDevice = new MutableLiveData<>(null);
private final MutableLiveData<Boolean> isServiceConnected = new MutableLiveData<>(false);
private final boolean isSppEnabled;
+ private final boolean isPassengerEnabled;
private final String bleDeviceNamePrefix;
private final BluetoothAdapter bluetoothAdapter;
@@ -92,12 +93,18 @@
private ParcelUuid associationIdentifier;
public AssociatedDeviceViewModel(
- @NonNull Application application, boolean isSppEnabled, String bleDeviceNamePrefix) {
+ @NonNull Application application,
+ boolean isSppEnabled,
+ String bleDeviceNamePrefix,
+ boolean isPassengerEnabled
+ ) {
this(
application,
isSppEnabled,
bleDeviceNamePrefix,
- new CompanionConnector(application, /* isForegroundProcess= */ true));
+ isPassengerEnabled,
+ new CompanionConnector(
+ application, /* isForegroundProcess= */ true, /* userType= */ Connector.USER_TYPE_ALL));
}
@VisibleForTesting
@@ -106,19 +113,28 @@
@NonNull Application application,
boolean isSppEnabled,
String bleDeviceNamePrefix,
+ boolean isPassengerEnabled,
Connector connector) {
super(application);
this.isSppEnabled = isSppEnabled;
this.bleDeviceNamePrefix = bleDeviceNamePrefix;
this.connector = connector;
+ this.isPassengerEnabled = isPassengerEnabled;
+
connector.setCallback(connectorCallback);
+
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
getApplication().registerReceiver(receiver, filter);
bluetoothAdapter =
application.getApplicationContext().getSystemService(BluetoothManager.class).getAdapter();
bluetoothState.postValue(BluetoothAdapter.STATE_ON);
- connector.connect();
+
+ // Registers for device callbacks
+ this.connector.setFeatureId(new ParcelUuid(UUID.randomUUID()));
+
+ this.connector.setCallback(connectorCallback);
+ this.connector.connect();
}
@Override
@@ -153,21 +169,13 @@
startAssociationInternal();
}
- /** Removes the current associated device. */
- public void removeCurrentDevice() {
- AssociatedDevice device = getAssociatedDevice();
- if (device == null) {
- return;
- }
+ /** Removes the association of the given device. */
+ public void removeDevice(@NonNull AssociatedDevice device) {
connector.removeAssociatedDevice(device.getDeviceId());
}
- /** Toggles connection on the current associated device. */
- public void toggleConnectionStatusForCurrentDevice() {
- AssociatedDevice device = getAssociatedDevice();
- if (device == null) {
- return;
- }
+ /** Toggles connection of the given associated device. */
+ public void toggleConnectionStatusForDevice(@NonNull AssociatedDevice device) {
if (device.isConnectionEnabled()) {
connector.disableAssociatedDeviceConnection(device.getDeviceId());
} else {
@@ -175,17 +183,29 @@
}
}
- /** Gets the associated device details. */
- public LiveData<AssociatedDeviceDetails> getCurrentDeviceDetails() {
- return currentDeviceDetails;
+ /** Mark the given device as belonging to the active driver. */
+ public void claimDevice(@NonNull AssociatedDevice device) {
+ connector.claimAssociatedDevice(device.getDeviceId());
}
- /** Starts feature activity for the current associated device. */
- public void startFeatureActivityForCurrentDevice(@NonNull String action) {
- AssociatedDevice device = getAssociatedDevice();
- if (device == null || action == null) {
- return;
- }
+ /** Mark the given device as unclaimed by any user. */
+ public void removeClaimOnDevice(@NonNull AssociatedDevice device) {
+ connector.removeAssociatedDeviceClaim(device.getDeviceId());
+ }
+
+ /**
+ * Gets a list of details for all associated device.
+ *
+ * <p>The list will always be non-{@code null}, and will be empty if there are no associated
+ * devices.
+ */
+ public LiveData<List<AssociatedDeviceDetails>> getAssociatedDevicesDetails() {
+ return associatedDevicesDetails;
+ }
+
+ /** Starts feature activity for the given associated device. */
+ public void startFeatureActivityForDevice(
+ @NonNull String action, @NonNull AssociatedDevice device) {
Intent intent = new Intent(action);
intent.putExtra(ASSOCIATED_DEVICE_DATA_NAME_EXTRA, device);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
@@ -213,7 +233,7 @@
return pairingCode;
}
- /** Value is {@code true} if the current associated device has been removed. */
+ /** Returns the value of a device whose association has been removed. */
public LiveData<AssociatedDevice> getRemovedDevice() {
return removedDevice;
}
@@ -270,29 +290,26 @@
}
private void updateDeviceDetails() {
- AssociatedDevice device = getAssociatedDevice();
- if (device == null) {
- return;
+ List<AssociatedDeviceDetails> associatedDevicesDetails = new ArrayList<>();
+ for (AssociatedDevice device : associatedDevices) {
+ associatedDevicesDetails.add(new AssociatedDeviceDetails(device, getConnectionState(device)));
}
- currentDeviceDetails.postValue(
- new AssociatedDeviceDetails(getAssociatedDevice(), isConnected()));
+ this.associatedDevicesDetails.postValue(associatedDevicesDetails);
}
- @Nullable
- private AssociatedDevice getAssociatedDevice() {
- if (associatedDevices.isEmpty()) {
- return null;
+ private ConnectionState getConnectionState(@NonNull AssociatedDevice device) {
+ logd(TAG, "Getting connection state for device " + device.getDeviceId() + ".");
+ ConnectedDevice connectedDevice = connector.getConnectedDeviceById(device.getDeviceId());
+ if (connectedDevice == null) {
+ logd(TAG, "Device is not detected.");
+ return ConnectionState.NOT_DETECTED;
}
- return associatedDevices.get(0);
- }
-
- private boolean isConnected() {
- if (associatedDevices.isEmpty() || connectedDevices.isEmpty()) {
- return false;
+ if (connectedDevice.hasSecureChannel()) {
+ logd(TAG, "Device is connected.");
+ return ConnectionState.CONNECTED;
}
- String associatedDeviceId = associatedDevices.get(0).getDeviceId();
- String connectedDeviceId = connectedDevices.get(0).getDeviceId();
- return associatedDeviceId.equals(connectedDeviceId);
+ logd(TAG, "Device is detected.");
+ return ConnectionState.DETECTED;
}
private void setAssociatedDevices(@NonNull List<AssociatedDevice> associatedDevices) {
@@ -301,12 +318,6 @@
updateDeviceDetails();
}
- private void setConnectedDevices(@NonNull List<ConnectedDevice> connectedDevices) {
- this.connectedDevices.clear();
- this.connectedDevices.addAll(connectedDevices);
- updateDeviceDetails();
- }
-
private void addOrUpdateAssociatedDevice(@NonNull AssociatedDevice device) {
associatedDevices.remove(device);
associatedDevices.add(device);
@@ -316,7 +327,7 @@
private void removeAssociatedDevice(AssociatedDevice device) {
if (associatedDevices.remove(device)) {
removedDevice.postValue(device);
- currentDeviceDetails.postValue(null);
+ updateDeviceDetails();
}
}
@@ -326,8 +337,14 @@
public void onConnected() {
logd(TAG, "Connected to platform.");
isServiceConnected.postValue(true);
- setConnectedDevices(connector.getConnectedDevices());
- connector.retrieveAssociatedDevicesForDriver(associatedDevicesRetrievedListener);
+
+ if (isPassengerEnabled) {
+ connector.retrieveAssociatedDevices(associatedDevicesRetrievedListener);
+ } else {
+ // If passenger is disabled, then there should only be one device and that will belong
+ // to the driver.
+ connector.retrieveAssociatedDevicesForDriver(associatedDevicesRetrievedListener);
+ }
}
@Override
@@ -355,13 +372,19 @@
@Override
public void onDeviceConnected(ConnectedDevice connectedDevice) {
- connectedDevices.add(connectedDevice);
+ logd(TAG, "Device " + connectedDevice.getDeviceId() + " has connected.");
updateDeviceDetails();
}
@Override
public void onDeviceDisconnected(ConnectedDevice connectedDevice) {
- connectedDevices.remove(connectedDevice);
+ logd(TAG, "Device " + connectedDevice.getDeviceId() + " has disconnected.");
+ updateDeviceDetails();
+ }
+
+ @Override
+ public void onSecureChannelEstablished(@NonNull ConnectedDevice device) {
+ logd(TAG, "Device " + device.getDeviceId() + " has established a secure channel.");
updateDeviceDetails();
}
};
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/ui/AssociatedDeviceViewModelFactory.java b/libs/connecteddevice/src/com/google/android/connecteddevice/ui/AssociatedDeviceViewModelFactory.java
index 2d9c1aa..fae26e7 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/ui/AssociatedDeviceViewModelFactory.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/ui/AssociatedDeviceViewModelFactory.java
@@ -26,18 +26,24 @@
private final Application application;
private final boolean isSppEnabled;
private final String bleDeviceNamePrefix;
+ private final boolean isPassengerEnabled;
public AssociatedDeviceViewModelFactory(
- Application application, boolean isSppEnabled, String bleDeviceNamePrefix) {
+ Application application,
+ boolean isSppEnabled,
+ String bleDeviceNamePrefix,
+ boolean isPassengerEnabled) {
this.application = application;
this.isSppEnabled = isSppEnabled;
this.bleDeviceNamePrefix = bleDeviceNamePrefix;
+ this.isPassengerEnabled = isPassengerEnabled;
}
@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> aClass) {
return aClass.cast(
- new AssociatedDeviceViewModel(application, isSppEnabled, bleDeviceNamePrefix));
+ new AssociatedDeviceViewModel(
+ application, isSppEnabled, bleDeviceNamePrefix, isPassengerEnabled));
}
}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/util/ByteUtils.java b/libs/connecteddevice/src/com/google/android/connecteddevice/util/ByteUtils.java
index 8948d9b..d51b036 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/util/ByteUtils.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/util/ByteUtils.java
@@ -22,6 +22,7 @@
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import java.security.InvalidParameterException;
import java.security.SecureRandom;
import java.util.UUID;
@@ -82,6 +83,10 @@
*/
public static byte[] hexStringToByteArray(String hex) {
int len = hex.length();
+ if (len % 2 != 0) {
+ throw new InvalidParameterException(
+ "Hex string must have a length that is evenly divisible by two.");
+ }
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] =
diff --git a/libs/connecteddevice/tests/unit/schemas/com.google.android.connecteddevice.trust.storage.TrustedDeviceDatabase/4.json b/libs/connecteddevice/tests/unit/schemas/com.google.android.connecteddevice.trust.storage.TrustedDeviceDatabase/4.json
new file mode 100644
index 0000000..bbe0bd5
--- /dev/null
+++ b/libs/connecteddevice/tests/unit/schemas/com.google.android.connecteddevice.trust.storage.TrustedDeviceDatabase/4.json
@@ -0,0 +1,104 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 4,
+ "identityHash": "89264c80333c0d361cd296170f2b814e",
+ "entities": [
+ {
+ "tableName": "trusted_devices",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `userId` INTEGER NOT NULL, `handle` INTEGER NOT NULL, `isValid` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "handle",
+ "columnName": "handle",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isValid",
+ "columnName": "isValid",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "feature_state",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `state` BLOB NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "BLOB",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "trusted_device_tokens",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `hashed_token` TEXT NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hashedToken",
+ "columnName": "hashed_token",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '89264c80333c0d361cd296170f2b814e')"
+ ]
+ }
+}
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
deleted file mode 100644
index 873e42c..0000000
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/ConnectedDeviceManagerTest.java
+++ /dev/null
@@ -1,659 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
-import static org.junit.Assert.assertThrows;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import androidx.annotation.NonNull;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.google.android.connecteddevice.ConnectedDeviceManager.ConnectionCallback;
-import com.google.android.connecteddevice.ConnectedDeviceManager.DeviceAssociationCallback;
-import com.google.android.connecteddevice.ConnectedDeviceManager.DeviceCallback;
-import com.google.android.connecteddevice.ConnectedDeviceManager.MessageDeliveryDelegate;
-import com.google.android.connecteddevice.connection.CarBluetoothManager;
-import com.google.android.connecteddevice.model.AssociatedDevice;
-import com.google.android.connecteddevice.model.ConnectedDevice;
-import com.google.android.connecteddevice.model.DeviceMessage;
-import com.google.android.connecteddevice.model.DeviceMessage.OperationType;
-import com.google.android.connecteddevice.model.Errors;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage.AssociatedDeviceCallback;
-import com.google.android.connecteddevice.util.ByteUtils;
-import com.google.common.collect.ImmutableList;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.UUID;
-import java.util.concurrent.Executor;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@RunWith(AndroidJUnit4.class)
-public class ConnectedDeviceManagerTest {
-
- private static final String TEST_DEVICE_ADDRESS = "00:11:22:33:44:55";
-
- private static final String TEST_DEVICE_NAME = "TEST_DEVICE_NAME";
-
- private final Executor directExecutor = directExecutor();
-
- private final UUID recipientId = UUID.randomUUID();
-
- private final List<String> userDeviceIds = new ArrayList<>();
-
- private final List<AssociatedDevice> userDevices = new ArrayList<>();
-
- @Mock private ConnectedDeviceStorage mockStorage;
-
- @Mock private CarBluetoothManager mockCarBluetoothManager;
- @Mock private ConnectionCallback mockConnectionCallback;
- @Mock private DeviceCallback mockDeviceCallback;
- @Mock private DeviceAssociationCallback mockDeviceAssociationCallback;
-
- private ConnectedDeviceManager connectedDeviceManager;
-
- private AssociatedDeviceCallback associatedDeviceCallback;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- ArgumentCaptor<AssociatedDeviceCallback> callbackCaptor =
- ArgumentCaptor.forClass(AssociatedDeviceCallback.class);
- connectedDeviceManager =
- new ConnectedDeviceManager(
- mockCarBluetoothManager,
- mockStorage,
- directExecutor,
- directExecutor,
- directExecutor);
- verify(mockStorage).registerAssociatedDeviceCallback(callbackCaptor.capture());
- when(mockStorage.getDriverAssociatedDevices()).thenReturn(userDevices);
- when(mockStorage.getDriverAssociatedDeviceIds()).thenReturn(userDeviceIds);
- associatedDeviceCallback = callbackCaptor.getValue();
- connectedDeviceManager.start();
- }
-
- @Test
- public void getActiveUserConnectedDevices_initiallyShouldReturnEmptyList() {
- assertThat(connectedDeviceManager.getActiveUserConnectedDevices()).isEmpty();
- }
-
- @Test
- public void getActiveUserConnectedDevices_includesNewlyConnectedDevice() {
- String deviceId = connectNewDevice();
- List<ConnectedDevice> activeUserDevices =
- connectedDeviceManager.getActiveUserConnectedDevices();
- ConnectedDevice expectedDevice =
- new ConnectedDevice(
- deviceId,
- /* deviceName= */ null,
- /* belongsToActiveUser= */ true,
- /* hasSecureChannel= */ false);
- assertThat(activeUserDevices).containsExactly(expectedDevice);
- }
-
- @Test
- public void getActiveUserConnectedDevices_excludesDevicesNotBelongingToActiveUser() {
- String deviceId = UUID.randomUUID().toString();
- String otherUserDeviceId = UUID.randomUUID().toString();
- when(mockStorage.getDriverAssociatedDeviceIds())
- .thenReturn(ImmutableList.of(otherUserDeviceId));
- connectedDeviceManager.addConnectedDevice(deviceId);
- assertThat(connectedDeviceManager.getActiveUserConnectedDevices()).isEmpty();
- }
-
- @Test
- public void getActiveUserConnectedDevices_reflectsSecureChannelEstablished() {
- String deviceId = connectNewDevice();
- connectedDeviceManager.onSecureChannelEstablished(deviceId);
- ConnectedDevice connectedDevice = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
- assertThat(connectedDevice.hasSecureChannel()).isTrue();
- }
-
- @Test
- public void getActiveUserConnectedDevices_excludesDisconnectedDevice() {
- String deviceId = connectNewDevice();
- connectedDeviceManager.removeConnectedDevice(deviceId);
- assertThat(connectedDeviceManager.getActiveUserConnectedDevices()).isEmpty();
- }
-
- @Test
- public void sendMessageSecurely_throwsIllegalStateExceptionIfNoSecureChannel() {
- connectNewDevice();
- ConnectedDevice device = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
- UUID recipientId = UUID.randomUUID();
- byte[] message = ByteUtils.randomBytes(10);
- DeviceMessage deviceMessage =
- DeviceMessage.createOutgoingMessage(
- recipientId, /* isMessageEncrypted= */ true, OperationType.CLIENT_MESSAGE, message);
- assertThrows(
- IllegalStateException.class,
- () -> connectedDeviceManager.sendMessage(device, deviceMessage));
- }
-
- @Test
- public void sendMessageSecurely_sendsEncryptedMessage() {
- String deviceId = connectNewDevice();
- connectedDeviceManager.onSecureChannelEstablished(deviceId);
- ConnectedDevice device = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
- UUID recipientId = UUID.randomUUID();
- byte[] message = ByteUtils.randomBytes(10);
- DeviceMessage deviceMessage =
- DeviceMessage.createOutgoingMessage(
- recipientId, /* isMessageEncrypted= */ true, OperationType.CLIENT_MESSAGE, message);
- connectedDeviceManager.sendMessage(device, deviceMessage);
- ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(DeviceMessage.class);
- verify(mockCarBluetoothManager).sendMessage(eq(deviceId), messageCaptor.capture());
- assertThat(messageCaptor.getValue().isMessageEncrypted()).isTrue();
- }
-
- @Test
- public void sendMessageSecurely_doesNotSendIfDeviceDisconnected() {
- String deviceId = connectNewDevice();
- ConnectedDevice device = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
- connectedDeviceManager.removeConnectedDevice(deviceId);
- UUID recipientId = UUID.randomUUID();
- byte[] message = ByteUtils.randomBytes(10);
- DeviceMessage deviceMessage =
- DeviceMessage.createOutgoingMessage(
- recipientId, /* isMessageEncrypted= */ true, OperationType.CLIENT_MESSAGE, message);
- connectedDeviceManager.sendMessage(device, deviceMessage);
- verify(mockCarBluetoothManager, never()).sendMessage(eq(deviceId), any(DeviceMessage.class));
- }
-
- @Test
- public void sendMessage_sendsMessageWithoutEncryption() {
- String deviceId = connectNewDevice();
- ConnectedDevice device = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
- UUID recipientId = UUID.randomUUID();
- byte[] message = ByteUtils.randomBytes(10);
- DeviceMessage deviceMessage =
- DeviceMessage.createOutgoingMessage(
- recipientId, /* isMessageEncrypted= */ false, OperationType.CLIENT_MESSAGE, message);
- connectedDeviceManager.sendMessage(device, deviceMessage);
- ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(DeviceMessage.class);
- verify(mockCarBluetoothManager).sendMessage(eq(deviceId), messageCaptor.capture());
- assertThat(messageCaptor.getValue().isMessageEncrypted()).isFalse();
- }
-
- @Test
- public void connectionCallback_onDeviceConnectedInvokedForNewlyConnectedDevice()
- throws InterruptedException {
- connectedDeviceManager.registerActiveUserConnectionCallback(
- mockConnectionCallback, directExecutor);
- String deviceId = connectNewDevice();
- ArgumentCaptor<ConnectedDevice> deviceCaptor = ArgumentCaptor.forClass(ConnectedDevice.class);
- verify(mockConnectionCallback).onDeviceConnected(deviceCaptor.capture());
- ConnectedDevice connectedDevice = deviceCaptor.getValue();
- assertThat(connectedDevice.getDeviceId()).isEqualTo(deviceId);
- assertThat(connectedDevice.hasSecureChannel()).isFalse();
- }
-
- @Test
- public void connectionCallback_onDeviceConnectedNotInvokedDeviceConnectedForDifferentUser()
- throws InterruptedException {
- connectedDeviceManager.registerActiveUserConnectionCallback(
- mockConnectionCallback, directExecutor);
- String deviceId = UUID.randomUUID().toString();
- String otherUserDeviceId = UUID.randomUUID().toString();
- when(mockStorage.getDriverAssociatedDeviceIds())
- .thenReturn(ImmutableList.of(otherUserDeviceId));
- connectedDeviceManager.addConnectedDevice(deviceId);
- }
-
- @Test
- public void connectionCallback_onDeviceConnectedNotInvokedForDifferentBleManager()
- throws InterruptedException {
- String deviceId = connectNewDevice();
- connectedDeviceManager.registerActiveUserConnectionCallback(
- mockConnectionCallback, directExecutor);
- connectedDeviceManager.addConnectedDevice(deviceId);
- }
-
- @Test
- public void connectionCallback_onDeviceDisconnectedInvokedForActiveUserDevice()
- throws InterruptedException {
- String deviceId = connectNewDevice();
- connectedDeviceManager.registerActiveUserConnectionCallback(
- mockConnectionCallback, directExecutor);
- connectedDeviceManager.removeConnectedDevice(deviceId);
- ArgumentCaptor<ConnectedDevice> deviceCaptor = ArgumentCaptor.forClass(ConnectedDevice.class);
- verify(mockConnectionCallback).onDeviceDisconnected(deviceCaptor.capture());
- assertThat(deviceCaptor.getValue().getDeviceId()).isEqualTo(deviceId);
- }
-
- @Test
- public void connectionCallback_onDeviceDisconnectedNotInvokedDeviceForDifferentUser()
- throws InterruptedException {
- String deviceId = UUID.randomUUID().toString();
- connectedDeviceManager.addConnectedDevice(deviceId);
- connectedDeviceManager.registerActiveUserConnectionCallback(
- mockConnectionCallback, directExecutor);
- connectedDeviceManager.removeConnectedDevice(deviceId);
- }
-
- @Test
- public void unregisterConnectionCallback_removesCallbackAndNotInvoked()
- throws InterruptedException {
- connectedDeviceManager.registerActiveUserConnectionCallback(
- mockConnectionCallback, directExecutor);
- connectedDeviceManager.unregisterConnectionCallback(mockConnectionCallback);
- connectNewDevice();
- }
-
- @Test
- public void registerDeviceCallback_denyListsDuplicateRecipientId() throws InterruptedException {
- connectNewDevice();
- ConnectedDevice connectedDevice = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
- DeviceCallback firstDeviceCallback = mock(DeviceCallback.class);
- DeviceCallback secondDeviceCallback = mock(DeviceCallback.class);
- DeviceCallback thirdDeviceCallback = mock(DeviceCallback.class);
-
- // Register three times for following chain of events:
- // 1. First callback registered without issue.
- // 2. Second callback with same recipientId triggers deny listing both callbacks and issues
- // error callbacks on both. Both callbacks should be unregistered at this point.
- // 3. Third callback gets rejected at registration and issues error callback.
-
- connectedDeviceManager.registerDeviceCallback(
- connectedDevice, recipientId, firstDeviceCallback, directExecutor);
- connectedDeviceManager.registerDeviceCallback(
- connectedDevice, recipientId, secondDeviceCallback, directExecutor);
- DeviceMessage message =
- DeviceMessage.createOutgoingMessage(
- recipientId,
- /* isMessageEncrypted= */ false,
- OperationType.CLIENT_MESSAGE,
- ByteUtils.randomBytes(10));
- connectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
- verify(firstDeviceCallback)
- .onDeviceError(connectedDevice, Errors.DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED);
- verify(secondDeviceCallback)
- .onDeviceError(connectedDevice, Errors.DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED);
- verify(firstDeviceCallback, never()).onMessageReceived(any(), any());
- verify(secondDeviceCallback, never()).onMessageReceived(any(), any());
-
- connectedDeviceManager.registerDeviceCallback(
- connectedDevice, recipientId, thirdDeviceCallback, directExecutor);
- verify(thirdDeviceCallback)
- .onDeviceError(connectedDevice, Errors.DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED);
- }
-
- @Test
- public void deviceCallback_onSecureChannelEstablishedInvoked() throws InterruptedException {
- connectNewDevice();
- ConnectedDevice connectedDevice = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
- connectedDeviceManager.registerDeviceCallback(
- connectedDevice, recipientId, mockDeviceCallback, directExecutor);
- connectedDeviceManager.onSecureChannelEstablished(connectedDevice.getDeviceId());
- connectedDevice = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
- verify(mockDeviceCallback).onSecureChannelEstablished(connectedDevice);
- }
-
- @Test
- public void deviceCallback_onMessageReceivedInvokedForSameRecipientId()
- throws InterruptedException {
- connectNewDevice();
- ConnectedDevice connectedDevice = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
- connectedDeviceManager.registerDeviceCallback(
- connectedDevice, recipientId, mockDeviceCallback, directExecutor);
- byte[] payload = ByteUtils.randomBytes(10);
- DeviceMessage message =
- DeviceMessage.createOutgoingMessage(
- recipientId, /* isMessageEncrypted= */ false, OperationType.CLIENT_MESSAGE, payload);
- connectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
- verify(mockDeviceCallback).onMessageReceived(connectedDevice, message);
- }
-
- @Test
- public void deviceCallback_onMessageReceivedNotInvokedForDifferentRecipientId()
- throws InterruptedException {
- connectNewDevice();
- ConnectedDevice connectedDevice = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
- connectedDeviceManager.registerDeviceCallback(
- connectedDevice, recipientId, mockDeviceCallback, directExecutor);
- byte[] payload = ByteUtils.randomBytes(10);
- DeviceMessage message =
- DeviceMessage.createOutgoingMessage(
- UUID.randomUUID(),
- /* isMessageEncrypted= */ false,
- OperationType.CLIENT_MESSAGE,
- payload);
- connectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
- }
-
- @Test
- public void deviceCallback_onDeviceErrorInvokedOnChannelError() throws InterruptedException {
- connectNewDevice();
- ConnectedDevice connectedDevice = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
- connectedDeviceManager.registerDeviceCallback(
- connectedDevice, recipientId, mockDeviceCallback, directExecutor);
- connectedDeviceManager.deviceErrorOccurred(connectedDevice.getDeviceId());
- verify(mockDeviceCallback)
- .onDeviceError(connectedDevice, Errors.DEVICE_ERROR_INVALID_SECURITY_KEY);
- }
-
- @Test
- public void unregisterDeviceCallback_removesCallbackAndNotInvoked() throws InterruptedException {
- connectNewDevice();
- ConnectedDevice connectedDevice = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
- connectedDeviceManager.registerDeviceCallback(
- connectedDevice, recipientId, mockDeviceCallback, directExecutor);
- connectedDeviceManager.unregisterDeviceCallback(
- connectedDevice, recipientId, mockDeviceCallback);
- connectedDeviceManager.onSecureChannelEstablished(connectedDevice.getDeviceId());
- }
-
- @Test
- public void registerDeviceCallback_sendsMissedMessageAfterRegistration()
- throws InterruptedException {
- connectNewDevice();
- ConnectedDevice connectedDevice = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
- byte[] payload = ByteUtils.randomBytes(10);
- DeviceMessage message =
- DeviceMessage.createOutgoingMessage(
- recipientId, /* isMessageEncrypted= */ false, OperationType.CLIENT_MESSAGE, payload);
- connectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
- connectedDeviceManager.registerDeviceCallback(
- connectedDevice, recipientId, mockDeviceCallback, directExecutor);
- verify(mockDeviceCallback).onMessageReceived(connectedDevice, message);
- }
-
- @Test
- public void registerDeviceCallback_sendsMultipleMissedMessagesAfterRegistration()
- throws InterruptedException {
- connectNewDevice();
- ConnectedDevice connectedDevice = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
- byte[] payload1 = ByteUtils.randomBytes(10);
- byte[] payload2 = ByteUtils.randomBytes(10);
- DeviceMessage message1 =
- DeviceMessage.createOutgoingMessage(
- recipientId, /* isMessageEncrypted= */ false, OperationType.CLIENT_MESSAGE, payload1);
- DeviceMessage message2 =
- DeviceMessage.createOutgoingMessage(
- recipientId, /* isMessageEncrypted= */ false, OperationType.CLIENT_MESSAGE, payload2);
- connectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message1);
- connectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message2);
- connectedDeviceManager.registerDeviceCallback(
- connectedDevice, recipientId, mockDeviceCallback, directExecutor);
- verify(mockDeviceCallback).onMessageReceived(connectedDevice, message1);
- verify(mockDeviceCallback).onMessageReceived(connectedDevice, message2);
- }
-
- @Test
- public void registerDeviceCallback_doesNotSendMissedMessageForDifferentRecipient()
- throws InterruptedException {
- connectNewDevice();
- ConnectedDevice connectedDevice = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
- byte[] payload = ByteUtils.randomBytes(10);
- DeviceMessage message =
- DeviceMessage.createOutgoingMessage(
- UUID.randomUUID(),
- /* isMessageEncrypted= */ false,
- OperationType.CLIENT_MESSAGE,
- payload);
- connectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
- connectedDeviceManager.registerDeviceCallback(
- connectedDevice, recipientId, mockDeviceCallback, directExecutor);
- }
-
- @Test
- public void registerDeviceCallback_doesNotSendMissedMessageForDifferentDevice()
- throws InterruptedException {
- connectNewDevice();
- connectNewDevice();
- List<ConnectedDevice> connectedDevices = connectedDeviceManager.getActiveUserConnectedDevices();
- ConnectedDevice connectedDevice = connectedDevices.get(0);
- ConnectedDevice otherDevice = connectedDevices.get(1);
- byte[] payload = ByteUtils.randomBytes(10);
- DeviceMessage message =
- DeviceMessage.createOutgoingMessage(
- recipientId, /* isMessageEncrypted= */ false, OperationType.CLIENT_MESSAGE, payload);
- connectedDeviceManager.onMessageReceived(otherDevice.getDeviceId(), message);
- connectedDeviceManager.registerDeviceCallback(
- connectedDevice, recipientId, mockDeviceCallback, directExecutor);
- }
-
- @Test
- public void onAssociationCompleted_disconnectsOriginalDeviceAndReconnectsAsActiveUser()
- throws InterruptedException {
- String deviceId = UUID.randomUUID().toString();
- connectedDeviceManager.addConnectedDevice(deviceId);
- connectedDeviceManager.registerActiveUserConnectionCallback(
- mockConnectionCallback, directExecutor);
- when(mockStorage.getDriverAssociatedDeviceIds()).thenReturn(ImmutableList.of(deviceId));
- connectedDeviceManager.onAssociationCompleted(deviceId);
- }
-
- @Test
- public void deviceAssociationCallback_onAssociatedDeviceAdded() throws InterruptedException {
- connectedDeviceManager.registerDeviceAssociationCallback(
- mockDeviceAssociationCallback, directExecutor);
- String deviceId = UUID.randomUUID().toString();
- AssociatedDevice testDevice =
- new AssociatedDevice(
- deviceId, TEST_DEVICE_ADDRESS, TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
- associatedDeviceCallback.onAssociatedDeviceAdded(testDevice);
- verify(mockDeviceAssociationCallback).onAssociatedDeviceAdded(eq(testDevice));
- }
-
- @Test
- public void deviceAssociationCallback_onAssociationDeviceRemoved() throws InterruptedException {
- connectedDeviceManager.registerDeviceAssociationCallback(
- mockDeviceAssociationCallback, directExecutor);
- String deviceId = UUID.randomUUID().toString();
- AssociatedDevice testDevice =
- new AssociatedDevice(
- deviceId, TEST_DEVICE_ADDRESS, TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
- associatedDeviceCallback.onAssociatedDeviceRemoved(testDevice);
- verify(mockDeviceAssociationCallback).onAssociatedDeviceRemoved(eq(testDevice));
- }
-
- @Test
- public void deviceAssociationCallback_onAssociatedDeviceUpdated() throws InterruptedException {
- connectedDeviceManager.registerDeviceAssociationCallback(
- mockDeviceAssociationCallback, directExecutor);
- String deviceId = UUID.randomUUID().toString();
- AssociatedDevice testDevice =
- new AssociatedDevice(
- deviceId, TEST_DEVICE_ADDRESS, TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
- associatedDeviceCallback.onAssociatedDeviceUpdated(testDevice);
- verify(mockDeviceAssociationCallback).onAssociatedDeviceUpdated(eq(testDevice));
- }
-
- @Test
- public void removeConnectedDevice_startsAdvertisingForActiveUserDeviceOnActiveUserDisconnect() {
- String deviceId = UUID.randomUUID().toString();
- when(mockStorage.getDriverAssociatedDeviceIds()).thenReturn(ImmutableList.of(deviceId));
- AssociatedDevice device =
- new AssociatedDevice(
- deviceId, TEST_DEVICE_ADDRESS, TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
- when(mockStorage.getDriverAssociatedDevices()).thenReturn(ImmutableList.of(device));
- connectedDeviceManager.addConnectedDevice(deviceId);
- connectedDeviceManager.removeConnectedDevice(deviceId);
- verify(mockCarBluetoothManager).connectToDevice(eq(UUID.fromString(deviceId)));
- }
-
- @Test
- public void removeConnectedDevice_startsAdvertisingForActiveUserDeviceOnLastDeviceDisconnect() {
- String deviceId = UUID.randomUUID().toString();
- String userDeviceId = UUID.randomUUID().toString();
- when(mockStorage.getDriverAssociatedDeviceIds()).thenReturn(ImmutableList.of(userDeviceId));
- AssociatedDevice userDevice =
- new AssociatedDevice(
- userDeviceId, TEST_DEVICE_ADDRESS, TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
- when(mockStorage.getDriverAssociatedDevices()).thenReturn(ImmutableList.of(userDevice));
- connectedDeviceManager.addConnectedDevice(deviceId);
- connectedDeviceManager.removeConnectedDevice(deviceId);
- verify(mockCarBluetoothManager).connectToDevice(eq(UUID.fromString(userDeviceId)));
- }
-
- @Test
- public void removeConnectedDevice_doesNotAdvertiseForNonActiveUserDeviceNotLastDevice() {
- String deviceId = UUID.randomUUID().toString();
- String userDeviceId = UUID.randomUUID().toString();
- when(mockStorage.getDriverAssociatedDeviceIds()).thenReturn(ImmutableList.of(userDeviceId));
- AssociatedDevice userDevice =
- new AssociatedDevice(
- userDeviceId, TEST_DEVICE_ADDRESS, TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
- when(mockStorage.getDriverAssociatedDevices()).thenReturn(ImmutableList.of(userDevice));
- connectedDeviceManager.addConnectedDevice(deviceId);
- connectedDeviceManager.addConnectedDevice(userDeviceId);
- connectedDeviceManager.removeConnectedDevice(deviceId);
- verify(mockCarBluetoothManager, never()).connectToDevice(any());
- }
-
- @Test
- public void removeConnectedDevice_startsAdvertisingForActiveUserDeviceWithNullDevice() {
- String deviceId = UUID.randomUUID().toString();
- when(mockStorage.getDriverAssociatedDeviceIds()).thenReturn(ImmutableList.of(deviceId));
- AssociatedDevice device =
- new AssociatedDevice(
- deviceId, TEST_DEVICE_ADDRESS, TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
- when(mockStorage.getDriverAssociatedDevices()).thenReturn(ImmutableList.of(device));
- connectedDeviceManager.removeConnectedDevice(deviceId);
- verify(mockCarBluetoothManager).connectToDevice(eq(UUID.fromString(deviceId)));
- }
-
- @Test
- public void removeActiveUserAssociatedDevice_deletesAssociatedDeviceFromStorage() {
- String deviceId = UUID.randomUUID().toString();
- connectedDeviceManager.removeActiveUserAssociatedDevice(deviceId);
- verify(mockStorage).removeAssociatedDevice(deviceId);
- }
-
- @Test
- public void removeActiveUserAssociatedDevice_disconnectsIfConnected() {
- String deviceId = connectNewDevice();
- connectedDeviceManager.removeActiveUserAssociatedDevice(deviceId);
- verify(mockCarBluetoothManager).disconnectDevice(deviceId);
- }
-
- @Test
- public void enableAssociatedDeviceConnection_enableDeviceConnectionInStorage() {
- String deviceId = UUID.randomUUID().toString();
- connectedDeviceManager.enableAssociatedDeviceConnection(deviceId);
- verify(mockStorage).updateAssociatedDeviceConnectionEnabled(deviceId, true);
- }
-
- @Test
- public void enableAssociatedDeviceConnection_startsDiscoveryAfterBeingDisabled() {
- UUID deviceId = UUID.randomUUID();
- when(mockStorage.getDriverAssociatedDeviceIds())
- .thenReturn(ImmutableList.of(deviceId.toString()));
- AssociatedDevice device =
- new AssociatedDevice(
- deviceId.toString(),
- TEST_DEVICE_ADDRESS,
- TEST_DEVICE_NAME,
- /* isConnectionEnabled= */ true);
- when(mockStorage.getDriverAssociatedDevices()).thenReturn(ImmutableList.of(device));
- connectedDeviceManager.connectToActiveUserDevice();
- connectedDeviceManager.disableAssociatedDeviceConnection(deviceId.toString());
- connectedDeviceManager.enableAssociatedDeviceConnection(deviceId.toString());
- verify(mockCarBluetoothManager, times(2)).connectToDevice(deviceId);
- }
-
- @Test
- public void disableAssociatedDeviceConnection_disableDeviceConnectionInStorage() {
- String deviceId = UUID.randomUUID().toString();
- connectedDeviceManager.disableAssociatedDeviceConnection(deviceId);
- verify(mockStorage).updateAssociatedDeviceConnectionEnabled(deviceId, false);
- }
-
- @Test
- public void disableAssociatedDeviceConnection_disconnectsIfConnected() {
- String deviceId = connectNewDevice();
- connectedDeviceManager.disableAssociatedDeviceConnection(deviceId);
- verify(mockCarBluetoothManager).disconnectDevice(deviceId);
- }
-
- @Test
- public void onMessageReceived_deliversMessageIfDelegateIsNull() throws InterruptedException {
- connectNewDevice();
- ConnectedDevice connectedDevice = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
- connectedDeviceManager.registerDeviceCallback(
- connectedDevice, recipientId, mockDeviceCallback, directExecutor);
- DeviceMessage message =
- DeviceMessage.createOutgoingMessage(
- recipientId,
- /* isMessageEncrypted= */ false,
- OperationType.CLIENT_MESSAGE,
- ByteUtils.randomBytes(10));
- connectedDeviceManager.setMessageDeliveryDelegate(null);
- connectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
- }
-
- @Test
- public void onMessageReceived_deliversMessageIfDelegateAccepts() throws InterruptedException {
- connectNewDevice();
- ConnectedDevice connectedDevice = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
- connectedDeviceManager.registerDeviceCallback(
- connectedDevice, recipientId, mockDeviceCallback, directExecutor);
- DeviceMessage message =
- DeviceMessage.createOutgoingMessage(
- recipientId,
- /* isMessageEncrypted= */ false,
- OperationType.CLIENT_MESSAGE,
- ByteUtils.randomBytes(10));
- MessageDeliveryDelegate delegate = device -> true;
- connectedDeviceManager.setMessageDeliveryDelegate(delegate);
- connectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
- }
-
- @Test
- public void onMessageReceived_doesNotDeliverMessageIfDelegateRejects()
- throws InterruptedException {
- connectNewDevice();
- ConnectedDevice connectedDevice = connectedDeviceManager.getActiveUserConnectedDevices().get(0);
- connectedDeviceManager.registerDeviceCallback(
- connectedDevice, recipientId, mockDeviceCallback, directExecutor);
- DeviceMessage message =
- DeviceMessage.createOutgoingMessage(
- recipientId,
- /* isMessageEncrypted= */ false,
- OperationType.CLIENT_MESSAGE,
- ByteUtils.randomBytes(10));
- MessageDeliveryDelegate delegate = device -> false;
- connectedDeviceManager.setMessageDeliveryDelegate(delegate);
- connectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
- }
-
- @NonNull
- private String connectNewDevice() {
- String deviceId = UUID.randomUUID().toString();
- AssociatedDevice device =
- new AssociatedDevice(
- deviceId, TEST_DEVICE_ADDRESS, TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
- userDeviceIds.add(deviceId);
- userDevices.add(device);
- connectedDeviceManager.addConnectedDevice(deviceId);
- return deviceId;
- }
-}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/api/CompanionConnectorTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/api/CompanionConnectorTest.kt
index 6516e95..b09c7d4 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/api/CompanionConnectorTest.kt
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/api/CompanionConnectorTest.kt
@@ -7,7 +7,6 @@
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.content.pm.ServiceInfo
-import android.os.IBinder
import android.os.Looper
import android.os.ParcelUuid
import android.os.RemoteException
@@ -16,14 +15,12 @@
import com.google.android.companionprotos.QueryResponse
import com.google.android.companionprotos.SystemQuery
import com.google.android.companionprotos.SystemQueryType
-import com.google.android.connecteddevice.api.Connector.Companion.ACTION_BIND_ASSOCIATION
import com.google.android.connecteddevice.api.Connector.Companion.ACTION_BIND_FEATURE_COORDINATOR
import com.google.android.connecteddevice.api.Connector.Companion.ACTION_BIND_FEATURE_COORDINATOR_FG
-import com.google.android.connecteddevice.api.Connector.Companion.ACTION_BIND_REMOTE_FEATURE
-import com.google.android.connecteddevice.api.Connector.Companion.ACTION_BIND_REMOTE_FEATURE_FG
import com.google.android.connecteddevice.api.Connector.Companion.USER_TYPE_ALL
import com.google.android.connecteddevice.api.Connector.Companion.USER_TYPE_DRIVER
import com.google.android.connecteddevice.api.Connector.Companion.USER_TYPE_PASSENGER
+import com.google.android.connecteddevice.core.util.mockToBeAlive
import com.google.android.connecteddevice.model.AssociatedDevice
import com.google.android.connecteddevice.model.ConnectedDevice
import com.google.android.connecteddevice.model.DeviceMessage
@@ -54,24 +51,14 @@
private val mockCallback = mock<Connector.Callback>()
- private val mockFeatureCoordinator = mock<IFeatureCoordinator.Stub>()
-
- private val mockConnectedDeviceManager = mock<IConnectedDeviceManager.Stub>()
-
- private val mockAssociatedDeviceManager = mock<IAssociatedDeviceManager.Stub>()
+ private val mockFeatureCoordinator = mockToBeAlive<IFeatureCoordinator>()
private val context = FakeContext(mockPackageManager)
- private val aliveBinder = mock<IBinder>()
-
private lateinit var defaultConnector: CompanionConnector
@Before
fun setUp() {
- whenever(aliveBinder.isBinderAlive).thenReturn(true)
- whenever(mockFeatureCoordinator.asBinder()).thenReturn(aliveBinder)
- whenever(mockConnectedDeviceManager.asBinder()).thenReturn(aliveBinder)
- whenever(mockAssociatedDeviceManager.asBinder()).thenReturn(aliveBinder)
defaultConnector =
CompanionConnector(context).apply {
featureCoordinator = mockFeatureCoordinator
@@ -82,7 +69,7 @@
@Test
fun connect_bindsWithFgActionWhenIsForegroundProcessIsTrue() {
- whenever(mockPackageManager.queryIntentServices(any(), any())).thenAnswer(defaultServiceAnswer)
+ setQueryIntentServicesAnswer(defaultServiceAnswer)
val connector = CompanionConnector(context, isForegroundProcess = true)
connector.connect()
@@ -92,7 +79,7 @@
@Test
fun connect_bindsWithBgActionWhenIsForegroundProcessIsFalse() {
- whenever(mockPackageManager.queryIntentServices(any(), any())).thenAnswer(defaultServiceAnswer)
+ setQueryIntentServicesAnswer(defaultServiceAnswer)
val connector = CompanionConnector(context, isForegroundProcess = false)
connector.connect()
@@ -102,65 +89,29 @@
@Test
fun connect_fgRetriesBindWithRemoteFeatureActionIfFeatureCoordinatorReturnsNullBinding() {
- whenever(mockPackageManager.queryIntentServices(any(), any())).thenAnswer(defaultServiceAnswer)
+ setQueryIntentServicesAnswer(defaultServiceAnswer)
val connector = CompanionConnector(context, isForegroundProcess = true)
connector.connect()
context.serviceConnection.firstOrNull()?.onNullBinding(ComponentName(PACKAGE_NAME, FG_NAME))
- assertThat(context.bindingActions)
- .containsExactly(
- ACTION_BIND_FEATURE_COORDINATOR_FG,
- ACTION_BIND_REMOTE_FEATURE_FG,
- ACTION_BIND_ASSOCIATION
- )
+ assertThat(context.bindingActions).containsExactly(ACTION_BIND_FEATURE_COORDINATOR_FG)
}
@Test
fun connect_bgRetriesBindWithRemoteFeatureActionIfFeatureCoordinatorReturnsNullBinding() {
- whenever(mockPackageManager.queryIntentServices(any(), any())).thenAnswer(defaultServiceAnswer)
+ setQueryIntentServicesAnswer(defaultServiceAnswer)
val connector = CompanionConnector(context, isForegroundProcess = false)
connector.connect()
context.serviceConnection.firstOrNull()?.onNullBinding(ComponentName(PACKAGE_NAME, BG_NAME))
- assertThat(context.bindingActions)
- .containsExactly(
- ACTION_BIND_FEATURE_COORDINATOR,
- ACTION_BIND_REMOTE_FEATURE,
- ACTION_BIND_ASSOCIATION
- )
- }
-
- @Test
- fun connect_fgRetriesBindWithRemoteFeatureActionIfFeatureCoordinatorActionMissing() {
- whenever(mockPackageManager.queryIntentServices(any(), any()))
- .thenAnswer(connectedDeviceManagerOnlyAnswer)
- val connector = CompanionConnector(context, isForegroundProcess = true)
-
- connector.connect()
- context.serviceConnection.firstOrNull()?.onNullBinding(ComponentName(PACKAGE_NAME, BG_NAME))
-
- assertThat(context.bindingActions)
- .containsExactly(ACTION_BIND_REMOTE_FEATURE_FG, ACTION_BIND_ASSOCIATION)
- }
-
- @Test
- fun connect_bgRetriesBindWithRemoteFeatureActionIfFeatureCoordinatorActionMissing() {
- whenever(mockPackageManager.queryIntentServices(any(), any()))
- .thenAnswer(connectedDeviceManagerOnlyAnswer)
- val connector = CompanionConnector(context, isForegroundProcess = false)
-
- connector.connect()
- context.serviceConnection.firstOrNull()?.onNullBinding(ComponentName(PACKAGE_NAME, BG_NAME))
-
- assertThat(context.bindingActions)
- .containsExactly(ACTION_BIND_REMOTE_FEATURE, ACTION_BIND_ASSOCIATION)
+ assertThat(context.bindingActions).containsExactly(ACTION_BIND_FEATURE_COORDINATOR)
}
@Test
fun disconnect_invokesOnDisconnectedWhenFeatureCoordinatorConnected() {
- whenever(mockPackageManager.queryIntentServices(any(), any())).thenAnswer(defaultServiceAnswer)
+ setQueryIntentServicesAnswer(defaultServiceAnswer)
val connector =
CompanionConnector(context, isForegroundProcess = false).apply { callback = mockCallback }
@@ -168,26 +119,7 @@
context
.serviceConnection
.firstOrNull()
- ?.onServiceConnected(ComponentName(PACKAGE_NAME, BG_NAME), mockFeatureCoordinator)
- connector.disconnect()
-
- verify(mockCallback).onDisconnected()
- }
-
- @Test
- fun disconnect_invokesOnDisconnectedWhenConnectedDeviceManagerConnected() {
- whenever(mockPackageManager.queryIntentServices(any(), any())).thenAnswer(defaultServiceAnswer)
- val connector =
- CompanionConnector(context, isForegroundProcess = false).apply {
- callback = mockCallback
- associatedDeviceManager = mockAssociatedDeviceManager
- }
-
- connector.connect()
- context
- .serviceConnection
- .firstOrNull()
- ?.onServiceConnected(ComponentName(PACKAGE_NAME, BG_NAME), mockConnectedDeviceManager)
+ ?.onServiceConnected(ComponentName(PACKAGE_NAME, BG_NAME), mockFeatureCoordinator.asBinder())
connector.disconnect()
verify(mockCallback).onDisconnected()
@@ -195,7 +127,7 @@
@Test
fun disconnect_notInvokedWithNoBoundService() {
- whenever(mockPackageManager.queryIntentServices(any(), any())).thenAnswer(defaultServiceAnswer)
+ setQueryIntentServicesAnswer(defaultServiceAnswer)
val connector =
CompanionConnector(context, isForegroundProcess = false).apply { callback = mockCallback }
@@ -206,25 +138,8 @@
}
@Test
- fun disconnect_nullsOutFeatureCoordinatorAndConnectedDeviceManager() {
- whenever(mockPackageManager.queryIntentServices(any(), any())).thenAnswer(defaultServiceAnswer)
- val connector = CompanionConnector(context, isForegroundProcess = false)
-
- connector.connect()
- context
- .serviceConnection
- .firstOrNull()
- ?.onServiceConnected(ComponentName(PACKAGE_NAME, BG_NAME), mockFeatureCoordinator)
- connector.disconnect()
-
- assertThat(connector.featureCoordinator).isNull()
- assertThat(connector.connectedDeviceManager).isNull()
- assertThat(connector.associatedDeviceManager).isNull()
- }
-
- @Test
fun disconnect_doesNotThrowWhileUnbindingUnboundService() {
- whenever(mockPackageManager.queryIntentServices(any(), any())).thenAnswer(defaultServiceAnswer)
+ setQueryIntentServicesAnswer(defaultServiceAnswer)
val connector =
CompanionConnector(FailingContext(mockPackageManager), isForegroundProcess = false).apply {
callback = mockCallback
@@ -235,7 +150,7 @@
@Test
fun onConnected_invokedWhenBindingSucceeds() {
- whenever(mockPackageManager.queryIntentServices(any(), any())).thenAnswer(defaultServiceAnswer)
+ setQueryIntentServicesAnswer(defaultServiceAnswer)
val connector =
CompanionConnector(context, isForegroundProcess = false).apply { callback = mockCallback }
@@ -243,14 +158,14 @@
context
.serviceConnection
.firstOrNull()
- ?.onServiceConnected(ComponentName(PACKAGE_NAME, BG_NAME), mockFeatureCoordinator)
+ ?.onServiceConnected(ComponentName(PACKAGE_NAME, BG_NAME), mockFeatureCoordinator.asBinder())
verify(mockCallback).onConnected()
}
@Test
fun onDisconnected_invokedWhenServiceDisconnects() {
- whenever(mockPackageManager.queryIntentServices(any(), any())).thenAnswer(defaultServiceAnswer)
+ setQueryIntentServicesAnswer(defaultServiceAnswer)
val connector =
CompanionConnector(context, isForegroundProcess = false).apply { callback = mockCallback }
@@ -259,7 +174,7 @@
context
.serviceConnection
.firstOrNull()
- ?.onServiceConnected(componentName, mockFeatureCoordinator)
+ ?.onServiceConnected(componentName, mockFeatureCoordinator.asBinder())
context.serviceConnection.firstOrNull()?.onServiceDisconnected(componentName)
verify(mockCallback).onDisconnected()
@@ -267,8 +182,7 @@
@Test
fun onFailedToConnect_invokedWhenServiceIsNotFound() {
- whenever(mockPackageManager.queryIntentServices(any(), any()))
- .thenAnswer(featureCoordinatorOnlyAnswer)
+ setQueryIntentServicesAnswer(defaultServiceAnswer)
val connector =
CompanionConnector(context, isForegroundProcess = false).apply { callback = mockCallback }
@@ -280,7 +194,7 @@
@Test
fun onFailedToConnect_invokedWithNullBindingFromFeatureCoordinatorAndNoCDMService() {
- whenever(mockPackageManager.queryIntentServices(any(), any())).thenAnswer(notFoundAnswer)
+ setQueryIntentServicesAnswer(notFoundAnswer)
val connector =
CompanionConnector(context, isForegroundProcess = false).apply { callback = mockCallback }
@@ -291,7 +205,7 @@
@Test
fun onFailedToConnect_invokedAfterBindingRetryLimitExceeded() {
- whenever(mockPackageManager.queryIntentServices(any(), any())).thenAnswer(defaultServiceAnswer)
+ setQueryIntentServicesAnswer(defaultServiceAnswer)
val failingContext = FailingContext(mockPackageManager)
val connector =
CompanionConnector(failingContext, isForegroundProcess = false).apply {
@@ -307,143 +221,17 @@
}
@Test
- fun featureCoordinatorAndConnectedDeviceManager_areNotNullAfterServiceConnection() {
- whenever(mockPackageManager.queryIntentServices(any(), any())).thenAnswer(defaultServiceAnswer)
+ fun featureCoordinator_isNotNullAfterServiceConnection() {
+ setQueryIntentServicesAnswer(defaultServiceAnswer)
val connector = CompanionConnector(context, isForegroundProcess = false)
connector.connect()
context
.serviceConnection
.firstOrNull()
- ?.onServiceConnected(ComponentName(PACKAGE_NAME, BG_NAME), mockFeatureCoordinator)
+ ?.onServiceConnected(ComponentName(PACKAGE_NAME, BG_NAME), mockFeatureCoordinator.asBinder())
assertThat(connector.featureCoordinator).isNotNull()
- assertThat(connector.connectedDeviceManager).isNotNull()
- }
-
- @Test
- fun connectedDeviceManager_isNotNullAfterServiceConnectionWhenLegacy() {
- whenever(mockPackageManager.queryIntentServices(any(), any()))
- .thenAnswer(connectedDeviceManagerOnlyAnswer)
- val connector = CompanionConnector(context, isForegroundProcess = false)
-
- connector.connect()
- context.serviceConnection[0].onServiceConnected(
- ComponentName(PACKAGE_NAME, BG_NAME),
- mockConnectedDeviceManager
- )
- context.serviceConnection[1].onServiceConnected(
- ComponentName(PACKAGE_NAME, BG_NAME),
- mockAssociatedDeviceManager
- )
- assertThat(connector.connectedDeviceManager).isNotNull()
- assertThat(connector.associatedDeviceManager).isNotNull()
- }
-
- @Test
- fun connectedDeviceManager_wrapsFeatureCoordinatorWhenNotLegacy() {
- val connectedDeviceManager =
- CompanionConnector.createConnectedDeviceManagerWrapper(mockFeatureCoordinator)
- val connectedDevice =
- ConnectedDevice(
- /* deviceId= */ UUID.randomUUID().toString(),
- /* deviceName= */ null,
- /* belongsToDriver= */ true,
- /* isConnectionEnabled= */ true
- )
-
- connectedDeviceManager.activeUserConnectedDevices
- verify(mockFeatureCoordinator).connectedDevicesForDriver
-
- val loggerId = 0
- val logRecords = ByteArray(0)
- connectedDeviceManager.processLogRecords(loggerId, logRecords)
- verify(mockFeatureCoordinator).processLogRecords(loggerId, logRecords)
-
- val connectionCallback = mock<IConnectionCallback>()
- connectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback)
- verify(mockFeatureCoordinator).registerDriverConnectionCallback(connectionCallback)
- connectedDeviceManager.unregisterConnectionCallback(connectionCallback)
- verify(mockFeatureCoordinator).unregisterConnectionCallback(connectionCallback)
-
- val deviceAssociationCallback = mock<IDeviceAssociationCallback>()
- connectedDeviceManager.registerDeviceAssociationCallback(deviceAssociationCallback)
- verify(mockFeatureCoordinator).registerDeviceAssociationCallback(deviceAssociationCallback)
- connectedDeviceManager.unregisterDeviceAssociationCallback(deviceAssociationCallback)
- verify(mockFeatureCoordinator).unregisterDeviceAssociationCallback(deviceAssociationCallback)
-
- val deviceCallback = mock<IDeviceCallback>()
- val recipientId = ParcelUuid(UUID.randomUUID())
- connectedDeviceManager.registerDeviceCallback(connectedDevice, recipientId, deviceCallback)
- verify(mockFeatureCoordinator)
- .registerDeviceCallback(connectedDevice, recipientId, deviceCallback)
- connectedDeviceManager.unregisterDeviceCallback(connectedDevice, recipientId, deviceCallback)
- verify(mockFeatureCoordinator)
- .unregisterDeviceCallback(connectedDevice, recipientId, deviceCallback)
-
- val logRequestedListener = mock<IOnLogRequestedListener>()
- connectedDeviceManager.registerOnLogRequestedListener(loggerId, logRequestedListener)
- verify(mockFeatureCoordinator).registerOnLogRequestedListener(loggerId, logRequestedListener)
- connectedDeviceManager.unregisterOnLogRequestedListener(loggerId, logRequestedListener)
- verify(mockFeatureCoordinator).unregisterOnLogRequestedListener(loggerId, logRequestedListener)
-
- val message =
- DeviceMessage.createOutgoingMessage(
- recipientId.uuid,
- /* isMessageEncrypted= */ false,
- DeviceMessage.OperationType.CLIENT_MESSAGE,
- ByteUtils.randomBytes(10)
- )
- connectedDeviceManager.sendMessage(connectedDevice, message)
- verify(mockFeatureCoordinator).sendMessage(connectedDevice, message)
- }
-
- @Test
- fun associatedDeviceManager_wrapsFeatureCoordinatorWhenNotLegacy() {
- val associatedDeviceManager =
- CompanionConnector.createAssociatedDeviceManagerWrapper(mockFeatureCoordinator)
- val mockAssociationCallback = mock<IAssociationCallback>()
- val mockDeviceAssociationCallback = mock<IDeviceAssociationCallback>()
- val mockConnectionCallback = mock<IConnectionCallback>()
- val mockListener = mock<IOnAssociatedDevicesRetrievedListener>()
- val deviceId = UUID.randomUUID().toString()
-
- associatedDeviceManager.startAssociation(mockAssociationCallback)
- verify(mockFeatureCoordinator).startAssociation(mockAssociationCallback)
-
- associatedDeviceManager.stopAssociation()
- verify(mockFeatureCoordinator).stopAssociation()
-
- associatedDeviceManager.retrievedActiveUserAssociatedDevices(mockListener)
- verify(mockFeatureCoordinator).retrieveAssociatedDevicesForDriver(mockListener)
-
- associatedDeviceManager.acceptVerification()
- verify(mockFeatureCoordinator).acceptVerification()
-
- associatedDeviceManager.removeAssociatedDevice(deviceId)
- verify(mockFeatureCoordinator).removeAssociatedDevice(deviceId)
-
- associatedDeviceManager.registerDeviceAssociationCallback(mockDeviceAssociationCallback)
- verify(mockFeatureCoordinator).registerDeviceAssociationCallback(mockDeviceAssociationCallback)
-
- associatedDeviceManager.unregisterDeviceAssociationCallback(mockDeviceAssociationCallback)
- verify(mockFeatureCoordinator)
- .unregisterDeviceAssociationCallback(mockDeviceAssociationCallback)
-
- associatedDeviceManager.activeUserConnectedDevices
- verify(mockFeatureCoordinator).connectedDevicesForDriver
-
- associatedDeviceManager.registerConnectionCallback(mockConnectionCallback)
- verify(mockFeatureCoordinator).registerDriverConnectionCallback(mockConnectionCallback)
-
- associatedDeviceManager.unregisterConnectionCallback(mockConnectionCallback)
- verify(mockFeatureCoordinator).unregisterConnectionCallback(mockConnectionCallback)
-
- associatedDeviceManager.enableAssociatedDeviceConnection(deviceId)
- verify(mockFeatureCoordinator).enableAssociatedDeviceConnection(deviceId)
-
- associatedDeviceManager.disableAssociatedDeviceConnection(deviceId)
- verify(mockFeatureCoordinator).disableAssociatedDeviceConnection(deviceId)
}
@Test
@@ -455,6 +243,29 @@
}
@Test
+ fun connect_multipleCallsOnlyRegistersCallbacksOnce() {
+ val featureId = ParcelUuid(UUID.randomUUID())
+ val connector =
+ CompanionConnector(context, userType = USER_TYPE_DRIVER).apply {
+ featureCoordinator = mockFeatureCoordinator
+ this.featureId = featureId
+ }
+ val device =
+ ConnectedDevice(
+ UUID.randomUUID().toString(),
+ /* deviceName= */ "",
+ /* belongsToDriver= */ true,
+ /* hasSecureChannel= */ false
+ )
+ whenever(mockFeatureCoordinator.connectedDevicesForDriver).thenReturn(listOf(device))
+
+ connector.connect()
+ connector.connect()
+
+ verify(mockFeatureCoordinator).registerDeviceCallback(eq(device), eq(featureId), any())
+ }
+
+ @Test
fun connect_userTypeDriverRegistersDriverConnectionCallbacks() {
val connector =
CompanionConnector(context, userType = USER_TYPE_DRIVER).apply {
@@ -622,69 +433,6 @@
}
@Test
- fun disconnect_unregistersConnectedDeviceManagerCallbacks() {
- defaultConnector.apply {
- featureCoordinator = null
- connectedDeviceManager = mockConnectedDeviceManager
- }
- val device =
- ConnectedDevice(
- UUID.randomUUID().toString(),
- /* deviceName= */ "",
- /* belongsToDriver= */ true,
- /* hasSecureChannel= */ false
- )
- whenever(mockConnectedDeviceManager.activeUserConnectedDevices).thenReturn(listOf(device))
-
- defaultConnector.disconnect()
-
- verify(mockConnectedDeviceManager).unregisterConnectionCallback(any())
- verify(mockConnectedDeviceManager).unregisterDeviceAssociationCallback(any())
- verify(mockConnectedDeviceManager).unregisterOnLogRequestedListener(any(), any())
- verify(mockConnectedDeviceManager)
- .unregisterDeviceCallback(eq(device), eq(defaultConnector.featureId), any())
- }
-
- @Test
- fun disconnect_connectedDeviceManagerWithoutFeatureIdDoesNotThrow() {
- defaultConnector.apply {
- featureId = null
- featureCoordinator = null
- connectedDeviceManager = mockConnectedDeviceManager
- }
- val device =
- ConnectedDevice(
- UUID.randomUUID().toString(),
- /* deviceName= */ "",
- /* belongsToDriver= */ true,
- /* hasSecureChannel= */ false
- )
- whenever(mockConnectedDeviceManager.activeUserConnectedDevices).thenReturn(listOf(device))
-
- defaultConnector.disconnect()
- }
-
- @Test
- fun disconnect_connectedDeviceManagerRemoteExceptionIsCaught() {
- whenever(mockConnectedDeviceManager.unregisterConnectionCallback(any()))
- .thenThrow(RemoteException())
- defaultConnector.apply {
- featureCoordinator = null
- connectedDeviceManager = mockConnectedDeviceManager
- }
- val device =
- ConnectedDevice(
- UUID.randomUUID().toString(),
- /* deviceName= */ "",
- /* belongsToDriver= */ true,
- /* hasSecureChannel= */ false
- )
- whenever(mockConnectedDeviceManager.activeUserConnectedDevices).thenReturn(listOf(device))
-
- defaultConnector.disconnect()
- }
-
- @Test
fun onDeviceConnected_registersDeviceCallbackWithFeatureId() {
val device =
ConnectedDevice(
@@ -765,31 +513,6 @@
}
@Test
- fun onDeviceDisconnected_unregistersDeviceCallbackConnectedDeviceManager() {
- defaultConnector.apply {
- featureCoordinator = null
- connectedDeviceManager = mockConnectedDeviceManager
- associatedDeviceManager = mockAssociatedDeviceManager
- }
- val device =
- ConnectedDevice(
- UUID.randomUUID().toString(),
- /* deviceName= */ "",
- /* belongsToDriver= */ true,
- /* hasSecureChannel= */ false
- )
- defaultConnector.connect()
-
- argumentCaptor<IConnectionCallback> {
- verify(mockConnectedDeviceManager).registerActiveUserConnectionCallback(capture())
- firstValue.onDeviceDisconnected(device)
- }
-
- verify(mockConnectedDeviceManager)
- .unregisterDeviceCallback(eq(device), eq(defaultConnector.featureId), any())
- }
-
- @Test
fun onSecureChannelEstablished_invokedWhenChannelEstablished() {
val device =
ConnectedDevice(
@@ -1040,7 +763,6 @@
@Test
fun onMessageFailedToSend_invokedWhenSendMessageIdCalledBeforeServiceConnection() {
defaultConnector.featureCoordinator = null
- defaultConnector.connectedDeviceManager = null
val deviceId = UUID.randomUUID().toString()
val message = ByteUtils.randomBytes(10)
@@ -1052,7 +774,6 @@
@Test
fun onMessageFailedToSend_invokedWhenSendMessageCalledBeforeServiceConnection() {
defaultConnector.featureCoordinator = null
- defaultConnector.connectedDeviceManager = null
val device =
ConnectedDevice(
UUID.randomUUID().toString(),
@@ -1137,7 +858,6 @@
@Test
fun getConnectedDeviceById_returnsNullBeforeServiceConnection() {
defaultConnector.featureCoordinator = null
- defaultConnector.connectedDeviceManager = null
assertThat(defaultConnector.getConnectedDeviceById(UUID.randomUUID().toString())).isNull()
}
@@ -1312,7 +1032,6 @@
@Test
fun sendQuery_queryCallbackOnQueryNotSentInvokedBeforeServiceConnectionWithId() {
defaultConnector.featureCoordinator = null
- defaultConnector.connectedDeviceManager = null
val deviceId = UUID.randomUUID().toString()
val request = ByteUtils.randomBytes(10)
val parameters = ByteUtils.randomBytes(10)
@@ -1326,7 +1045,6 @@
@Test
fun sendQuery_queryCallbackOnQueryNotSentInvokedBeforeServiceConnectionWithDevice() {
defaultConnector.featureCoordinator = null
- defaultConnector.connectedDeviceManager = null
val device =
ConnectedDevice(
UUID.randomUUID().toString(),
@@ -1561,7 +1279,6 @@
@Test
fun respondToQuery_doesNotThrowBeforeServiceConnectionWithDevice() {
defaultConnector.featureCoordinator = null
- defaultConnector.connectedDeviceManager = null
val device =
ConnectedDevice(
UUID.randomUUID().toString(),
@@ -1781,36 +1498,8 @@
}
@Test
- fun createLocalConnector_createsConnectorWithLegacyManagers() {
- val device =
- ConnectedDevice(
- UUID.randomUUID().toString(),
- /* deviceName= */ "",
- /* belongsToDriver= */ true,
- /* hasSecureChannel= */ true
- )
- whenever(mockConnectedDeviceManager.activeUserConnectedDevices).thenReturn(listOf(device))
-
- val connector =
- CompanionConnector.createLocalConnector(
- context,
- USER_TYPE_ALL,
- mockConnectedDeviceManager,
- mockAssociatedDeviceManager
- )
-
- assertThat(connector.connectedDeviceManager).isEqualTo(mockConnectedDeviceManager)
- assertThat(connector.associatedDeviceManager).isEqualTo(mockAssociatedDeviceManager)
- assertThat(connector.connectedDevices).containsExactly(device)
- assertThat(connector.isConnected).isTrue()
- }
-
- @Test
fun connectedDevices_returnsEmptyListWhenPlatformIsDisconnected() {
- defaultConnector.apply {
- featureCoordinator = null
- connectedDeviceManager = null
- }
+ defaultConnector.apply { featureCoordinator = null }
assertThat(defaultConnector.connectedDevices).isEmpty()
}
@@ -1875,12 +1564,6 @@
.isEqualTo(defaultConnector.featureCoordinator?.asBinder())
assertThat(defaultConnector.binderForAction(ACTION_BIND_FEATURE_COORDINATOR_FG))
.isEqualTo(defaultConnector.featureCoordinator?.asBinder())
- assertThat(defaultConnector.binderForAction(ACTION_BIND_REMOTE_FEATURE))
- .isEqualTo(defaultConnector.connectedDeviceManager?.asBinder())
- assertThat(defaultConnector.binderForAction(ACTION_BIND_REMOTE_FEATURE_FG))
- .isEqualTo(defaultConnector.connectedDeviceManager?.asBinder())
- assertThat(defaultConnector.binderForAction(ACTION_BIND_ASSOCIATION))
- .isEqualTo(defaultConnector.associatedDeviceManager?.asBinder())
assertThat(defaultConnector.binderForAction("")).isNull()
}
@@ -1894,22 +1577,6 @@
}
@Test
- fun startAssociation_startsAssociationOnAssociatedDeviceManager() {
- val legacyConnector =
- CompanionConnector.createLocalConnector(
- context,
- USER_TYPE_DRIVER,
- mockConnectedDeviceManager,
- mockAssociatedDeviceManager
- )
- val mockAssociationCallback = mock<IAssociationCallback>()
-
- legacyConnector.startAssociation(mockAssociationCallback)
-
- verify(mockAssociatedDeviceManager).startAssociation(mockAssociationCallback)
- }
-
- @Test
fun startAssociationWithId_startsAssociationWithIdentifierOnFeatureCoordinator() {
val mockAssociationCallback = mock<IAssociationCallback>()
val id = ParcelUuid(UUID.randomUUID())
@@ -1920,21 +1587,6 @@
}
@Test
- fun startAssociationWithId_doesNotThrowWhenLegacy() {
- val mockAssociationCallback = mock<IAssociationCallback>()
- val id = ParcelUuid(UUID.randomUUID())
- val legacyConnector =
- CompanionConnector.createLocalConnector(
- context,
- USER_TYPE_DRIVER,
- mockConnectedDeviceManager,
- mockAssociatedDeviceManager
- )
-
- legacyConnector.startAssociation(id, mockAssociationCallback)
- }
-
- @Test
fun stopAssociation_stopsAssociationOnFeatureCoordinator() {
defaultConnector.stopAssociation()
@@ -1942,21 +1594,6 @@
}
@Test
- fun stopAssociation_stopsAssociationOnAssociatedDeviceManager() {
- val legacyConnector =
- CompanionConnector.createLocalConnector(
- context,
- USER_TYPE_DRIVER,
- mockConnectedDeviceManager,
- mockAssociatedDeviceManager
- )
-
- legacyConnector.stopAssociation()
-
- verify(mockAssociatedDeviceManager).stopAssociation()
- }
-
- @Test
fun acceptVerification_acceptsVerificationOnFeatureCoordinator() {
defaultConnector.acceptVerification()
@@ -1964,21 +1601,6 @@
}
@Test
- fun acceptVerification_acceptsVerificationOnAssociatedDeviceManager() {
- val legacyConnector =
- CompanionConnector.createLocalConnector(
- context,
- USER_TYPE_DRIVER,
- mockConnectedDeviceManager,
- mockAssociatedDeviceManager
- )
-
- legacyConnector.acceptVerification()
-
- verify(mockAssociatedDeviceManager).acceptVerification()
- }
-
- @Test
fun removeAssociatedDevice_removesAssociatedDeviceOnFeatureCoordinator() {
val deviceId = UUID.randomUUID().toString()
@@ -1988,22 +1610,6 @@
}
@Test
- fun removeAssociatedDevice_removesAssociatedDeviceOnAssociatedDeviceManager() {
- val deviceId = UUID.randomUUID().toString()
- val legacyConnector =
- CompanionConnector.createLocalConnector(
- context,
- USER_TYPE_DRIVER,
- mockConnectedDeviceManager,
- mockAssociatedDeviceManager
- )
-
- legacyConnector.removeAssociatedDevice(deviceId)
-
- verify(mockAssociatedDeviceManager).removeAssociatedDevice(deviceId)
- }
-
- @Test
fun enableAssociatedDeviceConnection_enablesAssociatedDeviceConnectionOnFeatureCoordinator() {
val deviceId = UUID.randomUUID().toString()
@@ -2013,22 +1619,6 @@
}
@Test
- fun enableAssociatedDeviceConnection_enablesDeviceConnectionOnAssociatedDeviceManager() {
- val deviceId = UUID.randomUUID().toString()
- val legacyConnector =
- CompanionConnector.createLocalConnector(
- context,
- USER_TYPE_DRIVER,
- mockConnectedDeviceManager,
- mockAssociatedDeviceManager
- )
-
- legacyConnector.enableAssociatedDeviceConnection(deviceId)
-
- verify(mockAssociatedDeviceManager).enableAssociatedDeviceConnection(deviceId)
- }
-
- @Test
fun disableAssociatedDeviceConnection_disablesAssociatedDeviceConnectionOnFeatureCoordinator() {
val deviceId = UUID.randomUUID().toString()
@@ -2038,22 +1628,6 @@
}
@Test
- fun disableAssociatedDeviceConnection_disablesOnAssociatedDeviceManager() {
- val deviceId = UUID.randomUUID().toString()
- val legacyConnector =
- CompanionConnector.createLocalConnector(
- context,
- USER_TYPE_DRIVER,
- mockConnectedDeviceManager,
- mockAssociatedDeviceManager
- )
-
- legacyConnector.disableAssociatedDeviceConnection(deviceId)
-
- verify(mockAssociatedDeviceManager).disableAssociatedDeviceConnection(deviceId)
- }
-
- @Test
fun retrieveAssociatedDevices_retrievesAssociatedDevicesFromFeatureCoordinator() {
val mockListener = mock<IOnAssociatedDevicesRetrievedListener>()
@@ -2081,80 +1655,32 @@
}
@Test
- fun retrieveAssociatedDevices_retrievesActiveUserDevicesOnAssociatedDeviceManager() {
- val mockListener = mock<IOnAssociatedDevicesRetrievedListener>()
- val legacyConnector =
- CompanionConnector.createLocalConnector(
- context,
- USER_TYPE_DRIVER,
- mockConnectedDeviceManager,
- mockAssociatedDeviceManager
- )
+ fun claimAssociatedDevice_claimsDevice() {
+ val deviceId = UUID.randomUUID().toString()
- legacyConnector.retrieveAssociatedDevices(mockListener)
+ defaultConnector.claimAssociatedDevice(deviceId)
- verify(mockAssociatedDeviceManager).retrievedActiveUserAssociatedDevices(mockListener)
+ verify(mockFeatureCoordinator).claimAssociatedDevice(deviceId)
}
@Test
- fun retrieveAssociatedDevicesForDriver_retrievesActiveUserDevicesOnAssociatedDeviceManager() {
- val mockListener = mock<IOnAssociatedDevicesRetrievedListener>()
- val legacyConnector =
- CompanionConnector.createLocalConnector(
- context,
- USER_TYPE_DRIVER,
- mockConnectedDeviceManager,
- mockAssociatedDeviceManager
- )
+ fun removeAssociatedDeviceClaim_removesDeviceClaim() {
+ val deviceId = UUID.randomUUID().toString()
- legacyConnector.retrieveAssociatedDevicesForDriver(mockListener)
+ defaultConnector.removeAssociatedDeviceClaim(deviceId)
- verify(mockAssociatedDeviceManager).retrievedActiveUserAssociatedDevices(mockListener)
+ verify(mockFeatureCoordinator).removeAssociatedDeviceClaim(deviceId)
}
- @Test
- fun retrieveAssociatedDevicesForPassengers_issuesCallbackWithEmptyListWhenLegacy() {
- val mockListener = mock<IOnAssociatedDevicesRetrievedListener>()
- val legacyConnector =
- CompanionConnector.createLocalConnector(
- context,
- USER_TYPE_DRIVER,
- mockConnectedDeviceManager,
- mockAssociatedDeviceManager
- )
-
- legacyConnector.retrieveAssociatedDevicesForPassengers(mockListener)
-
- argumentCaptor<List<AssociatedDevice>> {
- verify(mockListener).onAssociatedDevicesRetrieved(capture())
- assertThat(firstValue).isEmpty()
- }
+ private fun setQueryIntentServicesAnswer(answer: Answer<List<ResolveInfo>>) {
+ whenever(mockPackageManager.queryIntentServices(any(), any<Int>())).thenAnswer(answer)
}
- /** All companion actions are resolved. */
private val defaultServiceAnswer = Answer {
val intent = it.arguments.first() as Intent
val resolveInfo =
ResolveInfo().apply { serviceInfo = ServiceInfo().apply { packageName = PACKAGE_NAME } }
when (intent.action) {
- ACTION_BIND_FEATURE_COORDINATOR_FG, ACTION_BIND_REMOTE_FEATURE_FG -> {
- resolveInfo.serviceInfo.name = FG_NAME
- listOf(resolveInfo)
- }
- ACTION_BIND_FEATURE_COORDINATOR, ACTION_BIND_REMOTE_FEATURE -> {
- resolveInfo.serviceInfo.name = BG_NAME
- listOf(resolveInfo)
- }
- else -> listOf()
- }
- }
-
- /** Only the feature coordinator actions are resolved. */
- private val featureCoordinatorOnlyAnswer = Answer {
- val intent = it.arguments.first() as Intent
- val resolveInfo =
- ResolveInfo().apply { serviceInfo = ServiceInfo().apply { packageName = PACKAGE_NAME } }
- when (intent.action) {
ACTION_BIND_FEATURE_COORDINATOR_FG -> {
resolveInfo.serviceInfo.name = FG_NAME
listOf(resolveInfo)
@@ -2167,24 +1693,6 @@
}
}
- /** Only the connected device manager actions are resolved. */
- private val connectedDeviceManagerOnlyAnswer = Answer {
- val intent = it.arguments.first() as Intent
- val resolveInfo =
- ResolveInfo().apply { serviceInfo = ServiceInfo().apply { packageName = PACKAGE_NAME } }
- when (intent.action) {
- ACTION_BIND_REMOTE_FEATURE_FG -> {
- resolveInfo.serviceInfo.name = FG_NAME
- listOf(resolveInfo)
- }
- ACTION_BIND_REMOTE_FEATURE -> {
- resolveInfo.serviceInfo.name = BG_NAME
- listOf(resolveInfo)
- }
- else -> listOf()
- }
- }
-
/** No companion actions are resolved. */
private val notFoundAnswer = Answer<List<ResolveInfo>> { listOf() }
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/api/FakeConnector.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/api/FakeConnector.kt
index 6c4dfcb..18b13b1 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/api/FakeConnector.kt
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/api/FakeConnector.kt
@@ -95,6 +95,18 @@
listener.onAssociatedDevicesRetrieved(associatedDevices.filter { it.userId != currentUserId })
}
+ override fun claimAssociatedDevice(deviceId: String) {
+ val device = associatedDevices.firstOrNull { it.deviceId == deviceId } ?: return
+ associatedDevices.remove(device)
+ associatedDevices.add(device.cloneWith(currentUserId))
+ }
+
+ override fun removeAssociatedDeviceClaim(deviceId: String) {
+ val device = associatedDevices.firstOrNull { it.deviceId == deviceId } ?: return
+ associatedDevices.remove(device)
+ associatedDevices.add(device.cloneWith(AssociatedDevice.UNCLAIMED_USER_ID))
+ }
+
fun addAssociatedDevice(device: AssociatedDevice) {
associatedDevices.add(device)
callback?.onAssociatedDeviceAdded(device)
@@ -105,4 +117,7 @@
callback?.onAssociatedDeviceRemoved(device)
}
}
+
+ private fun AssociatedDevice.cloneWith(userId: Int): AssociatedDevice =
+ AssociatedDevice(deviceId, deviceAddress, deviceName, isConnectionEnabled, userId)
}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/calendarsync/android/AttendeeContentDelegateTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/calendarsync/android/AttendeeContentDelegateTest.java
index 4222d25..77b34d2 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/calendarsync/android/AttendeeContentDelegateTest.java
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/calendarsync/android/AttendeeContentDelegateTest.java
@@ -40,7 +40,7 @@
.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_ACCEPTED)
.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_REQUIRED)
.put(Attendees.ATTENDEE_NAME, NAME)
- .build();
+ .buildOrThrow();
@Before
public void setUp() {
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/calendarsync/android/CalendarContentDelegateTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/calendarsync/android/CalendarContentDelegateTest.java
index 9077520..290aac9 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/calendarsync/android/CalendarContentDelegateTest.java
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/calendarsync/android/CalendarContentDelegateTest.java
@@ -48,7 +48,7 @@
.put(Calendars.CALENDAR_DISPLAY_NAME, NAME)
.put(Calendars.OWNER_ACCOUNT, ACCOUNT)
.put(Calendars.CALENDAR_TIME_ZONE, ZONE_ID.getId())
- .build();
+ .buildOrThrow();
private TestCalendarProvider testCalendarProvider;
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/calendarsync/android/EventContentDelegateTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/calendarsync/android/EventContentDelegateTest.java
index 0ccba8e..d512e4c 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/calendarsync/android/EventContentDelegateTest.java
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/calendarsync/android/EventContentDelegateTest.java
@@ -78,7 +78,7 @@
.put(Instances.RDATE, NULL_VALUE)
.put(Instances.ORIGINAL_ID, NULL_VALUE)
.put(Instances.ORIGINAL_INSTANCE_TIME, NULL_VALUE)
- .build();
+ .buildOrThrow();
private TestCalendarProvider testCalendarProvider;
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/AssociationSecureChannelTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/AssociationSecureChannelTest.java
deleted file mode 100644
index 8bb32e4..0000000
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/AssociationSecureChannelTest.java
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice.connection;
-
-import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.anyString;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.google.android.connecteddevice.model.DeviceMessage;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
-import com.google.android.connecteddevice.util.ByteUtils;
-import com.google.android.encryptionrunner.EncryptionRunner;
-import com.google.android.encryptionrunner.EncryptionRunnerFactory;
-import com.google.android.encryptionrunner.FakeEncryptionRunner;
-import java.util.UUID;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@RunWith(AndroidJUnit4.class)
-public final class AssociationSecureChannelTest {
- private static final UUID CLIENT_DEVICE_ID =
- UUID.fromString("a5645523-3280-410a-90c1-582a6c6f4969");
- private static final UUID SERVER_DEVICE_ID =
- UUID.fromString("a29f0c74-2014-4b14-ac02-be6ed15b545a");
- private static final byte[] CLIENT_SECRET = ByteUtils.randomBytes(32);
-
- @Mock private ConnectedDeviceStorage storageMock;
-
- @Mock
- private AssociationSecureChannel.ShowVerificationCodeListener showVerificationCodeListenerMock;
-
- @Mock private SecureChannel.Callback channelCallback;
-
- @Mock private DeviceMessageStream streamMock;
- private AssociationSecureChannel associationSecureChannel;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- when(storageMock.getUniqueId()).thenReturn(SERVER_DEVICE_ID);
- }
-
- @Test
- public void testEncryptionHandshake_association() {
- setupAssociationSecureChannel(channelCallback, EncryptionRunnerFactory.newFakeRunner());
- ArgumentCaptor<String> deviceIdCaptor = ArgumentCaptor.forClass(String.class);
- ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(DeviceMessage.class);
-
- initHandshakeMessage();
- verify(streamMock).writeMessage(messageCaptor.capture());
- byte[] response = messageCaptor.getValue().getMessage();
- assertThat(response).isEqualTo(FakeEncryptionRunner.INIT_RESPONSE);
-
- respondToContinueMessage();
- verify(showVerificationCodeListenerMock).showVerificationCode(anyString());
-
- associationSecureChannel.notifyOutOfBandAccepted();
- sendDeviceId();
-
- verify(channelCallback).onDeviceIdReceived(deviceIdCaptor.capture());
- verify(streamMock, times(2)).writeMessage(messageCaptor.capture());
- DeviceMessage deviceIdMessage = messageCaptor.getValue();
- associationSecureChannel.processMessage(deviceIdMessage);
- assertThat(deviceIdMessage.getMessage()).isEqualTo(ByteUtils.uuidToBytes(SERVER_DEVICE_ID));
- assertThat(deviceIdCaptor.getValue()).isEqualTo(CLIENT_DEVICE_ID.toString());
- verify(storageMock).saveEncryptionKey(eq(CLIENT_DEVICE_ID.toString()), any());
- verify(storageMock).saveChallengeSecret(CLIENT_DEVICE_ID.toString(), CLIENT_SECRET);
- verify(channelCallback).onSecureChannelEstablished();
- }
-
- @Test
- public void testEncryptionHandshake_association_wrongInitHandshakeMessage() {
- setupAssociationSecureChannel(channelCallback, EncryptionRunnerFactory.newFakeRunner());
-
- // Wrong init handshake message
- respondToContinueMessage();
-
- verify(channelCallback)
- .onEstablishSecureChannelFailure(eq(SecureChannel.CHANNEL_ERROR_INVALID_HANDSHAKE));
- }
-
- @Test
- public void testEncryptionHandshake_association_wrongRespondToContinueMessage()
- throws InterruptedException {
- setupAssociationSecureChannel(channelCallback, EncryptionRunnerFactory.newFakeRunner());
-
- initHandshakeMessage();
-
- // Wrong respond to continue message
- initHandshakeMessage();
-
- verify(channelCallback)
- .onEstablishSecureChannelFailure(eq(SecureChannel.CHANNEL_ERROR_INVALID_HANDSHAKE));
- }
-
- private void setupAssociationSecureChannel(
- SecureChannel.Callback callback, EncryptionRunner encryptionRunner) {
- associationSecureChannel =
- new AssociationSecureChannel(streamMock, storageMock, encryptionRunner);
- associationSecureChannel.registerCallback(callback);
- associationSecureChannel.setShowVerificationCodeListener(showVerificationCodeListenerMock);
- }
-
- private void sendDeviceId() {
- DeviceMessage message =
- DeviceMessage.createOutgoingMessage(
- /* recipient= */ null,
- /* isMessageEncrypted= */ true,
- DeviceMessage.OperationType.ENCRYPTION_HANDSHAKE,
- FakeEncryptionRunner.encryptDataWithFakeKey(
- ByteUtils.concatByteArrays(
- ByteUtils.uuidToBytes(CLIENT_DEVICE_ID), CLIENT_SECRET)));
- associationSecureChannel.onMessageReceived(message);
- }
-
- private void initHandshakeMessage() {
- DeviceMessage message =
- DeviceMessage.createOutgoingMessage(
- /* recipient= */ null,
- /* isMessageEncrypted= */ false,
- DeviceMessage.OperationType.ENCRYPTION_HANDSHAKE,
- FakeEncryptionRunner.INIT_MESSAGE);
- associationSecureChannel.onMessageReceived(message);
- }
-
- private void respondToContinueMessage() {
- DeviceMessage message =
- DeviceMessage.createOutgoingMessage(
- /* recipient= */ null,
- /* isMessageEncrypted= */ false,
- DeviceMessage.OperationType.ENCRYPTION_HANDSHAKE,
- FakeEncryptionRunner.CLIENT_RESPONSE);
- associationSecureChannel.onMessageReceived(message);
- }
-}
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 9228f86..11a20e3 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
@@ -16,11 +16,11 @@
package com.google.android.connecteddevice.connection
import android.content.Context
+import android.os.ParcelUuid
import android.util.Base64
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.android.companionprotos.CapabilitiesExchangeProto.CapabilitiesExchange
import com.google.android.companionprotos.CapabilitiesExchangeProto.CapabilitiesExchange.OobChannelType
import com.google.android.companionprotos.VersionExchangeProto
import com.google.android.connecteddevice.model.DeviceMessage
@@ -29,30 +29,26 @@
import com.google.android.connecteddevice.storage.ConnectedDeviceDatabase
import com.google.android.connecteddevice.storage.ConnectedDeviceStorage
import com.google.android.connecteddevice.storage.CryptoHelper
+import com.google.android.connecteddevice.transport.ConnectChallenge
import com.google.android.connecteddevice.transport.ConnectionProtocol
-import com.google.android.connecteddevice.transport.ConnectionProtocol.DataReceivedListener
+import com.google.android.connecteddevice.transport.IDataReceivedListener
+import com.google.android.connecteddevice.transport.IDataSendCallback
+import com.google.android.connecteddevice.transport.IDiscoveryCallback
import com.google.android.connecteddevice.transport.ProtocolDevice
-import com.google.android.connecteddevice.util.ThreadSafeCallbacks
import com.google.android.encryptionrunner.FakeEncryptionRunner
import com.google.common.truth.Truth.assertThat
import com.google.common.util.concurrent.MoreExecutors.directExecutor
-import com.google.protobuf.ExtensionRegistryLite
-import com.google.protobuf.InvalidProtocolBufferException
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.anyOrNull
import com.nhaarman.mockitokotlin2.argumentCaptor
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.eq
import com.nhaarman.mockitokotlin2.mock
-import com.nhaarman.mockitokotlin2.never
import com.nhaarman.mockitokotlin2.spy
-import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.validateMockitoUsage
import com.nhaarman.mockitokotlin2.verify
-import com.nhaarman.mockitokotlin2.verifyZeroInteractions
import com.nhaarman.mockitokotlin2.whenever
import java.util.UUID
-import kotlin.test.fail
import org.junit.After
import org.junit.Before
import org.junit.Test
@@ -60,7 +56,6 @@
private const val TEST_PROTOCOL_ID_1 = "testDevice1"
private const val TEST_PROTOCOL_ID_2 = "testDevice2"
-private const val OOB_CALLBACK_VALID_VERSION = 3
private val TEST_DEVICE_ID = UUID.randomUUID()
private val TEST_CHALLENGE = "test Challenge".toByteArray()
private val TEST_CHALLENGE_RESPONSE = "test Challenge response".toByteArray()
@@ -72,13 +67,12 @@
private val context = ApplicationProvider.getApplicationContext<Context>()
private val testProtocol1: TestProtocol = spy(TestProtocol(needDeviceVerification = false))
private val testProtocol2: TestProtocol = spy(TestProtocol(needDeviceVerification = true))
- private val mockCallback: ChannelResolver.Callback = mock()
- private val mockStreamFactory: ProtocolStreamFactory = mock()
- private val mockStream: ProtocolStream = mock()
- private val mockOobRunner: OobRunner = mock {
- on { supportedTypes } doReturn SUPPORTED_OOB_CAPABILITIES
- }
- private var mockEncryptionRunner: FakeEncryptionRunner = mock()
+ private val mockCallback = mock<ChannelResolver.Callback>()
+ private val mockStreamFactory = mock<ProtocolStreamFactory>()
+ private val mockStream = mock<ProtocolStream>()
+ private val mockOobRunner =
+ mock<OobRunner> { on { supportedTypes } doReturn SUPPORTED_OOB_CAPABILITIES }
+ private var mockEncryptionRunner = mock<FakeEncryptionRunner>()
private val testDevice1 = ProtocolDevice(testProtocol1, TEST_PROTOCOL_ID_1)
private val testDevice2 = ProtocolDevice(testProtocol2, TEST_PROTOCOL_ID_2)
private lateinit var spyStorage: ConnectedDeviceStorage
@@ -95,7 +89,7 @@
val database = connectedDeviceDatabase.associatedDeviceDao()
spyStorage =
spy(ConnectedDeviceStorage(context, Base64CryptoHelper(), database, directExecutor()))
- whenever(mockStreamFactory.createProtocolStream(any(), any())).thenReturn(mockStream)
+ whenever(mockStreamFactory.createProtocolStream(any())).thenReturn(mockStream)
whenever(spyStorage.hashWithChallengeSecret(any(), any())).thenReturn(TEST_CHALLENGE_RESPONSE)
channelResolver =
ChannelResolver(
@@ -144,18 +138,18 @@
.build()
.toByteArray()
channelResolver.resolveAssociation(mockOobRunner)
- argumentCaptor<DataReceivedListener>().apply {
- verify(testProtocol1).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_1), capture(), any())
+ argumentCaptor<IDataReceivedListener>().apply {
+ verify(testProtocol1).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_1), capture())
firstValue.onDataReceived(TEST_PROTOCOL_ID_1, unsupportedVersion)
}
verify(mockCallback).onChannelResolutionError()
}
@Test
- fun receivedSupportedVersion_sendVersionMessage() {
+ fun receivedSupportedVersion_sendVersionMessageAndSendOobData() {
channelResolver.resolveAssociation(mockOobRunner)
- argumentCaptor<DataReceivedListener>().apply {
- verify(testProtocol1).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_1), capture(), any())
+ argumentCaptor<IDataReceivedListener>().apply {
+ verify(testProtocol1).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_1), capture())
firstValue.onDataReceived(TEST_PROTOCOL_ID_1, createVersionMessage())
}
val expectedVersion = createVersionMessage()
@@ -163,72 +157,22 @@
verify(testProtocol1).sendData(eq(TEST_PROTOCOL_ID_1), capture(), anyOrNull())
assertThat(firstValue).isEqualTo(expectedVersion)
}
+
+ verify(mockOobRunner).sendOobData(any())
}
-
- @Test
- fun receivedCapabilityExchangeNotSupportedVersion_invokeOnChannelResolved() {
- val version =
- VersionExchangeProto.VersionExchange.newBuilder()
- .setMaxSupportedMessagingVersion(ChannelResolver.MAX_MESSAGING_VERSION)
- .setMinSupportedMessagingVersion(ChannelResolver.MIN_MESSAGING_VERSION)
- .setMaxSupportedSecurityVersion(
- ChannelResolver.SECURITY_VERSION_FOR_CAPABILITIES_EXCHANGE - 1
- )
- .setMinSupportedSecurityVersion(ChannelResolver.MIN_SECURITY_VERSION)
- .build()
- .toByteArray()
- channelResolver.resolveAssociation(mockOobRunner)
-
- argumentCaptor<DataReceivedListener>().apply {
- verify(testProtocol1).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_1), capture(), any())
- firstValue.onDataReceived(TEST_PROTOCOL_ID_1, version)
- }
- verify(mockCallback).onChannelResolved(any())
- }
-
- @Test
- fun receivedCapabilityExchangeSupportedVersion_sendCapabilityMessage() {
- val supportedOobChannelTypes = listOf(BT_RFCOMM_CHANNEL)
- channelResolver.resolveAssociation(mockOobRunner)
- val deviceCallback =
- argumentCaptor<DataReceivedListener>().apply {
- verify(testProtocol1).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_1), capture(), any())
- firstValue.onDataReceived(TEST_PROTOCOL_ID_1, createVersionMessage())
- }
- verify(testProtocol1).sendData(eq(TEST_PROTOCOL_ID_1), any(), anyOrNull())
-
- val capabilities =
- CapabilitiesExchange.newBuilder()
- .addAllSupportedOobChannels(supportedOobChannelTypes)
- .build()
- .toByteArray()
- deviceCallback.firstValue.onDataReceived(TEST_PROTOCOL_ID_1, capabilities)
- argumentCaptor<ByteArray> {
- verify(testProtocol1, times(2)).sendData(eq(TEST_PROTOCOL_ID_1), capture(), anyOrNull())
- val capabilityResponse =
- try {
- CapabilitiesExchange.parseFrom(lastValue, ExtensionRegistryLite.getEmptyRegistry())
- .supportedOobChannelsList
- } catch (e: InvalidProtocolBufferException) {
- fail("Failed to parse capabilities exchange message.")
- }
- assertThat(capabilityResponse).isEqualTo(supportedOobChannelTypes)
- }
- }
-
@Test
fun receivedInvalidChallenge_invokeOnError() {
channelResolver.resolveReconnect(TEST_DEVICE_ID, TEST_CHALLENGE)
channelResolver.addProtocolDevice(testDevice2)
- argumentCaptor<DataReceivedListener>().apply {
- verify(testProtocol2).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_2), capture(), any())
+ argumentCaptor<IDataReceivedListener>().apply {
+ verify(testProtocol2).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_2), capture())
firstValue.onDataReceived(TEST_PROTOCOL_ID_2, createVersionMessage())
}
val invalidChallengeMessage =
DeviceMessage.createOutgoingMessage(
/* recipient= */ null,
/* isMessageEncrypted= */ false,
- DeviceMessage.OperationType.ENCRYPTION_HANDSHAKE,
+ OperationType.ENCRYPTION_HANDSHAKE,
"Invalid test Challenge".toByteArray()
)
mockStream.messageReceivedListener?.onMessageReceived(invalidChallengeMessage)
@@ -239,15 +183,15 @@
fun receivedValidChallenge_sendChallengeResponse() {
channelResolver.resolveReconnect(TEST_DEVICE_ID, TEST_CHALLENGE)
channelResolver.addProtocolDevice(testDevice2)
- argumentCaptor<DataReceivedListener>().apply {
- verify(testProtocol2).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_2), capture(), any())
+ argumentCaptor<IDataReceivedListener>().apply {
+ verify(testProtocol2).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_2), capture())
firstValue.onDataReceived(TEST_PROTOCOL_ID_2, createVersionMessage())
}
val validChallengeMessage =
DeviceMessage.createOutgoingMessage(
/* recipient= */ null,
/* isMessageEncrypted= */ false,
- DeviceMessage.OperationType.ENCRYPTION_HANDSHAKE,
+ OperationType.ENCRYPTION_HANDSHAKE,
TEST_CHALLENGE
)
mockStream.messageReceivedListener?.onMessageReceived(validChallengeMessage)
@@ -263,8 +207,8 @@
fun receivedValidChallenge_invokeOnChannelResolved() {
channelResolver.resolveReconnect(TEST_DEVICE_ID, TEST_CHALLENGE)
channelResolver.addProtocolDevice(testDevice2)
- argumentCaptor<DataReceivedListener>().apply {
- verify(testProtocol2).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_2), capture(), any())
+ argumentCaptor<IDataReceivedListener>().apply {
+ verify(testProtocol2).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_2), capture())
firstValue.onDataReceived(TEST_PROTOCOL_ID_2, createVersionMessage())
}
val validChallengeMessage =
@@ -281,126 +225,13 @@
@Test
fun challengeNotRequired_invokeOnChannelResolved() {
channelResolver.resolveReconnect(TEST_DEVICE_ID, TEST_CHALLENGE)
- argumentCaptor<DataReceivedListener>().apply {
- verify(testProtocol1).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_1), capture(), any())
+ argumentCaptor<IDataReceivedListener>().apply {
+ verify(testProtocol1).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_1), capture())
firstValue.onDataReceived(TEST_PROTOCOL_ID_1, createVersionMessage())
}
verify(mockCallback).onChannelResolved(any())
}
- @Test
- fun onOobExchangeSuccess_V3_updateEncryptionRunnerAndResolveStream() {
- val testProtocolDevice = ProtocolDevice(testProtocol1, TEST_PROTOCOL_ID_1)
- val testOobChannels = listOf(BT_RFCOMM_CHANNEL)
- whenever(mockOobRunner.startOobDataExchange(eq(testProtocolDevice), any(), any(), any()))
- .thenReturn(true)
- channelResolver.resolveAssociation(mockOobRunner)
- val deviceCallback =
- argumentCaptor<DataReceivedListener>().apply {
- verify(testProtocol1).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_1), capture(), any())
- firstValue.onDataReceived(
- TEST_PROTOCOL_ID_1,
- createVersionMessage(OOB_CALLBACK_VALID_VERSION)
- )
- }
- val capabilities =
- CapabilitiesExchange.newBuilder()
- .addAllSupportedOobChannels(testOobChannels)
- .build()
- .toByteArray()
- deviceCallback.firstValue.onDataReceived(TEST_PROTOCOL_ID_1, capabilities)
- argumentCaptor<OobRunner.Callback>().apply {
- verify(mockOobRunner).startOobDataExchange(eq(testProtocolDevice), any(), any(), capture())
- firstValue.onOobDataExchangeSuccess()
- }
-
- verifyZeroInteractions(mockEncryptionRunner)
- verify(mockStreamFactory).createProtocolStream(eq(testProtocolDevice), any())
- }
-
- @Test
- fun onOobExchangeFailure_directlyResolveStream() {
- val testProtocolDevice = ProtocolDevice(testProtocol1, TEST_PROTOCOL_ID_1)
- val testOobChannels = listOf(BT_RFCOMM_CHANNEL)
- whenever(mockOobRunner.startOobDataExchange(eq(testProtocolDevice), any(), any(), any()))
- .thenReturn(true)
- channelResolver.resolveAssociation(mockOobRunner)
- val deviceCallback =
- argumentCaptor<DataReceivedListener>().apply {
- verify(testProtocol1).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_1), capture(), any())
- firstValue.onDataReceived(
- TEST_PROTOCOL_ID_1,
- createVersionMessage(OOB_CALLBACK_VALID_VERSION)
- )
- }
- val capabilities =
- CapabilitiesExchange.newBuilder()
- .addAllSupportedOobChannels(testOobChannels)
- .build()
- .toByteArray()
- deviceCallback.firstValue.onDataReceived(TEST_PROTOCOL_ID_1, capabilities)
- argumentCaptor<OobRunner.Callback>().apply {
- verify(mockOobRunner).startOobDataExchange(eq(testProtocolDevice), any(), any(), capture())
- firstValue.onOobDataExchangeFailure()
- }
-
- verify(mockEncryptionRunner).setIsReconnect(false)
- verify(mockStreamFactory).createProtocolStream(eq(testProtocolDevice), any())
- }
-
- @Test
- fun receivedCapabilityExchangeSupportedVersion_createOobChannelSuccessfully_waitForOobCallback() {
- val supportedOobChannelTypes = listOf(BT_RFCOMM_CHANNEL)
- val testProtocolDevice = ProtocolDevice(testProtocol1, TEST_PROTOCOL_ID_1)
- whenever(mockOobRunner.startOobDataExchange(eq(testProtocolDevice), any(), any(), any()))
- .thenReturn(true)
- channelResolver.resolveAssociation(mockOobRunner)
- val deviceCallback =
- argumentCaptor<DataReceivedListener>().apply {
- verify(testProtocol1).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_1), capture(), any())
- firstValue.onDataReceived(
- TEST_PROTOCOL_ID_1,
- createVersionMessage(OOB_CALLBACK_VALID_VERSION)
- )
- }
- verify(testProtocol1).sendData(eq(TEST_PROTOCOL_ID_1), any(), anyOrNull())
-
- val capabilities =
- CapabilitiesExchange.newBuilder()
- .addAllSupportedOobChannels(supportedOobChannelTypes)
- .build()
- .toByteArray()
- deviceCallback.firstValue.onDataReceived(TEST_PROTOCOL_ID_1, capabilities)
-
- verify(mockStreamFactory, never()).createProtocolStream(any(), any())
- }
-
- @Test
- fun receivedCapabilityExchangeSupportedVersion_createOobChannelFailed_directlyResolveStream() {
- val supportedOobChannelTypes = listOf(BT_RFCOMM_CHANNEL)
- val testProtocolDevice = ProtocolDevice(testProtocol1, TEST_PROTOCOL_ID_1)
- channelResolver.resolveAssociation(mockOobRunner)
- val deviceCallback =
- argumentCaptor<DataReceivedListener>().apply {
- verify(testProtocol1).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_1), capture(), any())
- firstValue.onDataReceived(
- TEST_PROTOCOL_ID_1,
- createVersionMessage(OOB_CALLBACK_VALID_VERSION)
- )
- }
- verify(testProtocol1).sendData(eq(TEST_PROTOCOL_ID_1), any(), anyOrNull())
-
- val capabilities =
- CapabilitiesExchange.newBuilder()
- .addAllSupportedOobChannels(supportedOobChannelTypes)
- .build()
- .toByteArray()
- deviceCallback.firstValue.onDataReceived(TEST_PROTOCOL_ID_1, capabilities)
-
- verify(mockStreamFactory).createProtocolStream(eq(testProtocolDevice), any())
- verify(mockEncryptionRunner).setIsReconnect(false)
- }
-
private fun createVersionMessage(
maxSecurityVersion: Int = ChannelResolver.MAX_SECURITY_VERSION
): ByteArray {
@@ -420,28 +251,28 @@
}
open class TestProtocol(private val needDeviceVerification: Boolean) : ConnectionProtocol() {
- val dataReceivedListenerList: MutableMap<String, ThreadSafeCallbacks<DataReceivedListener>> =
- dataReceivedListeners
- override val isDeviceVerificationRequired: Boolean = needDeviceVerification
+ val dataReceivedListenerList = dataReceivedListeners
+
+ override fun isDeviceVerificationRequired() = needDeviceVerification
override fun startAssociationDiscovery(
name: String,
- callback: DiscoveryCallback,
- identifier: UUID
+ identifier: ParcelUuid,
+ callback: IDiscoveryCallback
) {}
override fun startConnectionDiscovery(
- id: UUID,
+ id: ParcelUuid,
challenge: ConnectChallenge,
- callback: DiscoveryCallback
+ callback: IDiscoveryCallback
) {}
override fun stopAssociationDiscovery() {}
- override fun stopConnectionDiscovery(id: UUID) {}
+ override fun stopConnectionDiscovery(id: ParcelUuid) {}
- override fun sendData(protocolId: String, data: ByteArray, callback: DataSendCallback?) {}
+ override fun sendData(protocolId: String, data: ByteArray, callback: IDataSendCallback?) {}
override fun disconnectDevice(protocolId: String) {}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/ConnectionResolverTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/ConnectionResolverTest.java
deleted file mode 100644
index 58c6121..0000000
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/ConnectionResolverTest.java
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- * 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.
- */
-
-package com.google.android.connecteddevice.connection;
-
-import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.google.android.companionprotos.CapabilitiesExchangeProto.CapabilitiesExchange;
-import com.google.android.companionprotos.VersionExchangeProto.VersionExchange;
-import com.google.android.connecteddevice.connection.DeviceMessageStream.DataReceivedListener;
-import com.google.android.connecteddevice.connection.DeviceMessageStream.MessageReceivedErrorListener;
-import com.google.android.connecteddevice.util.ByteUtils;
-import com.google.protobuf.InvalidProtocolBufferException;
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@RunWith(AndroidJUnit4.class)
-public final class ConnectionResolverTest {
-
- private static final int WRITE_SIZE = 500;
-
- private DeviceMessageStream stream;
- private ConnectionResolver connectionResolver;
- @Mock private MessageReceivedErrorListener mockErrorListener;
- @Mock private DataReceivedListener mockDataReceivedListener;
- @Mock private ConnectionResolver.ConnectionResolvedListener mockConnectionResolvedListener;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- stream =
- spy(
- new DeviceMessageStream(WRITE_SIZE) {
- @Override
- protected void send(byte[] data) {}
- });
- stream.setMessageReceivedErrorListener(mockErrorListener);
- stream.setDataReceivedListener(mockDataReceivedListener);
- connectionResolver = new ConnectionResolver(stream, /* isCapabilitiesEligible= */ true);
- }
-
- @Test
- public void resolveConnection_invalidVersionExchangeMessage() {
- connectionResolver.resolveConnection(mockConnectionResolvedListener);
- connectionResolver.onMessageReceived(ByteUtils.randomBytes(WRITE_SIZE));
- verify(mockErrorListener).onMessageReceivedError(any(InvalidProtocolBufferException.class));
- verify(stream, times(0)).send(any());
- verify(mockConnectionResolvedListener, times(0)).onConnectionResolved(any());
- }
-
- @Test
- public void resolveConnection_invalidVersionNumbers() {
- connectionResolver.resolveConnection(mockConnectionResolvedListener);
- int invalidVersion = 0;
- byte[] phoneVersionBytes =
- VersionExchange.newBuilder()
- .setMinSupportedMessagingVersion(invalidVersion)
- .setMaxSupportedMessagingVersion(invalidVersion)
- .setMinSupportedSecurityVersion(invalidVersion)
- .setMaxSupportedSecurityVersion(invalidVersion)
- .build()
- .toByteArray();
- stream.onDataReceived(phoneVersionBytes);
- verify(mockErrorListener).onMessageReceivedError(any(IllegalStateException.class));
- verify(stream, times(0)).send(any());
- verify(mockConnectionResolvedListener, times(0)).onConnectionResolved(any());
- }
-
- @Test
- public void resolveConnection_securityVersion2_success() {
- connectionResolver.resolveConnection(mockConnectionResolvedListener);
-
- byte[] phoneVersionBytes = VersionExchange.newBuilder()
- .setMinSupportedMessagingVersion(ConnectionResolver.MESSAGING_VERSION)
- .setMaxSupportedMessagingVersion(ConnectionResolver.MESSAGING_VERSION)
- .setMinSupportedSecurityVersion(2)
- .setMaxSupportedSecurityVersion(2)
- .build()
- .toByteArray();
- byte[] expectedVersionBytes = VersionExchange.newBuilder()
- .setMinSupportedMessagingVersion(ConnectionResolver.MESSAGING_VERSION)
- .setMaxSupportedMessagingVersion(ConnectionResolver.MESSAGING_VERSION)
- .setMinSupportedSecurityVersion(ConnectionResolver.MIN_SECURITY_VERSION)
- .setMaxSupportedSecurityVersion(ConnectionResolver.MAX_SECURITY_VERSION)
- .build()
- .toByteArray();
- connectionResolver.onMessageReceived(phoneVersionBytes);
- verify(stream).writeRawBytes(expectedVersionBytes);
-
- ArgumentCaptor<ConnectionResolver.ResolvedConnection> resolvedConnectionCaptor =
- ArgumentCaptor.forClass(ConnectionResolver.ResolvedConnection.class);
- verify(mockConnectionResolvedListener).onConnectionResolved(resolvedConnectionCaptor.capture());
-
- ConnectionResolver.ResolvedConnection resolvedConnection = resolvedConnectionCaptor.getValue();
- assertThat(resolvedConnection.messagingVersion())
- .isEqualTo(ConnectionResolver.MESSAGING_VERSION);
- assertThat(resolvedConnection.securityVersion())
- .isEqualTo(2);
- assertThat(resolvedConnection.oobChannelTypes()).isNull();
- }
-
- @Test
- @Ignore("b/175066810")
- public void resolveConnection_isNotCapabilitiesEligible_capabilitesAreNotExchanged() {
- connectionResolver = new ConnectionResolver(stream, /* isCapabilitiesEligible= */ false);
- connectionResolver.resolveConnection(mockConnectionResolvedListener);
-
- byte[] phoneVersionBytes = createVersionExchangeMessage().toByteArray();
- connectionResolver.onMessageReceived(phoneVersionBytes);
- verify(stream).send(phoneVersionBytes);
- ArgumentCaptor<ConnectionResolver.ResolvedConnection> resolvedConnectionCaptor =
- ArgumentCaptor.forClass(ConnectionResolver.ResolvedConnection.class);
- verify(mockConnectionResolvedListener).onConnectionResolved(resolvedConnectionCaptor.capture());
-
- ConnectionResolver.ResolvedConnection resolvedConnection = resolvedConnectionCaptor.getValue();
- assertThat(resolvedConnection.messagingVersion())
- .isEqualTo(ConnectionResolver.MESSAGING_VERSION);
- assertThat(resolvedConnection.securityVersion())
- .isEqualTo(ConnectionResolver.MAX_SECURITY_VERSION);
- assertThat(resolvedConnection.oobChannelTypes()).isNull();
- }
-
- @Test
- @Ignore("b/175066810")
- public void resolveConnection_securityVersion3_success() {
- connectionResolver.resolveConnection(mockConnectionResolvedListener);
-
- byte[] phoneVersionBytes = createVersionExchangeMessage().toByteArray();
- connectionResolver.onMessageReceived(phoneVersionBytes);
- verify(stream).send(phoneVersionBytes);
-
- byte[] phoneCapabilitiesBytes = CapabilitiesExchange.getDefaultInstance().toByteArray();
- connectionResolver.onMessageReceived(phoneCapabilitiesBytes);
- verify(stream).send(phoneCapabilitiesBytes);
-
- ArgumentCaptor<ConnectionResolver.ResolvedConnection> resolvedConnectionCaptor =
- ArgumentCaptor.forClass(ConnectionResolver.ResolvedConnection.class);
- verify(mockConnectionResolvedListener).onConnectionResolved(resolvedConnectionCaptor.capture());
-
- ConnectionResolver.ResolvedConnection resolvedConnection = resolvedConnectionCaptor.getValue();
- assertThat(resolvedConnection.messagingVersion())
- .isEqualTo(ConnectionResolver.MESSAGING_VERSION);
- assertThat(resolvedConnection.securityVersion())
- .isEqualTo(ConnectionResolver.MAX_SECURITY_VERSION);
- assertThat(resolvedConnection.oobChannelTypes()).isNotNull();
- }
-
- @Test
- @Ignore("b/175066810")
- public void processCapabilitiesExchange_invalidCapabilitiesExchangeMessage() {
- connectionResolver.resolveConnection(mockConnectionResolvedListener);
-
- byte[] phoneVersionBytes = createVersionExchangeMessage().toByteArray();
- connectionResolver.onMessageReceived(phoneVersionBytes);
- verify(stream).send(phoneVersionBytes);
-
- stream.onDataReceived(ByteUtils.randomBytes(WRITE_SIZE));
- verify(mockErrorListener).onMessageReceivedError(any(InvalidProtocolBufferException.class));
- verifyNoMoreInteractions(stream);
- verify(mockConnectionResolvedListener, times(0)).onConnectionResolved(any());
- }
-
- private static VersionExchange createVersionExchangeMessage() {
- return VersionExchange.newBuilder()
- .setMinSupportedMessagingVersion(ConnectionResolver.MESSAGING_VERSION)
- .setMaxSupportedMessagingVersion(ConnectionResolver.MESSAGING_VERSION)
- .setMinSupportedSecurityVersion(ConnectionResolver.MIN_SECURITY_VERSION)
- .setMaxSupportedSecurityVersion(ConnectionResolver.MAX_SECURITY_VERSION)
- .build();
- }
-}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/DeviceMessageStreamTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/DeviceMessageStreamTest.java
deleted file mode 100644
index 08d7f28..0000000
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/DeviceMessageStreamTest.java
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice.connection;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import androidx.annotation.NonNull;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.google.android.companionprotos.DeviceMessageProto.Message;
-import com.google.android.companionprotos.OperationProto.OperationType;
-import com.google.android.companionprotos.PacketProto.Packet;
-import com.google.android.connecteddevice.connection.DeviceMessageStream.MessageReceivedErrorListener;
-import com.google.android.connecteddevice.connection.DeviceMessageStream.MessageReceivedListener;
-import com.google.android.connecteddevice.model.DeviceMessage;
-import com.google.android.connecteddevice.util.ByteUtils;
-import com.google.common.collect.Iterables;
-import com.google.protobuf.ByteString;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.ThreadLocalRandom;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@RunWith(AndroidJUnit4.class)
-public class DeviceMessageStreamTest {
-
- private static final int WRITE_SIZE = 500;
-
- private DeviceMessageStream stream;
-
- @Mock private MessageReceivedListener mockMessageReceivedListener;
- @Mock private MessageReceivedErrorListener mockErrorListener;
-
- @Before
- public void setup() {
- MockitoAnnotations.initMocks(this);
- stream =
- spy(
- new DeviceMessageStream(WRITE_SIZE) {
- @Override
- protected void send(byte[] data) {}
- });
- }
-
- @Test
- public void processPacket_notifiesWithEntireMessageForSinglePacketMessage() {
- stream.setMessageReceivedListener(mockMessageReceivedListener);
- byte[] data = ByteUtils.randomBytes(5);
-
- processMessage(data);
- ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(DeviceMessage.class);
- verify(mockMessageReceivedListener).onMessageReceived(messageCaptor.capture());
- DeviceMessage message = messageCaptor.getValue();
-
- assertThat(message.getMessage()).isEqualTo(data);
- assertThat(message.getOriginalMessageSize()).isEqualTo(data.length);
- }
-
- @Test
- public void processPacket_notifiesWithEntireMessageForMultiPacketMessage() {
- stream.setMessageReceivedListener(mockMessageReceivedListener);
- byte[] data = ByteUtils.randomBytes(750);
- processMessage(data);
- ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(DeviceMessage.class);
- verify(mockMessageReceivedListener).onMessageReceived(messageCaptor.capture());
- assertThat(Arrays.equals(data, messageCaptor.getValue().getMessage())).isTrue();
- }
-
- @Test
- public void processPacket_receivingMultipleMessagesInParallelParsesSuccessfully() {
- stream.setMessageReceivedListener(mockMessageReceivedListener);
- byte[] data1 = ByteUtils.randomBytes((int) (WRITE_SIZE * 1.5));
- byte[] data2 = ByteUtils.randomBytes((int) (WRITE_SIZE * 1.5));
- List<Packet> packets1 = createPackets(data1);
- List<Packet> packets2 = createPackets(data2);
-
- for (int i = 0; i < packets1.size(); i++) {
- stream.processPacket(packets1.get(i));
- if (i == packets1.size() - 1) {
- break;
- }
- stream.processPacket(packets2.get(i));
- }
- ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(DeviceMessage.class);
- verify(mockMessageReceivedListener).onMessageReceived(messageCaptor.capture());
- DeviceMessage message1 = messageCaptor.getValue();
-
- assertThat(message1.getMessage()).isEqualTo(data1);
- assertThat(message1.getOriginalMessageSize()).isEqualTo(data1.length);
-
- stream.setMessageReceivedListener(mockMessageReceivedListener);
- stream.processPacket(Iterables.getLast(packets2));
- verify(mockMessageReceivedListener, times(2)).onMessageReceived(messageCaptor.capture());
- DeviceMessage message2 = messageCaptor.getValue();
-
- assertThat(message2.getMessage()).isEqualTo(data2);
- assertThat(message2.getOriginalMessageSize()).isEqualTo(data2.length);
- }
-
- @Test
- public void processPacket_doesNotNotifyOfNewMessageIfNotAllPacketsReceived()
- throws InterruptedException {
- stream.setMessageReceivedListener(mockMessageReceivedListener);
- stream.setMessageReceivedErrorListener(mockErrorListener);
- byte[] data = ByteUtils.randomBytes((int) (WRITE_SIZE * 1.5));
- List<Packet> packets = createPackets(data);
- for (int i = 0; i < packets.size() - 1; i++) {
- stream.processPacket(packets.get(i));
- }
- verify(mockMessageReceivedListener, never()).onMessageReceived(any());
- verify(mockErrorListener, never()).onMessageReceivedError(any());
- }
-
- @Test
- public void processPacket_ignoresDuplicatePacket() {
- byte[] data = ByteUtils.randomBytes((int) (WRITE_SIZE * 2.5));
- stream.setMessageReceivedListener(mockMessageReceivedListener);
- ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(
- DeviceMessage.class);
- List<Packet> packets = createPackets(data);
- for (int i = 0; i < packets.size(); i++) {
- stream.processPacket(packets.get(i));
- stream.processPacket(packets.get(i)); // Process each packet twice.
- }
- verify(mockMessageReceivedListener).onMessageReceived(messageCaptor.capture());
- assertThat(Arrays.equals(data, messageCaptor.getValue().getMessage())).isTrue();
- }
-
- @Test
- public void processPacket_packetBeforeExpectedRangeNotifiesMessageError()
- throws InterruptedException {
- stream.setMessageReceivedErrorListener(mockErrorListener);
- List<Packet> packets = createPackets(ByteUtils.randomBytes((int) (WRITE_SIZE * 2.5)));
- stream.processPacket(packets.get(0));
- stream.processPacket(packets.get(1));
- stream.processPacket(packets.get(0));
- verify(mockErrorListener).onMessageReceivedError(any(IllegalStateException.class));
- }
-
- @Test
- public void processPacket_packetAfterExpectedNotifiesMessageError() throws InterruptedException {
- stream.setMessageReceivedErrorListener(mockErrorListener);
- List<Packet> packets = createPackets(ByteUtils.randomBytes((int) (WRITE_SIZE * 1.5)));
- stream.processPacket(packets.get(1));
- verify(mockErrorListener).onMessageReceivedError(any(IllegalStateException.class));
- }
-
-
- @NonNull
- private static List<Packet> createPackets(byte[] data) {
- try {
- Message message =
- Message.newBuilder()
- .setPayload(ByteString.copyFrom(data))
- .setOperation(OperationType.CLIENT_MESSAGE)
- .setOriginalSize(data.length)
- .build();
- return PacketFactory.makePackets(
- message.toByteArray(), ThreadLocalRandom.current().nextInt(), WRITE_SIZE);
- } catch (Exception e) {
- assertWithMessage("Uncaught exception while making packets.").fail();
- return new ArrayList<>();
- }
- }
-
- private void processMessage(byte[] data) {
- List<Packet> packets = createPackets(data);
- for (Packet packet : packets) {
- stream.processPacket(packet);
- }
- }
-}
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 3dc968a..4f1df72 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
@@ -16,10 +16,13 @@
package com.google.android.connecteddevice.connection
import android.content.Context
+import android.os.ParcelUuid
import android.util.Base64
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.android.companionprotos.VerificationCode
+import com.google.android.companionprotos.VerificationCodeState
import com.google.android.connecteddevice.connection.MultiProtocolSecureChannel.ChannelError
import com.google.android.connecteddevice.connection.MultiProtocolSecureChannel.MessageError
import com.google.android.connecteddevice.connection.MultiProtocolSecureChannel.ShowVerificationCodeListener
@@ -29,7 +32,10 @@
import com.google.android.connecteddevice.storage.ConnectedDeviceDatabase
import com.google.android.connecteddevice.storage.ConnectedDeviceStorage
import com.google.android.connecteddevice.storage.CryptoHelper
+import com.google.android.connecteddevice.transport.ConnectChallenge
import com.google.android.connecteddevice.transport.ConnectionProtocol
+import com.google.android.connecteddevice.transport.IDataSendCallback
+import com.google.android.connecteddevice.transport.IDiscoveryCallback
import com.google.android.connecteddevice.transport.ProtocolDevice
import com.google.android.connecteddevice.util.ByteUtils
import com.google.android.encryptionrunner.EncryptionRunnerFactory
@@ -37,6 +43,7 @@
import com.google.android.encryptionrunner.HandshakeException
import com.google.common.truth.Truth.assertThat
import com.google.common.util.concurrent.MoreExecutors.directExecutor
+import com.google.protobuf.ByteString
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.argumentCaptor
import com.nhaarman.mockitokotlin2.eq
@@ -62,10 +69,8 @@
@RunWith(AndroidJUnit4::class)
class MultiProtocolSecureChannelTest {
private val context = ApplicationProvider.getApplicationContext<Context>()
- private val stream1 =
- spy(ProtocolStream(ProtocolDevice(TestProtocol(), PROTOCOL_ID_1), directExecutor()))
- private val stream2 =
- spy(ProtocolStream(ProtocolDevice(TestProtocol(), PROTOCOL_ID_2), directExecutor()))
+ private val stream1 = spy(ProtocolStream(ProtocolDevice(TestProtocol(), PROTOCOL_ID_1)))
+ private val stream2 = spy(ProtocolStream(ProtocolDevice(TestProtocol(), PROTOCOL_ID_2)))
private val mockInflater: Inflater = mock()
private val mockCallback: MultiProtocolSecureChannel.Callback = mock()
private val mockOobRunner: OobRunner = mock()
@@ -199,7 +204,7 @@
}
@Test
- fun onDeviceMessageReceived_noExceptionWhenReceivedUnknowMessageType() {
+ fun onDeviceMessageReceived_noExceptionWhenReceivedUnknownMessageType() {
completeHandshakeAndSaveTheKey()
val message =
DeviceMessage.createOutgoingMessage(
@@ -247,7 +252,7 @@
}
@Test
- fun decompressMessage_returnsFlaseWhenThereAreDataFormatException() {
+ fun decompressMessage_returnsFalseWhenThereAreDataFormatException() {
completeHandshakeAndSaveTheKey()
whenever(mockInflater.inflate(any())).then { throw DataFormatException() }
val deviceMessage =
@@ -407,6 +412,17 @@
assertThat(response).isEqualTo(FakeEncryptionRunner.INIT_RESPONSE)
}
respondToContinueMessage()
+ val testVerificationCodeMessage =
+ VerificationCode.newBuilder().setState(VerificationCodeState.VISUAL_VERIFICATION).build()
+ val deviceMessage =
+ DeviceMessage.createOutgoingMessage(
+ /* recipient= */ null,
+ /* isMessageEncrypted= */ false,
+ OperationType.ENCRYPTION_HANDSHAKE,
+ testVerificationCodeMessage.toByteArray()
+ )
+ secureChannel.onDeviceMessageReceived(deviceMessage)
+
verify(mockShowVerificationCodeListener).showVerificationCode(any())
secureChannel.notifyVerificationCodeAccepted()
@@ -546,6 +562,18 @@
setupSecureChannel(false)
initHandshakeMessage()
respondToContinueMessage()
+
+ val testVerificationCodeMessage =
+ VerificationCode.newBuilder().setState(VerificationCodeState.VISUAL_VERIFICATION).build()
+ val deviceMessage =
+ DeviceMessage.createOutgoingMessage(
+ /* recipient= */ null,
+ /* isMessageEncrypted= */ false,
+ OperationType.ENCRYPTION_HANDSHAKE,
+ testVerificationCodeMessage.toByteArray()
+ )
+ secureChannel.onDeviceMessageReceived(deviceMessage)
+
verify(mockCallback).onEstablishSecureChannelFailure(ChannelError.CHANNEL_ERROR_INVALID_STATE)
}
@@ -569,76 +597,86 @@
}
@Test
- fun receivedOobVerificationCode_verifiedSuccessfully_notifyChannelEstablishedCallback() {
- val encryptedCode = "oobCode".toByteArray()
- val oobCode = FakeEncryptionRunner.VERIFICATION_CODE
- val carEncryptedCode = "carOobCode".toByteArray()
- whenever(mockOobRunner.decryptData(encryptedCode)).thenReturn(oobCode)
- whenever(mockOobRunner.encryptData(oobCode)).thenReturn(carEncryptedCode)
- setupOobSecureChannel(mockOobRunner)
+ fun processVerificationCodeMessage_oobVerification_verifyOobCode() {
+ setupSecureChannel(false)
initHandshakeMessage()
respondToContinueMessage()
- respondToOobCode(encryptedCode)
- verify(mockOobRunner).encryptData(oobCode)
- argumentCaptor<DeviceMessage>().apply {
- verify(stream1, times(2)).sendMessage(capture())
- val codeMessage = secondValue.message
- assertThat(codeMessage).isEqualTo(carEncryptedCode)
- }
- verify(mockCallback).onSecureChannelEstablished()
+ val testPayload = "testPayload".toByteArray()
+ val testVerificationCodeMessage =
+ VerificationCode.newBuilder().run {
+ setState(VerificationCodeState.OOB_VERIFICATION)
+ setPayload(ByteString.copyFrom(testPayload))
+ build()
+ }
+ val deviceMessage =
+ DeviceMessage.createOutgoingMessage(
+ /* recipient= */ null,
+ /* isMessageEncrypted= */ false,
+ OperationType.ENCRYPTION_HANDSHAKE,
+ testVerificationCodeMessage.toByteArray()
+ )
+ secureChannel.onDeviceMessageReceived(deviceMessage)
+
+ verify(mockOobRunner).decryptData(testPayload)
}
@Test
- fun receivedOobVerificationCode_decryptFailed_issueInvalidEncryptionKeyError() {
- val encryptedCode = "oobCode".toByteArray()
- whenever(mockOobRunner.decryptData(encryptedCode)).then { throw Exception() }
- setupOobSecureChannel(mockOobRunner)
+ fun createOobResponse_oobCodeMatch_sendCorrectMessage() {
+ val testPayload = "testPayload".toByteArray()
+ whenever(mockOobRunner.decryptData(testPayload))
+ .thenReturn(FakeEncryptionRunner.VERIFICATION_CODE)
+ whenever(mockOobRunner.encryptData(FakeEncryptionRunner.VERIFICATION_CODE))
+ .thenReturn(testPayload)
+ setupSecureChannel(false)
initHandshakeMessage()
respondToContinueMessage()
- respondToOobCode(encryptedCode)
+ val testVerificationCodeMessage =
+ VerificationCode.newBuilder().run {
+ setState(VerificationCodeState.OOB_VERIFICATION)
+ setPayload(ByteString.copyFrom(testPayload))
+ build()
+ }
+ val deviceMessage =
+ DeviceMessage.createOutgoingMessage(
+ /* recipient= */ null,
+ /* isMessageEncrypted= */ false,
+ OperationType.ENCRYPTION_HANDSHAKE,
+ testVerificationCodeMessage.toByteArray()
+ )
+ secureChannel.onDeviceMessageReceived(deviceMessage)
- verify(mockCallback)
- .onEstablishSecureChannelFailure(ChannelError.CHANNEL_ERROR_INVALID_ENCRYPTION_KEY)
+ val confirmationMessage =
+ argumentCaptor<DeviceMessage>().run {
+ verify(stream1, times(2)).sendMessage(capture())
+ secondValue.message
+ }
+ val verificationCodeMessage = VerificationCode.parseFrom(confirmationMessage)
+ assertThat(verificationCodeMessage.state).isEqualTo(VerificationCodeState.OOB_VERIFICATION)
}
@Test
- fun receivedOobVerificationCode_encryptFailed_issueInvalidEncryptionKeyError() {
- val encryptedCode = "oobCode".toByteArray()
- val incorrectOobCode = "incorrectCode".toByteArray()
- whenever(mockOobRunner.decryptData(encryptedCode)).thenReturn(incorrectOobCode)
- setupOobSecureChannel(mockOobRunner)
+ fun onVisualVerificationCodeConfirmed_sendConfirmationMessage() {
+ setupSecureChannel(false)
initHandshakeMessage()
respondToContinueMessage()
- respondToOobCode(encryptedCode)
-
- verify(mockCallback)
- .onEstablishSecureChannelFailure(ChannelError.CHANNEL_ERROR_INVALID_ENCRYPTION_KEY)
- }
-
- @Test
- fun receivedOobVerificationCode_codeDoesNotMatch_issueInvalidEncryptionKeyError() {
- val encryptedCode = "oobCode".toByteArray()
- val oobCode = FakeEncryptionRunner.VERIFICATION_CODE
- whenever(mockOobRunner.decryptData(encryptedCode)).thenReturn(oobCode)
- whenever(mockOobRunner.encryptData(oobCode)).then { throw Exception() }
- setupOobSecureChannel(mockOobRunner)
- initHandshakeMessage()
- respondToContinueMessage()
- respondToOobCode(encryptedCode)
-
- verify(mockCallback)
- .onEstablishSecureChannelFailure(ChannelError.CHANNEL_ERROR_INVALID_ENCRYPTION_KEY)
- }
-
- @Test
- fun receivedOobVerificationCode_oobRunnerIsNull_issueInvalidEncryptionKeyError() {
- setupOobSecureChannel()
- initHandshakeMessage()
- respondToContinueMessage()
- respondToOobCode(ByteArray(10))
-
- verify(mockCallback)
- .onEstablishSecureChannelFailure(ChannelError.CHANNEL_ERROR_INVALID_ENCRYPTION_KEY)
+ val testVerificationCodeMessage =
+ VerificationCode.newBuilder().setState(VerificationCodeState.VISUAL_VERIFICATION).build()
+ val deviceMessage =
+ DeviceMessage.createOutgoingMessage(
+ /* recipient= */ null,
+ /* isMessageEncrypted= */ false,
+ OperationType.ENCRYPTION_HANDSHAKE,
+ testVerificationCodeMessage.toByteArray()
+ )
+ secureChannel.onDeviceMessageReceived(deviceMessage)
+ secureChannel.notifyVerificationCodeAccepted()
+ val confirmationMessage =
+ argumentCaptor<DeviceMessage>().run {
+ verify(stream1, times(2)).sendMessage(capture())
+ secondValue.message
+ }
+ val verificationCodeMessage = VerificationCode.parseFrom(confirmationMessage)
+ assertThat(verificationCodeMessage.state).isEqualTo(VerificationCodeState.VISUAL_CONFIRMATION)
}
private fun setupSecureChannel(isReconnect: Boolean, deviceId: String? = null) {
@@ -646,10 +684,11 @@
encryptionRunner.setIsReconnect(isReconnect)
secureChannel =
spy(
- MultiProtocolSecureChannelPreV4(
+ MultiProtocolSecureChannel(
stream1,
spyStorage,
encryptionRunner,
+ mockOobRunner,
deviceId = deviceId,
inflater = mockInflater
)
@@ -657,22 +696,6 @@
)
}
- private fun setupOobSecureChannel(oobRunner: OobRunner? = null) {
- val oobEncryptionRunner = EncryptionRunnerFactory.newOobFakeRunner()
- secureChannel =
- spy(
- MultiProtocolSecureChannelPreV4(
- stream1,
- spyStorage,
- oobEncryptionRunner,
- oobRunner,
- deviceId = null,
- inflater = mockInflater
- )
- .apply { callback = mockCallback }
- )
- }
-
private fun initHandshakeMessage(message: ByteArray = FakeEncryptionRunner.INIT_MESSAGE) {
val deviceMessage =
DeviceMessage.createOutgoingMessage(
@@ -695,17 +718,6 @@
secureChannel.onDeviceMessageReceived(deviceMessage)
}
- private fun respondToOobCode(encryptedOobCode: ByteArray = "testCode".toByteArray()) {
- val message =
- DeviceMessage.createOutgoingMessage(
- /* recipient= */ null,
- /* isMessageEncrypted= */ false,
- OperationType.ENCRYPTION_HANDSHAKE,
- encryptedOobCode
- )
- secureChannel.onDeviceMessageReceived(message)
- }
-
private fun respondToResumeMessage(message: ByteArray = "Placeholder Message".toByteArray()) {
val deviceMessage =
DeviceMessage.createOutgoingMessage(
@@ -727,25 +739,25 @@
}
private open class TestProtocol : ConnectionProtocol() {
- override val isDeviceVerificationRequired = false
+ override fun isDeviceVerificationRequired() = false
override fun startAssociationDiscovery(
name: String,
- callback: DiscoveryCallback,
- identifier: UUID
+ identifier: ParcelUuid,
+ callback: IDiscoveryCallback
) {}
override fun startConnectionDiscovery(
- id: UUID,
+ id: ParcelUuid,
challenge: ConnectChallenge,
- callback: DiscoveryCallback
+ callback: IDiscoveryCallback
) {}
override fun stopAssociationDiscovery() {}
- override fun stopConnectionDiscovery(id: UUID) {}
+ override fun stopConnectionDiscovery(id: ParcelUuid) {}
- override fun sendData(protocolId: String, data: ByteArray, callback: DataSendCallback?) {}
+ override fun sendData(protocolId: String, data: ByteArray, callback: IDataSendCallback?) {}
override fun disconnectDevice(protocolId: String) {}
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 17927c5..f0a275a 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
@@ -16,6 +16,7 @@
package com.google.android.connecteddevice.connection
import android.content.Context
+import android.os.ParcelUuid
import android.util.Base64
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
@@ -29,7 +30,10 @@
import com.google.android.connecteddevice.storage.ConnectedDeviceDatabase
import com.google.android.connecteddevice.storage.ConnectedDeviceStorage
import com.google.android.connecteddevice.storage.CryptoHelper
+import com.google.android.connecteddevice.transport.ConnectChallenge
import com.google.android.connecteddevice.transport.ConnectionProtocol
+import com.google.android.connecteddevice.transport.IDataSendCallback
+import com.google.android.connecteddevice.transport.IDiscoveryCallback
import com.google.android.connecteddevice.transport.ProtocolDevice
import com.google.android.encryptionrunner.EncryptionRunnerFactory
import com.google.android.encryptionrunner.FakeEncryptionRunner
@@ -43,7 +47,6 @@
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
-import java.util.UUID
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -53,8 +56,7 @@
@RunWith(AndroidJUnit4::class)
class MultiProtocolSecureChannelV4Test {
private val context = ApplicationProvider.getApplicationContext<Context>()
- private val stream1 =
- spy(ProtocolStream(ProtocolDevice(TestProtocol(), PROTOCOL_ID), directExecutor()))
+ private val stream1 = spy(ProtocolStream(ProtocolDevice(TestProtocolV4(), PROTOCOL_ID)))
private lateinit var storage: ConnectedDeviceStorage
private val mockOobRunner: OobRunner = mock()
private val mockShowVerificationCodeListener: ShowVerificationCodeListener = mock()
@@ -68,7 +70,7 @@
.setQueryExecutor(directExecutor())
.build()
.associatedDeviceDao()
- storage = ConnectedDeviceStorage(context, Base64CryptoHelper(), database, directExecutor())
+ storage = ConnectedDeviceStorage(context, Base64CryptoHelperV4(), database, directExecutor())
}
@Test
fun processVerificationCodeMessage_oobVerification_verifyOobCode() {
@@ -78,8 +80,8 @@
val testPayload = "testPayload".toByteArray()
val testVerificationCodeMessage =
VerificationCode.newBuilder().run {
- setState(VerificationCodeState.OOB_VERIFICATION)
- setPayload(ByteString.copyFrom(testPayload))
+ state = VerificationCodeState.OOB_VERIFICATION
+ payload = ByteString.copyFrom(testPayload)
build()
}
val deviceMessage =
@@ -106,8 +108,8 @@
respondToContinueMessage(secureChannel)
val testVerificationCodeMessage =
VerificationCode.newBuilder().run {
- setState(VerificationCodeState.OOB_VERIFICATION)
- setPayload(ByteString.copyFrom(testPayload))
+ state = VerificationCodeState.OOB_VERIFICATION
+ payload = ByteString.copyFrom(testPayload)
build()
}
val deviceMessage =
@@ -220,26 +222,26 @@
}
}
-private class TestProtocol : ConnectionProtocol() {
- override val isDeviceVerificationRequired = false
+private class TestProtocolV4 : ConnectionProtocol() {
+ override fun isDeviceVerificationRequired() = false
override fun startAssociationDiscovery(
name: String,
- callback: DiscoveryCallback,
- identifier: UUID
+ identifier: ParcelUuid,
+ callback: IDiscoveryCallback
) {}
override fun startConnectionDiscovery(
- id: UUID,
+ id: ParcelUuid,
challenge: ConnectChallenge,
- callback: DiscoveryCallback
+ callback: IDiscoveryCallback
) {}
override fun stopAssociationDiscovery() {}
- override fun stopConnectionDiscovery(id: UUID) {}
+ override fun stopConnectionDiscovery(id: ParcelUuid) {}
- override fun sendData(protocolId: String, data: ByteArray, callback: DataSendCallback?) {}
+ override fun sendData(protocolId: String, data: ByteArray, callback: IDataSendCallback?) {}
override fun disconnectDevice(protocolId: String) {}
@@ -250,7 +252,7 @@
}
}
-private class Base64CryptoHelper : CryptoHelper {
+private class Base64CryptoHelperV4 : CryptoHelper {
override fun encrypt(value: ByteArray?): String? = Base64.encodeToString(value, Base64.DEFAULT)
override fun decrypt(value: String?): ByteArray? = Base64.decode(value, Base64.DEFAULT)
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/OobAssociationSecureChannelTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/OobAssociationSecureChannelTest.java
deleted file mode 100644
index 33e385a..0000000
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/OobAssociationSecureChannelTest.java
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice.connection;
-
-import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.google.android.connecteddevice.model.DeviceMessage;
-import com.google.android.connecteddevice.model.DeviceMessage.OperationType;
-import com.google.android.connecteddevice.oob.OobConnectionManager;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
-import com.google.android.connecteddevice.util.ByteUtils;
-import com.google.android.encryptionrunner.EncryptionRunnerFactory;
-import com.google.android.encryptionrunner.FakeEncryptionRunner;
-import java.util.UUID;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@RunWith(AndroidJUnit4.class)
-public class OobAssociationSecureChannelTest {
- private static final UUID CLIENT_DEVICE_ID =
- UUID.fromString("a5645523-3280-410a-90c1-582a6c6f4969");
-
- private static final UUID SERVER_DEVICE_ID =
- UUID.fromString("a29f0c74-2014-4b14-ac02-be6ed15b545a");
-
- private static final byte[] CLIENT_SECRET = ByteUtils.randomBytes(32);
-
- @Mock private DeviceMessageStream streamMock;
-
- @Mock private ConnectedDeviceStorage storageMock;
-
- @Mock private OobConnectionManager oobConnectionManagerMock;
-
- @Mock private SecureChannel.Callback channelCallback;
-
- private OobAssociationSecureChannel channel;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- when(storageMock.getUniqueId()).thenReturn(SERVER_DEVICE_ID);
- }
-
- @Test
- public void testEncryptionHandshake_oobAssociation() throws Exception {
- setupOobAssociationSecureChannel(channelCallback);
- ArgumentCaptor<String> deviceIdCaptor = ArgumentCaptor.forClass(String.class);
- ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(DeviceMessage.class);
-
- initHandshakeMessage();
- verify(streamMock).writeMessage(messageCaptor.capture());
- byte[] response = messageCaptor.getValue().getMessage();
- assertThat(response).isEqualTo(FakeEncryptionRunner.INIT_RESPONSE);
- reset(streamMock);
- respondToContinueMessage();
- respondToOobCode();
- verify(streamMock, times(2)).writeMessage(messageCaptor.capture());
- byte[] oobCodeResponse = messageCaptor.getAllValues().get(1).getMessage();
- assertThat(oobCodeResponse).isEqualTo(FakeEncryptionRunner.VERIFICATION_CODE);
- DeviceMessage severDeviceId = messageCaptor.getAllValues().get(2);
- channel.processMessage(severDeviceId);
- assertThat(severDeviceId.getMessage()).isEqualTo(ByteUtils.uuidToBytes(SERVER_DEVICE_ID));
- sendDeviceId();
- verify(channelCallback).onDeviceIdReceived(deviceIdCaptor.capture());
- assertThat(deviceIdCaptor.getValue()).isEqualTo(CLIENT_DEVICE_ID.toString());
- verify(storageMock).saveEncryptionKey(eq(CLIENT_DEVICE_ID.toString()), any());
- verify(storageMock).saveChallengeSecret(CLIENT_DEVICE_ID.toString(), CLIENT_SECRET);
-
- verify(channelCallback).onSecureChannelEstablished();
- }
-
- private void setupOobAssociationSecureChannel(SecureChannel.Callback callback) throws Exception {
- channel =
- new OobAssociationSecureChannel(
- streamMock,
- storageMock,
- oobConnectionManagerMock,
- EncryptionRunnerFactory.newOobFakeRunner());
- channel.registerCallback(callback);
-
- when(oobConnectionManagerMock.encryptVerificationCode(any()))
- .thenReturn(FakeEncryptionRunner.VERIFICATION_CODE);
- when(oobConnectionManagerMock.decryptVerificationCode(any()))
- .thenReturn(FakeEncryptionRunner.VERIFICATION_CODE);
- }
-
- private void sendDeviceId() {
- DeviceMessage message =
- DeviceMessage.createOutgoingMessage(
- /* recipient= */ null,
- /* isMessageEncrypted= */ true,
- OperationType.ENCRYPTION_HANDSHAKE,
- FakeEncryptionRunner.encryptDataWithFakeKey(
- ByteUtils.concatByteArrays(
- ByteUtils.uuidToBytes(CLIENT_DEVICE_ID), CLIENT_SECRET)));
- channel.onMessageReceived(message);
- }
-
- private void initHandshakeMessage() {
- DeviceMessage message =
- DeviceMessage.createOutgoingMessage(
- /* recipient= */ null,
- /* isMessageEncrypted= */ false,
- OperationType.ENCRYPTION_HANDSHAKE,
- FakeEncryptionRunner.INIT_MESSAGE);
- channel.onMessageReceived(message);
- }
-
- private void respondToContinueMessage() {
- DeviceMessage message =
- DeviceMessage.createOutgoingMessage(
- /* recipient= */ null,
- /* isMessageEncrypted= */ false,
- OperationType.ENCRYPTION_HANDSHAKE,
- FakeEncryptionRunner.CLIENT_RESPONSE);
- channel.onMessageReceived(message);
- }
-
- private void respondToOobCode() {
- DeviceMessage message =
- DeviceMessage.createOutgoingMessage(
- /* recipient= */ null,
- /* isMessageEncrypted= */ false,
- OperationType.ENCRYPTION_HANDSHAKE,
- FakeEncryptionRunner.VERIFICATION_CODE);
- channel.onMessageReceived(message);
- }
-}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/ProtocolStreamTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/ProtocolStreamTest.kt
index 24305c3..0312c91 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/ProtocolStreamTest.kt
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/ProtocolStreamTest.kt
@@ -15,6 +15,7 @@
*/
package com.google.android.connecteddevice.connection
+import android.os.ParcelUuid
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.android.companionprotos.DeviceMessageProto.Message
import com.google.android.companionprotos.OperationProto.OperationType
@@ -22,7 +23,10 @@
import com.google.android.connecteddevice.connection.ProtocolStream.MessageReceivedListener
import com.google.android.connecteddevice.connection.ProtocolStream.ProtocolDisconnectListener
import com.google.android.connecteddevice.model.DeviceMessage
+import com.google.android.connecteddevice.transport.ConnectChallenge
import com.google.android.connecteddevice.transport.ConnectionProtocol
+import com.google.android.connecteddevice.transport.IDataSendCallback
+import com.google.android.connecteddevice.transport.IDiscoveryCallback
import com.google.android.connecteddevice.transport.ProtocolDevice
import com.google.android.connecteddevice.util.ByteUtils
import com.google.common.truth.Truth
@@ -51,7 +55,7 @@
private val protocol = spy(TestProtocol())
- private val stream = ProtocolStream(ProtocolDevice(protocol, PROTOCOL_ID), directExecutor())
+ private val stream = ProtocolStream(ProtocolDevice(protocol, PROTOCOL_ID))
@Test
fun sendMessage_smallMessageSendsSinglePacket() {
@@ -133,8 +137,7 @@
@Test
fun protocolOnDataFailedToSend_disconnectsProtocol() {
val failingProtocol = spy(FailingSendProtocol())
- val failingStream =
- ProtocolStream(ProtocolDevice(failingProtocol, PROTOCOL_ID), directExecutor())
+ val failingStream = ProtocolStream(ProtocolDevice(failingProtocol, PROTOCOL_ID))
val recipient = UUID.randomUUID()
val message = ByteUtils.randomBytes(MAX_WRITE_SIZE / 2)
failingStream.sendMessage(
@@ -246,8 +249,8 @@
}
}
- open class TestProtocol : ConnectionProtocol() {
- override val isDeviceVerificationRequired = false
+ open class TestProtocol : ConnectionProtocol(directExecutor()) {
+ override fun isDeviceVerificationRequired() = false
fun receiveData(data: ByteArray) {
dataReceivedListeners[PROTOCOL_ID]?.invoke { it.onDataReceived(PROTOCOL_ID, data) }
@@ -255,21 +258,21 @@
override fun startAssociationDiscovery(
name: String,
- callback: DiscoveryCallback,
- identifier: UUID
+ identifier: ParcelUuid,
+ callback: IDiscoveryCallback
) {}
override fun startConnectionDiscovery(
- id: UUID,
+ id: ParcelUuid,
challenge: ConnectChallenge,
- callback: DiscoveryCallback
+ callback: IDiscoveryCallback
) {}
override fun stopAssociationDiscovery() {}
- override fun stopConnectionDiscovery(id: UUID) {}
+ override fun stopConnectionDiscovery(id: ParcelUuid) {}
- override fun sendData(protocolId: String, data: ByteArray, callback: DataSendCallback?) {
+ override fun sendData(protocolId: String, data: ByteArray, callback: IDataSendCallback?) {
callback?.onDataSentSuccessfully()
}
@@ -284,26 +287,26 @@
}
}
- open class FailingSendProtocol : ConnectionProtocol() {
- override val isDeviceVerificationRequired = false
+ open class FailingSendProtocol : ConnectionProtocol(directExecutor()) {
+ override fun isDeviceVerificationRequired() = false
override fun startAssociationDiscovery(
name: String,
- callback: DiscoveryCallback,
- identifier: UUID
+ identifier: ParcelUuid,
+ callback: IDiscoveryCallback
) {}
override fun startConnectionDiscovery(
- id: UUID,
+ id: ParcelUuid,
challenge: ConnectChallenge,
- callback: DiscoveryCallback
+ callback: IDiscoveryCallback
) {}
override fun stopAssociationDiscovery() {}
- override fun stopConnectionDiscovery(id: UUID) {}
+ override fun stopConnectionDiscovery(id: ParcelUuid) {}
- override fun sendData(protocolId: String, data: ByteArray, callback: DataSendCallback?) {
+ override fun sendData(protocolId: String, data: ByteArray, callback: IDataSendCallback?) {
callback?.onDataFailedToSend()
}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/SecureChannelTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/SecureChannelTest.java
deleted file mode 100644
index 358de56..0000000
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/SecureChannelTest.java
+++ /dev/null
@@ -1,226 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice.connection;
-
-import static com.google.android.connecteddevice.connection.SecureChannel.CHANNEL_ERROR_INVALID_HANDSHAKE;
-import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.isNull;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.google.android.connecteddevice.model.DeviceMessage;
-import com.google.android.connecteddevice.model.DeviceMessage.OperationType;
-import com.google.android.connecteddevice.util.ByteUtils;
-import com.google.android.encryptionrunner.EncryptionRunnerFactory;
-import com.google.android.encryptionrunner.HandshakeException;
-import com.google.android.encryptionrunner.Key;
-import java.security.SignatureException;
-import java.util.UUID;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@RunWith(AndroidJUnit4.class)
-public class SecureChannelTest {
- @Mock private DeviceMessageStream mockStream;
- @Mock private SecureChannel.Callback mockCallback;
-
- private Key fakeKey;
-
- private SecureChannel secureChannel;
-
- @Before
- public void setUp() throws SignatureException {
- MockitoAnnotations.initMocks(this);
-
- fakeKey =
- spy(
- new Key() {
- @Override
- public byte[] asBytes() {
- return new byte[0];
- }
-
- @Override
- public byte[] encryptData(byte[] data) {
- return data;
- }
-
- @Override
- public byte[] decryptData(byte[] encryptedData) {
- return encryptedData;
- }
-
- @Override
- public byte[] getUniqueSession() {
- return new byte[0];
- }
- });
-
- secureChannel =
- new SecureChannel(mockStream, EncryptionRunnerFactory.newFakeRunner()) {
- @Override
- void processHandshake(byte[] message) {}
- };
- secureChannel.setEncryptionKey(fakeKey);
- }
-
- @Test
- public void processMessage_doesNothingForUnencryptedMessage() throws SignatureException {
- byte[] payload = ByteUtils.randomBytes(10);
- DeviceMessage message =
- DeviceMessage.createOutgoingMessage(
- UUID.randomUUID(),
- /* isMessageEncrypted= */ false,
- OperationType.CLIENT_MESSAGE,
- payload);
- secureChannel.processMessage(message);
- assertThat(message.getMessage()).isEqualTo(payload);
- verify(fakeKey, never()).decryptData(any());
- }
-
- @Test
- public void processMessage_decryptsEncryptedMessage() throws SignatureException {
- byte[] payload = ByteUtils.randomBytes(10);
- DeviceMessage message =
- DeviceMessage.createOutgoingMessage(
- UUID.randomUUID(),
- /* isMessageEncrypted= */ true,
- OperationType.CLIENT_MESSAGE,
- payload);
- secureChannel.processMessage(message);
- verify(fakeKey).decryptData(any());
- }
-
- @Test
- public void processMessage_onMessageReceivedErrorForEncryptedMessageWithNoKey()
- throws InterruptedException {
- DeviceMessage message =
- DeviceMessage.createOutgoingMessage(
- UUID.randomUUID(),
- /* isMessageEncrypted= */ true,
- OperationType.CLIENT_MESSAGE,
- ByteUtils.randomBytes(10));
-
- secureChannel.setEncryptionKey(null);
- secureChannel.registerCallback(mockCallback);
- secureChannel.processMessage(message);
-
- verify(mockCallback).onMessageReceivedError(isNull());
- assertThat(message.getMessage()).isNull();
- }
-
- @Test
- public void onMessageReceived_onEstablishSecureChannelFailureBadHandshakeMessage() {
- DeviceMessage message =
- DeviceMessage.createOutgoingMessage(
- UUID.randomUUID(),
- /* isMessageEncrypted= */ true,
- OperationType.ENCRYPTION_HANDSHAKE,
- ByteUtils.randomBytes(10));
-
- secureChannel.setEncryptionKey(null);
- secureChannel.registerCallback(mockCallback);
- secureChannel.onMessageReceived(message);
-
- verify(mockCallback).onEstablishSecureChannelFailure(CHANNEL_ERROR_INVALID_HANDSHAKE);
- }
-
- @Test
- public void onMessageReceived_onMessageReceivedNotIssuedForNullMessage() {
- DeviceMessage message =
- DeviceMessage.createOutgoingMessage(
- UUID.randomUUID(),
- /* isMessageEncrypted= */ false,
- OperationType.CLIENT_MESSAGE,
- /* message= */ null);
-
- secureChannel.registerCallback(mockCallback);
- secureChannel.onMessageReceived(message);
-
- verify(mockCallback, never()).onMessageReceived(any());
- }
-
- @Test
- public void onMessageReceived_processHandshakeExceptionIssuesSecureChannelFailureCallback() {
- SecureChannel secureChannel =
- new SecureChannel(mockStream, EncryptionRunnerFactory.newFakeRunner()) {
- @Override
- void processHandshake(byte[] message) throws HandshakeException {
- throw new HandshakeException("test");
- }
- };
- secureChannel.registerCallback(mockCallback);
- DeviceMessage message =
- DeviceMessage.createOutgoingMessage(
- UUID.randomUUID(),
- /* isMessageEncrypted= */ true,
- OperationType.ENCRYPTION_HANDSHAKE,
- ByteUtils.randomBytes(10));
-
- secureChannel.onMessageReceived(message);
-
- verify(mockCallback).onEstablishSecureChannelFailure(CHANNEL_ERROR_INVALID_HANDSHAKE);
- }
-
- @Test
- public void decompressMessage_returnsOriginalMessageIfOriginalSizeIsZero() {
- byte[] message = ByteUtils.randomBytes(10);
- DeviceMessage deviceMessage =
- DeviceMessage.createOutgoingMessage(
- UUID.randomUUID(),
- /* isMessageEncrypted= */ false,
- OperationType.CLIENT_MESSAGE,
- message);
- deviceMessage.setOriginalMessageSize(0);
- assertThat(secureChannel.decompressMessage(deviceMessage)).isTrue();
- assertThat(deviceMessage.getMessage()).isEqualTo(message);
- }
-
- @Test
- public void compressMessage_returnsCompressedMessageWithOriginalSize() {
- byte[] message = new byte[100];
- DeviceMessage deviceMessage =
- DeviceMessage.createOutgoingMessage(
- UUID.randomUUID(),
- /* isMessageEncrypted= */ false,
- OperationType.CLIENT_MESSAGE,
- message);
- secureChannel.compressMessage(deviceMessage);
- assertThat(deviceMessage.getMessage()).isNotEqualTo(message);
- assertThat(deviceMessage.getOriginalMessageSize()).isEqualTo(message.length);
- }
-
- @Test
- public void compressedMessageCanBeDecompressed() {
- byte[] message = new byte[100];
- DeviceMessage deviceMessage =
- DeviceMessage.createOutgoingMessage(
- UUID.randomUUID(),
- /* isMessageEncrypted= */ false,
- OperationType.CLIENT_MESSAGE,
- message);
- secureChannel.compressMessage(deviceMessage);
- assertThat(secureChannel.decompressMessage(deviceMessage)).isTrue();
- assertThat(deviceMessage.getMessage()).isEqualTo(message);
- }
-}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/ble/CarBlePeripheralManagerTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/ble/CarBlePeripheralManagerTest.java
deleted file mode 100644
index 00b9ba5..0000000
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/ble/CarBlePeripheralManagerTest.java
+++ /dev/null
@@ -1,299 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice.connection.ble;
-
-import static com.google.common.truth.Truth.assertThat;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGattService;
-import android.bluetooth.BluetoothManager;
-import android.bluetooth.le.AdvertiseCallback;
-import android.bluetooth.le.AdvertiseData;
-import android.bluetooth.le.AdvertiseSettings;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.ParcelUuid;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.google.android.companionprotos.VersionExchangeProto.VersionExchange;
-import com.google.android.connecteddevice.connection.AssociationCallback;
-import com.google.android.connecteddevice.connection.AssociationSecureChannel;
-import com.google.android.connecteddevice.connection.ConnectionResolver;
-import com.google.android.connecteddevice.connection.SecureChannel;
-import com.google.android.connecteddevice.model.AssociatedDevice;
-import com.google.android.connecteddevice.model.OobData;
-import com.google.android.connecteddevice.model.StartAssociationResponse;
-import com.google.android.connecteddevice.oob.OobConnectionManager;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
-import com.google.android.connecteddevice.transport.ble.BlePeripheralManager;
-import com.google.android.connecteddevice.util.ByteUtils;
-import java.time.Duration;
-import java.util.Arrays;
-import java.util.UUID;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-import org.robolectric.shadows.ShadowLooper;
-
-@RunWith(AndroidJUnit4.class)
-public class CarBlePeripheralManagerTest {
- private static final UUID ASSOCIATION_SERVICE_UUID = UUID.randomUUID();
- private static final UUID RECONNECT_SERVICE_UUID = UUID.randomUUID();
- private static final UUID RECONNECT_DATA_UUID = UUID.randomUUID();
- private static final UUID ADVERTISE_DATA_CHARACTERISTIC_UUID = UUID.randomUUID();
- private static final UUID WRITE_UUID = UUID.randomUUID();
- private static final UUID READ_UUID = UUID.randomUUID();
- private static final int DEVICE_NAME_LENGTH_LIMIT = 2;
- private static final String TEST_REMOTE_DEVICE_ADDRESS = "00:11:22:33:AA:BB";
- private static final UUID TEST_REMOTE_DEVICE_ID = UUID.randomUUID();
- private static final String TEST_VERIFICATION_CODE = "000000";
- private static final String TEST_ENCRYPTED_VERIFICATION_CODE = "12345";
- private static final Duration RECONNECT_ADVERTISEMENT_DURATION = Duration.ofMillis(2);
- private static final int DEFAULT_MTU_SIZE = 23;
- private static final boolean COMPRESSION_ENABLED = true;
- private static final boolean EXCHANGE_CAPABILITIES = false;
-
- @Rule public final MockitoRule mockito = MockitoJUnit.rule();
-
- @Mock private BlePeripheralManager mockBlePeripheralManager;
- @Mock private ConnectedDeviceStorage mockConnectedDeviceStorage;
- @Mock private OobConnectionManager mockOobConnectionManager;
- @Mock private AssociationCallback mockAssociationCallback;
- private CarBlePeripheralManager carBlePeripheralManager;
-
- @Before
- public void setUp() throws Exception {
- carBlePeripheralManager =
- new CarBlePeripheralManager(
- mockBlePeripheralManager,
- mockConnectedDeviceStorage,
- ASSOCIATION_SERVICE_UUID,
- RECONNECT_SERVICE_UUID,
- RECONNECT_DATA_UUID,
- ADVERTISE_DATA_CHARACTERISTIC_UUID,
- WRITE_UUID,
- READ_UUID,
- RECONNECT_ADVERTISEMENT_DURATION,
- DEFAULT_MTU_SIZE,
- COMPRESSION_ENABLED,
- EXCHANGE_CAPABILITIES);
-
- when(mockOobConnectionManager.encryptVerificationCode(TEST_VERIFICATION_CODE.getBytes(UTF_8)))
- .thenReturn(TEST_ENCRYPTED_VERIFICATION_CODE.getBytes(UTF_8));
- when(mockOobConnectionManager.decryptVerificationCode(
- TEST_ENCRYPTED_VERIFICATION_CODE.getBytes(UTF_8)))
- .thenReturn(TEST_VERIFICATION_CODE.getBytes(UTF_8));
-
- carBlePeripheralManager.start();
- }
-
- @After
- public void tearDown() {
- if (carBlePeripheralManager != null) {
- carBlePeripheralManager.stop();
- }
- }
-
- @Test
- public void testStartAssociationAdvertisingSuccess() {
- byte[] testDeviceName = getNameForAssociation();
- startAssociation(mockAssociationCallback, testDeviceName);
- ArgumentCaptor<AdvertiseData> advertisementDataCaptor =
- ArgumentCaptor.forClass(AdvertiseData.class);
- ArgumentCaptor<AdvertiseData> scanResponseDataCaptor =
- ArgumentCaptor.forClass(AdvertiseData.class);
- verify(mockBlePeripheralManager)
- .startAdvertising(
- any(), advertisementDataCaptor.capture(), scanResponseDataCaptor.capture(), any());
- AdvertiseData advertisementData = advertisementDataCaptor.getValue();
- ParcelUuid serviceUuid = new ParcelUuid(ASSOCIATION_SERVICE_UUID);
- assertThat(advertisementData.getServiceUuids()).contains(serviceUuid);
- AdvertiseData scanResponseData = scanResponseDataCaptor.getValue();
- assertThat(scanResponseData.getIncludeDeviceName()).isFalse();
- ParcelUuid dataUuid = new ParcelUuid(RECONNECT_DATA_UUID);
- assertThat(scanResponseData.getServiceData().get(dataUuid)).isEqualTo(testDeviceName);
- }
-
- @Test
- public void testStartAssociationAdvertisingFailure() {
- startAssociation(mockAssociationCallback, getNameForAssociation());
- ArgumentCaptor<AdvertiseCallback> callbackCaptor =
- ArgumentCaptor.forClass(AdvertiseCallback.class);
- verify(mockBlePeripheralManager)
- .startAdvertising(any(), any(), any(), callbackCaptor.capture());
- AdvertiseCallback advertiseCallback = callbackCaptor.getValue();
- int testErrorCode = 2;
- advertiseCallback.onStartFailure(testErrorCode);
- verify(mockAssociationCallback).onAssociationStartFailure();
- }
-
- @Test
- public void testNotifyAssociationSuccess() {
- byte[] testDeviceName = getNameForAssociation();
- startAssociation(mockAssociationCallback, testDeviceName);
- ArgumentCaptor<AdvertiseCallback> callbackCaptor =
- ArgumentCaptor.forClass(AdvertiseCallback.class);
- verify(mockBlePeripheralManager)
- .startAdvertising(any(), any(), any(), callbackCaptor.capture());
- AdvertiseCallback advertiseCallback = callbackCaptor.getValue();
- AdvertiseSettings settings = new AdvertiseSettings.Builder().build();
- advertiseCallback.onStartSuccess(settings);
- OobData emptyOobData = new OobData(new byte[0], new byte[0], new byte[0]);
- verify(mockAssociationCallback)
- .onAssociationStartSuccess(
- new StartAssociationResponse(
- emptyOobData, testDeviceName, ByteUtils.byteArrayToHexString(testDeviceName)));
- }
-
- @Test
- public void testShowVerificationCode() {
- AssociationSecureChannel channel = getChannelForAssociation(mockAssociationCallback);
- channel.getShowVerificationCodeListener().showVerificationCode(TEST_VERIFICATION_CODE);
- verify(mockAssociationCallback).onVerificationCodeAvailable(eq(TEST_VERIFICATION_CODE));
- }
-
- @Test
- public void testAssociationSuccess() {
- SecureChannel channel = getChannelForAssociation(mockAssociationCallback);
- SecureChannel.Callback channelCallback = channel.getCallback();
- assertThat(channelCallback).isNotNull();
- channelCallback.onDeviceIdReceived(TEST_REMOTE_DEVICE_ID.toString());
- channelCallback.onSecureChannelEstablished();
- ArgumentCaptor<AssociatedDevice> deviceCaptor = ArgumentCaptor.forClass(AssociatedDevice.class);
- verify(mockConnectedDeviceStorage).addAssociatedDeviceForDriver(deviceCaptor.capture());
- AssociatedDevice device = deviceCaptor.getValue();
- assertThat(device.getDeviceId()).isEqualTo(TEST_REMOTE_DEVICE_ID.toString());
- verify(mockAssociationCallback).onAssociationCompleted(eq(TEST_REMOTE_DEVICE_ID.toString()));
- }
-
- @Test
- public void testAssociationFailure_channelError() {
- SecureChannel channel = getChannelForAssociation(mockAssociationCallback);
- SecureChannel.Callback channelCallback = channel.getCallback();
- int testErrorCode = 1;
- assertThat(channelCallback).isNotNull();
- channelCallback.onDeviceIdReceived(TEST_REMOTE_DEVICE_ID.toString());
- channelCallback.onEstablishSecureChannelFailure(testErrorCode);
- verify(mockAssociationCallback).onAssociationError(eq(testErrorCode));
- }
-
- @Test
- public void connectToDevice_embedAdvertiseDataInCharacteristic() {
- carBlePeripheralManager.setTimeoutHandler(new Handler(Looper.getMainLooper()));
- when(mockConnectedDeviceStorage.hashWithChallengeSecret(any(), any()))
- .thenReturn(ByteUtils.randomBytes(32));
- carBlePeripheralManager.connectToDevice(UUID.randomUUID());
-
- ArgumentCaptor<AdvertiseData> advertiseDataCaptor =
- ArgumentCaptor.forClass(AdvertiseData.class);
- ArgumentCaptor<BluetoothGattService> gattServiceCaptor =
- ArgumentCaptor.forClass(BluetoothGattService.class);
- verify(mockBlePeripheralManager)
- .startAdvertising(gattServiceCaptor.capture(), advertiseDataCaptor.capture(), any(), any());
- assertThat(
- Arrays.equals(
- gattServiceCaptor
- .getValue()
- .getCharacteristic(ADVERTISE_DATA_CHARACTERISTIC_UUID)
- .getValue(),
- advertiseDataCaptor.getValue().getServiceData().values().iterator().next()))
- .isTrue();
- }
-
- @Test
- public void connectToDevice_stopsAdvertisingAfterTimeout() {
- carBlePeripheralManager.setTimeoutHandler(new Handler(Looper.getMainLooper()));
- when(mockConnectedDeviceStorage.hashWithChallengeSecret(any(), any()))
- .thenReturn(ByteUtils.randomBytes(32));
- carBlePeripheralManager.connectToDevice(UUID.randomUUID());
- ArgumentCaptor<AdvertiseCallback> callbackCaptor =
- ArgumentCaptor.forClass(AdvertiseCallback.class);
- verify(mockBlePeripheralManager)
- .startAdvertising(any(), any(), any(), callbackCaptor.capture());
- callbackCaptor.getValue().onStartSuccess(null);
- // Simulate the timeout.
- ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
- verify(mockBlePeripheralManager).stopAdvertising(any(AdvertiseCallback.class));
- }
-
- @Test
- public void disconnectDevice_stopsAdvertisingForPendingReconnect() {
- when(mockConnectedDeviceStorage.hashWithChallengeSecret(any(), any()))
- .thenReturn(ByteUtils.randomBytes(32));
- UUID deviceId = UUID.randomUUID();
- carBlePeripheralManager.connectToDevice(deviceId);
- reset(mockBlePeripheralManager);
- carBlePeripheralManager.disconnectDevice(deviceId.toString());
- verify(mockBlePeripheralManager).cleanup();
- }
-
- private AssociationSecureChannel getChannelForAssociation(AssociationCallback callback) {
- BlePeripheralManager.Callback bleManagerCallback =
- startAssociation(callback, getNameForAssociation());
- BluetoothDevice bluetoothDevice =
- ApplicationProvider.getApplicationContext()
- .getSystemService(BluetoothManager.class)
- .getAdapter()
- .getRemoteDevice(TEST_REMOTE_DEVICE_ADDRESS);
- bleManagerCallback.onRemoteDeviceConnected(bluetoothDevice);
- ArgumentCaptor<BlePeripheralManager.OnCharacteristicWriteListener> listenerCaptor =
- ArgumentCaptor.forClass(BlePeripheralManager.OnCharacteristicWriteListener.class);
- verify(mockBlePeripheralManager).addOnCharacteristicWriteListener(listenerCaptor.capture());
- // Use the MIN_SECURITY_VERSION for both to avoid further complexity introduced with
- // the capabilities exchange that has nothing to do with the tests here. In the future this
- // class will be deprecated in favor of a protocol agnostic version.
- VersionExchange versionExchangeMessage =
- VersionExchange.newBuilder()
- .setMinSupportedMessagingVersion(ConnectionResolver.MESSAGING_VERSION)
- .setMaxSupportedMessagingVersion(ConnectionResolver.MESSAGING_VERSION)
- .setMinSupportedSecurityVersion(ConnectionResolver.MIN_SECURITY_VERSION)
- .setMaxSupportedSecurityVersion(ConnectionResolver.MIN_SECURITY_VERSION)
- .build();
- listenerCaptor
- .getValue()
- .onCharacteristicWrite(
- bluetoothDevice,
- carBlePeripheralManager.readCharacteristic,
- versionExchangeMessage.toByteArray());
- return (AssociationSecureChannel) carBlePeripheralManager.getConnectedDeviceChannel();
- }
-
- private BlePeripheralManager.Callback startAssociation(
- AssociationCallback callback, byte[] deviceName) {
- ArgumentCaptor<BlePeripheralManager.Callback> callbackCaptor =
- ArgumentCaptor.forClass(BlePeripheralManager.Callback.class);
- carBlePeripheralManager.startAssociation(deviceName, callback);
- verify(mockBlePeripheralManager).registerCallback(callbackCaptor.capture());
- return callbackCaptor.getValue();
- }
-
- private static byte[] getNameForAssociation() {
- return ByteUtils.randomBytes(DEVICE_NAME_LENGTH_LIMIT);
- }
-}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/spp/CarSppManagerTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/spp/CarSppManagerTest.java
deleted file mode 100644
index ed1dedf..0000000
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/spp/CarSppManagerTest.java
+++ /dev/null
@@ -1,331 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice.connection.spp;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothManager;
-import android.os.RemoteException;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.google.android.companionprotos.VersionExchangeProto.VersionExchange;
-import com.google.android.connecteddevice.connection.AssociationCallback;
-import com.google.android.connecteddevice.connection.AssociationSecureChannel;
-import com.google.android.connecteddevice.connection.CarBluetoothManager;
-import com.google.android.connecteddevice.connection.ConnectionResolver;
-import com.google.android.connecteddevice.connection.ReconnectSecureChannel;
-import com.google.android.connecteddevice.connection.SecureChannel;
-import com.google.android.connecteddevice.model.AssociatedDevice;
-import com.google.android.connecteddevice.model.OobData;
-import com.google.android.connecteddevice.model.StartAssociationResponse;
-import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
-import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder;
-import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder.OnMessageReceivedListener;
-import com.google.android.connecteddevice.transport.spp.PendingConnection;
-import java.util.UUID;
-import java.util.concurrent.Executor;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-import org.mockito.stubbing.Answer;
-
-@RunWith(AndroidJUnit4.class)
-public class CarSppManagerTest {
- private static final String TEST_REMOTE_DEVICE_ADDRESS = "00:11:22:33:AA:BB";
- private static final UUID TEST_REMOTE_DEVICE_ID = UUID.randomUUID();
- private static final UUID TEST_SERVICE_UUID_1 = UUID.randomUUID();
- private static final boolean IS_SECURE = true;
- private static final String TEST_VERIFICATION_CODE = "000000";
- private static final int MAX_PACKET_SIZE = 700;
- private static final boolean COMPRESSION_ENABLED = true;
- private static final boolean EXCHANGE_CAPABILITIES = false;
- private static final BluetoothDevice TEST_BLUETOOTH_DEVICE =
- ApplicationProvider.getApplicationContext()
- .getSystemService(BluetoothManager.class)
- .getAdapter()
- .getRemoteDevice(TEST_REMOTE_DEVICE_ADDRESS);
-
- @Rule public final MockitoRule mockito = MockitoJUnit.rule();
- private final Executor callbackExecutor = directExecutor();
-
- @Mock private CarBluetoothManager.Callback mockCallback;
- @Mock private AssociationCallback mockAssociationCallback;
- @Mock private ConnectedDeviceSppDelegateBinder mockSppBinder;
- @Mock private ConnectedDeviceStorage mockStorage;
- private CarSppManager carSppManager;
- private ConnectionResultCaptor connectionResultCaptor;
-
- @Before
- public void setUp() throws Exception {
- connectionResultCaptor = new ConnectionResultCaptor();
- doAnswer(connectionResultCaptor).when(mockSppBinder).connectAsServer(any(), eq(true));
- carSppManager =
- new CarSppManager(
- mockSppBinder,
- mockStorage,
- TEST_SERVICE_UUID_1,
- MAX_PACKET_SIZE,
- COMPRESSION_ENABLED,
- EXCHANGE_CAPABILITIES);
- carSppManager.registerCallback(mockCallback, callbackExecutor);
- }
-
- @After
- public void tearDown() {
- if (carSppManager != null) {
- carSppManager.stop();
- }
- }
-
- @Test
- public void testStartAssociationSuccess() throws RemoteException {
- carSppManager.initiateConnectionToDevice(TEST_REMOTE_DEVICE_ID);
-
- carSppManager.startAssociation(/* nameForAssociation= */ null, mockAssociationCallback);
-
- verify(mockSppBinder).unregisterConnectionCallback(TEST_REMOTE_DEVICE_ID);
- verify(mockSppBinder).connectAsServer(TEST_SERVICE_UUID_1, IS_SECURE);
- OobData emptyOobData = new OobData(new byte[0], new byte[0], new byte[0]);
- verify(mockAssociationCallback)
- .onAssociationStartSuccess(new StartAssociationResponse(emptyOobData, new byte[0], ""));
- }
-
- @Test
- public void testStartAssociationFailure() throws RemoteException {
- when(mockSppBinder.connectAsServer(TEST_SERVICE_UUID_1, IS_SECURE)).thenReturn(null);
-
- carSppManager.startAssociation(/* nameForAssociation= */ null, mockAssociationCallback);
-
- verify(mockAssociationCallback).onAssociationStartFailure();
- }
-
- @Test
- public void testShowVerificationCode() {
- AssociationSecureChannel channel = getChannelForAssociation(mockAssociationCallback);
-
- channel.getShowVerificationCodeListener().showVerificationCode(TEST_VERIFICATION_CODE);
-
- verify(mockAssociationCallback).onVerificationCodeAvailable(TEST_VERIFICATION_CODE);
- }
-
- @Test
- public void testAssociationSuccess() {
- SecureChannel channel = getChannelForAssociation(mockAssociationCallback);
- SecureChannel.Callback channelCallback = channel.getCallback();
-
- assertThat(channelCallback).isNotNull();
-
- channelCallback.onDeviceIdReceived(TEST_REMOTE_DEVICE_ID.toString());
- channelCallback.onSecureChannelEstablished();
-
- ArgumentCaptor<AssociatedDevice> deviceCaptor = ArgumentCaptor.forClass(AssociatedDevice.class);
- verify(mockStorage).addAssociatedDeviceForDriver(deviceCaptor.capture());
- AssociatedDevice device = deviceCaptor.getValue();
-
- assertThat(device.getDeviceId()).isEqualTo(TEST_REMOTE_DEVICE_ID.toString());
-
- verify(mockAssociationCallback).onAssociationCompleted(TEST_REMOTE_DEVICE_ID.toString());
- }
-
- @Test
- public void testInitiateConnectionToDevice() throws RemoteException {
- carSppManager.initiateConnectionToDevice(TEST_SERVICE_UUID_1);
-
- verify(mockSppBinder).registerConnectionCallback(eq(TEST_SERVICE_UUID_1), any());
- verify(mockSppBinder).connectAsServer(TEST_SERVICE_UUID_1, IS_SECURE);
- }
-
- @Test
- public void testReset() throws RemoteException {
- carSppManager.startAssociation(/* nameForAssociation= */ null, mockAssociationCallback);
- PendingConnection connection = connectionResultCaptor.getResult();
- connection.notifyConnected(TEST_BLUETOOTH_DEVICE, TEST_BLUETOOTH_DEVICE.getName());
-
- carSppManager.reset();
-
- verify(mockSppBinder).disconnect(connection.toConnection(TEST_BLUETOOTH_DEVICE));
- }
-
- @Test
- public void testResetBeforeConnection() throws RemoteException {
- carSppManager.startAssociation(/* nameForAssociation= */ null, mockAssociationCallback);
-
- carSppManager.reset();
-
- verify(mockSppBinder).cancelConnectionAttempt(any());
- }
-
- @Test
- public void testAssociationCallbackOnErrorSucceed() {
- SecureChannel channel = getChannelForAssociation(mockAssociationCallback);
- SecureChannel.Callback channelCallback = channel.getCallback();
- channelCallback.onDeviceIdReceived(TEST_REMOTE_DEVICE_ID.toString());
- channelCallback.onSecureChannelEstablished();
-
- carSppManager.reconnectOnErrorListener.onError(
- connectionResultCaptor.getResult().toConnection(TEST_BLUETOOTH_DEVICE));
-
- verify(mockCallback).onDeviceDisconnected(TEST_REMOTE_DEVICE_ID.toString());
- assertThat(carSppManager.currentConnection).isNull();
- }
-
- @Test
- public void testAssociationCallbackOnErrorFailed() {
- SecureChannel channel = getChannelForAssociation(mockAssociationCallback);
- SecureChannel.Callback channelCallback = channel.getCallback();
- channelCallback.onDeviceIdReceived(TEST_REMOTE_DEVICE_ID.toString());
- channelCallback.onSecureChannelEstablished();
-
- connectionResultCaptor.getResult().notifyConnectionError();
-
- verify(mockCallback, never()).onDeviceDisconnected(TEST_REMOTE_DEVICE_ID.toString());
- assertThat(carSppManager.currentConnection).isNotNull();
- }
-
- @Test
- public void testAssociationCallbackOnConnected() {
- carSppManager.startAssociation(/* nameForAssociation= */ null, mockAssociationCallback);
- PendingConnection connection = connectionResultCaptor.getResult();
- connection.notifyConnected(TEST_BLUETOOTH_DEVICE, TEST_BLUETOOTH_DEVICE.getName());
-
- assertThat(carSppManager.currentConnection)
- .isEqualTo(connection.toConnection(TEST_BLUETOOTH_DEVICE));
- }
-
- @Test
- public void testAssociationCallbackOnConnectAttemptFailed() {
- carSppManager.startAssociation(/* nameForAssociation= */ null, mockAssociationCallback);
- connectionResultCaptor.getResult().notifyConnectionError();
-
- verify(mockCallback, never()).onDeviceDisconnected(any());
- }
-
- @Test
- public void testReconnectCallbackOnConnected() {
- carSppManager.startAssociation(/* nameForAssociation= */ null, mockAssociationCallback);
- PendingConnection connection = connectionResultCaptor.getResult();
- connection.notifyConnected(TEST_BLUETOOTH_DEVICE, TEST_BLUETOOTH_DEVICE.getName());
-
- assertThat(carSppManager.currentConnection)
- .isEqualTo(connection.toConnection(TEST_BLUETOOTH_DEVICE));
- }
-
- @Test
- public void testReconnectCallbackOnErrorSucceed() {
- SecureChannel channel = getChannelForReconnect();
- SecureChannel.Callback channelCallback = channel.getCallback();
- channelCallback.onSecureChannelEstablished();
-
- carSppManager.reconnectOnErrorListener.onError(
- connectionResultCaptor.getResult().toConnection(TEST_BLUETOOTH_DEVICE));
-
- verify(mockCallback).onDeviceDisconnected(TEST_REMOTE_DEVICE_ID.toString());
- assertThat(carSppManager.currentConnection).isNull();
- }
-
- @Test
- public void testReconnectCallbackOnErrorFailed() {
- SecureChannel channel = getChannelForReconnect();
- SecureChannel.Callback channelCallback = channel.getCallback();
- channelCallback.onSecureChannelEstablished();
-
- connectionResultCaptor.getResult().notifyConnectionError();
-
- verify(mockCallback, never()).onDeviceDisconnected(TEST_REMOTE_DEVICE_ID.toString());
- assertThat(carSppManager.currentConnection).isNotNull();
- }
-
- @Test
- public void testReconnectCallbackOnConnectAttemptFailed() {
- carSppManager.startAssociation(/* nameForAssociation= */ null, mockAssociationCallback);
- connectionResultCaptor.getResult().notifyConnectionError();
-
- verify(mockCallback, never()).onDeviceDisconnected(any());
- }
-
- private AssociationSecureChannel getChannelForAssociation(AssociationCallback callback) {
- carSppManager.startAssociation(/* nameForAssociation= */ null, callback);
-
- connectionResultCaptor
- .getResult()
- .notifyConnected(TEST_BLUETOOTH_DEVICE, TEST_BLUETOOTH_DEVICE.getName());
- completeVersionExchange();
-
- return (AssociationSecureChannel) carSppManager.getConnectedDeviceChannel();
- }
-
- private ReconnectSecureChannel getChannelForReconnect() {
- carSppManager.initiateConnectionToDevice(TEST_REMOTE_DEVICE_ID);
-
- connectionResultCaptor
- .getResult()
- .notifyConnected(TEST_BLUETOOTH_DEVICE, TEST_BLUETOOTH_DEVICE.getName());
- completeVersionExchange();
-
- return (ReconnectSecureChannel) carSppManager.getConnectedDeviceChannel();
- }
-
- private void completeVersionExchange() {
- ArgumentCaptor<OnMessageReceivedListener> listenerCaptor =
- ArgumentCaptor.forClass(OnMessageReceivedListener.class);
- verify(mockSppBinder)
- .setOnMessageReceivedListener(
- eq(carSppManager.currentConnection), listenerCaptor.capture());
- // Use the MIN_SECURITY_VERSION for both to avoid further complexity introduced with
- // the capabilities exchange that has nothing to do with the tests here. In the future this
- // class will be deprecated in favor of a protocol agnostic version.
- VersionExchange versionExchangeMessage =
- VersionExchange.newBuilder()
- .setMinSupportedMessagingVersion(ConnectionResolver.MESSAGING_VERSION)
- .setMaxSupportedMessagingVersion(ConnectionResolver.MESSAGING_VERSION)
- .setMinSupportedSecurityVersion(ConnectionResolver.MIN_SECURITY_VERSION)
- .setMaxSupportedSecurityVersion(ConnectionResolver.MIN_SECURITY_VERSION)
- .build();
- listenerCaptor.getValue().onMessageReceived(versionExchangeMessage.toByteArray());
- }
-
- private static class ConnectionResultCaptor implements Answer<PendingConnection> {
- private PendingConnection result;
-
- public PendingConnection getResult() {
- return result;
- }
-
- @Override
- public PendingConnection answer(InvocationOnMock invocationOnMock) {
- UUID uuid = invocationOnMock.getArgument(0);
- boolean isSecure = invocationOnMock.getArgument(1);
- result = new PendingConnection(uuid, isSecure);
- return result;
- }
- }
-}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/spp/SppDeviceMessageStreamTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/spp/SppDeviceMessageStreamTest.java
deleted file mode 100644
index 48951bd..0000000
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/spp/SppDeviceMessageStreamTest.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice.connection.spp;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-
-import android.bluetooth.BluetoothManager;
-import android.os.ParcelUuid;
-import android.os.RemoteException;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.google.android.connecteddevice.connection.DeviceMessageStream.DataReceivedListener;
-import com.google.android.connecteddevice.connection.DeviceMessageStream.MessageReceivedErrorListener;
-import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder;
-import com.google.android.connecteddevice.transport.spp.Connection;
-import java.util.UUID;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-
-@RunWith(AndroidJUnit4.class)
-public class SppDeviceMessageStreamTest {
- private static final int MAX_WRITE_SIZE = 700;
- private static final byte[] TEST_DATA = "testData".getBytes(UTF_8);
- private static final UUID TEST_UUID = UUID.randomUUID();
- private static final boolean IS_SECURE = true;
-
- private static final Connection TEST_CONNECTION =
- new Connection(
- new ParcelUuid(TEST_UUID),
- ApplicationProvider.getApplicationContext()
- .getSystemService(BluetoothManager.class)
- .getAdapter()
- .getRemoteDevice("00:11:22:33:44:55"),
- IS_SECURE,
- "testName");
- @Rule public final MockitoRule mockito = MockitoJUnit.rule();
-
- @Mock private ConnectedDeviceSppDelegateBinder mockSppBinder;
- @Mock private MessageReceivedErrorListener mockErrorListener;
- @Mock private DataReceivedListener mockListener;
-
- private SppDeviceMessageStream sppDeviceMessageStream;
-
- @Before
- public void setUp() {
- sppDeviceMessageStream =
- spy(new SppDeviceMessageStream(mockSppBinder, TEST_CONNECTION, MAX_WRITE_SIZE));
- sppDeviceMessageStream.setMessageReceivedErrorListener(mockErrorListener);
- sppDeviceMessageStream.setDataReceivedListener(mockListener);
- }
-
- @Test
- public void testSend() throws RemoteException {
- sppDeviceMessageStream.send(TEST_DATA);
- verify(mockSppBinder).sendMessage(TEST_CONNECTION, TEST_DATA);
- verify(sppDeviceMessageStream).sendCompleted();
- }
-
- @Test
- public void testOnMessageReceived_connectionIsNotResolved() {
- verify(mockSppBinder).setOnMessageReceivedListener(eq(TEST_CONNECTION), any());
-
- sppDeviceMessageStream.onMessageReceived(TEST_DATA);
-
- verify(mockListener).onDataReceived(TEST_DATA);
- }
-
- @Test
- public void testOnMessageReceived_connectionIsResolved() {
- sppDeviceMessageStream.setConnectionResolved(true);
- verify(mockSppBinder).setOnMessageReceivedListener(eq(TEST_CONNECTION), any());
-
- sppDeviceMessageStream.onMessageReceived(TEST_DATA);
-
- verify(mockErrorListener).onMessageReceivedError(any());
- }
-}
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 2fae8f9..75316fc 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
@@ -17,13 +17,13 @@
import android.content.Context
import android.database.sqlite.SQLiteCantOpenDatabaseException
+import android.os.ParcelUuid
import android.util.Base64
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.android.connecteddevice.api.IAssociationCallback
import com.google.android.connecteddevice.connection.MultiProtocolSecureChannel
-import com.google.android.connecteddevice.connection.MultiProtocolSecureChannelPreV4
import com.google.android.connecteddevice.connection.ProtocolStream
import com.google.android.connecteddevice.core.DeviceController.Callback
import com.google.android.connecteddevice.core.util.mockToBeAlive
@@ -40,9 +40,11 @@
import com.google.android.connecteddevice.storage.ConnectedDeviceStorage
import com.google.android.connecteddevice.storage.ConnectedDeviceStorage.CHALLENGE_SECRET_BYTES
import com.google.android.connecteddevice.storage.CryptoHelper
+import com.google.android.connecteddevice.transport.ConnectChallenge
import com.google.android.connecteddevice.transport.ConnectionProtocol
+import com.google.android.connecteddevice.transport.IDataSendCallback
+import com.google.android.connecteddevice.transport.IDiscoveryCallback
import com.google.android.connecteddevice.util.ByteUtils
-import com.google.android.connecteddevice.util.ThreadSafeCallbacks
import com.google.android.encryptionrunner.EncryptionRunnerFactory
import com.google.common.truth.Truth.assertThat
import com.google.common.util.concurrent.MoreExecutors.directExecutor
@@ -72,13 +74,13 @@
class MultiProtocolDeviceControllerTest {
private val context = ApplicationProvider.getApplicationContext<Context>()
private val testConnectionProtocol: TestConnectionProtocol = spy(TestConnectionProtocol())
- private val mockCallback: Callback = mock()
- private val mockStream: ProtocolStream = mock()
- private val mockOobRunner: OobRunner = mock { on { generateOobData() } doReturn TEST_OOB_DATA }
- private val mockAssociationCallback: IAssociationCallback = mockToBeAlive()
- private val mockDeadAssociationCallback: IAssociationCallback = mockToBeDead()
+ private val mockCallback = mock<Callback>()
+ private val mockStream = mock<ProtocolStream>()
+ private val mockOobRunner = mock<OobRunner> { on { generateOobData() } doReturn TEST_OOB_DATA }
+ private val mockAssociationCallback = mockToBeAlive<IAssociationCallback>()
+ private val mockDeadAssociationCallback = mockToBeDead<IAssociationCallback>()
private val protocols = setOf(testConnectionProtocol)
- private val testAssociationServiceUuid = UUID.randomUUID()
+ private val testAssociationServiceUuid = ParcelUuid(UUID.randomUUID())
private lateinit var deviceController: MultiProtocolDeviceController
private lateinit var secureChannel: MultiProtocolSecureChannel
private lateinit var spyStorage: ConnectedDeviceStorage
@@ -100,18 +102,14 @@
protocols,
spyStorage,
mockOobRunner,
- testAssociationServiceUuid,
+ testAssociationServiceUuid.uuid,
enablePassenger = false,
callbackExecutor = directExecutor()
)
deviceController.registerCallback(mockCallback, directExecutor())
secureChannel =
spy(
- MultiProtocolSecureChannelPreV4(
- mockStream,
- spyStorage,
- EncryptionRunnerFactory.newFakeRunner()
- )
+ MultiProtocolSecureChannel(mockStream, spyStorage, EncryptionRunnerFactory.newFakeRunner())
)
}
@@ -146,7 +144,7 @@
protocols,
transientErrorStorage,
mockOobRunner,
- testAssociationServiceUuid,
+ testAssociationServiceUuid.uuid,
enablePassenger = false,
callbackExecutor = directExecutor()
)
@@ -157,7 +155,7 @@
@Test
fun start_connectsToPassengerDevicesWhenPassengerEnabled() {
- val driverId = UUID.randomUUID()
+ val driverId = ParcelUuid(UUID.randomUUID())
val driverDevice =
AssociatedDevice(
driverId.toString(),
@@ -165,7 +163,7 @@
/* deviceName= */ null,
/* isConnectionEnabled= */ true
)
- val passengerId = UUID.randomUUID()
+ val passengerId = ParcelUuid(UUID.randomUUID())
val passengerDevice =
AssociatedDevice(
passengerId.toString(),
@@ -181,7 +179,7 @@
protocols,
spyStorage,
mockOobRunner,
- testAssociationServiceUuid,
+ testAssociationServiceUuid.uuid,
enablePassenger = true,
callbackExecutor = directExecutor()
)
@@ -194,15 +192,15 @@
@Test
fun start_doesNotConnectToPassengerDevicesWhenPassengerDisabled() {
- val driverId = UUID.randomUUID()
+ val driverId = ParcelUuid(UUID.randomUUID())
val driverDevice =
AssociatedDevice(
- driverId.toString(),
+ driverId.uuid.toString(),
/* deviceAddress= */ "",
/* deviceName= */ null,
/* isConnectionEnabled= */ true
)
- val passengerId = UUID.randomUUID()
+ val passengerId = ParcelUuid(UUID.randomUUID())
val passengerDevice =
AssociatedDevice(
passengerId.toString(),
@@ -218,7 +216,7 @@
protocols,
spyStorage,
mockOobRunner,
- testAssociationServiceUuid,
+ testAssociationServiceUuid.uuid,
enablePassenger = false,
callbackExecutor = directExecutor()
)
@@ -236,18 +234,18 @@
deviceController.startAssociation(deviceName, mockAssociationCallback)
verify(testConnectionProtocol)
- .startAssociationDiscovery(eq(deviceName), any(), eq(testAssociationServiceUuid))
+ .startAssociationDiscovery(eq(deviceName), eq(testAssociationServiceUuid), any())
}
@Test
fun startAssociation_startedSuccessfully() {
val deviceName = "TestDeviceName"
- val testIdentifier = UUID.randomUUID()
+ val testIdentifier = ParcelUuid(UUID.randomUUID())
- deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier)
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+ deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier.uuid)
+ argumentCaptor<IDiscoveryCallback>().apply {
verify(testConnectionProtocol)
- .startAssociationDiscovery(eq(deviceName), capture(), eq(testIdentifier))
+ .startAssociationDiscovery(eq(deviceName), eq(testIdentifier), capture())
firstValue.onDiscoveryStartedSuccessfully()
}
@@ -270,12 +268,12 @@
@Test
fun startAssociation_callbackBinderIsDead_DoNotThrow() {
val deviceName = "TestDeviceName"
- val testIdentifier = UUID.randomUUID()
+ val testIdentifier = ParcelUuid(UUID.randomUUID())
- deviceController.startAssociation(deviceName, mockDeadAssociationCallback, testIdentifier)
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+ deviceController.startAssociation(deviceName, mockDeadAssociationCallback, testIdentifier.uuid)
+ argumentCaptor<IDiscoveryCallback>().apply {
verify(testConnectionProtocol)
- .startAssociationDiscovery(eq(deviceName), capture(), eq(testIdentifier))
+ .startAssociationDiscovery(eq(deviceName), eq(testIdentifier), capture())
firstValue.onDiscoveryStartedSuccessfully()
}
verify(mockAssociationCallback, never())
@@ -291,12 +289,12 @@
@Test
fun startAssociation_onDiscoveryFailedToStartInvokesOnAssociationStartFailure() {
val deviceName = "TestDeviceName"
- val testIdentifier = UUID.randomUUID()
+ val testIdentifier = ParcelUuid(UUID.randomUUID())
- deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier)
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+ deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier.uuid)
+ argumentCaptor<IDiscoveryCallback>().apply {
verify(testConnectionProtocol)
- .startAssociationDiscovery(eq(deviceName), capture(), eq(testIdentifier))
+ .startAssociationDiscovery(eq(deviceName), eq(testIdentifier), capture())
firstValue.onDiscoveryFailedToStart()
}
@@ -306,12 +304,12 @@
@Test
fun startAssociation_onDiscoveryFailedToStart_callbackBinderDeadDoNotThrow() {
val deviceName = "TestDeviceName"
- val testIdentifier = UUID.randomUUID()
+ val testIdentifier = ParcelUuid(UUID.randomUUID())
- deviceController.startAssociation(deviceName, mockDeadAssociationCallback, testIdentifier)
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+ deviceController.startAssociation(deviceName, mockDeadAssociationCallback, testIdentifier.uuid)
+ argumentCaptor<IDiscoveryCallback>().apply {
verify(testConnectionProtocol)
- .startAssociationDiscovery(eq(deviceName), capture(), eq(testIdentifier))
+ .startAssociationDiscovery(eq(deviceName), eq(testIdentifier), capture())
firstValue.onDiscoveryFailedToStart()
}
@@ -320,9 +318,9 @@
@Test
fun initiateConnectionToDevice_invokesStartConnectionDiscovery() {
- val testUuid = UUID.randomUUID()
+ val testUuid = ParcelUuid(UUID.randomUUID())
- deviceController.initiateConnectionToDevice(testUuid)
+ deviceController.initiateConnectionToDevice(testUuid.uuid)
verify(testConnectionProtocol).startConnectionDiscovery(eq(testUuid), any(), any())
}
@@ -353,14 +351,14 @@
protocols,
spyStorage,
mockOobRunner,
- testAssociationServiceUuid,
+ testAssociationServiceUuid.uuid,
enablePassenger = false,
callbackExecutor = directExecutor()
)
deviceController.registerCallback(mockCallback, directExecutor())
deviceController.start()
deviceController.initiateConnectionToDevice(deviceId)
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+ argumentCaptor<IDiscoveryCallback>().apply {
verify(testConnectionProtocol).startConnectionDiscovery(any(), any(), capture())
firstValue.onDeviceConnected(UUID.randomUUID().toString())
}
@@ -377,7 +375,7 @@
fun onDeviceConnected_registerDeviceDisconnectedListener() {
val testUuid = UUID.randomUUID()
deviceController.initiateConnectionToDevice(testUuid)
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+ argumentCaptor<IDiscoveryCallback>().apply {
verify(testConnectionProtocol).startConnectionDiscovery(any(), any(), capture())
firstValue.onDeviceConnected(UUID.randomUUID().toString())
}
@@ -401,7 +399,7 @@
)
)
deviceController.initiateConnectionToDevice(deviceId)
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+ argumentCaptor<IDiscoveryCallback>().apply {
verify(testConnectionProtocol).startConnectionDiscovery(any(), any(), capture())
firstValue.onDeviceConnected(testProtocolId.toString())
}
@@ -417,10 +415,10 @@
@Test
fun onDeviceDisconnected_duringAssociation_invokesAssociationErrorCallback() {
val testProtocolId = UUID.randomUUID()
- val testIdentifier = UUID.randomUUID()
- deviceController.startAssociation("deviceName", mockAssociationCallback, testIdentifier)
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
- verify(testConnectionProtocol).startAssociationDiscovery(any(), capture(), eq(testIdentifier))
+ val testIdentifier = ParcelUuid(UUID.randomUUID())
+ deviceController.startAssociation("deviceName", mockAssociationCallback, testIdentifier.uuid)
+ argumentCaptor<IDiscoveryCallback>().apply {
+ verify(testConnectionProtocol).startAssociationDiscovery(any(), eq(testIdentifier), capture())
firstValue.onDeviceConnected(testProtocolId.toString())
}
@@ -435,7 +433,7 @@
@Test
fun onDeviceDisconnected_afterAssociationCompleted_doNotInvokesAssociationErrorCallback() {
val deviceId = UUID.randomUUID()
- val testIdentifier = UUID.randomUUID()
+ val testIdentifier = ParcelUuid(UUID.randomUUID())
val testProtocolId = UUID.randomUUID()
val secret = ByteUtils.randomBytes(CHALLENGE_SECRET_BYTES)
val testDeviceMessage =
@@ -446,9 +444,9 @@
ByteUtils.uuidToBytes(deviceId) + secret
)
- deviceController.startAssociation("deviceName", mockAssociationCallback, testIdentifier)
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
- verify(testConnectionProtocol).startAssociationDiscovery(any(), capture(), eq(testIdentifier))
+ deviceController.startAssociation("deviceName", mockAssociationCallback, testIdentifier.uuid)
+ argumentCaptor<IDiscoveryCallback>().apply {
+ verify(testConnectionProtocol).startAssociationDiscovery(any(), eq(testIdentifier), capture())
firstValue.onDeviceConnected(testProtocolId.toString())
}
@@ -471,7 +469,7 @@
@Test
fun onDeviceDisconnected_attemptsReconnectIfDeviceIsEnabled() {
- val deviceId = UUID.randomUUID()
+ val deviceId = ParcelUuid(UUID.randomUUID())
val testProtocolId = UUID.randomUUID()
val associatedDevice =
AssociatedDevice(
@@ -481,8 +479,8 @@
/* isConnectionEnabled= */ true
)
spyStorage.addAssociatedDeviceForDriver(associatedDevice)
- deviceController.initiateConnectionToDevice(deviceId)
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+ deviceController.initiateConnectionToDevice(deviceId.uuid)
+ argumentCaptor<IDiscoveryCallback>().apply {
verify(testConnectionProtocol).startConnectionDiscovery(eq(deviceId), any(), capture())
firstValue.onDeviceConnected(testProtocolId.toString())
}
@@ -497,7 +495,7 @@
@Test
fun onDeviceDisconnected_doesNotAttemptReconnectForDisabledDevice() {
- val deviceId = UUID.randomUUID()
+ val deviceId = ParcelUuid(UUID.randomUUID())
val testProtocolId = UUID.randomUUID()
val associatedDevice =
AssociatedDevice(
@@ -507,8 +505,8 @@
/* isConnectionEnabled= */ true
)
spyStorage.addAssociatedDeviceForDriver(associatedDevice)
- deviceController.initiateConnectionToDevice(deviceId)
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+ deviceController.initiateConnectionToDevice(deviceId.uuid)
+ argumentCaptor<IDiscoveryCallback>().apply {
verify(testConnectionProtocol).startConnectionDiscovery(eq(deviceId), any(), capture())
firstValue.onDeviceConnected(testProtocolId.toString())
}
@@ -551,7 +549,7 @@
"test message".toByteArray()
)
deviceController.initiateConnectionToDevice(deviceId)
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+ argumentCaptor<IDiscoveryCallback>().apply {
verify(testConnectionProtocol).startConnectionDiscovery(any(), any(), capture())
firstValue.onDeviceConnected(testProtocolId.toString())
}
@@ -575,7 +573,7 @@
)
deviceController.initiateConnectionToDevice(testUuid)
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+ argumentCaptor<IDiscoveryCallback>().apply {
verify(testConnectionProtocol).startConnectionDiscovery(any(), any(), capture())
firstValue.onDeviceConnected(UUID.randomUUID().toString())
}
@@ -589,7 +587,7 @@
val testProtocolId = UUID.randomUUID()
deviceController.initiateConnectionToDevice(testUuid)
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+ argumentCaptor<IDiscoveryCallback>().apply {
verify(testConnectionProtocol).startConnectionDiscovery(any(), any(), capture())
firstValue.onDeviceConnected(testProtocolId.toString())
}
@@ -602,12 +600,12 @@
@Test
fun notifyVerificationCodeAccepted_notifiesChannel() {
val deviceName = "TestDeviceName"
- val testIdentifier = UUID.randomUUID()
+ val testIdentifier = ParcelUuid(UUID.randomUUID())
- deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier)
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+ deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier.uuid)
+ argumentCaptor<IDiscoveryCallback>().apply {
verify(testConnectionProtocol)
- .startAssociationDiscovery(eq(deviceName), capture(), eq(testIdentifier))
+ .startAssociationDiscovery(eq(deviceName), eq(testIdentifier), capture())
firstValue.onDeviceConnected(UUID.randomUUID().toString())
}
val device =
@@ -629,12 +627,12 @@
@Test
fun notifyVerificationCodeAccepted_deviceWithoutChannelDoesNotThrow() {
val deviceName = "TestDeviceName"
- val testIdentifier = UUID.randomUUID()
+ val testIdentifier = ParcelUuid(UUID.randomUUID())
- deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier)
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+ deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier.uuid)
+ argumentCaptor<IDiscoveryCallback>().apply {
verify(testConnectionProtocol)
- .startAssociationDiscovery(eq(deviceName), capture(), eq(testIdentifier))
+ .startAssociationDiscovery(eq(deviceName), eq(testIdentifier), capture())
firstValue.onDeviceConnected(UUID.randomUUID().toString())
}
@@ -645,7 +643,7 @@
fun handleSecureChannelMessage_firstAssociationMessageSavesIdAndSecretAndIssuesDeviceConnected() {
val deviceName = "TestDeviceName"
val deviceId = UUID.randomUUID()
- val testIdentifier = UUID.randomUUID()
+ val testIdentifier = ParcelUuid(UUID.randomUUID())
val secret = ByteUtils.randomBytes(CHALLENGE_SECRET_BYTES)
val testDeviceMessage =
DeviceMessage.createOutgoingMessage(
@@ -655,10 +653,10 @@
ByteUtils.uuidToBytes(deviceId) + secret
)
- deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier)
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+ deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier.uuid)
+ argumentCaptor<IDiscoveryCallback>().apply {
verify(testConnectionProtocol)
- .startAssociationDiscovery(eq(deviceName), capture(), eq(testIdentifier))
+ .startAssociationDiscovery(eq(deviceName), eq(testIdentifier), capture())
firstValue.onDeviceConnected(UUID.randomUUID().toString())
}
@@ -680,7 +678,7 @@
fun handleSecureChannelMessage_associationStorageErrorInvokesOnAssociationErrorCallback() {
val deviceName = "TestDeviceName"
val deviceId = UUID.randomUUID()
- val testIdentifier = UUID.randomUUID()
+ val testIdentifier = ParcelUuid(UUID.randomUUID())
val secret = ByteUtils.randomBytes(CHALLENGE_SECRET_BYTES - 1)
val testDeviceMessage =
DeviceMessage.createOutgoingMessage(
@@ -690,10 +688,10 @@
ByteUtils.uuidToBytes(deviceId) + secret
)
- deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier)
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+ deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier.uuid)
+ argumentCaptor<IDiscoveryCallback>().apply {
verify(testConnectionProtocol)
- .startAssociationDiscovery(eq(deviceName), capture(), eq(testIdentifier))
+ .startAssociationDiscovery(eq(deviceName), eq(testIdentifier), capture())
firstValue.onDeviceConnected(UUID.randomUUID().toString())
}
@@ -731,7 +729,7 @@
ByteUtils.randomBytes(10)
)
deviceController.initiateConnectionToDevice(deviceId)
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+ argumentCaptor<IDiscoveryCallback>().apply {
verify(testConnectionProtocol).startConnectionDiscovery(any(), any(), capture())
firstValue.onDeviceConnected(testProtocolId.toString())
}
@@ -749,7 +747,7 @@
fun handleSecureChannelMessage_firstMessagePersistsDeviceAsDriverWhenPassengerDisabled() {
val deviceName = "TestDeviceName"
val deviceId = UUID.randomUUID()
- val testIdentifier = UUID.randomUUID()
+ val testIdentifier = ParcelUuid(UUID.randomUUID())
val secret = ByteUtils.randomBytes(CHALLENGE_SECRET_BYTES)
val testDeviceMessage =
DeviceMessage.createOutgoingMessage(
@@ -763,15 +761,15 @@
protocols,
spyStorage,
mockOobRunner,
- testAssociationServiceUuid,
+ testAssociationServiceUuid.uuid,
enablePassenger = false,
callbackExecutor = directExecutor()
)
- deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier)
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+ deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier.uuid)
+ argumentCaptor<IDiscoveryCallback>().apply {
verify(testConnectionProtocol)
- .startAssociationDiscovery(eq(deviceName), capture(), eq(testIdentifier))
+ .startAssociationDiscovery(eq(deviceName), eq(testIdentifier), capture())
firstValue.onDeviceConnected(UUID.randomUUID().toString())
}
@@ -792,30 +790,30 @@
@Test
fun handleSecureChannelMessage_firstMessagePersistsDeviceAsUnclaimedWhenPassengerEnabled() {
val deviceName = "TestDeviceName"
- val deviceId = UUID.randomUUID()
- val testIdentifier = UUID.randomUUID()
+ val deviceId = ParcelUuid(UUID.randomUUID())
+ val testIdentifier = ParcelUuid(UUID.randomUUID())
val secret = ByteUtils.randomBytes(CHALLENGE_SECRET_BYTES)
val testDeviceMessage =
DeviceMessage.createOutgoingMessage(
null,
true,
OperationType.CLIENT_MESSAGE,
- ByteUtils.uuidToBytes(deviceId) + secret
+ ByteUtils.uuidToBytes(deviceId.uuid) + secret
)
deviceController =
MultiProtocolDeviceController(
protocols,
spyStorage,
mockOobRunner,
- testAssociationServiceUuid,
+ testAssociationServiceUuid.uuid,
enablePassenger = true,
callbackExecutor = directExecutor()
)
- deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier)
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+ deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier.uuid)
+ argumentCaptor<IDiscoveryCallback>().apply {
verify(testConnectionProtocol)
- .startAssociationDiscovery(eq(deviceName), capture(), eq(testIdentifier))
+ .startAssociationDiscovery(eq(deviceName), eq(testIdentifier), capture())
firstValue.onDeviceConnected(UUID.randomUUID().toString())
}
@@ -836,7 +834,7 @@
@Test
fun connectedDevices_returnsAllConnectedDevices() {
- val activeUserDeviceId = UUID.randomUUID()
+ val activeUserDeviceId = ParcelUuid(UUID.randomUUID())
val activeUserDevice =
AssociatedDevice(
activeUserDeviceId.toString(),
@@ -844,7 +842,7 @@
"userDeviceName",
/* isConnectionEnabled= */ true
)
- val otherUserDeviceId = UUID.randomUUID()
+ val otherUserDeviceId = ParcelUuid(UUID.randomUUID())
val otherUserDevice =
AssociatedDevice(
otherUserDeviceId.toString(),
@@ -869,20 +867,20 @@
protocols,
spyStorage,
mockOobRunner,
- testAssociationServiceUuid,
+ testAssociationServiceUuid.uuid,
enablePassenger = false,
callbackExecutor = directExecutor()
)
deviceController.registerCallback(mockCallback, directExecutor())
deviceController.start()
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+ argumentCaptor<IDiscoveryCallback>().apply {
verify(testConnectionProtocol)
.startConnectionDiscovery(eq(activeUserDeviceId), any(), capture())
firstValue.onDeviceConnected(activeUserDeviceId.toString())
}
- deviceController.initiateConnectionToDevice(otherUserDeviceId)
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+ deviceController.initiateConnectionToDevice(otherUserDeviceId.uuid)
+ argumentCaptor<IDiscoveryCallback>().apply {
verify(testConnectionProtocol)
.startConnectionDiscovery(eq(otherUserDeviceId), any(), capture())
firstValue.onDeviceConnected(otherUserDeviceId.toString())
@@ -930,18 +928,18 @@
@Test
fun connectedDevices_returnsEmptyListWithNoAssociatedDevices() {
- val activeUserDeviceId = UUID.randomUUID()
- val otherUserDeviceId = UUID.randomUUID()
+ val activeUserDeviceId = ParcelUuid(UUID.randomUUID())
+ val otherUserDeviceId = ParcelUuid(UUID.randomUUID())
whenever(spyStorage.driverAssociatedDevices).thenReturn(listOf())
whenever(spyStorage.allAssociatedDevices).thenReturn(listOf())
- deviceController.initiateConnectionToDevice(activeUserDeviceId)
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+ deviceController.initiateConnectionToDevice(activeUserDeviceId.uuid)
+ argumentCaptor<IDiscoveryCallback>().apply {
verify(testConnectionProtocol)
.startConnectionDiscovery(eq(activeUserDeviceId), any(), capture())
firstValue.onDeviceConnected(activeUserDeviceId.toString())
}
- deviceController.initiateConnectionToDevice(otherUserDeviceId)
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+ deviceController.initiateConnectionToDevice(otherUserDeviceId.uuid)
+ argumentCaptor<IDiscoveryCallback>().apply {
verify(testConnectionProtocol)
.startConnectionDiscovery(eq(otherUserDeviceId), any(), capture())
firstValue.onDeviceConnected(otherUserDeviceId.toString())
@@ -954,7 +952,7 @@
@Test
fun disconnectDevice_stopsDiscoveryAndDisconnectsAllProtocolsForDevice() {
- val deviceId = UUID.randomUUID()
+ val deviceId = ParcelUuid(UUID.randomUUID())
val testProtocolId1 = UUID.randomUUID().toString()
val testProtocolId2 = UUID.randomUUID().toString()
val protocol1 = spy(TestConnectionProtocol())
@@ -964,22 +962,22 @@
setOf(protocol1, protocol2),
spyStorage,
mockOobRunner,
- testAssociationServiceUuid,
+ testAssociationServiceUuid.uuid,
enablePassenger = false,
callbackExecutor = directExecutor()
)
.apply { registerCallback(mockCallback, directExecutor()) }
- deviceController.initiateConnectionToDevice(deviceId)
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+ deviceController.initiateConnectionToDevice(deviceId.uuid)
+ argumentCaptor<IDiscoveryCallback>().apply {
verify(protocol1).startConnectionDiscovery(any(), any(), capture())
firstValue.onDeviceConnected(testProtocolId1)
}
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+ argumentCaptor<IDiscoveryCallback>().apply {
verify(protocol2).startConnectionDiscovery(any(), any(), capture())
firstValue.onDeviceConnected(testProtocolId2)
}
- deviceController.disconnectDevice(deviceId)
+ deviceController.disconnectDevice(deviceId.uuid)
verify(protocol1).disconnectDevice(testProtocolId1)
verify(protocol1).stopConnectionDiscovery(deviceId)
@@ -1002,13 +1000,13 @@
setOf(protocol),
spyStorage,
mockOobRunner,
- testAssociationServiceUuid,
+ testAssociationServiceUuid.uuid,
enablePassenger = false,
callbackExecutor = directExecutor()
)
.apply { registerCallback(mockCallback, directExecutor()) }
deviceController.initiateConnectionToDevice(deviceId)
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+ argumentCaptor<IDiscoveryCallback>().apply {
verify(protocol).startConnectionDiscovery(any(), any(), capture())
firstValue.onDeviceConnected(testProtocolId)
}
@@ -1040,13 +1038,13 @@
setOf(protocol),
spyStorage,
mockOobRunner,
- testAssociationServiceUuid,
+ testAssociationServiceUuid.uuid,
enablePassenger = false,
callbackExecutor = directExecutor()
)
.apply { registerCallback(mockCallback, directExecutor()) }
deviceController.start()
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+ argumentCaptor<IDiscoveryCallback>().apply {
verify(protocol).startConnectionDiscovery(any(), any(), capture())
firstValue.onDeviceConnected(testProtocolId)
}
@@ -1060,13 +1058,13 @@
@Test
fun stopAssociation_disconnectPendingDeviceAndClearOobRunner() {
val deviceName = "TestDeviceName"
- val testIdentifier = UUID.randomUUID()
+ val testIdentifier = ParcelUuid(UUID.randomUUID())
val testProtocolId = UUID.randomUUID().toString()
- deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier)
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+ deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier.uuid)
+ argumentCaptor<IDiscoveryCallback>().apply {
verify(testConnectionProtocol)
- .startAssociationDiscovery(eq(deviceName), capture(), eq(testIdentifier))
+ .startAssociationDiscovery(eq(deviceName), eq(testIdentifier), capture())
firstValue.onDeviceConnected(testProtocolId)
}
deviceController.stopAssociation()
@@ -1078,8 +1076,8 @@
@Test
fun start_initiatesConnectionToAllEnabledActiveUserDevices() {
- val enabledDeviceId = UUID.randomUUID()
- val disabledDeviceId = UUID.randomUUID()
+ val enabledDeviceId = ParcelUuid(UUID.randomUUID())
+ val disabledDeviceId = ParcelUuid(UUID.randomUUID())
val enabledDevice =
AssociatedDevice(
enabledDeviceId.toString(),
@@ -1158,12 +1156,12 @@
private fun startAssociation() {
val deviceName = "TestDeviceName"
- val testIdentifier = UUID.randomUUID()
+ val testIdentifier = ParcelUuid(UUID.randomUUID())
- deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier)
- argumentCaptor<ConnectionProtocol.DiscoveryCallback>().apply {
+ deviceController.startAssociation(deviceName, mockAssociationCallback, testIdentifier.uuid)
+ argumentCaptor<IDiscoveryCallback>().apply {
verify(testConnectionProtocol)
- .startAssociationDiscovery(eq(deviceName), capture(), eq(testIdentifier))
+ .startAssociationDiscovery(eq(deviceName), eq(testIdentifier), capture())
firstValue.onDeviceConnected(UUID.randomUUID().toString())
}
}
@@ -1174,30 +1172,28 @@
override fun decrypt(value: String?): ByteArray? = Base64.decode(value, Base64.DEFAULT)
}
- open class TestConnectionProtocol : ConnectionProtocol() {
- val deviceDisconnectedListenerList:
- MutableMap<String, ThreadSafeCallbacks<DeviceDisconnectedListener>> =
- deviceDisconnectedListeners
+ open class TestConnectionProtocol : ConnectionProtocol(directExecutor()) {
+ val deviceDisconnectedListenerList = deviceDisconnectedListeners
- override val isDeviceVerificationRequired = false
+ override fun isDeviceVerificationRequired() = false
override fun startAssociationDiscovery(
name: String,
- callback: DiscoveryCallback,
- identifier: UUID
+ identifier: ParcelUuid,
+ callback: IDiscoveryCallback
) {}
override fun startConnectionDiscovery(
- id: UUID,
+ id: ParcelUuid,
challenge: ConnectChallenge,
- callback: DiscoveryCallback
+ callback: IDiscoveryCallback
) {}
override fun stopAssociationDiscovery() {}
- override fun stopConnectionDiscovery(id: UUID) {}
+ override fun stopConnectionDiscovery(id: ParcelUuid) {}
- override fun sendData(protocolId: String, data: ByteArray, callback: DataSendCallback?) {}
+ override fun sendData(protocolId: String, data: ByteArray, callback: IDataSendCallback?) {}
override fun disconnectDevice(protocolId: String) {}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/BluetoothRfcommChannelTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/BluetoothRfcommChannelTest.java
index 647a852..432b7d4 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/BluetoothRfcommChannelTest.java
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/BluetoothRfcommChannelTest.java
@@ -16,7 +16,6 @@
package com.google.android.connecteddevice.oob;
-import static com.google.android.connecteddevice.model.OobEligibleDevice.OOB_TYPE_BLUETOOTH;
import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.mockito.ArgumentMatchers.any;
@@ -29,24 +28,24 @@
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
+import android.os.ParcelUuid;
import android.os.RemoteException;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.google.android.connecteddevice.model.OobEligibleDevice;
import com.google.android.connecteddevice.transport.BluetoothDeviceProvider;
+import com.google.android.connecteddevice.transport.ConnectChallenge;
import com.google.android.connecteddevice.transport.ConnectionProtocol;
+import com.google.android.connecteddevice.transport.IDataSendCallback;
+import com.google.android.connecteddevice.transport.IDiscoveryCallback;
import com.google.android.connecteddevice.transport.ProtocolDevice;
import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder;
import com.google.android.connecteddevice.transport.spp.PendingConnection;
-import com.google.android.connecteddevice.transport.spp.PendingSentMessage;
import com.google.common.collect.ImmutableSet;
-import com.google.common.primitives.Bytes;
import java.util.UUID;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.MockitoJUnit;
@@ -57,8 +56,6 @@
@RunWith(AndroidJUnit4.class)
public class BluetoothRfcommChannelTest {
private static final String DEVICE_ADDRESS = "00:11:22:33:AA:BB";
- private static final OobEligibleDevice OOB_ELIGIBLE_DEVICE =
- new OobEligibleDevice(DEVICE_ADDRESS, OOB_TYPE_BLUETOOTH);
private static final byte[] TEST_MESSAGE = "someData".getBytes(UTF_8);
private static final BluetoothDevice TEST_BLUETOOTH_DEVICE =
ApplicationProvider.getApplicationContext()
@@ -80,54 +77,20 @@
}
@Test
- public void completeOobExchange_success() throws Exception {
- PendingSentMessage pendingSentMessage = new PendingSentMessage();
- when(mockSppDelegateBinder.sendMessage(any(), any())).thenReturn(pendingSentMessage);
- PendingConnection connection = establishConnection();
-
- OobConnectionManager oobConnectionManager = new OobConnectionManager();
- oobConnectionManager.startOobExchange(bluetoothRfcommChannel);
-
- ArgumentCaptor<byte[]> messageArgumentCaptor = ArgumentCaptor.forClass(byte[].class);
- verify(mockSppDelegateBinder).sendMessage(any(), messageArgumentCaptor.capture());
- byte[] oobData = messageArgumentCaptor.getValue();
-
- assertThat(oobData)
- .isEqualTo(
- Bytes.concat(
- oobConnectionManager.decryptionIv,
- oobConnectionManager.encryptionIv,
- oobConnectionManager.encryptionKey.getEncoded()));
-
- pendingSentMessage.notifyMessageSent();
- verify(mockSppDelegateBinder).disconnect(connection.toConnection(TEST_BLUETOOTH_DEVICE));
- }
-
- @Test
- public void completeOobExchange_pendingSentMessageIsNull_callOnFailed() throws Exception {
- establishConnection();
-
- new OobConnectionManager().startOobExchange(bluetoothRfcommChannel);
-
- verify(mockSppDelegateBinder).sendMessage(any(), any());
- verify(mockCallback).onOobExchangeFailure();
- }
-
- @Test
public void completeOobExchange_createRfcommSocketFails_callOnFailed() throws Exception {
doThrow(RemoteException.class)
.when(mockSppDelegateBinder)
.connectAsClient(any(), any(), anyBoolean());
bluetoothRfcommChannel.completeOobDataExchange(
- OOB_ELIGIBLE_DEVICE, mockCallback, () -> ImmutableSet.of(TEST_BLUETOOTH_DEVICE));
+ TEST_BLUETOOTH_DEVICE, mockCallback, () -> ImmutableSet.of(TEST_BLUETOOTH_DEVICE));
verify(mockCallback).onOobExchangeFailure();
}
@Test
public void completeOobExchange_noBondedDevices_callOnFailed() {
bluetoothRfcommChannel.completeOobDataExchange(
- OOB_ELIGIBLE_DEVICE, mockCallback, ImmutableSet::of);
+ TEST_BLUETOOTH_DEVICE, mockCallback, ImmutableSet::of);
verify(mockCallback).onOobExchangeFailure();
}
@@ -140,7 +103,7 @@
.getAdapter()
.getRemoteDevice("BB:AA:33:22:11:00");
bluetoothRfcommChannel.completeOobDataExchange(
- OOB_ELIGIBLE_DEVICE, mockCallback, () -> ImmutableSet.of(otherBtDevice));
+ TEST_BLUETOOTH_DEVICE, mockCallback, () -> ImmutableSet.of(otherBtDevice));
verify(mockCallback).onOobExchangeFailure();
}
@@ -191,7 +154,7 @@
.connectAsClient(any(), any(), anyBoolean());
bluetoothRfcommChannel.completeOobDataExchange(
- OOB_ELIGIBLE_DEVICE, mockCallback, () -> ImmutableSet.of(TEST_BLUETOOTH_DEVICE));
+ TEST_BLUETOOTH_DEVICE, mockCallback, () -> ImmutableSet.of(TEST_BLUETOOTH_DEVICE));
verify(mockCallback, never()).onOobExchangeSuccess();
verify(mockCallback, never()).onOobExchangeFailure();
@@ -240,7 +203,7 @@
.connectAsClient(any(), any(), anyBoolean());
bluetoothRfcommChannel.completeOobDataExchange(
- OOB_ELIGIBLE_DEVICE, mockCallback, () -> ImmutableSet.of(TEST_BLUETOOTH_DEVICE));
+ TEST_BLUETOOTH_DEVICE, mockCallback, () -> ImmutableSet.of(TEST_BLUETOOTH_DEVICE));
verify(mockSppDelegateBinder).connectAsClient(any(), any(), anyBoolean());
return connectionCaptor.getResult();
@@ -264,7 +227,6 @@
private static class TestConnectionProtocol extends ConnectionProtocol
implements BluetoothDeviceProvider {
-
@Override
public BluetoothDevice getBluetoothDeviceById(String protocolId) {
return null;
@@ -277,20 +239,20 @@
@Override
public void startAssociationDiscovery(
- String name, DiscoveryCallback callback, UUID identifier) {}
+ String name, ParcelUuid identifier, IDiscoveryCallback callback) {}
@Override
public void startConnectionDiscovery(
- UUID id, ConnectChallenge challenge, DiscoveryCallback callback) {}
+ ParcelUuid id, ConnectChallenge challenge, IDiscoveryCallback callback) {}
@Override
public void stopAssociationDiscovery() {}
@Override
- public void stopConnectionDiscovery(UUID id) {}
+ public void stopConnectionDiscovery(ParcelUuid id) {}
@Override
- public void sendData(String protocolId, byte[] data, DataSendCallback callback) {}
+ public void sendData(String protocolId, byte[] data, IDataSendCallback callback) {}
@Override
public void disconnectDevice(String protocolId) {}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/OobConnectionManagerTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/OobConnectionManagerTest.java
deleted file mode 100644
index 4fff6bc..0000000
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/OobConnectionManagerTest.java
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice.oob;
-
-import static com.google.common.truth.Truth.assertThat;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.junit.Assert.assertThrows;
-
-import android.security.keystore.KeyProperties;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.google.android.connecteddevice.model.OobEligibleDevice;
-import com.google.android.connecteddevice.transport.ProtocolDevice;
-import com.google.common.primitives.Bytes;
-import java.security.InvalidKeyException;
-import java.security.SecureRandom;
-import javax.crypto.AEADBadTagException;
-import javax.crypto.KeyGenerator;
-import javax.crypto.SecretKey;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-public class OobConnectionManagerTest {
- private static final byte[] TEST_MESSAGE = "testMessage".getBytes(UTF_8);
- private static final byte[] TEST_ENCRYPTION_IV =
- new byte[OobConnectionManager.NONCE_LENGTH_BYTES];
- private static final byte[] TEST_DECRYPTION_IV =
- new byte[OobConnectionManager.NONCE_LENGTH_BYTES];
-
- private TestChannel testChannel;
- private SecretKey testKey;
- private byte[] testOobData;
-
- @Before
- public void setUp() throws Exception {
- testChannel = new TestChannel();
- testKey = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES).generateKey();
-
- SecureRandom secureRandom = new SecureRandom();
- secureRandom.nextBytes(TEST_ENCRYPTION_IV);
- secureRandom.nextBytes(TEST_DECRYPTION_IV);
-
- testOobData = Bytes.concat(TEST_DECRYPTION_IV, TEST_ENCRYPTION_IV, testKey.getEncoded());
- }
-
- @Test
- public void testInitAsServer_keyIsNull() {
- OobConnectionManager oobConnectionManager = new OobConnectionManager();
- assertThat(oobConnectionManager.encryptionKey).isNull();
- }
-
- @Test
- public void testServer_onSetOobData_setsKeyAndNonce() {
- OobConnectionManager oobConnectionManager = new OobConnectionManager();
- oobConnectionManager.setOobData(testOobData);
- assertThat(oobConnectionManager.encryptionKey).isEqualTo(testKey);
- // The decryption IV for the server is the encryption IV for the client and vice versa
- assertThat(oobConnectionManager.decryptionIv).isEqualTo(TEST_ENCRYPTION_IV);
- assertThat(oobConnectionManager.encryptionIv).isEqualTo(TEST_DECRYPTION_IV);
- }
-
- @Test
- public void testInitAsClient_keyAndNoncesAreNonNullAndSent() {
- OobConnectionManager oobConnectionManager = new OobConnectionManager();
- oobConnectionManager.startOobExchange(testChannel);
- assertThat(oobConnectionManager.encryptionKey).isNotNull();
- assertThat(oobConnectionManager.encryptionIv).isNotNull();
- assertThat(oobConnectionManager.decryptionIv).isNotNull();
- assertThat(testChannel.sentOobData)
- .isEqualTo(
- Bytes.concat(
- oobConnectionManager.decryptionIv,
- oobConnectionManager.encryptionIv,
- oobConnectionManager.encryptionKey.getEncoded()));
- }
-
- @Test
- public void testServerEncryptAndClientDecrypt() throws Exception {
- OobConnectionManager clientOobConnectionManager = new OobConnectionManager();
- clientOobConnectionManager.startOobExchange(testChannel);
- OobConnectionManager serverOobConnectionManager = new OobConnectionManager();
- serverOobConnectionManager.setOobData(testChannel.sentOobData);
-
- byte[] encryptedTestMessage = clientOobConnectionManager.encryptVerificationCode(TEST_MESSAGE);
- byte[] decryptedTestMessage =
- serverOobConnectionManager.decryptVerificationCode(encryptedTestMessage);
-
- assertThat(decryptedTestMessage).isEqualTo(TEST_MESSAGE);
- }
-
- @Test
- public void testClientEncryptAndServerDecrypt() throws Exception {
- OobConnectionManager clientOobConnectionManager = new OobConnectionManager();
- clientOobConnectionManager.startOobExchange(testChannel);
- OobConnectionManager serverOobConnectionManager = new OobConnectionManager();
- serverOobConnectionManager.setOobData(testChannel.sentOobData);
-
- byte[] encryptedTestMessage = serverOobConnectionManager.encryptVerificationCode(TEST_MESSAGE);
- byte[] decryptedTestMessage =
- clientOobConnectionManager.decryptVerificationCode(encryptedTestMessage);
-
- assertThat(decryptedTestMessage).isEqualTo(TEST_MESSAGE);
- }
-
- @Test
- public void testEncryptAndDecryptWithDifferentNonces_throwsAEADBadTagException()
- throws Exception {
- // The OobConnectionManager stores a different nonce for encryption and decryption, so it
- // can't decrypt messages that it encrypted itself. It can only send encrypted messages to
- // an OobConnectionManager on another device that share its nonces and encryption key.
- OobConnectionManager oobConnectionManager = new OobConnectionManager();
- oobConnectionManager.startOobExchange(testChannel);
- byte[] encryptedMessage = oobConnectionManager.encryptVerificationCode(TEST_MESSAGE);
- assertThrows(
- AEADBadTagException.class,
- () -> oobConnectionManager.decryptVerificationCode(encryptedMessage));
- }
-
- @Test
- public void testDecryptWithShortMessage_throwsAEADBadTagException() {
- OobConnectionManager oobConnectionManager = new OobConnectionManager();
- oobConnectionManager.startOobExchange(testChannel);
- assertThrows(
- AEADBadTagException.class,
- () -> oobConnectionManager.decryptVerificationCode("short".getBytes(UTF_8)));
- }
-
- @Test
- public void testEncryptWithNullKey_throwsInvalidKeyException() {
- OobConnectionManager oobConnectionManager = new OobConnectionManager();
- assertThrows(
- InvalidKeyException.class,
- () -> oobConnectionManager.encryptVerificationCode(TEST_MESSAGE));
- }
-
- @Test
- public void testDecryptWithNullKey_throwsInvalidKeyException() {
- OobConnectionManager oobConnectionManager = new OobConnectionManager();
- assertThrows(
- InvalidKeyException.class,
- () -> oobConnectionManager.decryptVerificationCode(TEST_MESSAGE));
- }
-
- private static class TestChannel implements OobChannel {
- byte[] sentOobData = null;
-
- @Override
- public boolean completeOobDataExchange(ProtocolDevice protocolDevice, Callback callback) {
- return false;
- }
-
- @Override
- public void completeOobDataExchange(OobEligibleDevice device, Callback callback) {}
-
- @Override
- public void sendOobData(byte[] oobData) {
- sentOobData = oobData;
- }
-
- @Override
- public void interrupt() {}
- }
-}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/OobRunnerTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/OobRunnerTest.kt
index 58eed81..4d6e895 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/OobRunnerTest.kt
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/OobRunnerTest.kt
@@ -16,23 +16,22 @@
package com.google.android.connecteddevice.oob
+import android.os.ParcelUuid
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.android.companionprotos.CapabilitiesExchangeProto.CapabilitiesExchange.OobChannelType
-import com.google.android.companionprotos.OutOfBandAssociationToken
import com.google.android.connecteddevice.model.OobData
-import com.google.android.connecteddevice.model.OobEligibleDevice
+import com.google.android.connecteddevice.transport.ConnectChallenge
import com.google.android.connecteddevice.transport.ConnectionProtocol
+import com.google.android.connecteddevice.transport.IDataSendCallback
+import com.google.android.connecteddevice.transport.IDiscoveryCallback
import com.google.android.connecteddevice.transport.ProtocolDevice
import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.any
-import com.nhaarman.mockitokotlin2.argumentCaptor
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.spy
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
-import java.io.IOException
-import java.util.UUID
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
@@ -40,9 +39,7 @@
@RunWith(AndroidJUnit4::class)
class OobRunnerTest {
- private val mockOobRunnerCallback: OobRunner.Callback = mock()
- private val mockOobChannelFactory: OobChannelFactory = mock()
- private val testSecurityVersion = OobRunner.MIN_SECURITY_VERSION_FOR_OOB_PROTO
+ private val mockOobChannelFactory = mock<OobChannelFactory>()
private val supportedTypesSingle = listOf(OobChannelType.BT_RFCOMM.name)
private val supportedTypesTwo =
listOf(OobChannelType.BT_RFCOMM.name, OobChannelType.OOB_CHANNEL_UNKNOWN.name)
@@ -61,70 +58,14 @@
fun startOobDataExchange_startOobDataExchangeSuccessfully_returnTrue() {
testOobChannel.oobDataExchangeResult = true
- assertThat(
- oobRunner.startOobDataExchange(
- testProtocolDevice,
- listOf(OobChannelType.BT_RFCOMM),
- testSecurityVersion,
- mockOobRunnerCallback
- )
- )
- .isTrue()
- }
-
- @Test
- fun startOobDataExchange_PreOobDataProto_sendRawBytes() {
- testOobChannel.oobDataExchangeResult = true
- oobRunner.generateOobData()
-
- assertThat(
- oobRunner.startOobDataExchange(
- testProtocolDevice,
- listOf(OobChannelType.BT_RFCOMM),
- testSecurityVersion - 1,
- mockOobRunnerCallback
- )
- )
- .isTrue()
- testOobChannel.oobChannelCallback!!.onOobExchangeSuccess()
-
- val data =
- argumentCaptor<ByteArray>().run {
- verify(testOobChannel).sendOobData(capture())
- firstValue
- }
-
- assertThrows(IOException::class.java) { OutOfBandAssociationToken.parseFrom(data) }
+ assertThat(oobRunner.sendOobData(testProtocolDevice)).isTrue()
}
@Test
fun startOobDataExchange_startOobDataExchangeFailed_returnFalse() {
testOobChannel.oobDataExchangeResult = false
- assertThat(
- oobRunner.startOobDataExchange(
- testProtocolDevice,
- listOf(OobChannelType.BT_RFCOMM),
- testSecurityVersion,
- mockOobRunnerCallback
- )
- )
- .isFalse()
- }
-
- @Test
- fun startOobDataExchange_unsupportedChannelType_returnFalse() {
- testOobChannel.oobDataExchangeResult = true
-
- assertThat(
- oobRunner.startOobDataExchange(
- testProtocolDevice,
- listOf(OobChannelType.OOB_CHANNEL_UNKNOWN),
- testSecurityVersion,
- mockOobRunnerCallback
- )
- )
- .isFalse()
+ assertThat(oobRunner.sendOobData(testProtocolDevice)).isFalse()
}
@Test
@@ -132,74 +73,15 @@
val oobRunner = OobRunner(mockOobChannelFactory, supportedTypesTwo)
testOobChannel.oobDataExchangeResult = false
- assertThat(
- oobRunner.startOobDataExchange(
- testProtocolDevice,
- listOf(OobChannelType.BT_RFCOMM, OobChannelType.OOB_CHANNEL_UNKNOWN),
- testSecurityVersion,
- mockOobRunnerCallback
- )
- )
- .isFalse()
+ assertThat(oobRunner.sendOobData(testProtocolDevice)).isFalse()
verify(mockOobChannelFactory, times(2)).createOobChannel(any())
}
@Test
- fun startOobDataExchange_onOobExchangeSuccess_invokeSuccessCallback() {
- testOobChannel.oobDataExchangeResult = true
- oobRunner.generateOobData()
- oobRunner.startOobDataExchange(
- testProtocolDevice,
- listOf(OobChannelType.BT_RFCOMM),
- testSecurityVersion,
- mockOobRunnerCallback
- )
- assertThat(testOobChannel.oobChannelCallback).isNotNull()
- testOobChannel.oobChannelCallback!!.onOobExchangeSuccess()
-
- verify(mockOobRunnerCallback).onOobDataExchangeSuccess()
- }
-
- @Test
- fun startOobDataExchange_onOobExchangeSuccessWithoutOobData_invokeFailureCallback() {
- testOobChannel.oobDataExchangeResult = true
- oobRunner.startOobDataExchange(
- testProtocolDevice,
- listOf(OobChannelType.BT_RFCOMM),
- testSecurityVersion,
- mockOobRunnerCallback
- )
- testOobChannel.oobChannelCallback!!.onOobExchangeSuccess()
-
- verify(mockOobRunnerCallback).onOobDataExchangeFailure()
- }
-
- @Test
- fun startOobDataExchange_onOobExchangeFailure_invokeFailureCallback() {
- testOobChannel.oobDataExchangeResult = true
- oobRunner.generateOobData()
- oobRunner.startOobDataExchange(
- testProtocolDevice,
- listOf(OobChannelType.BT_RFCOMM),
- testSecurityVersion,
- mockOobRunnerCallback
- )
- assertThat(testOobChannel.oobChannelCallback).isNotNull()
- testOobChannel.oobChannelCallback!!.onOobExchangeFailure()
-
- verify(mockOobRunnerCallback).onOobDataExchangeFailure()
- }
-
- @Test
fun reset_resetAllStatus() {
testOobChannel.oobDataExchangeResult = true
oobRunner.generateOobData()
- oobRunner.startOobDataExchange(
- testProtocolDevice,
- listOf(OobChannelType.BT_RFCOMM),
- testSecurityVersion,
- mockOobRunnerCallback
- )
+ oobRunner.sendOobData(testProtocolDevice)
oobRunner.reset()
assertThat(testOobChannel.isInterrupted).isTrue()
assertThat(oobRunner.encryptionKey).isNull()
@@ -285,11 +167,6 @@
return oobDataExchangeResult
}
- override fun completeOobDataExchange(
- device: OobEligibleDevice,
- callback: OobChannel.Callback
- ) {}
-
override fun sendOobData(oobData: ByteArray) {}
override fun interrupt() {
@@ -298,23 +175,22 @@
}
private class TestConnectionProtocol : ConnectionProtocol() {
- override val isDeviceVerificationRequired: Boolean
- get() = false
+ override fun isDeviceVerificationRequired() = false
override fun startAssociationDiscovery(
name: String,
- callback: DiscoveryCallback,
- identifier: UUID
+ identifier: ParcelUuid,
+ callback: IDiscoveryCallback
) {}
override fun startConnectionDiscovery(
- id: UUID,
+ id: ParcelUuid,
challenge: ConnectChallenge,
- callback: DiscoveryCallback
+ callback: IDiscoveryCallback
) {}
override fun stopAssociationDiscovery() {}
- override fun stopConnectionDiscovery(id: UUID) {}
- override fun sendData(protocolId: String, data: ByteArray, callback: DataSendCallback?) {}
+ override fun stopConnectionDiscovery(id: ParcelUuid) {}
+ override fun sendData(protocolId: String, data: ByteArray, callback: IDataSendCallback?) {}
override fun disconnectDevice(protocolId: String) {}
override fun getMaxWriteSize(protocolId: String): Int {
return 0
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/PassThroughChannelTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/PassThroughChannelTest.kt
index 1966648..3ec4eec 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/PassThroughChannelTest.kt
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/PassThroughChannelTest.kt
@@ -16,13 +16,16 @@
package com.google.android.connecteddevice.oob
+import android.os.ParcelUuid
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.android.connecteddevice.transport.ConnectChallenge
import com.google.android.connecteddevice.transport.ConnectionProtocol
+import com.google.android.connecteddevice.transport.IDataSendCallback
+import com.google.android.connecteddevice.transport.IDiscoveryCallback
import com.google.android.connecteddevice.transport.ProtocolDevice
import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify
-import java.util.UUID
import org.junit.Test
import org.junit.runner.RunWith
@@ -40,23 +43,22 @@
}
private class TestConnectionProtocol : ConnectionProtocol() {
- override val isDeviceVerificationRequired: Boolean
- get() = false
+ override fun isDeviceVerificationRequired() = false
override fun startAssociationDiscovery(
name: String,
- callback: DiscoveryCallback,
- identifier: UUID
+ identifier: ParcelUuid,
+ callback: IDiscoveryCallback
) {}
override fun startConnectionDiscovery(
- id: UUID,
+ id: ParcelUuid,
challenge: ConnectChallenge,
- callback: DiscoveryCallback
+ callback: IDiscoveryCallback
) {}
override fun stopAssociationDiscovery() {}
- override fun stopConnectionDiscovery(id: UUID) {}
- override fun sendData(protocolId: String, data: ByteArray, callback: DataSendCallback?) {}
+ override fun stopConnectionDiscovery(id: ParcelUuid) {}
+ override fun sendData(protocolId: String, data: ByteArray, callback: IDataSendCallback?) {}
override fun disconnectDevice(protocolId: String) {}
override fun getMaxWriteSize(protocolId: String): Int {
return 0
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/service/ConnectedDeviceManagerBinderTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/service/ConnectedDeviceManagerBinderTest.java
deleted file mode 100644
index 6244c96..0000000
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/service/ConnectedDeviceManagerBinderTest.java
+++ /dev/null
@@ -1,237 +0,0 @@
-/*
- * Copyright (C) 2020 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.
- */
-
-package com.google.android.connecteddevice.service;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.refEq;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-
-import android.os.ParcelUuid;
-import androidx.annotation.NonNull;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import com.google.android.connecteddevice.ConnectedDeviceManager;
-import com.google.android.connecteddevice.ConnectedDeviceManager.ConnectionCallback;
-import com.google.android.connecteddevice.ConnectedDeviceManager.DeviceAssociationCallback;
-import com.google.android.connecteddevice.ConnectedDeviceManager.DeviceCallback;
-import com.google.android.connecteddevice.api.IConnectionCallback;
-import com.google.android.connecteddevice.api.IDeviceAssociationCallback;
-import com.google.android.connecteddevice.api.IDeviceCallback;
-import com.google.android.connecteddevice.api.IOnLogRequestedListener;
-import com.google.android.connecteddevice.logging.LoggingManager;
-import com.google.android.connecteddevice.logging.LoggingManager.OnLogRequestedListener;
-import com.google.android.connecteddevice.model.AssociatedDevice;
-import com.google.android.connecteddevice.model.ConnectedDevice;
-import com.google.android.connecteddevice.model.DeviceMessage;
-import com.google.android.connecteddevice.model.DeviceMessage.OperationType;
-import com.google.android.connecteddevice.util.ByteUtils;
-import java.util.UUID;
-import java.util.concurrent.Executor;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-
-@RunWith(AndroidJUnit4.class)
-public class ConnectedDeviceManagerBinderTest {
-
- private final ParcelUuid recipientId = new ParcelUuid(UUID.randomUUID());
-
- @Rule public final MockitoRule mockito = MockitoJUnit.rule();
- @Mock private ConnectedDeviceManager mockConnectedDeviceManager;
- @Mock private LoggingManager mockLoggingManager;
-
- private ConnectedDeviceManagerBinder binder;
-
- @Before
- public void setUp() {
- binder =
- new ConnectedDeviceManagerBinder(
- mockConnectedDeviceManager, mockLoggingManager);
- }
-
- @Test
- public void registerActiveUserConnectionCallback_mirrorsConnectedDeviceManager() {
- binder.registerActiveUserConnectionCallback(createConnectionCallback());
- verify(mockConnectedDeviceManager)
- .registerActiveUserConnectionCallback(any(ConnectionCallback.class), any(Executor.class));
- }
-
- @Test
- public void unregisterConnectionCallback_mirrorsConnectedDeviceManager() {
- IConnectionCallback callback = createConnectionCallback();
- binder.registerActiveUserConnectionCallback(callback);
- binder.unregisterConnectionCallback(callback);
- verify(mockConnectedDeviceManager).unregisterConnectionCallback(any(ConnectionCallback.class));
- }
-
- @Test
- public void registerDeviceCallback_mirrorsConnectedDeviceManager() {
- IDeviceCallback deviceCallback = createDeviceCallback();
- ConnectedDevice connectedDevice =
- new ConnectedDevice(
- UUID.randomUUID().toString(),
- /* deviceName = */ null,
- /* belongsToActiveUser = */ false,
- /* hasSecureChannel = */ false);
- binder.registerDeviceCallback(connectedDevice, recipientId, deviceCallback);
- verify(mockConnectedDeviceManager)
- .registerDeviceCallback(
- refEq(connectedDevice), refEq(recipientId.getUuid()),
- any(DeviceCallback.class), any(Executor.class));
- }
-
- @Test
- public void unregisterDeviceCallback_mirrorsConnectedDeviceManager() {
- IDeviceCallback deviceCallback = createDeviceCallback();
- ConnectedDevice connectedDevice =
- new ConnectedDevice(
- UUID.randomUUID().toString(),
- /* deviceName = */ null,
- /* belongsToActiveUser = */ false,
- /* hasSecureChannel = */ false);
- binder.registerDeviceCallback(connectedDevice, recipientId, deviceCallback);
- binder.unregisterDeviceCallback(connectedDevice, recipientId, deviceCallback);
- verify(mockConnectedDeviceManager)
- .unregisterDeviceCallback(
- refEq(connectedDevice),
- refEq(recipientId.getUuid()),
- any(DeviceCallback.class));
- }
-
- @Test
- public void registerOnLogRequestedListener_mirrorsLoggingManager() {
- IOnLogRequestedListener listener = createOnLogRequestedListener();
- int testLoggerId = 1;
- binder.registerOnLogRequestedListener(testLoggerId, listener);
- verify(mockLoggingManager)
- .registerLogRequestedListener(
- eq(testLoggerId), any(OnLogRequestedListener.class), any(Executor.class));
- }
-
- @Test
- public void unregisterOnLogRequestedListener_mirrorsLoggingManager() {
- IOnLogRequestedListener listener = createOnLogRequestedListener();
- int testLoggerId = 1;
- binder.registerOnLogRequestedListener(testLoggerId, listener);
- binder.unregisterOnLogRequestedListener(testLoggerId, listener);
- verify(mockLoggingManager)
- .unregisterLogRequestedListener(eq(testLoggerId), any(OnLogRequestedListener.class));
- }
-
- @Test
- public void sendMessage_mirrorsConnectedDeviceManager() {
- ConnectedDevice connectedDevice =
- new ConnectedDevice(
- UUID.randomUUID().toString(),
- /* deviceName = */ null,
- /* belongsToActiveUser = */ false,
- /* hasSecureChannel = */ true);
- DeviceMessage message =
- DeviceMessage.createOutgoingMessage(
- recipientId.getUuid(),
- /* isMessageEncrypted= */ true,
- OperationType.CLIENT_MESSAGE,
- ByteUtils.randomBytes(10));
- binder.sendMessage(connectedDevice, message);
- verify(mockConnectedDeviceManager).sendMessage(connectedDevice, message);
- }
-
-
- @Test
- public void registerDeviceAssociationCallback_mirrorsConnectedDeviceManager() {
- IDeviceAssociationCallback associationCallback =
- createDeviceAssociationCallback();
- binder.registerDeviceAssociationCallback(associationCallback);
- verify(mockConnectedDeviceManager)
- .registerDeviceAssociationCallback(
- any(DeviceAssociationCallback.class), any(Executor.class));
- }
-
- @Test
- public void unregisterDeviceAssociationCallback_mirrorsConnectedDeviceManager() {
- IDeviceAssociationCallback associationCallback =
- createDeviceAssociationCallback();
- binder.registerDeviceAssociationCallback(associationCallback);
- binder.unregisterDeviceAssociationCallback(associationCallback);
- verify(mockConnectedDeviceManager)
- .unregisterDeviceAssociationCallback(any(DeviceAssociationCallback.class));
- }
-
- @NonNull
- private static IConnectionCallback createConnectionCallback() {
- return spy(
- new IConnectionCallback.Stub() {
- @Override
- public void onDeviceConnected(ConnectedDevice connectedDevice) {
- }
-
- @Override
- public void onDeviceDisconnected(ConnectedDevice connectedDevice) {
- }
- });
- }
-
- @NonNull
- private static IDeviceCallback createDeviceCallback() {
- return spy(
- new IDeviceCallback.Stub() {
- @Override
- public void onSecureChannelEstablished(ConnectedDevice connectedDevice) {
- }
-
- @Override
- public void onMessageReceived(ConnectedDevice connectedDevice, DeviceMessage message) {
- }
-
- @Override
- public void onDeviceError(ConnectedDevice connectedDevice, int error) {
- }
- });
- }
-
- @NonNull
- private static IDeviceAssociationCallback createDeviceAssociationCallback() {
- return spy(
- new IDeviceAssociationCallback.Stub() {
- @Override
- public void onAssociatedDeviceAdded(AssociatedDevice device) {
- }
-
- @Override
- public void onAssociatedDeviceRemoved(AssociatedDevice device) {
- }
-
- @Override
- public void onAssociatedDeviceUpdated(AssociatedDevice device) {
- }
- });
- }
-
- @NonNull
- private static IOnLogRequestedListener createOnLogRequestedListener() {
- return spy(
- new IOnLogRequestedListener.Stub() {
- @Override
- public void onLogRecordsRequested() {}
- });
- }
-}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/ConnectionProtocolTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/ConnectionProtocolTest.kt
index 1aaf952..3cb93eb 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/ConnectionProtocolTest.kt
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/ConnectionProtocolTest.kt
@@ -1,13 +1,13 @@
package com.google.android.connecteddevice.transport
+import android.os.ParcelUuid
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.android.connecteddevice.util.ThreadSafeCallbacks
+import com.google.android.connecteddevice.core.util.mockToBeAlive
import com.google.common.truth.Truth.assertThat
import com.google.common.util.concurrent.MoreExecutors.directExecutor
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.never
import com.nhaarman.mockitokotlin2.verify
-import java.util.UUID
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -15,10 +15,9 @@
@RunWith(AndroidJUnit4::class)
class ConnectionProtocolTest {
private lateinit var testProtocol: TestConnectionProtocol
- private val mockDataReceivedListener: ConnectionProtocol.DataReceivedListener = mock()
- private val mockDisconnectedListener: ConnectionProtocol.DeviceDisconnectedListener = mock()
- private val mockMaxDataSizeChangedListener: ConnectionProtocol.DeviceMaxDataSizeChangedListener =
- mock()
+ private val mockDataReceivedListener = mockToBeAlive<IDataReceivedListener>()
+ private val mockDisconnectedListener = mockToBeAlive<IDeviceDisconnectedListener>()
+ private val mockMaxDataSizeChangedListener = mockToBeAlive<IDeviceMaxDataSizeChangedListener>()
@Before
fun setUp() {
@@ -29,11 +28,7 @@
fun registerDataReceivedListener_invokeListenerWithCall() {
val testProtocolId = "testProtocolId"
val testData = "testData".toByteArray()
- testProtocol.registerDataReceivedListener(
- testProtocolId,
- mockDataReceivedListener,
- directExecutor()
- )
+ testProtocol.registerDataReceivedListener(testProtocolId, mockDataReceivedListener)
testProtocol.invokeDataReceivedListeners(testProtocolId, testData)
@@ -46,11 +41,7 @@
val testData = "testData".toByteArray()
testProtocol.notifyDataReceived(testProtocolId, testData)
- testProtocol.registerDataReceivedListener(
- testProtocolId,
- mockDataReceivedListener,
- directExecutor()
- )
+ testProtocol.registerDataReceivedListener(testProtocolId, mockDataReceivedListener)
verify(mockDataReceivedListener).onDataReceived(testProtocolId, testData)
}
@@ -61,17 +52,9 @@
val testData = "testData".toByteArray()
testProtocol.notifyDataReceived(testProtocolId, testData)
- testProtocol.registerDataReceivedListener(
- testProtocolId,
- mockDataReceivedListener,
- directExecutor()
- )
- val mockSecondDataReceivedListener: ConnectionProtocol.DataReceivedListener = mock()
- testProtocol.registerDataReceivedListener(
- testProtocolId,
- mockSecondDataReceivedListener,
- directExecutor()
- )
+ testProtocol.registerDataReceivedListener(testProtocolId, mockDataReceivedListener)
+ val mockSecondDataReceivedListener: IDataReceivedListener = mock()
+ testProtocol.registerDataReceivedListener(testProtocolId, mockSecondDataReceivedListener)
verify(mockDataReceivedListener).onDataReceived(testProtocolId, testData)
verify(mockSecondDataReceivedListener, never()).onDataReceived(testProtocolId, testData)
@@ -81,11 +64,7 @@
fun unregisterDataReceivedListener_doNotInvokeListenerWithCall() {
val testProtocolId = "testProtocolId"
val testData = "testData".toByteArray()
- testProtocol.registerDataReceivedListener(
- testProtocolId,
- mockDataReceivedListener,
- directExecutor()
- )
+ testProtocol.registerDataReceivedListener(testProtocolId, mockDataReceivedListener)
testProtocol.unregisterDataReceivedListener(testProtocolId, mockDataReceivedListener)
testProtocol.invokeDataReceivedListeners(testProtocolId, testData)
@@ -101,11 +80,7 @@
@Test
fun unregisterDataReceivedListener_onLastListenerUnregistered_keyCleared() {
val testProtocolId = "testProtocolId"
- testProtocol.registerDataReceivedListener(
- testProtocolId,
- mockDataReceivedListener,
- directExecutor()
- )
+ testProtocol.registerDataReceivedListener(testProtocolId, mockDataReceivedListener)
testProtocol.unregisterDataReceivedListener(testProtocolId, mockDataReceivedListener)
@@ -115,11 +90,7 @@
@Test
fun registerDeviceDisconnectListener_invokeListenerWithCall() {
val testProtocolId = "testProtocolId"
- testProtocol.registerDeviceDisconnectedListener(
- testProtocolId,
- mockDisconnectedListener,
- directExecutor()
- )
+ testProtocol.registerDeviceDisconnectedListener(testProtocolId, mockDisconnectedListener)
testProtocol.invokeDeviceDisconnectedListeners(testProtocolId)
@@ -129,11 +100,7 @@
@Test
fun unregisterDeviceDisconnectListener_doNotInvokeListenerWithCall() {
val testProtocolId = "testProtocolId"
- testProtocol.registerDeviceDisconnectedListener(
- testProtocolId,
- mockDisconnectedListener,
- directExecutor()
- )
+ testProtocol.registerDeviceDisconnectedListener(testProtocolId, mockDisconnectedListener)
testProtocol.unregisterDeviceDisconnectListener(testProtocolId, mockDisconnectedListener)
testProtocol.invokeDeviceDisconnectedListeners(testProtocolId)
@@ -149,11 +116,7 @@
@Test
fun unregisterDeviceDisconnectListener_onLastListenerUnregistered_keyCleared() {
val testProtocolId = "testProtocolId"
- testProtocol.registerDeviceDisconnectedListener(
- testProtocolId,
- mockDisconnectedListener,
- directExecutor()
- )
+ testProtocol.registerDeviceDisconnectedListener(testProtocolId, mockDisconnectedListener)
testProtocol.unregisterDeviceDisconnectListener(testProtocolId, mockDisconnectedListener)
@@ -166,8 +129,7 @@
val testSize = 20
testProtocol.registerDeviceMaxDataSizeChangedListener(
testProtocolId,
- mockMaxDataSizeChangedListener,
- directExecutor()
+ mockMaxDataSizeChangedListener
)
testProtocol.invokeDeviceMaxDataSizeChangedListener(testProtocolId, testSize)
@@ -181,8 +143,7 @@
val testSize = 20
testProtocol.registerDeviceMaxDataSizeChangedListener(
testProtocolId,
- mockMaxDataSizeChangedListener,
- directExecutor()
+ mockMaxDataSizeChangedListener
)
testProtocol.unregisterDeviceMaxDataSizeChangedListener(
testProtocolId,
@@ -208,8 +169,7 @@
val testProtocolId = "testProtocolId"
testProtocol.registerDeviceMaxDataSizeChangedListener(
testProtocolId,
- mockMaxDataSizeChangedListener,
- directExecutor()
+ mockMaxDataSizeChangedListener
)
testProtocol.unregisterDeviceMaxDataSizeChangedListener(
@@ -225,20 +185,11 @@
val testProtocolId = "testProtocolId"
val testData = "testData".toByteArray()
val testSize = 20
- testProtocol.registerDataReceivedListener(
- testProtocolId,
- mockDataReceivedListener,
- directExecutor()
- )
- testProtocol.registerDeviceDisconnectedListener(
- testProtocolId,
- mockDisconnectedListener,
- directExecutor()
- )
+ testProtocol.registerDataReceivedListener(testProtocolId, mockDataReceivedListener)
+ testProtocol.registerDeviceDisconnectedListener(testProtocolId, mockDisconnectedListener)
testProtocol.registerDeviceMaxDataSizeChangedListener(
testProtocolId,
- mockMaxDataSizeChangedListener,
- directExecutor()
+ mockMaxDataSizeChangedListener
)
testProtocol.reset()
@@ -259,46 +210,37 @@
testProtocol.notifyDataReceived(testProtocolId, testData)
testProtocol.reset()
- testProtocol.registerDataReceivedListener(
- testProtocolId,
- mockDataReceivedListener,
- directExecutor()
- )
+ testProtocol.registerDataReceivedListener(testProtocolId, mockDataReceivedListener)
verify(mockDataReceivedListener, never()).onDataReceived(testProtocolId, testData)
}
- class TestConnectionProtocol : ConnectionProtocol() {
- override val isDeviceVerificationRequired = false
+ class TestConnectionProtocol : ConnectionProtocol(directExecutor()) {
+ override fun isDeviceVerificationRequired() = false
- val deviceDisconnectedListenerList:
- MutableMap<String, ThreadSafeCallbacks<DeviceDisconnectedListener>> =
- deviceDisconnectedListeners
+ val deviceDisconnectedListenerList = deviceDisconnectedListeners
- val dataReceivedListenerList: MutableMap<String, ThreadSafeCallbacks<DataReceivedListener>> =
- dataReceivedListeners
+ val dataReceivedListenerList = dataReceivedListeners
- val maxDataSizeChangedListenerList:
- MutableMap<String, ThreadSafeCallbacks<DeviceMaxDataSizeChangedListener>> =
- maxDataSizeChangedListeners
+ val maxDataSizeChangedListenerList = maxDataSizeChangedListeners
override fun startAssociationDiscovery(
name: String,
- callback: DiscoveryCallback,
- identifier: UUID
+ identifier: ParcelUuid,
+ callback: IDiscoveryCallback,
) {}
override fun startConnectionDiscovery(
- id: UUID,
+ id: ParcelUuid,
challenge: ConnectChallenge,
- callback: DiscoveryCallback
+ callback: IDiscoveryCallback
) {}
override fun stopAssociationDiscovery() {}
- override fun stopConnectionDiscovery(id: UUID) {}
+ override fun stopConnectionDiscovery(id: ParcelUuid) {}
- override fun sendData(protocolId: String, data: ByteArray, callback: DataSendCallback?) {}
+ override fun sendData(protocolId: String, data: ByteArray, callback: IDataSendCallback?) {}
override fun disconnectDevice(protocolId: String) {}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/ble/BlePeripheralProtocolTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/ble/BlePeripheralProtocolTest.kt
index 4168ef4..24e78e6 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/ble/BlePeripheralProtocolTest.kt
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/ble/BlePeripheralProtocolTest.kt
@@ -6,14 +6,16 @@
import android.bluetooth.BluetoothManager
import android.bluetooth.le.AdvertiseCallback
import android.content.Context
+import android.os.ParcelUuid
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.android.connecteddevice.transport.ConnectionProtocol.ConnectChallenge
-import com.google.android.connecteddevice.transport.ConnectionProtocol.DataReceivedListener
-import com.google.android.connecteddevice.transport.ConnectionProtocol.DataSendCallback
-import com.google.android.connecteddevice.transport.ConnectionProtocol.DeviceDisconnectedListener
-import com.google.android.connecteddevice.transport.ConnectionProtocol.DeviceMaxDataSizeChangedListener
-import com.google.android.connecteddevice.transport.ConnectionProtocol.DiscoveryCallback
+import com.google.android.connecteddevice.core.util.mockToBeAlive
+import com.google.android.connecteddevice.transport.ConnectChallenge
+import com.google.android.connecteddevice.transport.IDataReceivedListener
+import com.google.android.connecteddevice.transport.IDataSendCallback
+import com.google.android.connecteddevice.transport.IDeviceDisconnectedListener
+import com.google.android.connecteddevice.transport.IDeviceMaxDataSizeChangedListener
+import com.google.android.connecteddevice.transport.IDiscoveryCallback
import com.google.android.connecteddevice.util.ByteUtils
import com.google.common.truth.Truth.assertThat
import com.google.common.util.concurrent.MoreExecutors.directExecutor
@@ -37,7 +39,7 @@
@RunWith(AndroidJUnit4::class)
class BlePeripheralProtocolTest {
- private val testIdentifier = UUID.randomUUID()
+ private val testIdentifier = ParcelUuid(UUID.randomUUID())
private val testReconnectServiceUuid = UUID.randomUUID()
private val testReconnectDataUuid = UUID.randomUUID()
private val testAdvertiseDataCharacteristicUuid = UUID.randomUUID()
@@ -48,13 +50,13 @@
private val testChallenge =
ConnectChallenge("TestChallenge".toByteArray(), "TestSalt".toByteArray())
- private val mockBlePeripheralManager: BlePeripheralManager = mock()
- private val mockDiscoveryCallback: DiscoveryCallback = mock()
- private val mockDataSendCallback: DataSendCallback = mock()
- private val mockSecondDataSendCallback: DataSendCallback = mock()
- private val mockDisconnectedListener: DeviceDisconnectedListener = mock()
- private val mockDataReceivedListener: DataReceivedListener = mock()
- private val mockMaxDataSizeChangedListener: DeviceMaxDataSizeChangedListener = mock()
+ private val mockBlePeripheralManager = mock<BlePeripheralManager>()
+ private val mockDiscoveryCallback = mockToBeAlive<IDiscoveryCallback>()
+ private val mockDataSendCallback = mockToBeAlive<IDataSendCallback>()
+ private val mockSecondDataSendCallback = mockToBeAlive<IDataSendCallback>()
+ private val mockDisconnectedListener = mockToBeAlive<IDeviceDisconnectedListener>()
+ private val mockDataReceivedListener = mockToBeAlive<IDataReceivedListener>()
+ private val mockMaxDataSizeChangedListener = mockToBeAlive<IDeviceMaxDataSizeChangedListener>()
private lateinit var blePeripheralProtocol: BlePeripheralProtocol
private lateinit var testBluetoothDevice: BluetoothDevice
@@ -74,7 +76,8 @@
testWriteCharacteristicUuid,
testReadCharacteristicUuid,
testMaxReconnectAdvertisementDuration,
- TEST_DEFAULT_MTU_SIZE
+ TEST_DEFAULT_MTU_SIZE,
+ directExecutor()
)
}
@@ -82,13 +85,13 @@
fun startAssociationDiscovery_startsWithIdentifier() {
blePeripheralProtocol.startAssociationDiscovery(
TEST_DEVICE_NAME,
- mockDiscoveryCallback,
- testIdentifier
+ testIdentifier,
+ mockDiscoveryCallback
)
argumentCaptor<BluetoothGattService>().apply {
verify(mockBlePeripheralManager).startAdvertising(capture(), any(), any(), any())
- assertThat(firstValue.uuid).isEqualTo(testIdentifier)
+ assertThat(firstValue.uuid).isEqualTo(testIdentifier.uuid)
}
}
@@ -96,8 +99,8 @@
fun startAssociationDiscovery_startedSuccessfully() {
blePeripheralProtocol.startAssociationDiscovery(
TEST_DEVICE_NAME,
- mockDiscoveryCallback,
- testIdentifier
+ testIdentifier,
+ mockDiscoveryCallback
)
argumentCaptor<AdvertiseCallback>().apply {
verify(mockBlePeripheralManager).startAdvertising(any(), any(), any(), capture())
@@ -110,8 +113,8 @@
fun startAssociationDiscovery_failedToStart() {
blePeripheralProtocol.startAssociationDiscovery(
TEST_DEVICE_NAME,
- mockDiscoveryCallback,
- testIdentifier
+ testIdentifier,
+ mockDiscoveryCallback
)
argumentCaptor<AdvertiseCallback>().apply {
verify(mockBlePeripheralManager).startAdvertising(any(), any(), any(), capture())
@@ -125,8 +128,8 @@
establishConnection(testBluetoothDevice)
blePeripheralProtocol.startAssociationDiscovery(
TEST_DEVICE_NAME,
- mockDiscoveryCallback,
- testIdentifier
+ testIdentifier,
+ mockDiscoveryCallback
)
// Should only be invoked once for establishing connection.
verify(mockBlePeripheralManager).startAdvertising(any(), any(), any(), any())
@@ -136,8 +139,8 @@
fun startAssociationDiscovery_multipleTimesSucceed() {
blePeripheralProtocol.startAssociationDiscovery(
TEST_DEVICE_NAME,
- mockDiscoveryCallback,
- testIdentifier
+ testIdentifier,
+ mockDiscoveryCallback
)
argumentCaptor<AdvertiseCallback>().apply {
verify(mockBlePeripheralManager, atLeastOnce())
@@ -147,8 +150,8 @@
blePeripheralProtocol.stopAssociationDiscovery()
blePeripheralProtocol.startAssociationDiscovery(
TEST_DEVICE_NAME,
- mockDiscoveryCallback,
- testIdentifier
+ testIdentifier,
+ mockDiscoveryCallback
)
argumentCaptor<AdvertiseCallback>().apply {
verify(mockBlePeripheralManager, atLeastOnce())
@@ -165,8 +168,8 @@
fun stopAssociationDiscovery_stopSuccessfully() {
blePeripheralProtocol.startAssociationDiscovery(
TEST_DEVICE_NAME,
- mockDiscoveryCallback,
- testIdentifier
+ testIdentifier,
+ mockDiscoveryCallback
)
blePeripheralProtocol.stopAssociationDiscovery()
val advertiseCallback =
@@ -178,9 +181,8 @@
@Test
fun startConnectionDiscovery_startedSuccessfully() {
- val testDeviceId = UUID.randomUUID()
blePeripheralProtocol.startConnectionDiscovery(
- testDeviceId,
+ testIdentifier,
testChallenge,
mockDiscoveryCallback
)
@@ -193,9 +195,8 @@
@Test
fun startConnectionDiscovery_failedToStart() {
- val testDeviceId = UUID.randomUUID()
blePeripheralProtocol.startConnectionDiscovery(
- testDeviceId,
+ testIdentifier,
testChallenge,
mockDiscoveryCallback
)
@@ -210,7 +211,7 @@
fun startConnectionDiscovery_alreadyInConnection() {
establishConnection(testBluetoothDevice)
blePeripheralProtocol.startConnectionDiscovery(
- UUID.randomUUID(),
+ testIdentifier,
testChallenge,
mockDiscoveryCallback
)
@@ -220,13 +221,12 @@
@Test
fun stopConnectionDiscovery_stopSuccessfully() {
- val testDeviceId = UUID.randomUUID()
blePeripheralProtocol.startConnectionDiscovery(
- testDeviceId,
+ testIdentifier,
testChallenge,
mockDiscoveryCallback
)
- blePeripheralProtocol.stopConnectionDiscovery(testDeviceId)
+ blePeripheralProtocol.stopConnectionDiscovery(testIdentifier)
val advertiseCallback =
argumentCaptor<AdvertiseCallback>()
.apply { verify(mockBlePeripheralManager).startAdvertising(any(), any(), any(), capture()) }
@@ -299,8 +299,8 @@
fun reset_stopStartedAdvertising() {
blePeripheralProtocol.startAssociationDiscovery(
TEST_DEVICE_NAME,
- mockDiscoveryCallback,
- testIdentifier
+ testIdentifier,
+ mockDiscoveryCallback
)
blePeripheralProtocol.reset()
val advertiseCallback =
@@ -334,11 +334,7 @@
BluetoothGattCharacteristic.PERMISSION_WRITE
)
val testProtocolId = establishConnection(testBluetoothDevice)
- blePeripheralProtocol.registerDataReceivedListener(
- testProtocolId,
- mockDataReceivedListener,
- directExecutor()
- )
+ blePeripheralProtocol.registerDataReceivedListener(testProtocolId, mockDataReceivedListener)
argumentCaptor<BlePeripheralManager.OnCharacteristicWriteListener>().apply {
verify(mockBlePeripheralManager).addOnCharacteristicWriteListener(capture())
firstValue.onCharacteristicWrite(testBluetoothDevice, testReadCharacteristic, testMessage)
@@ -351,8 +347,7 @@
val testProtocolId = establishConnection(testBluetoothDevice)
blePeripheralProtocol.registerDeviceDisconnectedListener(
testProtocolId,
- mockDisconnectedListener,
- directExecutor()
+ mockDisconnectedListener
)
argumentCaptor<BlePeripheralManager.Callback>().apply {
verify(mockBlePeripheralManager).registerCallback(capture())
@@ -367,8 +362,7 @@
val testMtuSize = 20
blePeripheralProtocol.registerDeviceMaxDataSizeChangedListener(
testProtocolId,
- mockMaxDataSizeChangedListener,
- directExecutor()
+ mockMaxDataSizeChangedListener
)
argumentCaptor<BlePeripheralManager.Callback>().apply {
verify(mockBlePeripheralManager).registerCallback(capture())
@@ -416,8 +410,8 @@
private fun establishConnection(bluetoothDevice: BluetoothDevice): String {
blePeripheralProtocol.startAssociationDiscovery(
TEST_DEVICE_NAME,
- mockDiscoveryCallback,
- testIdentifier
+ testIdentifier,
+ mockDiscoveryCallback
)
argumentCaptor<BlePeripheralManager.Callback>().apply {
verify(mockBlePeripheralManager).registerCallback(capture())
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/spp/SppProtocolTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/spp/SppProtocolTest.kt
index c199de5..c71988d 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/spp/SppProtocolTest.kt
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/spp/SppProtocolTest.kt
@@ -18,14 +18,16 @@
import android.bluetooth.BluetoothManager
import android.content.Context
+import android.os.ParcelUuid
import android.os.RemoteException
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.android.connecteddevice.transport.ConnectionProtocol.ConnectChallenge
-import com.google.android.connecteddevice.transport.ConnectionProtocol.DataReceivedListener
-import com.google.android.connecteddevice.transport.ConnectionProtocol.DataSendCallback
-import com.google.android.connecteddevice.transport.ConnectionProtocol.DeviceDisconnectedListener
-import com.google.android.connecteddevice.transport.ConnectionProtocol.DiscoveryCallback
+import com.google.android.connecteddevice.core.util.mockToBeAlive
+import com.google.android.connecteddevice.transport.ConnectChallenge
+import com.google.android.connecteddevice.transport.IDataReceivedListener
+import com.google.android.connecteddevice.transport.IDataSendCallback
+import com.google.android.connecteddevice.transport.IDeviceDisconnectedListener
+import com.google.android.connecteddevice.transport.IDiscoveryCallback
import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder.OnErrorListener
import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder.OnMessageReceivedListener
import com.google.android.connecteddevice.transport.spp.PendingConnection.OnConnectedListener
@@ -45,72 +47,69 @@
@RunWith(AndroidJUnit4::class)
class SppProtocolTest {
- private val mockPendingConnection: PendingConnection = mock()
- private val mockDiscoveryCallback: DiscoveryCallback = mock()
- private val mockDataSendCallback: DataSendCallback = mock()
- private val mockDisconnectedListener: DeviceDisconnectedListener = mock()
- private val mockDataReceivedListener: DataReceivedListener = mock()
- private val mockSppBinder: ConnectedDeviceSppDelegateBinder = mock {
- on { connectAsServer(any(), any()) } doReturn mockPendingConnection
- }
- private val invalidMockSppBinder: ConnectedDeviceSppDelegateBinder = mock()
+ private val mockPendingConnection = mock<PendingConnection>()
+ private val mockDiscoveryCallback = mockToBeAlive<IDiscoveryCallback>()
+ private val mockDataSendCallback = mockToBeAlive<IDataSendCallback>()
+ private val mockDisconnectedListener = mockToBeAlive<IDeviceDisconnectedListener>()
+ private val mockDataReceivedListener = mockToBeAlive<IDataReceivedListener>()
+ private val mockSppBinder =
+ mock<ConnectedDeviceSppDelegateBinder> {
+ on { connectAsServer(any(), any()) } doReturn mockPendingConnection
+ }
+ private val invalidMockSppBinder = mock<ConnectedDeviceSppDelegateBinder>()
private val testMaxSize = 0
- private val testIdentifier = UUID.randomUUID()
+ private val testIdentifier = ParcelUuid(UUID.randomUUID())
private val testProtocolId = UUID.randomUUID()
private val testName = "TestName"
private val testMessage = "TestMessage".toByteArray()
private val testChallenge =
ConnectChallenge("TestChallenge".toByteArray(), "TestSalt".toByteArray())
- private val sppProtocol = SppProtocol(mockSppBinder, testMaxSize)
+ private val sppProtocol = SppProtocol(mockSppBinder, testMaxSize, directExecutor())
private val invalidSppProtocol = SppProtocol(invalidMockSppBinder, testMaxSize)
@Test
fun startAssociationDiscovery_startedSuccessfully() {
- sppProtocol.startAssociationDiscovery(testName, mockDiscoveryCallback, testIdentifier)
+ sppProtocol.startAssociationDiscovery(testName, testIdentifier, mockDiscoveryCallback)
- verify(mockSppBinder).connectAsServer(eq(testIdentifier), any())
+ verify(mockSppBinder).connectAsServer(eq(testIdentifier.uuid), any())
verify(mockDiscoveryCallback).onDiscoveryStartedSuccessfully()
}
@Test
fun startAssociationDiscovery_startsWithIdentifier() {
- val testIdentifier = UUID.randomUUID()
+ sppProtocol.startAssociationDiscovery(testName, testIdentifier, mockDiscoveryCallback)
- sppProtocol.startAssociationDiscovery(testName, mockDiscoveryCallback, testIdentifier)
-
- verify(mockSppBinder).connectAsServer(eq(testIdentifier), any())
+ verify(mockSppBinder).connectAsServer(eq(testIdentifier.uuid), any())
}
@Test
fun startAssociationDiscovery_startedFailed() {
- invalidSppProtocol.startAssociationDiscovery(testName, mockDiscoveryCallback, testIdentifier)
+ invalidSppProtocol.startAssociationDiscovery(testName, testIdentifier, mockDiscoveryCallback)
verify(mockDiscoveryCallback).onDiscoveryFailedToStart()
}
@Test
fun startConnectionDiscovery_startedSuccessfully() {
- val testDeviceId = UUID.randomUUID()
+ sppProtocol.startConnectionDiscovery(testIdentifier, testChallenge, mockDiscoveryCallback)
- sppProtocol.startConnectionDiscovery(testDeviceId, testChallenge, mockDiscoveryCallback)
-
- verify(mockSppBinder).connectAsServer(eq(testDeviceId), any())
+ verify(mockSppBinder).connectAsServer(eq(testIdentifier.uuid), any())
verify(mockDiscoveryCallback).onDiscoveryStartedSuccessfully()
}
@Test
fun stopAssociationDiscovery_stopSuccessfully() {
- sppProtocol.startAssociationDiscovery(testName, mockDiscoveryCallback, testIdentifier)
+ sppProtocol.startAssociationDiscovery(testName, testIdentifier, mockDiscoveryCallback)
+
sppProtocol.stopAssociationDiscovery()
+
verify(mockSppBinder).cancelConnectionAttempt(mockPendingConnection)
}
@Test
fun stopConnectionDiscovery_stopSuccessfully() {
- val testDeviceId = UUID.randomUUID()
-
- sppProtocol.startConnectionDiscovery(testDeviceId, testChallenge, mockDiscoveryCallback)
- sppProtocol.stopConnectionDiscovery(testDeviceId)
+ sppProtocol.startConnectionDiscovery(testIdentifier, testChallenge, mockDiscoveryCallback)
+ sppProtocol.stopConnectionDiscovery(testIdentifier)
verify(mockSppBinder).cancelConnectionAttempt(mockPendingConnection)
}
@@ -144,7 +143,7 @@
@Test
fun reset_cancelConnectionAttempt() {
- sppProtocol.startAssociationDiscovery(testName, mockDiscoveryCallback, testIdentifier)
+ sppProtocol.startAssociationDiscovery(testName, testIdentifier, mockDiscoveryCallback)
sppProtocol.reset()
verify(mockSppBinder).cancelConnectionAttempt(mockPendingConnection)
@@ -166,7 +165,7 @@
@Test
fun onMessageReceived_informCallback() {
val protocolId = establishConnection()
- sppProtocol.registerDataReceivedListener(protocolId, mockDataReceivedListener, directExecutor())
+ sppProtocol.registerDataReceivedListener(protocolId, mockDataReceivedListener)
argumentCaptor<OnMessageReceivedListener>().apply {
verify(mockSppBinder).setOnMessageReceivedListener(any(), capture())
@@ -182,18 +181,14 @@
argumentCaptor<UUID>().apply {
verify(mockSppBinder).registerConnectionCallback(capture(), any())
- assertThat(firstValue).isEqualTo(testIdentifier)
+ assertThat(firstValue).isEqualTo(testIdentifier.uuid)
}
}
@Test
fun onDeviceDisconnected_informCallback() {
val protocolId = establishConnection()
- sppProtocol.registerDeviceDisconnectedListener(
- protocolId,
- mockDisconnectedListener,
- directExecutor()
- )
+ sppProtocol.registerDeviceDisconnectedListener(protocolId, mockDisconnectedListener)
val connection =
argumentCaptor<Connection>()
@@ -211,7 +206,7 @@
@Test
fun startDiscovery_throwException_startDiscoveryFailed() {
doThrow(RemoteException()).`when`(mockSppBinder).connectAsServer(any(), any())
- sppProtocol.startAssociationDiscovery(testName, mockDiscoveryCallback, testIdentifier)
+ sppProtocol.startAssociationDiscovery(testName, testIdentifier, mockDiscoveryCallback)
verify(mockDiscoveryCallback).onDiscoveryFailedToStart()
}
@@ -265,10 +260,10 @@
ApplicationProvider.getApplicationContext<Context>()
.getSystemService(BluetoothManager::class.java)
val testBluetoothDevice = bluetoothManager.adapter.getRemoteDevice(testMacAddress)
- sppProtocol.startAssociationDiscovery(testName, mockDiscoveryCallback, testIdentifier)
+ sppProtocol.startAssociationDiscovery(testName, testIdentifier, mockDiscoveryCallback)
argumentCaptor<OnConnectedListener>().apply {
verify(mockPendingConnection).setOnConnectedListener(capture())
- firstValue.onConnected(testIdentifier, testBluetoothDevice, false, testName)
+ firstValue.onConnected(testIdentifier.uuid, testBluetoothDevice, false, testName)
}
return argumentCaptor<String>()
.apply { verify(mockDiscoveryCallback).onDeviceConnected(capture()) }
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/trust/TrustedDeviceAgentServiceTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/trust/TrustedDeviceAgentServiceTest.kt
new file mode 100644
index 0000000..04f3c81
--- /dev/null
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/trust/TrustedDeviceAgentServiceTest.kt
@@ -0,0 +1,130 @@
+package com.google.android.connecteddevice.trust
+
+import android.app.ActivityManager
+import android.app.KeyguardManager
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import com.google.android.connecteddevice.trust.api.ITrustedDeviceAgentDelegate
+import com.google.android.connecteddevice.trust.api.ITrustedDeviceManager
+import com.google.android.connecteddevice.util.ByteUtils
+import com.nhaarman.mockitokotlin2.argumentCaptor
+import com.nhaarman.mockitokotlin2.atLeastOnce
+import com.nhaarman.mockitokotlin2.mock
+import com.nhaarman.mockitokotlin2.never
+import com.nhaarman.mockitokotlin2.verify
+import java.lang.IllegalStateException
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.Robolectric
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows.shadowOf
+import org.robolectric.shadows.ShadowKeyguardManager
+
+@RunWith(RobolectricTestRunner::class)
+class TrustedDeviceAgentServiceTest {
+
+ private val context = ApplicationProvider.getApplicationContext<Context>()
+
+ private val userId = ActivityManager.getCurrentUser()
+
+ private val mockTrustedDeviceManager = mock<ITrustedDeviceManager>()
+
+ private lateinit var keyguardManager: ShadowKeyguardManager
+
+ private lateinit var service: TestTrustedDeviceAgentService
+
+ private lateinit var delegate: ITrustedDeviceAgentDelegate
+
+ @Before
+ fun setup() {
+ keyguardManager =
+ shadowOf(context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager)
+ service =
+ Robolectric.buildService(TestTrustedDeviceAgentService::class.java)
+ .apply { get().trustedDeviceManager = mockTrustedDeviceManager }
+ .create()
+ .get()
+ service.onCreate()
+ argumentCaptor<ITrustedDeviceAgentDelegate> {
+ verify(mockTrustedDeviceManager, atLeastOnce()).setTrustedDeviceAgentDelegate(capture())
+ delegate = firstValue
+ }
+ }
+
+ @Test
+ fun unlockUserWithToken_invokesCallbackAfterTokenReceived_DeviceAndUserAreLocked() {
+ lockUser()
+
+ sendToken()
+ unlockUser()
+
+ verify(mockTrustedDeviceManager).onUserUnlocked()
+ }
+
+ @Test
+ fun unlockUserWithToken_invokesCallbackAfterTokenReceived_DeviceIsLockedAndUserIsUnlocked() {
+ keyguardManager.setIsDeviceLocked(true)
+ service.isUserUnlocked = true
+
+ sendToken()
+
+ verify(mockTrustedDeviceManager).onUserUnlocked()
+ }
+
+ @Test
+ fun unlockUserWithToken_doesNotInvokeCallbackIfDeviceIsNotLocked() {
+ keyguardManager.setIsDeviceLocked(false)
+
+ sendToken()
+ unlockUser()
+
+ verify(mockTrustedDeviceManager, never()).onUserUnlocked()
+ }
+
+ @Test
+ fun onUserUnlock_doesNotInvokeCallbackIfTokenWasNotUsed() {
+ keyguardManager.setIsDeviceLocked(true)
+
+ unlockUser()
+
+ verify(mockTrustedDeviceManager, never()).onUserUnlocked()
+ }
+
+ private fun unlockUser() {
+ service.isUserUnlocked = true
+ service.maybeDismissLockscreen()
+ }
+
+ private fun lockUser() {
+ keyguardManager.setIsDeviceLocked(true)
+ service.isUserUnlocked = false
+ }
+
+ private fun sendToken() {
+ try {
+ delegate.unlockUserWithToken(ByteUtils.randomBytes(ESCROW_TOKEN_LENGTH), HANDLE, userId)
+ } catch (e: IllegalStateException) {
+ // This is expected because we aren't actually connected to a real TrustAgent.
+ }
+ }
+
+ companion object {
+ private const val ESCROW_TOKEN_LENGTH = 8
+
+ private const val HANDLE = 10L
+ }
+}
+
+class TestTrustedDeviceAgentService : TrustedDeviceAgentService() {
+
+ var isUserUnlocked = true
+
+ override fun bindToService() {
+ setupManager()
+ }
+
+ override fun isUserUnlocked(userId: Int): Boolean {
+ return isUserUnlocked
+ }
+}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/trust/TrustedDeviceManagerTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/trust/TrustedDeviceManagerTest.java
index 1260ba3..edfc1fc 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/trust/TrustedDeviceManagerTest.java
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/trust/TrustedDeviceManagerTest.java
@@ -3,6 +3,9 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
@@ -24,15 +27,19 @@
import com.google.android.connecteddevice.trust.api.ITrustedDeviceCallback;
import com.google.android.connecteddevice.trust.api.ITrustedDeviceEnrollmentCallback;
import com.google.android.connecteddevice.trust.api.TrustedDevice;
+import com.google.android.connecteddevice.trust.proto.PhoneAuthProto.PhoneCredentials;
import com.google.android.connecteddevice.trust.proto.TrustedDeviceMessageProto.TrustedDeviceMessage;
import com.google.android.connecteddevice.trust.proto.TrustedDeviceMessageProto.TrustedDeviceMessage.MessageType;
import com.google.android.connecteddevice.trust.proto.TrustedDeviceMessageProto.TrustedDeviceState;
import com.google.android.connecteddevice.trust.storage.TrustedDeviceDatabase;
import com.google.android.connecteddevice.trust.storage.TrustedDeviceEntity;
+import com.google.android.connecteddevice.trust.storage.TrustedDeviceTokenEntity;
+import com.google.android.connecteddevice.util.ByteUtils;
import com.google.common.collect.ImmutableList;
import com.google.protobuf.ByteString;
import java.util.Arrays;
import java.util.List;
+import java.util.UUID;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -44,7 +51,7 @@
@RunWith(AndroidJUnit4.class)
public final class TrustedDeviceManagerTest {
- private static final String DEFAULT_DEVICE_ID = "deviceId";
+ private static final String DEFAULT_DEVICE_ID = UUID.randomUUID().toString();
// Note: This token needs to be of length 8 to be valid.
private static final byte[] FAKE_TOKEN = "12345678".getBytes(UTF_8);
@@ -58,7 +65,7 @@
new ConnectedDevice(
DEFAULT_DEVICE_ID,
"secureConnectedDevice",
- /* belongsToActiveUser= */ true,
+ /* belongsToDriver= */ true,
/* hasSecureChannel= */ true);
private final Connector fakeConnector = spy(new FakeConnector());
@@ -73,6 +80,7 @@
private TrustedDeviceManager manager;
private TrustedDeviceFeature feature;
private TrustedDeviceDatabase database;
+ private TrustedDeviceFeature.Callback featureCallback;
@Before
public void setUp() throws RemoteException {
@@ -102,6 +110,10 @@
manager.setTrustedDeviceAgentDelegate(trustAgentDelegate);
manager.registerTrustedDeviceEnrollmentCallback(enrollmentCallback);
manager.registerTrustedDeviceCallback(trustedDeviceCallback);
+ ArgumentCaptor<TrustedDeviceFeature.Callback> captor =
+ ArgumentCaptor.forClass(TrustedDeviceFeature.Callback.class);
+ verify(feature).setCallback(captor.capture());
+ featureCallback = captor.getValue();
}
@After
@@ -135,6 +147,17 @@
}
@Test
+ public void enrollment_storesHashedToken() throws RemoteException {
+ triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
+
+ executeAndVerifyValidEnrollFlow();
+
+ TrustedDeviceTokenEntity entity =
+ database.trustedDeviceDao().getTrustedDeviceHashedToken(DEFAULT_DEVICE_ID);
+ assertThat(entity).isNotNull();
+ }
+
+ @Test
public void testStateSync_removesStoredTrustedDevice() throws RemoteException {
triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
executeAndVerifyValidEnrollFlow();
@@ -174,10 +197,7 @@
ConnectedDevice unenrolledDevice =
new ConnectedDevice(
- "unenrolled",
- "unenrolled",
- /* belongsToActiveUser= */ true,
- /* hasSecureChannel= */ true);
+ "unenrolled", "unenrolled", /* belongsToDriver= */ true, /* hasSecureChannel= */ true);
manager.featureCallback.onMessageReceived(
unenrolledDevice, createStateSyncMessage(/* enabled= */ false));
@@ -324,6 +344,105 @@
assertThat(trustedDeviceListCaptor.getValue()).containsExactly(expectedTrustedDevice);
}
+ @Test
+ public void unlock_validTokenPassedToDelegate() throws RemoteException {
+ triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
+ executeAndVerifyValidEnrollFlow();
+
+ TrustedDeviceMessage unlockMessage =
+ TrustedDeviceMessage.newBuilder()
+ .setType(MessageType.UNLOCK_CREDENTIALS)
+ .setPayload(
+ PhoneCredentials.newBuilder()
+ .setEscrowToken(ByteString.copyFrom(FAKE_TOKEN))
+ .setHandle(ByteString.copyFrom(ByteUtils.longToBytes(FAKE_HANDLE)))
+ .build()
+ .toByteString())
+ .build();
+ featureCallback.onMessageReceived(SECURE_CONNECTED_DEVICE, unlockMessage.toByteArray());
+
+ verify(trustAgentDelegate).unlockUserWithToken(FAKE_TOKEN, FAKE_HANDLE, DEFAULT_USER_ID);
+ }
+
+ @Test
+ public void unlock_invalidTokenIsNotPassedToDelegate() throws RemoteException {
+ triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
+ executeAndVerifyValidEnrollFlow();
+ byte[] incorrectToken = ByteUtils.randomBytes(8);
+
+ TrustedDeviceMessage unlockMessage =
+ TrustedDeviceMessage.newBuilder()
+ .setType(MessageType.UNLOCK_CREDENTIALS)
+ .setPayload(
+ PhoneCredentials.newBuilder()
+ .setEscrowToken(ByteString.copyFrom(incorrectToken))
+ .setHandle(ByteString.copyFrom(ByteUtils.longToBytes(FAKE_HANDLE)))
+ .build()
+ .toByteString())
+ .build();
+ featureCallback.onMessageReceived(SECURE_CONNECTED_DEVICE, unlockMessage.toByteArray());
+
+ verify(trustAgentDelegate, never()).unlockUserWithToken(any(), anyLong(), anyInt());
+ }
+
+ @Test
+ public void unlock_missingHashedTokenIsNotPassedToDelegate() throws RemoteException {
+ triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
+ executeAndVerifyValidEnrollFlow();
+ database.trustedDeviceDao().removeTrustedDeviceHashedToken(DEFAULT_DEVICE_ID);
+
+ TrustedDeviceMessage unlockMessage =
+ TrustedDeviceMessage.newBuilder()
+ .setType(MessageType.UNLOCK_CREDENTIALS)
+ .setPayload(
+ PhoneCredentials.newBuilder()
+ .setEscrowToken(ByteString.copyFrom(FAKE_TOKEN))
+ .setHandle(ByteString.copyFrom(ByteUtils.longToBytes(FAKE_HANDLE)))
+ .build()
+ .toByteString())
+ .build();
+ featureCallback.onMessageReceived(SECURE_CONNECTED_DEVICE, unlockMessage.toByteArray());
+
+ verify(trustAgentDelegate, never()).unlockUserWithToken(any(), anyLong(), anyInt());
+ }
+
+ @Test
+ public void removeTrustedDevice_removesHashedToken() throws RemoteException {
+ triggerDeviceConnected(SECURE_CONNECTED_DEVICE);
+ executeAndVerifyValidEnrollFlow();
+ ArgumentCaptor<TrustedDevice> captor = ArgumentCaptor.forClass(TrustedDevice.class);
+ verify(trustedDeviceCallback).onTrustedDeviceAdded(captor.capture());
+ TrustedDevice enrolledDevice = captor.getValue();
+
+ manager.removeTrustedDevice(enrolledDevice);
+
+ TrustedDeviceTokenEntity entity =
+ database.trustedDeviceDao().getTrustedDeviceHashedToken(DEFAULT_DEVICE_ID);
+ assertThat(entity).isNull();
+ }
+
+ @Test
+ public void setTrustedDeviceAgentDelegate_generatesFeatureStateAndRemovesAllInvalidDevices()
+ throws RemoteException {
+ String deviceId = UUID.randomUUID().toString();
+ TrustedDeviceEntity entity =
+ new TrustedDeviceEntity(
+ new TrustedDevice(deviceId, DEFAULT_USER_ID, FAKE_HANDLE), /* isValid= */ false);
+ database.trustedDeviceDao().addOrReplaceTrustedDevice(entity);
+ TrustedDeviceManager manager =
+ new TrustedDeviceManager(
+ database,
+ feature,
+ /* databaseExecutor= */ directExecutor(),
+ /* remoteCallbackExecutor= */ directExecutor());
+
+ manager.setTrustedDeviceAgentDelegate(trustAgentDelegate);
+
+ assertThat(database.trustedDeviceDao().getFeatureState(deviceId)).isNotNull();
+ assertThat(database.trustedDeviceDao().getTrustedDevice(deviceId)).isNull();
+ verify(trustAgentDelegate).removeEscrowToken(FAKE_HANDLE, DEFAULT_USER_ID);
+ }
+
/**
* Runs through a valid enrollment flow and verifies that the current flow is run.
*
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/trust/storage/MigrationTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/trust/storage/MigrationTest.java
index 146db85..8dabeeb 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/trust/storage/MigrationTest.java
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/trust/storage/MigrationTest.java
@@ -1,11 +1,10 @@
package com.google.android.connecteddevice.trust.storage;
+import static com.google.android.connecteddevice.trust.storage.TrustedDeviceDatabaseProvider.DATABASE_NAME;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import android.content.ContentValues;
import android.database.sqlite.SQLiteDatabase;
-import androidx.room.Room;
import androidx.room.migration.Migration;
import androidx.room.testing.MigrationTestHelper;
import androidx.sqlite.db.SupportSQLiteDatabase;
@@ -14,24 +13,29 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import java.io.IOException;
+import java.util.UUID;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class MigrationTest {
- private static final String TEST_DB_NAME = "migration_test";
+ private static final int CURRENT_VERSION = 4;
+
private static final String TRUSTED_DEVICES_TABLE = "trusted_devices";
private static final String TRUSTED_DEVICES_ID_COLUMN = "id";
private static final String TRUSTED_DEVICES_USER_ID_COLUMN = "userId";
private static final String TRUSTED_DEVICES_HANDLE_COLUMN = "handle";
+ private static final String TRUSTED_DEVICES_IS_VALID_COLUMN = "isValid";
- private static final String DEFAULT_DEVICE_ID = "id";
+ private static final String DEFAULT_DEVICE_ID = UUID.randomUUID().toString();
private static final int DEFAULT_USER_ID = 11;
private static final long DEFAULT_HANDLE = 11L;
private static final boolean DEFAULT_IS_VALID = true;
+ private static final int VERSION_IS_VALID_ADDED = 3;
+
@Rule
public MigrationTestHelper helper =
new MigrationTestHelper(
@@ -40,77 +44,93 @@
new FrameworkSQLiteOpenHelperFactory());
@Test
- public void testMigrate1To3() throws IOException {
+ public void testMigrate1To4() throws IOException {
+ int startingVersion = 1;
// Create the database in version 1
- SupportSQLiteDatabase db = helper.createDatabase(TEST_DB_NAME, /* version= */ 1);
- // Insert test data
- insertTrustedDevice(db);
- // Prepare for the next version
- db.close();
+ try (SupportSQLiteDatabase db = helper.createDatabase(DATABASE_NAME, startingVersion)) {
+ insertTrustedDevice(db, startingVersion);
+ }
- // Re-open the database with version 3 and provide MIGRATION_1_2 and
- // MIGRATION_2_3 as the migration process.
- helper.runMigrationsAndValidate(
- TEST_DB_NAME, /* version= */ 3, /* validateDroppedTables= */ true, ALL_MIGRATIONS);
-
- // MigrationTestHelper automatically verifies the schema changes
TrustedDeviceEntity dbEntity =
getMigratedRoomDatabase().trustedDeviceDao().getTrustedDevice(DEFAULT_DEVICE_ID);
-
TrustedDeviceEntity expectedDevice =
new TrustedDeviceEntity(
- DEFAULT_DEVICE_ID, DEFAULT_USER_ID, DEFAULT_HANDLE, DEFAULT_IS_VALID);
+ DEFAULT_DEVICE_ID, DEFAULT_USER_ID, DEFAULT_HANDLE, /* isValid= */ false);
assertThat(dbEntity).isEqualTo(expectedDevice);
}
@Test
- public void testMigrate2To3() throws IOException {
+ public void testMigrate2To4() throws IOException {
+ int startingVersion = 2;
// Create the database in version 2
- SupportSQLiteDatabase db = helper.createDatabase(TEST_DB_NAME, /* version= */ 2);
- // Insert test data
- insertTrustedDevice(db);
- // Prepare for the next version
- db.close();
+ try (SupportSQLiteDatabase db = helper.createDatabase(DATABASE_NAME, startingVersion)) {
+ insertTrustedDevice(db, startingVersion);
+ }
- // Re-open the database with version 3 and provide MIGRATION_1_2 and
- // MIGRATION_2_3 as the migration process.
- helper.runMigrationsAndValidate(
- TEST_DB_NAME, /* version= */ 3, /* validateDroppedTables= */ true, ALL_MIGRATIONS);
-
- // MigrationTestHelper automatically verifies the schema changes
TrustedDeviceEntity dbEntity =
getMigratedRoomDatabase().trustedDeviceDao().getTrustedDevice(DEFAULT_DEVICE_ID);
TrustedDeviceEntity expectedDevice =
new TrustedDeviceEntity(
- DEFAULT_DEVICE_ID, DEFAULT_USER_ID, DEFAULT_HANDLE, DEFAULT_IS_VALID);
+ DEFAULT_DEVICE_ID, DEFAULT_USER_ID, DEFAULT_HANDLE, /* isValid= */ false);
assertThat(dbEntity).isEqualTo(expectedDevice);
}
+ @Test
+ public void testMigrate3To4() throws IOException {
+ int startingVersion = 3;
+ // Create the database in version 3
+ try (SupportSQLiteDatabase db = helper.createDatabase(DATABASE_NAME, startingVersion)) {
+ insertTrustedDevice(db, startingVersion);
+ }
+
+ TrustedDeviceEntity dbEntity =
+ getMigratedRoomDatabase().trustedDeviceDao().getTrustedDevice(DEFAULT_DEVICE_ID);
+
+ TrustedDeviceEntity expectedDevice =
+ new TrustedDeviceEntity(
+ DEFAULT_DEVICE_ID, DEFAULT_USER_ID, DEFAULT_HANDLE, /* isValid= */ false);
+ assertThat(dbEntity).isEqualTo(expectedDevice);
+ }
+
+ @Test
+ public void validateSchemaChanges() throws IOException {
+ int startingVersion = 1;
+ try (SupportSQLiteDatabase db = helper.createDatabase(DATABASE_NAME, startingVersion)) {
+ insertTrustedDevice(db, startingVersion);
+ }
+
+ for (int version = startingVersion + 1; version <= CURRENT_VERSION; version++) {
+ helper.runMigrationsAndValidate(
+ DATABASE_NAME, version, /* validateDroppedTables= */ true, ALL_MIGRATIONS);
+ }
+ }
+
private TrustedDeviceDatabase getMigratedRoomDatabase() {
TrustedDeviceDatabase database =
- Room.databaseBuilder(
- ApplicationProvider.getApplicationContext(),
- TrustedDeviceDatabase.class,
- TEST_DB_NAME)
- .allowMainThreadQueries()
- .setQueryExecutor(directExecutor())
- .build();
+ new TrustedDeviceDatabaseProvider(
+ ApplicationProvider.getApplicationContext(), /* allowMainThreadQueries= */ true)
+ .database;
// close the database and release any stream resources when the test finishes
helper.closeWhenFinished(database);
return database;
}
- private void insertTrustedDevice(SupportSQLiteDatabase db) {
+ private void insertTrustedDevice(SupportSQLiteDatabase db, int version) {
ContentValues values = new ContentValues();
values.put(TRUSTED_DEVICES_ID_COLUMN, DEFAULT_DEVICE_ID);
values.put(TRUSTED_DEVICES_USER_ID_COLUMN, DEFAULT_USER_ID);
values.put(TRUSTED_DEVICES_HANDLE_COLUMN, DEFAULT_HANDLE);
+ if (version >= VERSION_IS_VALID_ADDED) {
+ values.put(TRUSTED_DEVICES_IS_VALID_COLUMN, DEFAULT_IS_VALID);
+ }
db.insert(TRUSTED_DEVICES_TABLE, SQLiteDatabase.CONFLICT_REPLACE, values);
}
private static final Migration[] ALL_MIGRATIONS =
new Migration[] {
- TrustedDeviceDatabaseProvider.MIGRATION_1_2, TrustedDeviceDatabaseProvider.MIGRATION_2_3
+ TrustedDeviceDatabaseProvider.MIGRATION_1_2,
+ TrustedDeviceDatabaseProvider.MIGRATION_2_3,
+ TrustedDeviceDatabaseProvider.MIGRATION_3_4
};
}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/ui/AssociatedDeviceDetailsTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/ui/AssociatedDeviceDetailsTest.kt
new file mode 100644
index 0000000..2320b4d
--- /dev/null
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/ui/AssociatedDeviceDetailsTest.kt
@@ -0,0 +1,98 @@
+/*
+ * 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.
+ */
+
+package com.google.android.connecteddevice.ui
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.android.connecteddevice.model.AssociatedDevice
+import com.google.android.connecteddevice.ui.AssociatedDeviceDetails.ConnectionState.CONNECTED
+import com.google.android.connecteddevice.ui.AssociatedDeviceDetails.ConnectionState.NOT_DETECTED
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+final class AssociatedDeviceDetailsTest {
+ @Test
+ fun testEquality_associatedDeviceEquals() {
+ val details1 = AssociatedDeviceDetails(createAssociatedDevice(), CONNECTED)
+ val details2 = AssociatedDeviceDetails(createAssociatedDevice(), CONNECTED)
+
+ assertThat(details1).isEqualTo(details2)
+ assertThat(details1.hashCode()).isEqualTo(details2.hashCode())
+ }
+
+ @Test
+ fun testEquality_associatedDeviceEquals_differentConnectionState() {
+ val details1 = AssociatedDeviceDetails(createAssociatedDevice(), CONNECTED)
+ val details2 = AssociatedDeviceDetails(createAssociatedDevice(), NOT_DETECTED)
+
+ assertThat(details1).isEqualTo(details2)
+ assertThat(details1.hashCode()).isEqualTo(details2.hashCode())
+ }
+
+ @Test
+ fun testEquality_associatedDeviceDifferent() {
+ val details1 = AssociatedDeviceDetails(createAssociatedDevice(), CONNECTED)
+ val details2 = AssociatedDeviceDetails(createAssociateddetails2(), CONNECTED)
+
+ assertThat(details1).isNotEqualTo(details2)
+ }
+
+ @Test
+ fun testGetters_matchAssociatedDevice() {
+ val associatedDevice = createAssociatedDevice()
+ val connectionState = CONNECTED
+ val details = AssociatedDeviceDetails(associatedDevice, connectionState)
+
+ assertThat(details.deviceId).isEqualTo(associatedDevice.deviceId)
+ assertThat(details.deviceName).isEqualTo(associatedDevice.deviceName)
+ assertThat(details.deviceAddress).isEqualTo(associatedDevice.deviceAddress)
+ assertThat(details.isConnectionEnabled).isEqualTo(associatedDevice.isConnectionEnabled)
+ assertThat(details.userId).isEqualTo(associatedDevice.userId)
+ assertThat(details.connectionState).isEqualTo(connectionState)
+ }
+
+ private companion object {
+ const val TEST_ASSOCIATED_DEVICE_ID = "test_device_id"
+ const val TEST_ASSOCIATED_DEVICE_NAME = "test_device_name"
+ const val TEST_ASSOCIATED_DEVICE_ADDRESS = "test_device_address"
+
+ const val TEST_ASSOCIATED_DEVICE_ID_2 = "test_device_id_2"
+ const val TEST_ASSOCIATED_DEVICE_NAME_2 = "test_device_name_2"
+ const val TEST_ASSOCIATED_DEVICE_ADDRESS_2 = "test_device_address_2"
+
+ fun createAssociatedDevice(): AssociatedDevice =
+ AssociatedDevice(
+ TEST_ASSOCIATED_DEVICE_ID,
+ TEST_ASSOCIATED_DEVICE_ADDRESS,
+ TEST_ASSOCIATED_DEVICE_NAME,
+ /* isConnectionEnabled= */ true
+ )
+
+ /**
+ * Returns an [AssociatedDevice] that is different from that returned by
+ * [createAssociatedDevice].
+ */
+ fun createAssociateddetails2(): AssociatedDevice =
+ AssociatedDevice(
+ TEST_ASSOCIATED_DEVICE_ID_2,
+ TEST_ASSOCIATED_DEVICE_ADDRESS_2,
+ TEST_ASSOCIATED_DEVICE_NAME_2,
+ /* isConnectionEnabled= */ true
+ )
+ }
+}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/ui/AssociatedDeviceViewModelTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/ui/AssociatedDeviceViewModelTest.java
index a73c2a2..e2d5c89 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/ui/AssociatedDeviceViewModelTest.java
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/ui/AssociatedDeviceViewModelTest.java
@@ -23,6 +23,7 @@
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.app.Application;
import android.bluetooth.BluetoothAdapter;
@@ -36,11 +37,13 @@
import com.google.android.connecteddevice.api.FakeConnector;
import com.google.android.connecteddevice.api.IAssociationCallback;
import com.google.android.connecteddevice.model.AssociatedDevice;
-import com.google.android.connecteddevice.model.AssociatedDeviceDetails;
import com.google.android.connecteddevice.model.ConnectedDevice;
import com.google.android.connecteddevice.model.OobData;
import com.google.android.connecteddevice.model.StartAssociationResponse;
+import com.google.android.connecteddevice.ui.AssociatedDeviceDetails.ConnectionState;
import com.google.android.connecteddevice.ui.AssociatedDeviceViewModel.AssociationState;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
import java.util.UUID;
import org.junit.Before;
import org.junit.Rule;
@@ -75,7 +78,7 @@
private final FakeConnector fakeConnector = spy(new FakeConnector());
@Mock private Observer<AssociationState> mockAssociationStateObserver;
- @Mock private Observer<AssociatedDeviceDetails> mockDeviceDetailsObserver;
+ @Mock private Observer<List<AssociatedDeviceDetails>> mockDeviceDetailsObserver;
@Mock private Observer<String> mockCarNameObserver;
@Mock private Observer<String> mockPairingCodeObserver;
@Mock private Observer<AssociatedDevice> mockRemovedDeviceObserver;
@@ -88,7 +91,11 @@
public void setUp() throws RemoteException {
viewModel =
new AssociatedDeviceViewModel(
- application, /* isSppEnabled= */ false, TEST_BLE_DEVICE_NAME_PREFIX, fakeConnector);
+ application,
+ /* isSppEnabled= */ false,
+ TEST_BLE_DEVICE_NAME_PREFIX,
+ /* isPassengerEnabled= */ false,
+ fakeConnector);
adapter.enable();
}
@@ -99,23 +106,26 @@
}
@Test
- public void removeCurrentDevice() {
- fakeConnector.addAssociatedDevice(createAssociatedDevice(/* isConnectionEnabled= */ true));
- viewModel.removeCurrentDevice();
+ public void removeDevice() {
+ AssociatedDevice device = createAssociatedDevice(/* isConnectionEnabled= */ true);
+ fakeConnector.addAssociatedDevice(device);
+ viewModel.removeDevice(device);
verify(fakeConnector).removeAssociatedDevice(eq(TEST_ASSOCIATED_DEVICE_ID));
}
@Test
- public void toggleConnectionStatusForCurrentDevice_disableDevice() {
- fakeConnector.addAssociatedDevice(createAssociatedDevice(/* isConnectionEnabled= */ true));
- viewModel.toggleConnectionStatusForCurrentDevice();
+ public void toggleConnectionStatusForDevice_disableDevice() {
+ AssociatedDevice device = createAssociatedDevice(/* isConnectionEnabled= */ true);
+ fakeConnector.addAssociatedDevice(device);
+ viewModel.toggleConnectionStatusForDevice(device);
verify(fakeConnector).disableAssociatedDeviceConnection(eq(TEST_ASSOCIATED_DEVICE_ID));
}
@Test
- public void toggleConnectionStatusForCurrentDevice_enableDevice() {
- fakeConnector.addAssociatedDevice(createAssociatedDevice(/* isConnectionEnabled= */ false));
- viewModel.toggleConnectionStatusForCurrentDevice();
+ public void toggleConnectionStatusForDevice_enableDevice() {
+ AssociatedDevice device = createAssociatedDevice(/* isConnectionEnabled= */ false);
+ fakeConnector.addAssociatedDevice(device);
+ viewModel.toggleConnectionStatusForDevice(device);
verify(fakeConnector).enableAssociatedDeviceConnection(eq(TEST_ASSOCIATED_DEVICE_ID));
}
@@ -201,12 +211,72 @@
}
@Test
- public void retrieveAssociatedDevice() {
+ public void retrieveAssociatedDeviceForDriver() {
AssociatedDevice testDevice = createAssociatedDevice(/* isConnectionEnabled= */ true);
fakeConnector.addAssociatedDevice(testDevice);
- viewModel.getCurrentDeviceDetails().observeForever(mockDeviceDetailsObserver);
- assertThat(viewModel.getCurrentDeviceDetails().getValue().getAssociatedDevice())
- .isEqualTo(testDevice);
+
+ AssociatedDeviceDetails expectedDetails =
+ new AssociatedDeviceDetails(testDevice, ConnectionState.NOT_DETECTED);
+ viewModel.getAssociatedDevicesDetails().observeForever(mockDeviceDetailsObserver);
+ assertThat(viewModel.getAssociatedDevicesDetails().getValue()).containsExactly(expectedDetails);
+ }
+
+ @Test
+ public void retrieveAssociatedDeviceForDriver_onConnection() {
+ // Adding devices before the creation of the ViewModel since the connector is connected
+ // upon construction.
+ AssociatedDevice driverDevice = createAssociatedDevice(/* isConnectionEnabled= */ true);
+ fakeConnector.addAssociatedDevice(driverDevice);
+ fakeConnector.claimAssociatedDevice(driverDevice.getDeviceId());
+
+ AssociatedDevice nonClaimedDevice = createAssociatedDevice(/* isConnectionEnabled= */ true);
+ fakeConnector.addAssociatedDevice(nonClaimedDevice);
+
+ viewModel =
+ new AssociatedDeviceViewModel(
+ application,
+ /* isSppEnabled= */ false,
+ TEST_BLE_DEVICE_NAME_PREFIX,
+ /* isPassengerEnabled= */ false,
+ fakeConnector);
+ adapter.enable();
+
+ AssociatedDeviceDetails expectedDetails =
+ new AssociatedDeviceDetails(driverDevice, ConnectionState.NOT_DETECTED);
+ viewModel.getAssociatedDevicesDetails().observeForever(mockDeviceDetailsObserver);
+
+ // Only the driver device should show up with passenger mode disabled.
+ assertThat(viewModel.getAssociatedDevicesDetails().getValue()).containsExactly(expectedDetails);
+ }
+
+ @Test
+ public void retrieveAssociatedDevices_onConnection_passengerEnabled() {
+ // Adding devices before the creation of the ViewModel since the connector is connected
+ // upon construction.
+ AssociatedDevice driverDevice = createAssociatedDevice(/* isConnectionEnabled= */ true);
+ fakeConnector.addAssociatedDevice(driverDevice);
+ fakeConnector.claimAssociatedDevice(driverDevice.getDeviceId());
+
+ AssociatedDevice nonClaimedDevice = createAssociatedDevice(/* isConnectionEnabled= */ true);
+ fakeConnector.addAssociatedDevice(nonClaimedDevice);
+
+ viewModel =
+ new AssociatedDeviceViewModel(
+ application,
+ /* isSppEnabled= */ false,
+ TEST_BLE_DEVICE_NAME_PREFIX,
+ /* isPassengerEnabled= */ true,
+ fakeConnector);
+ adapter.enable();
+
+ AssociatedDeviceDetails driverDetails =
+ new AssociatedDeviceDetails(driverDevice, ConnectionState.NOT_DETECTED);
+ AssociatedDeviceDetails passengerDetails =
+ new AssociatedDeviceDetails(nonClaimedDevice, ConnectionState.NOT_DETECTED);
+
+ viewModel.getAssociatedDevicesDetails().observeForever(mockDeviceDetailsObserver);
+ assertThat(viewModel.getAssociatedDevicesDetails().getValue()).containsExactly(
+ driverDetails, passengerDetails);
}
@Test
@@ -219,17 +289,20 @@
TEST_ASSOCIATED_DEVICE_ADDRESS,
TEST_ASSOCIATED_DEVICE_NAME_2,
/* isConnectionEnabled= */ true);
+
+ AssociatedDeviceDetails expectedDetails =
+ new AssociatedDeviceDetails(updatedDevice, ConnectionState.NOT_DETECTED);
+
fakeConnector.getCallback().onAssociatedDeviceUpdated(updatedDevice);
- viewModel.getCurrentDeviceDetails().observeForever(mockDeviceDetailsObserver);
- assertThat(viewModel.getCurrentDeviceDetails().getValue().getDeviceName())
- .isEqualTo(TEST_ASSOCIATED_DEVICE_NAME_2);
+ viewModel.getAssociatedDevicesDetails().observeForever(mockDeviceDetailsObserver);
+ assertThat(viewModel.getAssociatedDevicesDetails().getValue()).containsExactly(expectedDetails);
}
@Test
public void removeAssociatedDevice() {
AssociatedDevice testDevice = createAssociatedDevice(/* isConnectionEnabled= */ true);
fakeConnector.addAssociatedDevice(testDevice);
- viewModel.removeCurrentDevice();
+ viewModel.removeDevice(testDevice);
verify(fakeConnector).removeAssociatedDevice(eq(TEST_ASSOCIATED_DEVICE_ID));
}
@@ -245,28 +318,49 @@
}
@Test
- public void associatedDeviceConnected() {
+ public void onDeviceConnected_updateConnectionStatusToDetected() {
AssociatedDevice testAssociatedDevice = createAssociatedDevice(/* isConnectionEnabled= */ true);
fakeConnector.addAssociatedDevice(testAssociatedDevice);
- ConnectedDevice testConnectedDevice = createConnectedDevice();
+ ConnectedDevice testConnectedDevice = createDetectedDevice();
+ when(fakeConnector.getConnectedDevices()).thenReturn(ImmutableList.of(testConnectedDevice));
fakeConnector.getCallback().onDeviceConnected(testConnectedDevice);
- viewModel.getCurrentDeviceDetails().observeForever(mockDeviceDetailsObserver);
- assertThat(viewModel.getCurrentDeviceDetails().getValue().isConnected()).isTrue();
+ AssociatedDeviceDetails expectedDetails =
+ new AssociatedDeviceDetails(testAssociatedDevice, ConnectionState.DETECTED);
+
+ viewModel.getAssociatedDevicesDetails().observeForever(mockDeviceDetailsObserver);
+ assertThat(viewModel.getAssociatedDevicesDetails().getValue()).containsExactly(expectedDetails);
}
@Test
- public void associatedDeviceDisconnected() {
+ public void onSecureChannelEstablished_updatesConnectionStatusToConnected() {
+ AssociatedDevice testAssociatedDevice = createAssociatedDevice(/* isConnectionEnabled= */ true);
+ fakeConnector.addAssociatedDevice(testAssociatedDevice);
+ ConnectedDevice testConnectedDevice = createConnectedDevice();
+ when(fakeConnector.getConnectedDevices()).thenReturn(ImmutableList.of(testConnectedDevice));
+
+ fakeConnector.getCallback().onSecureChannelEstablished(testConnectedDevice);
+
+ AssociatedDeviceDetails expectedDetails =
+ new AssociatedDeviceDetails(testAssociatedDevice, ConnectionState.CONNECTED);
+ viewModel.getAssociatedDevicesDetails().observeForever(mockDeviceDetailsObserver);
+ assertThat(viewModel.getAssociatedDevicesDetails().getValue()).containsExactly(expectedDetails);
+ }
+
+ @Test
+ public void onDeviceDisconnected_updatesConnectionStatusToNotDetected() {
AssociatedDevice testAssociatedDevice = createAssociatedDevice(/* isConnectionEnabled= */ true);
fakeConnector.addAssociatedDevice(testAssociatedDevice);
ConnectedDevice testConnectedDevice = createConnectedDevice();
- fakeConnector.getCallback().onDeviceConnected(testConnectedDevice);
fakeConnector.getCallback().onDeviceDisconnected(testConnectedDevice);
- viewModel.getCurrentDeviceDetails().observeForever(mockDeviceDetailsObserver);
- assertThat(viewModel.getCurrentDeviceDetails().getValue().isConnected()).isFalse();
+ AssociatedDeviceDetails expectedDetails =
+ new AssociatedDeviceDetails(testAssociatedDevice, ConnectionState.NOT_DETECTED);
+
+ viewModel.getAssociatedDevicesDetails().observeForever(mockDeviceDetailsObserver);
+ assertThat(viewModel.getAssociatedDevicesDetails().getValue()).containsExactly(expectedDetails);
}
@Test
@@ -275,10 +369,31 @@
new AssociatedDeviceViewModel(
ApplicationProvider.getApplicationContext(),
/* isSppEnabled= */ false,
- TEST_BLE_DEVICE_NAME_PREFIX);
+ TEST_BLE_DEVICE_NAME_PREFIX,
+ /* isPassengerEnabled= */ false);
associatedDeviceViewModel.onCleared();
}
+ @Test
+ public void claimDevice_claimsAssociatedDevice() {
+ AssociatedDevice device = createAssociatedDevice(/* isConnectionEnabled= */ true);
+ fakeConnector.addAssociatedDevice(device);
+
+ viewModel.claimDevice(device);
+
+ verify(fakeConnector).claimAssociatedDevice(device.getDeviceId());
+ }
+
+ @Test
+ public void removeClaimOnDevice_removesAssociatedDeviceClaim() {
+ AssociatedDevice device = createAssociatedDevice(/* isConnectionEnabled= */ true);
+ fakeConnector.addAssociatedDevice(device);
+
+ viewModel.removeClaimOnDevice(device);
+
+ verify(fakeConnector).removeAssociatedDeviceClaim(device.getDeviceId());
+ }
+
private void captureAssociationCallback() {
ArgumentCaptor<IAssociationCallback> associationCallbackCaptor =
ArgumentCaptor.forClass(IAssociationCallback.class);
@@ -298,7 +413,15 @@
return new ConnectedDevice(
TEST_ASSOCIATED_DEVICE_ID,
TEST_ASSOCIATED_DEVICE_NAME_1,
- /* belongsToActiveUser= */ true,
+ /* belongsToDriver= */ true,
/* hasSecureChannel= */ true);
}
+
+ private static ConnectedDevice createDetectedDevice() {
+ return new ConnectedDevice(
+ TEST_ASSOCIATED_DEVICE_ID,
+ TEST_ASSOCIATED_DEVICE_NAME_1,
+ /* belongsToDriver= */ true,
+ /* hasSecureChannel= */ false);
+ }
}