Project import generated by Copybara
Included changes:
Release-Id: aae-companiondevice-android_20220330.00_RC02
Change-Id: I82de0aedcf48eeb38fae8f85888f1b5b4dffd92e
diff --git a/companiondevice/AndroidManifest.xml b/companiondevice/AndroidManifest.xml
index 0a80967..138c9e3 100644
--- a/companiondevice/AndroidManifest.xml
+++ b/companiondevice/AndroidManifest.xml
@@ -88,8 +88,8 @@
android:resource="@string/car_association_service_uuid" />
<meta-data android:name="com.google.android.connecteddevice.transport_protocols"
android:resource="@array/transport_protocols" />
- <meta-data android:name="com.google.android.connecteddevice.supported_oob_channels"
- android:resource="@array/supported_oob_channels"/>
+ <meta-data android:name="com.google.android.connecteddevice.car_eap_oob_protocol_name"
+ android:resource="@string/car_eap_oob_protocol_name" />
</service>
<service
@@ -121,6 +121,12 @@
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.supported_oob_channels"
+ android:resource="@array/supported_oob_channels"/>
+ <meta-data android:name="com.google.android.connecteddevice.car_eap_client_name"
+ android:resource="@string/car_eap_client_name" />
+ <meta-data android:name="com.google.android.connecteddevice.car_eap_service_name"
+ android:resource="@string/car_eap_service_name" />
</service>
<activity android:name=".AssociationActivity"
android:exported="true"
diff --git a/companiondevice/build.gradle b/companiondevice/build.gradle
index 83c039e..c12b2f5 100644
--- a/companiondevice/build.gradle
+++ b/companiondevice/build.gradle
@@ -14,7 +14,7 @@
applicationId "com.google.android.companiondevicesupport"
minSdkVersion 29
targetSdkVersion 31
- versionCode 1357
+ versionCode 1441
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
diff --git a/companiondevice/res/values/config.xml b/companiondevice/res/values/config.xml
index 3f9d9b2..4ce667a 100644
--- a/companiondevice/res/values/config.xml
+++ b/companiondevice/res/values/config.xml
@@ -20,6 +20,11 @@
<!-- Platform values -->
<string name="car_association_service_uuid" translatable="false">5e2a68a4-27be-43f9-8d1e-4546976fabd7</string>
+ <!-- EAP OOB values -->
+ <string name="car_eap_oob_protocol_name" translatable="false">com.google.companion.oob-association</string>
+ <string name="car_eap_client_name" translatable="false">com.google.android.connecteddevice.transport</string>
+ <string name="car_eap_service_name" translatable="false">com.panasonic.iapx.serviceconnector</string>
+
<!-- Services to start early in the single user (user 0). -->
<string-array name="early_single_user_services" translatable="false">
<item>com.google.android.companiondevicesupport/com.google.android.connecteddevice.trust.TrustedDeviceManagerService</item>
@@ -40,8 +45,8 @@
<!-- Supported OOB verification approaches. -->
<string-array name="supported_oob_channels" translatable="false">
- <item>BT_RFCOMM</item>
- <item>PRE_ASSOCIATION</item>
+ <item>eap</item>
+ <item>spp</item>
</string-array>
<!-- Supported transport protocols. -->
diff --git a/companiondevice/res/values/strings.xml b/companiondevice/res/values/strings.xml
index ecf5294..d64dfa3 100644
--- a/companiondevice/res/values/strings.xml
+++ b/companiondevice/res/values/strings.xml
@@ -39,7 +39,7 @@
<!-- Instruction for connecting to car [CHAR LIMIT=100] -->
<string name="connect_to_targe_car_instruction_text">Connect to <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>
<!-- Instruction for scanning QR code [CHAR LIMIT=150] -->
- <string name="qr_instruction_text">Scan the QR code or open MyCompanion to connect to <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>
+ <string name="qr_instruction_text">Use your phone to scan the QR code or open MyCompanion to connect to <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>
<!-- Instruction for connecting to car [CHAR LIMIT=100] -->
<string name="connect_to_car_instruction_text">Connect to the car</string>
<!-- Title for confirm pairing code fragment [CHAR LIMIT=60]-->
@@ -74,6 +74,13 @@
<string name="turn_on_bluetooth_dialog_title">Turn on Bluetooth?</string>
<!-- Message for turn on bluetooth dialog [CHAR LIMIT=150]-->
<string name="turn_on_bluetooth_dialog_message">To connect to your companion device, your car needs Bluetooth.</string>
+ <!-- Title for CompanionDevice not available dialog [CHAR LIMIT=40] -->
+ <string name="companion_not_available_dialog_title">This setting isn\’t available</string>
+ <!-- Message for CompanionDevice not available dialog [CHAR LIMIT=150]-->
+ <string name="companion_not_available_dialog_message">Guest profiles can\’t access CompanionDevice</string>
+ <!-- Switch user message for CompanionDevice not available dialog [CHAR LIMIT=200]-->
+ <string name="companion_not_available_dialog_switch_user_message">Guest profiles can\’t access CompanionDevice. To change your profile, go to Settings > Users.</string>
+
<!-- Accept button text [CHAR LIMIT=20] -->
<string name="accept">Accept</string>
@@ -111,6 +118,8 @@
<string name="disconnect">Disconnect</string>
<!-- Skip button text [CHAR LIMIT=20] -->
<string name="skip">Skip</string>
+ <!-- change profile button text [CHAR LIMIT=40] -->
+ <string name="change_profile">Change profile</string>
<!-- Connected with secure channel status text [CHAR LIMIT=20]-->
<string name="connected">Connected</string>
diff --git a/companiondevice/src/com/google/android/companiondevicesupport/AssociationActivity.java b/companiondevice/src/com/google/android/companiondevicesupport/AssociationActivity.java
index e89a04c..266648a 100644
--- a/companiondevice/src/com/google/android/companiondevicesupport/AssociationActivity.java
+++ b/companiondevice/src/com/google/android/companiondevicesupport/AssociationActivity.java
@@ -21,16 +21,19 @@
import static com.android.car.ui.toolbar.Toolbar.State.SUBPAGE;
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.Manifest.permission;
import android.app.AlertDialog;
import android.app.Dialog;
import android.bluetooth.BluetoothAdapter;
+import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
+import android.os.UserManager;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
@@ -40,6 +43,7 @@
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelStoreOwner;
@@ -67,6 +71,7 @@
private static final String TURN_ON_BLUETOOTH_FRAGMENT_TAG = "TurnOnBluetoothFragment";
private static final String ASSOCIATION_ERROR_FRAGMENT_TAG = "AssociationErrorFragment";
private static final String TURN_ON_BLUETOOTH_DIALOG_TAG = "TurnOnBluetoothDialog";
+ private static final String COMPANION_NOT_AVAILABLE_DIALOG_TAG = "CompanionNotAvailableDialog";
private static final String EXTRA_AUTH_IS_SETUP_WIZARD = "is_setup_wizard";
private static final String EXTRA_AUTH_IS_SETUP_PROFILE = "is_setup_profile_association";
private static final String EXTRA_USE_IMMERSIVE_MODE = "useImmersiveMode";
@@ -74,6 +79,12 @@
private static final String ASSOCIATED_DEVICE_DETAILS_BACKSTACK_NAME =
"AssociatedDeviceDetailsBackstack";
+ private static final String PROFILE_SWITCH_PACKAGE_NAME = "com.android.car.settings";
+ private static final String PROFILE_SWITCH_CLASS_NAME_R =
+ "com.android.car.settings.users.UserSwitcherActivity";
+ private static final String PROFILE_SWITCH_CLASS_NAME =
+ "com.android.car.settings.profiles.ProfileSwitcherActivity";
+
private final List<String> requiredPermissions = new ArrayList<>();
private ToolbarController toolbar;
@@ -148,6 +159,10 @@
protected void onStart() {
super.onStart();
handleImmersive();
+ UserManager userManager = getSystemService(UserManager.class);
+ if (userManager.isGuestUser()) {
+ showCompanionNotAvailableDialog();
+ }
}
@Override
@@ -292,6 +307,7 @@
case STARTED:
runOnUiThread(
() -> {
+ logd(TAG, "Association state is started; Show companion landing fragment.");
showCompanionLandingFragment();
showProgressBar();
});
@@ -313,7 +329,12 @@
});
model
.getAssociatedDevicesDetails()
- .observe(this, this::handleAssociatedDevicesDetailsChange);
+ .observe(
+ this,
+ devicesDetails -> {
+ boolean isServiceConnected = model.isServiceConnected().getValue();
+ handleAssociatedDevicesDetailsAndConnectionChange(devicesDetails, isServiceConnected);
+ });
model
.getRemovedDevice()
.observe(
@@ -329,12 +350,9 @@
this,
isServiceConnected -> {
logd(TAG, "Service connection status: " + isServiceConnected);
- if (isServiceConnected) {
- showCompanionLandingFragment();
- hideProgressBar();
- } else {
- showLoadingScreen();
- }
+ List<AssociatedDeviceDetails> devicesDetails =
+ model.getAssociatedDevicesDetails().getValue();
+ handleAssociatedDevicesDetailsAndConnectionChange(devicesDetails, isServiceConnected);
});
}
@@ -349,7 +367,13 @@
}
}
- private void handleAssociatedDevicesDetailsChange(List<AssociatedDeviceDetails> devicesDetails) {
+ private void handleAssociatedDevicesDetailsAndConnectionChange(
+ List<AssociatedDeviceDetails> devicesDetails, boolean isServiceConnected) {
+ if (!isServiceConnected) {
+ logw(TAG, "Service not connected, ignore device details change.");
+ showLoadingScreen();
+ return;
+ }
// An empty list means that there are no more associated devices.
if (devicesDetails.isEmpty()) {
handleEmptyDeviceList();
@@ -440,13 +464,13 @@
"Device list is empty, but landing fragment already showing. Nothing more to be done.");
return;
}
-
+ logd(TAG, "No associated device, showing landing screen.");
+ hideProgressBar();
showCompanionLandingFragment();
}
private void showCompanionLandingFragment() {
maybeClearDetailsFragmentFromBackstack();
- dismissButtons();
if (getResources().getBoolean(R.bool.enable_qr_code)) {
logd(TAG, "Showing LandingFragment with QR code.");
@@ -459,6 +483,7 @@
if (fragment != null && fragment.isVisible()) {
return;
}
+ dismissButtons();
fragment = CompanionLandingFragment.newInstance(isStartedForSuw);
launchFragment(fragment, COMPANION_LANDING_FRAGMENT_TAG);
}
@@ -473,10 +498,9 @@
}
fragment =
CompanionQrCodeLandingFragment.newInstance(isStartedForSuw, isStartedForSetupProfile);
+ dismissButtons();
launchFragment(fragment, COMPANION_LANDING_FRAGMENT_TAG);
- if (isStartedForSetupProfile) {
- showSkipButton();
- }
+ showSkipButton();
}
private void showTurnOnBluetoothFragment() {
@@ -567,6 +591,7 @@
private void dismissButtons() {
if (isStartedForSuw) {
+ logd(TAG, "Dismissing SUW toolbar buttons.");
carSetupWizardLayout.setPrimaryToolbarButtonVisible(false);
carSetupWizardLayout.setSecondaryToolbarButtonVisible(false);
return;
@@ -576,8 +601,11 @@
private void showSkipButton() {
if (!isStartedForSuw || hideSkipButton) {
+ logw(TAG, "Not in SUW or hideSkipButton is true; Do not show skip button.");
return;
}
+ logd(TAG, "Show skip button on SUW page.");
+ carSetupWizardLayout.setPrimaryToolbarButtonFlat(true);
carSetupWizardLayout.setPrimaryToolbarButtonText(getString(R.string.skip));
carSetupWizardLayout.setPrimaryToolbarButtonVisible(true);
carSetupWizardLayout.setPrimaryToolbarButtonListener(
@@ -628,6 +656,11 @@
}
}
+ private void showCompanionNotAvailableDialog() {
+ CompanionNotAvailableDialogFragment fragment = new CompanionNotAvailableDialogFragment();
+ fragment.show(getSupportFragmentManager(), COMPANION_NOT_AVAILABLE_DIALOG_TAG);
+ }
+
/** 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
@@ -706,4 +739,44 @@
.create();
}
}
+
+ /** Dialog fragment to notify CompanionDevice is not available to guest user. */
+ public static class CompanionNotAvailableDialogFragment extends DialogFragment {
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ @StringRes
+ int messageResId =
+ VERSION.SDK_INT <= VERSION_CODES.R
+ ? R.string.companion_not_available_dialog_switch_user_message
+ : R.string.companion_not_available_dialog_message;
+ AlertDialog.Builder builder =
+ new AlertDialog.Builder(getActivity())
+ .setTitle(getString(R.string.companion_not_available_dialog_title))
+ .setMessage(getString(messageResId))
+ .setNegativeButton(getString(R.string.ok), (d, w) -> getActivity().finish())
+ .setCancelable(false);
+ addChangeProfileButton(builder);
+ Dialog dialog = builder.create();
+ dialog.setCanceledOnTouchOutside(/* cancel= */ false);
+ return dialog;
+ }
+
+ private void addChangeProfileButton(AlertDialog.Builder builder) {
+ if (VERSION.SDK_INT <= VERSION_CODES.Q) {
+ return;
+ }
+ String profileSwitcherClassName =
+ VERSION.SDK_INT >= VERSION_CODES.S
+ ? PROFILE_SWITCH_CLASS_NAME
+ : PROFILE_SWITCH_CLASS_NAME_R;
+ builder.setPositiveButton(
+ getString(R.string.change_profile),
+ (d, w) -> {
+ Intent intent = new Intent();
+ intent.setComponent(
+ new ComponentName(PROFILE_SWITCH_PACKAGE_NAME, profileSwitcherClassName));
+ getActivity().startActivity(intent);
+ });
+ }
+ }
}
diff --git a/libs/connecteddevice/build.gradle b/libs/connecteddevice/build.gradle
index 6105fad..13c674b 100644
--- a/libs/connecteddevice/build.gradle
+++ b/libs/connecteddevice/build.gradle
@@ -13,12 +13,12 @@
}
android {
- compileSdkVersion 30
+ compileSdkVersion 31
aidlPackageWhiteList "com/google/android/connecteddevice/api/IDeviceAssociationCallback.aidl"
defaultConfig {
minSdkVersion 29
- targetSdkVersion 30
+ targetSdkVersion 31
versionCode 1
versionName "1.0"
@@ -51,7 +51,7 @@
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
- aidl.srcDirs = java.srcDirs
+ aidl.srcDirs = [java.srcDirs, 'src/com/google/android/connecteddevice/transport/eap']
proto {
srcDir java.srcDirs
}
diff --git a/libs/connecteddevice/res/values/config.xml b/libs/connecteddevice/res/values/config.xml
new file mode 100644
index 0000000..281973d
--- /dev/null
+++ b/libs/connecteddevice/res/values/config.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources>
+ <!-- Current version of the SDK -->
+ <string name="hu_companion_sdk_version" translatable="false">1.3.1</string>
+</resources>
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 49809d5..fb349c1 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ChannelResolver.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/connection/ChannelResolver.kt
@@ -157,11 +157,6 @@
.build()
device.protocol.sendData(device.protocolId, carVersion.toByteArray(), /* callback= */ null)
- if (!isReconnect) {
- logd(TAG, "Send OOB data to remote device $device.")
- oobRunner?.sendOobData(device)
- }
-
resolveStream(device)
}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/core/FeatureCoordinator.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/core/FeatureCoordinator.kt
index 0860ace..34afa5c 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/core/FeatureCoordinator.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/core/FeatureCoordinator.kt
@@ -16,6 +16,7 @@
package com.google.android.connecteddevice.core
import android.os.ParcelUuid
+import androidx.annotation.GuardedBy
import androidx.annotation.VisibleForTesting
import com.google.android.connecteddevice.api.IAssociationCallback
import com.google.android.connecteddevice.api.IConnectionCallback
@@ -34,12 +35,14 @@
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.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.locks.ReentrantLock
+import kotlin.concurrent.withLock
/** Coordinator between features and connected devices. */
class FeatureCoordinator
@@ -59,21 +62,24 @@
private val allConnectionCallbacks = AidlThreadSafeCallbacks<IConnectionCallback>()
- // deviceId -> (recipientId -> callbacks)
+ private val lock = ReentrantLock()
+
+ // deviceId -> (recipientId -> callbacks)s
+ @GuardedBy("lock")
private val deviceCallbacks:
MutableMap<String, MutableMap<ParcelUuid, AidlThreadSafeCallbacks<IDeviceCallback>>> =
ConcurrentHashMap()
- // recipientId -> (deviceId -> message bytes)
- private val recipientMissedMessages:
- MutableMap<ParcelUuid, MutableMap<String, MutableList<DeviceMessage>>> =
- 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 val blockedRecipients: MutableSet<ParcelUuid> = CopyOnWriteArraySet()
+ @GuardedBy("lock") private val blockedRecipients = mutableSetOf<ParcelUuid>()
+
+ // recipientId -> (deviceId -> message bytes)
+ private val recipientMissedMessages:
+ MutableMap<ParcelUuid, MutableMap<String, MutableList<DeviceMessage>>> =
+ ConcurrentHashMap()
init {
controller.registerCallback(createDeviceControllerCallback(), callbackExecutor)
@@ -89,7 +95,7 @@
/** Disconnect all devices and reset state. */
fun reset() {
logd(TAG, "Resetting coordinator.")
- deviceCallbacks.clear()
+ lock.withLock { deviceCallbacks.clear() }
controller.reset()
recipientMissedMessages.clear()
}
@@ -125,18 +131,42 @@
recipientId: ParcelUuid,
callback: IDeviceCallback
) {
- if (blockedRecipients.contains(recipientId)) {
+ val registrationSuccessful =
+ lock.withLock { registerDeviceCallbackLocked(connectedDevice, recipientId, callback) }
+
+ if (registrationSuccessful) {
+ notifyOfMissedMessages(connectedDevice, recipientId, callback)
+ } else {
notifyRecipientBlocked(connectedDevice, recipientId, callback)
- return
+ }
+ }
+
+ /**
+ * Registers the given [callback] to be notified of device events on the specified
+ * [connectedDevice] that are specific to the [recipientId].
+ *
+ * If the registration is successful, then `true` is returned. The caller of this method should
+ * have acquired [lock].
+ */
+ @GuardedBy("lock")
+ private fun registerDeviceCallbackLocked(
+ connectedDevice: ConnectedDevice,
+ recipientId: ParcelUuid,
+ callback: IDeviceCallback
+ ): Boolean {
+ if (recipientId in blockedRecipients) {
+ return false
}
val recipientCallbacks =
deviceCallbacks.computeIfAbsent(connectedDevice.deviceId) { ConcurrentHashMap() }
val newCallbacks =
AidlThreadSafeCallbacks<IDeviceCallback>().apply { add(callback, callbackExecutor) }
+
recipientCallbacks.computeIfPresent(recipientId) { _, callbacks ->
if (callbacks.isEmpty) null else callbacks
}
+
val previousCallbacks = recipientCallbacks.putIfAbsent(recipientId, newCallbacks)
// Device already has a callback registered with this recipient UUID. For the
@@ -148,15 +178,14 @@
previousCallbacks.invoke {
it.onDeviceError(connectedDevice, DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED)
}
- notifyRecipientBlocked(connectedDevice, recipientId, callback)
- return
+ return false
}
logd(
TAG,
"New callback registered on device ${connectedDevice.deviceId} for recipient $recipientId."
)
- notifyOfMissedMessages(connectedDevice, recipientId, callback)
+ return true
}
private fun notifyRecipientBlocked(
@@ -194,14 +223,40 @@
recipientId: ParcelUuid,
callback: IDeviceCallback
) {
+ lock.withLock { unregisterDeviceCallbackLocked(connectedDevice, recipientId, callback) }
+ }
+
+ /**
+ * Unregisters the given [callback] from being notified of device events for the specified
+ * [recipientId] on the [connectedDevice].
+ *
+ * The caller should ensure that they have acquired [lock].
+ */
+ @GuardedBy("lock")
+ private fun unregisterDeviceCallbackLocked(
+ connectedDevice: ConnectedDevice,
+ recipientId: ParcelUuid,
+ callback: IDeviceCallback
+ ) {
+ val callbacks = deviceCallbacks[connectedDevice.deviceId]?.get(recipientId)
+ if (callbacks == null) {
+ logw(
+ TAG,
+ "Request to unregister callback on device ${connectedDevice.deviceId} for recipient " +
+ "$recipientId, but none registered."
+ )
+ return
+ }
+
+ callbacks.remove(callback)
+
logd(
TAG,
"Device callback unregistered on device ${connectedDevice.deviceId} for recipient " +
"$recipientId."
)
- val callbacks = deviceCallbacks[connectedDevice.deviceId]?.get(recipientId)
- callbacks?.remove(callback)
- if (callbacks?.isEmpty == true) {
+
+ if (callbacks.isEmpty) {
deviceCallbacks[connectedDevice.deviceId]?.remove(recipientId)
}
}
@@ -341,12 +396,23 @@
@VisibleForTesting
internal fun onSecureChannelEstablishedInternal(connectedDevice: ConnectedDevice) {
+ val callbacks = lock.withLock { deviceCallbacks[connectedDevice.deviceId]?.values }
+ if (callbacks == null) {
+ logd(
+ TAG,
+ "A secure channel has been established with ${connectedDevice.deviceId}, but no " +
+ "callbacks registered to be notified."
+ )
+ return
+ }
+
logd(
TAG,
"Notifying callbacks that a secure channel has been established with " +
"${connectedDevice.deviceId}."
)
- deviceCallbacks[connectedDevice.deviceId]?.values?.forEach { callback ->
+
+ for (callback in callbacks) {
callback.invoke { it.onSecureChannelEstablished(connectedDevice) }
}
}
@@ -360,8 +426,13 @@
)
return
}
+
logd(TAG, "Received a new message for ${message.recipient} from ${connectedDevice.deviceId}.")
- val callback = deviceCallbacks[connectedDevice.deviceId]?.get(ParcelUuid(message.recipient))
+
+ val callback =
+ lock.withLock {
+ deviceCallbacks[connectedDevice.deviceId]?.get(ParcelUuid(message.recipient))
+ }
if (callback == null) {
logd(TAG, "Recipient has not registered a callback yet. Saving missed message.")
@@ -369,6 +440,8 @@
return
}
+ logd(TAG, "Notifying callback for recipient ${message.recipient}")
+
callback.invoke { it.onMessageReceived(connectedDevice, message) }
}
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 321518f..6c744f8 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/core/MultiProtocolDeviceController.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/core/MultiProtocolDeviceController.kt
@@ -208,7 +208,7 @@
}
val startAssociationResponse =
StartAssociationResponse(
- oobRunner.generateOobData(),
+ oobRunner.sendOobData(),
ByteUtils.hexStringToByteArray(nameForAssociation),
nameForAssociation
)
@@ -731,6 +731,7 @@
}
newDevice
}
+ oobRunner.reset()
}
private fun convertTempAssociationDeviceToRealDevice(
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/model/TransportProtocols.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/model/TransportProtocols.kt
index 235766f..8c8b0c6 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/model/TransportProtocols.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/model/TransportProtocols.kt
@@ -22,4 +22,7 @@
/** Classic Bluetooth SPP protocol. */
const val PROTOCOL_SPP = "spp"
+
+ /** Classic Bluetooth protocol for iOS device. */
+ const val PROTOCOL_EAP = "eap"
}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/notificationmsg/common/BaseNotificationDelegate.java b/libs/connecteddevice/src/com/google/android/connecteddevice/notificationmsg/common/BaseNotificationDelegate.java
index 803c74a..8196328 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/notificationmsg/common/BaseNotificationDelegate.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/notificationmsg/common/BaseNotificationDelegate.java
@@ -28,6 +28,8 @@
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
@@ -374,7 +376,11 @@
.setClassName(context, context.getClass().getName())
.putExtra(EXTRA_CONVERSATION_KEY, conversationKey);
- return PendingIntent.getForegroundService(
- context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ int flags = PendingIntent.FLAG_UPDATE_CURRENT;
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.S) {
+ flags |= PendingIntent.FLAG_MUTABLE;
+ }
+
+ return PendingIntent.getForegroundService(context, notificationId, intent, flags);
}
}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/oob/BluetoothRfcommChannel.java b/libs/connecteddevice/src/com/google/android/connecteddevice/oob/BluetoothRfcommChannel.java
deleted file mode 100644
index cab01ed..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/oob/BluetoothRfcommChannel.java
+++ /dev/null
@@ -1,186 +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 static com.google.android.connecteddevice.util.SafeLog.logw;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.ParcelUuid;
-import android.os.RemoteException;
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-import com.google.android.connecteddevice.transport.BluetoothDeviceProvider;
-import com.google.android.connecteddevice.transport.IConnectionProtocol;
-import com.google.android.connecteddevice.transport.ProtocolDevice;
-import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder;
-import com.google.android.connecteddevice.transport.spp.Connection;
-import com.google.android.connecteddevice.transport.spp.PendingConnection;
-import com.google.android.connecteddevice.transport.spp.PendingSentMessage;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/** Handles out of band data exchange over a secure RFCOMM channel. */
-public class BluetoothRfcommChannel implements OobChannel {
- private static final String TAG = "BluetoothRfcommChannel";
- // TODO(b/159500330): Generate random UUID.
- private static final UUID RFCOMM_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
- private static final int CONNECTION_TIMEOUT_MS = 500;
-
- private final AtomicBoolean isInterrupted = new AtomicBoolean();
- private final ConnectedDeviceSppDelegateBinder sppDelegateBinder;
- private Connection activeConnection;
- private PendingConnection activePendingConnection;
-
- public BluetoothRfcommChannel(ConnectedDeviceSppDelegateBinder sppDelegateBinder) {
- this.sppDelegateBinder = sppDelegateBinder;
- }
-
- @Override
- public boolean completeOobDataExchange(@NonNull ProtocolDevice device, @NonNull byte[] oobData) {
- IConnectionProtocol protocol = device.getProtocol();
- if (!(protocol instanceof BluetoothDeviceProvider)) {
- logw(TAG, "Protocol is not supported by current OOB channel, ignored.");
- return false;
- }
- BluetoothDevice remoteDevice =
- ((BluetoothDeviceProvider) protocol).getBluetoothDeviceById(device.getProtocolId());
- completeOobDataExchange(
- remoteDevice, () -> BluetoothAdapter.getDefaultAdapter().getBondedDevices(), oobData);
- return true;
- }
-
- @VisibleForTesting
- void completeOobDataExchange(
- BluetoothDevice remoteDevice, BondedDevicesResolver bondedDevicesResolver, byte[] oobData) {
- Set<BluetoothDevice> bondedDevices = bondedDevicesResolver.getBondedDevices();
- if (bondedDevices == null || !bondedDevices.contains(remoteDevice)) {
- loge(
- TAG,
- "This device has not been bonded to device with address " + remoteDevice.getAddress());
- return;
- }
-
- try {
- PendingConnection connection =
- sppDelegateBinder.connectAsClient(RFCOMM_UUID, remoteDevice, /* isSecure= */ true);
- if (connection == null) {
- loge(TAG, "Connection with " + remoteDevice.getName() + " failed.");
- return;
- }
-
- activePendingConnection = connection;
- Handler handler = new Handler(Looper.getMainLooper());
- handler.postDelayed(
- () -> {
- logd(TAG, "Cancelling connection with " + remoteDevice.getName());
- try {
- sppDelegateBinder.cancelConnectionAttempt(connection);
- } catch (RemoteException e) {
- logw(TAG, "Failed to cancel connection attempt with " + remoteDevice.getName());
- }
- loge(TAG, "Connection with " + remoteDevice.getName() + " timed out.");
- },
- CONNECTION_TIMEOUT_MS);
-
- connection
- .setOnConnectedListener(
- (uuid, btDevice, isSecure, deviceName) -> {
- handler.removeCallbacksAndMessages(null);
- activePendingConnection = null;
- activeConnection =
- new Connection(new ParcelUuid(uuid), btDevice, isSecure, deviceName);
- sendOobData(oobData);
- })
- .setOnConnectionErrorListener(
- () -> {
- handler.removeCallbacksAndMessages(null);
- loge(TAG, "Connection with " + remoteDevice.getName() + " failed.");
- });
- } catch (RemoteException e) {
- loge(TAG, "Connection with " + remoteDevice.getName() + " failed.", e);
- }
- }
-
- private void sendOobData(byte[] oobData) {
- if (isInterrupted.get()) {
- logd(TAG, "Oob connection is interrupted, data will not be set.");
- return;
- }
-
- if (activeConnection == null) {
- loge(TAG, "Connection is null, oob data cannot be sent");
- return;
- }
-
- try {
- PendingSentMessage pendingSentMessage =
- sppDelegateBinder.sendMessage(activeConnection, oobData);
-
- if (pendingSentMessage == null) {
- loge(TAG, "Sending oob data failed");
- return;
- }
-
- pendingSentMessage.setOnSuccessListener(
- () -> {
- try {
- disconnect();
- } catch (RemoteException e) {
- logw(TAG, "Sending oob data succeeded, but disconnect failed");
- }
- });
- } catch (RemoteException e) {
- loge(TAG, "Sending oob data failed", e);
- }
- }
-
- @Override
- public void interrupt() {
- logd(TAG, "Interrupt received.");
- isInterrupted.set(true);
- try {
- disconnect();
- } catch (RemoteException e) {
- loge(TAG, "Disconnect failed", e);
- }
- }
-
- private void disconnect() throws RemoteException {
- if (activeConnection != null) {
- sppDelegateBinder.disconnect(activeConnection);
- activeConnection = null;
- }
- if (activePendingConnection != null) {
- sppDelegateBinder.cancelConnectionAttempt(activePendingConnection);
- activePendingConnection = null;
- }
- }
-
- /**
- * Interface for determining all the devices that the current device is Bluetooth bonded to, used
- * for testing.
- */
- interface BondedDevicesResolver {
- Set<BluetoothDevice> getBondedDevices();
- }
-}
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 7bb2085..f7dbf7b 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.transport.ProtocolDevice;
/**
* An interface for handling out of band data exchange. This interface should be implemented for
@@ -26,21 +25,19 @@
* <p>Usage is:
*
* <ol>
- * <li>Call {@link OobChannel#completeOobDataExchange(ProtocolDevice, byte[])}
+ * <li>Call {@link OobChannel#completeOobDataExchange(byte[])}
* <li>Provide way to stop the OOB data exchange through {@link OobChannel#interrupt()}
* </ol>
*/
public interface OobChannel {
/**
- * Exchange out of band data with a remote device. This must be done prior to the start of the
- * association with that device.
+ * Exchange out of band data with a remote device.
*
- * @param protocolDevice The remote device to exchange out of band data with
* @param oobData The data that will be sent to remote device via OOB channel
* @return {@code true} if the data exchange is started successfully, otherwise return {@code
* false}
*/
- boolean completeOobDataExchange(@NonNull ProtocolDevice protocolDevice, @NonNull byte[] oobData);
+ boolean completeOobDataExchange(@NonNull byte[] oobData);
/** Interrupt the current data exchange and prevent callbacks from being issued. */
void interrupt();
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/oob/OobChannelFactory.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/oob/OobChannelFactory.kt
deleted file mode 100644
index 2a353a7..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/oob/OobChannelFactory.kt
+++ /dev/null
@@ -1,40 +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.oob
-
-import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder
-
-/** Factory that creates [OobChannel]. */
-open class OobChannelFactory(private val sppBinder: ConnectedDeviceSppDelegateBinder) {
- /**
- * Returns [OobChannel] of the type [oobChannelType].
- *
- * Throws [IllegalArgumentException] if the [oobChannelType] passed is not supported.
- */
- open fun createOobChannel(oobChannelType: String): OobChannel =
- when (oobChannelType) {
- BT_RFCOMM -> BluetoothRfcommChannel(sppBinder)
- PRE_ASSOCIATION -> PassThroughChannel()
- else -> throw IllegalArgumentException("Unknown OOB channel type: $oobChannelType")
- }
-
- companion object {
- /** Should be passed to [createOobChannel] to create corresponding OOB channel. */
- const val BT_RFCOMM = "BT_RFCOMM"
- const val PRE_ASSOCIATION = "PRE_ASSOCIATION"
- }
-}
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 17d0c07..7b154c7 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/oob/OobRunner.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/oob/OobRunner.kt
@@ -20,6 +20,7 @@
import androidx.annotation.VisibleForTesting
import com.google.android.companionprotos.OutOfBandAssociationToken
import com.google.android.connecteddevice.model.OobData
+import com.google.android.connecteddevice.transport.ProtocolDelegate
import com.google.android.connecteddevice.transport.ProtocolDevice
import com.google.android.connecteddevice.util.SafeLog.logd
import com.google.android.connecteddevice.util.SafeLog.loge
@@ -32,18 +33,19 @@
import javax.crypto.spec.IvParameterSpec
/**
- * Manages all OOB related actions, those actions should be ordered as below:
- * 1. [generateOobData] returns [ByteArray] which contains the OOB key.
- * 2. [sendOobData] to the remote [ProtocolDevice].
- * 3. [encryptData] and [decryptData] data after the OOB data exchange succeed. Will throw
+ * Manages all OOB related actions,
+ *
+ * The actions taken by this runner should be ordered as below:
+ * 1. [sendOobData] to the remote [ProtocolDevice], OOB data is generated at the same time.
+ * 2. [encryptData] and [decryptData] data after the OOB data exchange succeed. Will throw
* [IllegalStateException] if attempting to encrypt/decrypt without first having called
- * [generateOobData].
+ * [sendOobData].
*/
open class OobRunner
@JvmOverloads
constructor(
- private val oobChannelFactory: OobChannelFactory,
- open val supportedTypes: List<String>,
+ private val delegate: ProtocolDelegate,
+ private val oobProtocolName: String,
internal val keyAlgorithm: String = KeyProperties.KEY_ALGORITHM_AES
) {
@VisibleForTesting internal var ihuIv = ByteArray(NONCE_LENGTH_BYTES)
@@ -58,10 +60,13 @@
loge(TAG, "Unable to create cipher with $ALGORITHM.", e)
throw IllegalStateException(e)
}
- private var oobData: OobData? = null
- /** Generate OOB data which should be exchanged with remote device. */
- open fun generateOobData(): OobData {
+ /**
+ * Returns generates OOB data and iterates through all available OOB channels, establish OOB
+ * connections and send OOB data to remote device.
+ */
+ open fun sendOobData(): OobData {
+ logd(TAG, "Listening for OOB connection to send OOB data.")
val keyGenerator =
try {
KeyGenerator.getInstance(keyAlgorithm)
@@ -75,28 +80,12 @@
secureRandom.nextBytes(ihuIv)
secureRandom.nextBytes(mobileIv)
val oobData = OobData(secretKey.encoded, ihuIv, mobileIv)
- this.oobData = oobData
- return oobData
- }
-
- /**
- * Iterate through all available OOB channels, establish OOB channels and send OOB data to remote
- * device.
- */
- open fun sendOobData(protocolDevice: ProtocolDevice) {
- val dataToSend = oobData
- if (dataToSend == null) {
- loge(TAG, "OOB data unavailable, failed to send OOB data.")
- return
- }
establishedOobChannels.clear()
- for (oobType in supportedTypes) {
- logd(TAG, "Establish OOB channel and send OOB data with $oobType.")
- val oobChannel = oobChannelFactory.createOobChannel(oobType)
- if (oobChannel.completeOobDataExchange(protocolDevice, toOobProto(dataToSend))) {
- establishedOobChannels.add(oobChannel)
- }
+ val oobChannel = TransportOobChannel(delegate, oobProtocolName)
+ if (oobChannel.completeOobDataExchange(toOobProto(oobData))) {
+ establishedOobChannels.add(oobChannel)
}
+ return oobData
}
/** Encrypt [data] with OOB key, throw exception when encryption failed. */
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/oob/PassThroughChannel.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/oob/PassThroughChannel.kt
deleted file mode 100644
index d67610c..0000000
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/oob/PassThroughChannel.kt
+++ /dev/null
@@ -1,35 +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.oob
-
-import com.google.android.connecteddevice.transport.ProtocolDevice
-
-/**
- * An out of band data exchange channel.
- *
- * This class performs no-op. [completeOobDataExchange] returns `true` immediately.
- */
-class PassThroughChannel : OobChannel {
- override fun completeOobDataExchange(
- protocolDevice: ProtocolDevice,
- oobData: ByteArray
- ): Boolean {
- return true
- }
-
- override fun interrupt() {}
-}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/oob/TransportOobChannel.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/oob/TransportOobChannel.kt
new file mode 100644
index 0000000..dfb4311
--- /dev/null
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/oob/TransportOobChannel.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.oob
+
+import android.os.ParcelUuid
+import com.google.android.connecteddevice.transport.IConnectionProtocol
+import com.google.android.connecteddevice.transport.IDataSendCallback
+import com.google.android.connecteddevice.transport.IDiscoveryCallback
+import com.google.android.connecteddevice.transport.ProtocolDelegate
+import com.google.android.connecteddevice.transport.ProtocolDevice
+import com.google.android.connecteddevice.util.SafeLog.logd
+import com.google.android.connecteddevice.util.SafeLog.logw
+import java.util.concurrent.CopyOnWriteArrayList
+
+/** Manages OOB data exchange through [ConnectionProtocol]s. */
+class TransportOobChannel(
+ private val delegate: ProtocolDelegate,
+ private val protocolName: String
+) : OobChannel {
+ /** List of [ProtocolDevice]s which is doing OOB data exchange. */
+ private val ongoingOobDataExchange = CopyOnWriteArrayList<ProtocolDevice>()
+
+ override fun completeOobDataExchange(oobData: ByteArray): Boolean {
+ val protocols = delegate.oobProtocols
+ if (protocols.isEmpty()) {
+ logw(TAG, "No available protocol to send OOB data, ignore.")
+ return false
+ }
+ for (protocol in protocols) {
+ protocol.startAssociationDiscovery(
+ protocolName,
+ DEFAULT_UUID,
+ generateDiscoveryCallback(protocol, oobData)
+ )
+ }
+ return true
+ }
+
+ private fun generateDiscoveryCallback(
+ protocol: IConnectionProtocol,
+ oobData: ByteArray
+ ): IDiscoveryCallback {
+ return object : IDiscoveryCallback.Stub() {
+ override fun onDeviceConnected(protocolId: String) {
+ val protocolDevice = ProtocolDevice(protocol, protocolId)
+ ongoingOobDataExchange.add(protocolDevice)
+ logd(
+ TAG,
+ "Oob channel established with id: $protocolId, protocol: $protocol, send OOB data."
+ )
+ protocol.sendData(protocolId, oobData, generateDataSentCallback(protocolDevice))
+ }
+
+ override fun onDiscoveryStartedSuccessfully() {
+ logd(TAG, "OOB discovery started successfully via protocol: $protocol")
+ }
+
+ override fun onDiscoveryFailedToStart() {
+ logw(TAG, "Failed to start OOB discovery via protocol: $protocol")
+ }
+ }
+ }
+
+ private fun generateDataSentCallback(device: ProtocolDevice): IDataSendCallback {
+ return object : IDataSendCallback.Stub() {
+ override fun onDataSentSuccessfully() {
+ logd(TAG, "OOB data sent successfully with protocol id: $device.protocolId.")
+ }
+
+ override fun onDataFailedToSend() {
+ logw(TAG, "OOB data failed to send through protocol: $device.protocol, ignored.")
+ device.protocol.disconnectDevice(device.protocolId)
+ ongoingOobDataExchange.remove(device)
+ }
+ }
+ }
+
+ override fun interrupt() {
+ for (protocolDevice in ongoingOobDataExchange) {
+ protocolDevice.protocol.disconnectDevice(protocolDevice.protocolId)
+ }
+ ongoingOobDataExchange.clear()
+ }
+
+ companion object {
+ private const val TAG = "TransportOobChannel"
+ private val DEFAULT_UUID = ParcelUuid.fromString("00001101-0000-1000-8000-00805F9B34FB")
+ }
+}
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 bbc0c8c..e8476a6 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceService.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/service/ConnectedDeviceService.java
@@ -18,7 +18,6 @@
import static com.google.android.connecteddevice.util.SafeLog.logd;
import static com.google.android.connecteddevice.util.SafeLog.loge;
-import static java.util.Objects.requireNonNull;
import android.content.Intent;
import android.os.IBinder;
@@ -30,7 +29,6 @@
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.oob.OobChannelFactory;
import com.google.android.connecteddevice.oob.OobRunner;
import com.google.android.connecteddevice.storage.ConnectedDeviceStorage;
import com.google.android.connecteddevice.system.SystemFeature;
@@ -39,8 +37,6 @@
import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder;
import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder.OnRemoteCallbackSetListener;
import com.google.android.connecteddevice.util.EventLog;
-import java.util.Arrays;
-import java.util.List;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@@ -55,8 +51,14 @@
private static final String META_ASSOCIATION_SERVICE_UUID =
"com.google.android.connecteddevice.association_service_uuid";
- private static final String META_SUPPORTED_OOB_CHANNELS =
- "com.google.android.connecteddevice.supported_oob_channels";
+ private static final String META_EAP_OOB_PROTOCOL_NAME =
+ "com.google.android.connecteddevice.car_eap_oob_protocol_name";
+
+ // The name should be reverse-DNS strings.
+ // Source:
+ // https://developer.apple.com/library/archive/featuredarticles/ExternalAccessoryPT/Introduction/Introduction.html#//apple_ref/doc/uid/TP40009502
+ private static final String DEFAULT_EAP_OOB_PROTOCOL_NAME =
+ "com.google.companion.oob-association";
private static final String META_ENABLE_PASSENGER =
"com.google.android.connecteddevice.enable_passenger";
@@ -121,15 +123,11 @@
return;
}
logd(TAG, "Initializing FeatureCoordinator version of the platform.");
- List<String> oobTypes =
- Arrays.asList(
- requireNonNull(
- getMetaStringArray(
- META_SUPPORTED_OOB_CHANNELS, /* defaultValue= */ new String[0])));
-
- OobRunner oobRunner = new OobRunner(new OobChannelFactory(sppDelegateBinder), oobTypes);
UUID associationUuid = UUID.fromString(requireMetaString(META_ASSOCIATION_SERVICE_UUID));
boolean enablePassenger = getMetaBoolean(META_ENABLE_PASSENGER, ENABLE_PASSENGER_BY_DEFAULT);
+ String oobProtocolName =
+ getMetaString(META_EAP_OOB_PROTOCOL_NAME, DEFAULT_EAP_OOB_PROTOCOL_NAME);
+ OobRunner oobRunner = new OobRunner(protocolDelegate, oobProtocolName);
DeviceController deviceController =
new MultiProtocolDeviceController(
protocolDelegate, storage, oobRunner, associationUuid, enablePassenger);
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/service/MetaDataService.java b/libs/connecteddevice/src/com/google/android/connecteddevice/service/MetaDataService.java
index 68a5924..6306dde 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/service/MetaDataService.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/service/MetaDataService.java
@@ -55,11 +55,10 @@
}
/**
- * Return a string from the service's meta-data, or default value if no meta-data matches
- * the provided name.
+ * Return a string from the service's meta-data, or default value if no meta-data matches the
+ * provided name.
*/
- @Nullable
- protected final String getMetaString(@NonNull String name, @Nullable String defaultValue) {
+ protected final String getMetaString(@NonNull String name, @NonNull String defaultValue) {
if (!bundle.containsKey(name)) {
return defaultValue;
}
@@ -103,10 +102,8 @@
* Return a string array from the service's meta-data, or default value if no meta-data matches
* the provided name.
*/
- @Nullable
protected final String[] getMetaStringArray(
- @NonNull String name,
- @Nullable String[] defaultValue) {
+ @NonNull String name, @NonNull String[] defaultValue) {
if (!bundle.containsKey(name)) {
return defaultValue;
}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/service/TransportService.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/service/TransportService.kt
index 2513696..332581f 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/service/TransportService.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/service/TransportService.kt
@@ -32,6 +32,7 @@
import com.google.android.connecteddevice.transport.IProtocolDelegate
import com.google.android.connecteddevice.transport.ble.BlePeripheralProtocol
import com.google.android.connecteddevice.transport.ble.OnDeviceBlePeripheralManager
+import com.google.android.connecteddevice.transport.eap.EapProtocol
import com.google.android.connecteddevice.transport.proxy.NetworkSocketFactory
import com.google.android.connecteddevice.transport.proxy.ProxyBlePeripheralManager
import com.google.android.connecteddevice.transport.spp.SppProtocol
@@ -57,9 +58,11 @@
/** Maps the currently supported protocol names to the actual protocol implementation. */
@VisibleForTesting
- internal val supportedProtocols = ConcurrentHashMap<String, ConnectionProtocol>()
+ internal val initializedProtocols = ConcurrentHashMap<String, ConnectionProtocol>()
- private lateinit var supportedTransportProtocols: Set<String>
+ private lateinit var supportedOobProtocolsName: Set<String>
+
+ private lateinit var supportedTransportProtocolsName: Set<String>
private lateinit var bluetoothManager: BluetoothManager
@@ -77,7 +80,7 @@
override fun onServiceConnected(name: ComponentName, service: IBinder) {
delegate = IProtocolDelegate.Stub.asInterface(service)
logd(TAG, "Successfully bound to service and received delegate.")
- initializeProtocols(supportedTransportProtocols)
+ initializeProtocols(supportedTransportProtocolsName + supportedOobProtocolsName)
}
override fun onServiceDisconnected(name: ComponentName?) {
@@ -102,16 +105,17 @@
super.onCreate()
logd(TAG, "Service created.")
bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
- supportedTransportProtocols =
- getMetaStringArray(META_SUPPORTED_TRANSPORT_PROTOCOLS, DEFAULT_TRANSPORT_PROTOCOLS)?.toSet()
- ?: emptySet()
- if (supportedTransportProtocols.isEmpty()) {
+ supportedTransportProtocolsName =
+ getMetaStringArray(META_SUPPORTED_TRANSPORT_PROTOCOLS, DEFAULT_TRANSPORT_PROTOCOLS).toSet()
+ supportedOobProtocolsName =
+ getMetaStringArray(META_SUPPORTED_OOB_CHANNELS, emptyArray()).toSet()
+ if (supportedTransportProtocolsName.isEmpty()) {
loge(
TAG,
"Transport protocols are empty. There must be at least one protocol provided to start " +
"this service. Reverting to default values."
)
- supportedTransportProtocols = DEFAULT_TRANSPORT_PROTOCOLS.toSet()
+ supportedTransportProtocolsName = DEFAULT_TRANSPORT_PROTOCOLS.toSet()
}
registerReceiver(
@@ -126,7 +130,7 @@
override fun onDestroy() {
logd(TAG, "Service destroyed.")
- disconnectProtocols(supportedTransportProtocols)
+ disconnectProtocols(initializedProtocols.keys)
try {
unregisterReceiver(bluetoothBroadcastReceiver)
} catch (e: IllegalArgumentException) {
@@ -144,7 +148,8 @@
private fun onBluetoothStateChanged(state: Int) {
logd(TAG, "The bluetooth state has changed to $state.")
- val supportedBluetoothProtocols = supportedTransportProtocols.intersect(BLUETOOTH_PROTOCOLS)
+ val supportedBluetoothProtocols =
+ (supportedTransportProtocolsName + supportedOobProtocolsName).intersect(BLUETOOTH_PROTOCOLS)
when (state) {
BluetoothAdapter.STATE_ON -> {
EventLog.onBleOn()
@@ -165,26 +170,36 @@
logd(TAG, "Processing ${targetProtocols.size} supported protocols.")
for (protocol in targetProtocols) {
logd(TAG, "Adding protocol $protocol to supported protocols.")
- when (protocol) {
- TransportProtocols.PROTOCOL_BLE_PERIPHERAL -> maybeAddBlePeripheralProtocol()
- TransportProtocols.PROTOCOL_SPP -> maybeAddSppProtocol()
- else -> loge(TAG, "Protocol type $protocol is not recognized. Ignoring.")
- }
+ addBluetoothProtocol(protocol)
}
}
- private fun maybeAddBlePeripheralProtocol() {
+ private fun addBluetoothProtocol(protocolName: String) {
if (!bluetoothManager.adapter.isEnabled) {
- logd(TAG, "Bluetooth adapter is currently disabled. Skipping BlePeripheralProtocol.")
+ logd(TAG, "Bluetooth adapter is currently disabled. Skipping $protocolName.")
return
}
- if (supportedProtocols.containsKey(TransportProtocols.PROTOCOL_BLE_PERIPHERAL)) {
- logd(TAG, "A BlePeripheralProtocol has already been created. Aborting.")
+ if (initializedProtocols.containsKey(protocolName)) {
+ logd(TAG, "A $protocolName has already been created. Aborting.")
return
}
- val protocol = createBlePeripheralProtocol()
- supportedProtocols[TransportProtocols.PROTOCOL_BLE_PERIPHERAL] = protocol
- delegate?.addProtocol(protocol)
+ val protocol: ConnectionProtocol =
+ when (protocolName) {
+ TransportProtocols.PROTOCOL_BLE_PERIPHERAL -> createBlePeripheralProtocol()
+ TransportProtocols.PROTOCOL_SPP -> createSppProtocol()
+ TransportProtocols.PROTOCOL_EAP -> createEapProtocol()
+ else -> {
+ loge(TAG, "Protocol type $protocolName is not recognized. Ignoring.")
+ return
+ }
+ }
+ initializedProtocols[protocolName] = protocol
+ if (protocolName in supportedTransportProtocolsName) {
+ delegate?.addProtocol(protocol)
+ }
+ if (protocolName in supportedOobProtocolsName) {
+ delegate?.addOobProtocol(protocol)
+ }
}
private fun createBlePeripheralProtocol(): BlePeripheralProtocol {
@@ -222,29 +237,31 @@
)
}
- private fun maybeAddSppProtocol() {
- if (!bluetoothManager.adapter.isEnabled) {
- logd(TAG, "Bluetooth adapter is currently disabled. Skipping SppProtocol.")
- return
- }
- if (supportedProtocols.containsKey(TransportProtocols.PROTOCOL_SPP)) {
- logd(TAG, "A SppProtocol has already been created. Aborting.")
- return
- }
+ private fun createSppProtocol(): SppProtocol {
val maxSppPacketSize = getMetaInt(META_SPP_PACKET_BYTES, DEFAULT_SPP_PACKET_SIZE_BYTES)
- val protocol = SppProtocol(context = this, maxSppPacketSize)
- supportedProtocols[TransportProtocols.PROTOCOL_SPP] = protocol
- delegate?.addProtocol(protocol)
+ return SppProtocol(context = this, maxSppPacketSize)
+ }
+
+ private fun createEapProtocol(): EapProtocol {
+ val maxSppPacketSize = getMetaInt(META_SPP_PACKET_BYTES, DEFAULT_SPP_PACKET_SIZE_BYTES)
+ val eapClientName = getMetaString(META_EAP_CLIENT_NAME, "")
+ val eapServiceName = getMetaString(META_EAP_SERVICE_NAME, "")
+ return EapProtocol(eapClientName, eapServiceName, maxSppPacketSize)
}
private fun disconnectProtocols(targetProtocols: Set<String>) {
for (protocolName in targetProtocols) {
- val protocol = supportedProtocols[protocolName]
+ val protocol = initializedProtocols[protocolName]
if (protocol != null) {
logd(TAG, "Disconnecting $protocolName.")
protocol.reset()
- delegate?.removeProtocol(protocol)
- supportedProtocols.remove(protocolName)
+ initializedProtocols.remove(protocolName)
+ if (protocolName in supportedTransportProtocolsName) {
+ delegate?.removeProtocol(protocol)
+ }
+ if (protocolName in supportedOobProtocolsName) {
+ delegate?.removeOobProtocol(protocol)
+ }
}
}
}
@@ -256,13 +273,24 @@
const val ACTION_BIND_PROTOCOL = "com.google.android.connecteddevice.BIND_PROTOCOL"
/** `string-array` Supported transport protocols. */
- private const val META_SUPPORTED_TRANSPORT_PROTOCOLS =
+ @VisibleForTesting
+ internal const val META_SUPPORTED_TRANSPORT_PROTOCOLS =
"com.google.android.connecteddevice.transport_protocols"
+ @VisibleForTesting
+ internal const val META_SUPPORTED_OOB_CHANNELS =
+ "com.google.android.connecteddevice.supported_oob_channels"
+
// The mac address randomly rotates every 7-15 minutes. To be safe, we will rotate our
// reconnect advertisement every 6 minutes to avoid crossing a rotation.
private val MAX_ADVERTISEMENT_DURATION = Duration.ofMinutes(6)
+ private const val META_EAP_CLIENT_NAME =
+ "com.google.android.connecteddevice.car_eap_client_name"
+
+ private const val META_EAP_SERVICE_NAME =
+ "com.google.android.connecteddevice.car_eap_service_name"
+
/** `String` UUID for reconnection advertisement. */
private const val META_RECONNECT_SERVICE_UUID =
"com.google.android.connecteddevice.reconnect_service_uuid"
@@ -312,6 +340,10 @@
private const val PROXY_ENABLED_BY_DEFAULT = false
private val BLUETOOTH_PROTOCOLS =
- listOf(TransportProtocols.PROTOCOL_BLE_PERIPHERAL, TransportProtocols.PROTOCOL_SPP)
+ setOf(
+ TransportProtocols.PROTOCOL_BLE_PERIPHERAL,
+ TransportProtocols.PROTOCOL_SPP,
+ TransportProtocols.PROTOCOL_EAP
+ )
}
}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IProtocolDelegate.aidl b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IProtocolDelegate.aidl
index f03779c..dcc5ddd 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IProtocolDelegate.aidl
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/IProtocolDelegate.aidl
@@ -23,6 +23,12 @@
/** Add a protocol to the collection of supported protocols. */
void addProtocol(in IConnectionProtocol protocol);
+ /** Add an OOB protocol to the collection of supported OOB protocols. */
+ void addOobProtocol(in IConnectionProtocol protocol);
+
/** Remove a protocol from the collection of supported protocols. */
void removeProtocol(in IConnectionProtocol protocol);
+
+ /** Remove an OOB protocol to the collection of supported OOB protocols. */
+ void removeOobProtocol(in IConnectionProtocol protocol);
}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ProtocolDelegate.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ProtocolDelegate.kt
index 62af14d..0f29c62 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ProtocolDelegate.kt
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/ProtocolDelegate.kt
@@ -24,11 +24,20 @@
/** The list of currently attached [IConnectionProtocol]s. */
val protocols: List<IConnectionProtocol>
get() {
- scrubDeadProtocols()
+ scrubDeadProtocols(_protocols)
return _protocols
}
- /** `true` if there are currently no protocols attached. `false` otherwise. */
+ private val _oobProtocols = mutableListOf<IConnectionProtocol>()
+
+ /** Protocols that support OOB data exchange. */
+ val oobProtocols: List<IConnectionProtocol>
+ get() {
+ scrubDeadProtocols(_oobProtocols)
+ return _oobProtocols
+ }
+
+ /** `true` if there are currently no general transport protocols attached. `false` otherwise. */
val isEmpty: Boolean
get() = protocols.isEmpty()
@@ -39,6 +48,24 @@
/** Callback registered for protocol changes. */
var callback: Callback? = null
+ override fun addOobProtocol(protocol: IConnectionProtocol) {
+ _oobProtocols.add(protocol)
+ logd(
+ TAG,
+ "Added a new OOB protocol. There are now ${oobProtocols.size} attached OOB " + "protocols."
+ )
+ }
+
+ override fun removeOobProtocol(protocol: IConnectionProtocol) {
+ scrubDeadProtocols(_oobProtocols)
+ _oobProtocols.removeAll { it.asBinder() == protocol.asBinder() }
+ logd(
+ TAG,
+ "Removed a OOB protocol. There are ${oobProtocols.size} remaining OOB attached " +
+ "protocols."
+ )
+ }
+
override fun addProtocol(protocol: IConnectionProtocol) {
_protocols.add(protocol)
logd(TAG, "Added a new protocol. There are now ${protocols.size} attached protocols.")
@@ -46,7 +73,7 @@
}
override fun removeProtocol(protocol: IConnectionProtocol) {
- scrubDeadProtocols()
+ scrubDeadProtocols(_protocols)
if (!_protocols.removeAll { it.asBinder() == protocol.asBinder() }) {
return
}
@@ -54,8 +81,8 @@
callback?.onProtocolRemoved(protocol)
}
- private fun scrubDeadProtocols() =
- _protocols.removeAll {
+ private fun scrubDeadProtocols(protocols: MutableList<IConnectionProtocol>) =
+ protocols.removeAll {
val isNotAlive = !it.asBinder().isBinderAlive
if (isNotAlive) {
callback?.onProtocolRemoved(it)
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/eap/EapProtocol.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/eap/EapProtocol.kt
new file mode 100644
index 0000000..08aef1a
--- /dev/null
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/eap/EapProtocol.kt
@@ -0,0 +1,179 @@
+/*
+ * 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.eap
+
+import android.annotation.SuppressLint
+import android.os.IBinder
+import android.os.ParcelUuid
+import android.os.RemoteException
+import androidx.annotation.VisibleForTesting
+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.DirectExecutor
+import com.google.android.connecteddevice.util.SafeLog.logd
+import com.google.android.connecteddevice.util.SafeLog.loge
+import com.panasonic.iapx.IDeviceConnection
+import com.panasonic.iapx.IDeviceConnectionDelegate
+import com.panasonic.iapx.IServiceConnector
+import com.panasonic.iapx.IServiceConnectorDelegate
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.Executor
+
+/** Defines how connection is established and data is transferred via EAP channel. */
+class EapProtocol(
+ private val eapClientName: String,
+ private val eapServiceName: String,
+ private val maxPacketSize: Int,
+ callbackExecutor: Executor = DirectExecutor()
+) : ConnectionProtocol(callbackExecutor) {
+ // Stores the connected EAP session id to the real connection pair.
+ private val sessions = ConcurrentHashMap<Long, IDeviceConnection>()
+ private val ongoingDiscoveries = ConcurrentHashMap<String, IDiscoveryCallback>()
+
+ private val serviceDelegate =
+ object : IServiceConnectorDelegate.Stub() {
+ override fun OnServiceConnectionChange(status: Int) {
+ logd(TAG, "Service connection status has changed to $status.")
+ if (status == IServiceConnector.kServiceConnectionReady) {
+ logd(TAG, "Connection to EAP service is ready to use.")
+ }
+ }
+ }
+
+ init {
+ bindToEapService()
+ }
+
+ @SuppressLint("PrivateApi")
+ private fun bindToEapService() {
+ logd(TAG, "Attempting to connect to EAP service $eapServiceName.")
+ val serviceBinder =
+ Class.forName("android.os.ServiceManager")
+ .getMethod("getService", java.lang.String::class.java)
+ .invoke(null, eapServiceName) as?
+ IBinder
+ if (serviceBinder == null) {
+ loge(TAG, "Unable to bind to EAP service. Aborting.")
+ return
+ }
+ val connector = IServiceConnector.Stub.asInterface(serviceBinder)
+ logd(TAG, "Attempting to connect to the EAP client.")
+ connectClient(connector)
+ }
+
+ @VisibleForTesting
+ internal fun connectClient(connector: IServiceConnector) {
+ try {
+ connector.ConnectClient(
+ eapClientName,
+ serviceDelegate.asBinder(),
+ generateDeviceDelegate().asBinder()
+ )
+ } catch (e: RemoteException) {
+ loge(TAG, "Failed to connect the EAP client.", e)
+ }
+ }
+
+ override fun startAssociationDiscovery(
+ name: String,
+ identifier: ParcelUuid,
+ callback: IDiscoveryCallback,
+ ) {
+ ongoingDiscoveries[name] = callback
+ }
+
+ private fun generateDeviceDelegate(): IDeviceConnectionDelegate {
+ return object : IDeviceConnectionDelegate.Stub() {
+ override fun OnConnectionReady(connection: IDeviceConnection?, transportType: Int) {
+ logd(TAG, "Connection is ready on transport $transportType.")
+ }
+
+ override fun OnConnectionClosed(connection: IDeviceConnection?) {
+ logd(TAG, "Connection has been closed.")
+ }
+
+ override fun OnEAPSessionStart(
+ connection: IDeviceConnection,
+ eapSessionId: Long,
+ eapProtocolName: String?
+ ) {
+ logd(TAG, "Starting new session $eapSessionId with protocol $eapProtocolName.")
+ if (ongoingDiscoveries.containsKey(eapProtocolName)) {
+ sessions[eapSessionId] = connection
+ ongoingDiscoveries[eapProtocolName]?.onDeviceConnected(eapSessionId.toString())
+ }
+ }
+
+ override fun OnEAPSessionStop(connection: IDeviceConnection?, eapSessionId: Long) {
+ logd(TAG, "Device disconnected. id: $eapSessionId.")
+ if (sessions.containsKey(eapSessionId)) {
+ sessions.remove(eapSessionId)
+ deviceDisconnectedListeners[eapSessionId.toString()]?.invoke {
+ it.onDeviceDisconnected(eapSessionId.toString())
+ }
+ }
+ }
+
+ override fun OnEAPData(connection: IDeviceConnection?, eapSessionId: Long, data: ByteArray?) {
+ logd(TAG, "Received new data from session $eapSessionId.")
+ if (data != null) {
+ notifyDataReceived(eapSessionId.toString(), data)
+ }
+ }
+
+ override fun OnDeviceNameUpdate(connection: IDeviceConnection?, name: String?) {}
+
+ override fun OnDeviceTransientUUIDUpdate(connection: IDeviceConnection?, uuid: String?) {}
+ }
+ }
+
+ override fun stopAssociationDiscovery() {
+ ongoingDiscoveries.clear()
+ }
+
+ override fun reset() {
+ super.reset()
+ sessions.clear()
+ ongoingDiscoveries.clear()
+ }
+
+ override fun sendData(protocolId: String, data: ByteArray, callback: IDataSendCallback?) {
+ sessions[protocolId.toLong()]?.SendEAPData(protocolId.toLong(), data)
+ ?: loge(TAG, "Unable to find device with session $protocolId.")
+ }
+
+ override fun getMaxWriteSize(protocolId: String): Int {
+ return maxPacketSize
+ }
+
+ override fun isDeviceVerificationRequired() = false
+
+ override fun startConnectionDiscovery(
+ id: ParcelUuid,
+ challenge: ConnectChallenge,
+ callback: IDiscoveryCallback,
+ ) {}
+
+ override fun stopConnectionDiscovery(id: ParcelUuid) {}
+
+ override fun disconnectDevice(protocolId: String) {}
+
+ companion object {
+ private const val TAG = "EapProtocol"
+ }
+}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/eap/com/panasonic/iapx/IDeviceConnection.aidl b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/eap/com/panasonic/iapx/IDeviceConnection.aidl
new file mode 100644
index 0000000..1794f8a
--- /dev/null
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/eap/com/panasonic/iapx/IDeviceConnection.aidl
@@ -0,0 +1,22 @@
+/*
+ * 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.panasonic.iapx;
+
+interface IDeviceConnection {
+ oneway void DoNotDisturbAnymore() = 0;
+ oneway void SendEAPData(in long eapSessionId, in byte[] data) = 3000;
+}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/eap/com/panasonic/iapx/IDeviceConnectionDelegate.aidl b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/eap/com/panasonic/iapx/IDeviceConnectionDelegate.aidl
new file mode 100644
index 0000000..8e41e6b
--- /dev/null
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/eap/com/panasonic/iapx/IDeviceConnectionDelegate.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.panasonic.iapx;
+
+import com.panasonic.iapx.IDeviceConnection;
+
+interface IDeviceConnectionDelegate {
+ oneway void OnConnectionReady(in com.panasonic.iapx.IDeviceConnection connection, in int transportType) = 0;
+ oneway void OnConnectionClosed(in com.panasonic.iapx.IDeviceConnection connection) = 1;
+ oneway void OnDeviceNameUpdate(in com.panasonic.iapx.IDeviceConnection connection, in String name) = 100;
+ oneway void OnDeviceTransientUUIDUpdate(in com.panasonic.iapx.IDeviceConnection connection, in String uuid) = 101;
+ oneway void OnEAPSessionStart(in com.panasonic.iapx.IDeviceConnection connection, in long eapSessionId, in String eapProtocolName) = 3000;
+ oneway void OnEAPSessionStop(in com.panasonic.iapx.IDeviceConnection connection, in long eapSessionId) = 3001;
+ oneway void OnEAPData(in com.panasonic.iapx.IDeviceConnection connection, in long eapSessionId, in byte[] data) = 3010;
+}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/eap/com/panasonic/iapx/IServiceConnector.aidl b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/eap/com/panasonic/iapx/IServiceConnector.aidl
new file mode 100644
index 0000000..afcfc4a
--- /dev/null
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/eap/com/panasonic/iapx/IServiceConnector.aidl
@@ -0,0 +1,25 @@
+/*
+ * 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.panasonic.iapx;
+
+interface IServiceConnector {
+ void ConnectClient(in String uniqueClientName, in IBinder serviceDelegate, in IBinder deviceConnectionDelegate) = 0;
+ void DisconnectClient(in String uniqueClientName, in IBinder serviceDelegate) = 1;
+ const int kServiceConnectionReady = 1;
+ const int kServiceConnectionRefused = 2;
+ const int kServiceConnectionLost = 3;
+}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/eap/com/panasonic/iapx/IServiceConnectorDelegate.aidl b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/eap/com/panasonic/iapx/IServiceConnectorDelegate.aidl
new file mode 100644
index 0000000..52828f3
--- /dev/null
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/eap/com/panasonic/iapx/IServiceConnectorDelegate.aidl
@@ -0,0 +1,21 @@
+/*
+ * 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.panasonic.iapx;
+
+interface IServiceConnectorDelegate {
+ oneway void OnServiceConnectionChange(in int status) = 0;
+}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/eap/com/panasonic/iapx/ITransport.aidl b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/eap/com/panasonic/iapx/ITransport.aidl
new file mode 100644
index 0000000..f04a30f
--- /dev/null
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/eap/com/panasonic/iapx/ITransport.aidl
@@ -0,0 +1,25 @@
+/*
+ * 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.panasonic.iapx;
+
+interface ITransport {
+ const int kTypeSerial = 0;
+ const int kTypeUSBHostMode = 1;
+ const int kTypeUSBDeviceMode = 2;
+ const int kTypeBluetooth = 3;
+ const int kTypeCarPlay = 4;
+}
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/spp/SppManager.java b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/spp/SppManager.java
index 3295955..724b394 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/transport/spp/SppManager.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/transport/spp/SppManager.java
@@ -68,7 +68,6 @@
private final ExecutorService taskExecutor = Executors.newSingleThreadExecutor();
private final ExecutorService writeExecutor = Executors.newSingleThreadExecutor();
private final Executor taskCallbackExecutor;
- private BluetoothDevice device;
// Only the first registered {@code OnMessageReceivedListener} will receive the missed messages.
private final ConcurrentLinkedQueue<byte[]> missedMessages = new ConcurrentLinkedQueue<>();
@@ -134,8 +133,13 @@
+ " is "
+ missedMessages.size());
byte[] missedMessage = missedMessages.poll();
- receivedListeners.invoke(
- receivedListener -> receivedListener.onMessageReceived(device, missedMessage));
+ // Invokes message received callback only when there is currently a connection.
+ if (connectedSocket != null) {
+ receivedListeners.invoke(
+ receivedListener ->
+ receivedListener.onMessageReceived(
+ connectedSocket.getRemoteDevice(), missedMessage));
+ }
}
}
@@ -306,6 +310,8 @@
try {
if (connectedSocket != null) {
+ callbacks.invoke(
+ callback -> callback.onRemoteDeviceDisconnected(connectedSocket.getRemoteDevice()));
connectedSocket.close();
}
} catch (IOException e) {
@@ -314,8 +320,6 @@
connectedSocket = null;
state = ConnectionState.DISCONNECTED;
-
- callbacks.invoke(callback -> callback.onRemoteDeviceDisconnected(device));
}
/**
@@ -325,14 +329,13 @@
* @param device The BluetoothDevice that has been connected
*/
@GuardedBy("lock")
- private void startConnectionLocked(BluetoothSocket socket, BluetoothDevice device) {
+ private void startConnectionLocked(BluetoothSocket socket) {
logd(TAG, "Connected over Bluetooth socket. Started listening for incoming messages");
- this.device = device;
connectedSocket = socket;
-
state = ConnectionState.CONNECTED;
- callbacks.invoke(callback -> callback.onRemoteDeviceConnected(device));
+ callbacks.invoke(
+ callback -> callback.onRemoteDeviceConnected(connectedSocket.getRemoteDevice()));
InputStream inputStream;
try {
@@ -366,7 +369,7 @@
case LISTEN:
case CONNECTING:
logd(TAG, "Starting connection with device " + socket.getRemoteDevice());
- startConnectionLocked(socket, socket.getRemoteDevice());
+ startConnectionLocked(socket);
break;
case CONNECTED:
loge(TAG, "AcceptTask completed while in CONNECTED state. Cosing socket.");
@@ -402,7 +405,10 @@
+ " while no listener registered, storing the message");
return;
}
- receivedListeners.invoke(listener -> listener.onMessageReceived(device, message));
+ if (connectedSocket != null) {
+ receivedListeners.invoke(
+ listener -> listener.onMessageReceived(connectedSocket.getRemoteDevice(), message));
+ }
}
@Override
@@ -419,7 +425,7 @@
public void onConnectionSuccess(BluetoothSocket socket) {
synchronized (lock) {
logd(TAG, "onConnectionSucceeded for device " + socket.getRemoteDevice());
- startConnectionLocked(socket, socket.getRemoteDevice());
+ startConnectionLocked(socket);
}
}
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 d86d1a7..8c95ca7 100644
--- a/libs/connecteddevice/src/com/google/android/connecteddevice/ui/AssociatedDeviceViewModel.java
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/ui/AssociatedDeviceViewModel.java
@@ -28,6 +28,8 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.Looper;
import android.os.ParcelUuid;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
@@ -61,6 +63,8 @@
private static final Duration DISCOVERABLE_DURATION = Duration.ofMinutes(2);
+ private static final Duration CONNECT_TIME_OUT_DURATION = Duration.ofSeconds(10);
+
/** States of association process. */
public enum AssociationState {
NONE,
@@ -94,6 +98,8 @@
private final boolean isPassengerEnabled;
private final String bleDeviceNamePrefix;
private final BluetoothAdapter bluetoothAdapter;
+ private final Handler handler = new Handler(Looper.getMainLooper());
+ private final Runnable connectTimeoutRunnable = this::onConnectTimeout;
private final Connector connector;
@@ -141,7 +147,7 @@
this.connector.setFeatureId(new ParcelUuid(UUID.randomUUID()));
this.connector.setCallback(connectorCallback);
- this.connector.connect();
+ connect();
}
@Override
@@ -155,6 +161,10 @@
/** Confirms that the pairing code matches. */
public void acceptVerification() {
pairingCode.postValue(null);
+ if (!connector.isConnected()) {
+ loge(TAG, "Failed to accept verification, connector is not connected.");
+ return;
+ }
connector.acceptVerification();
}
@@ -166,6 +176,10 @@
}
advertisedCarName.postValue(null);
pairingCode.postValue(null);
+ if (!connector.isConnected()) {
+ loge(TAG, "Failed to stop association, connector is not connected.");
+ return;
+ }
connector.stopAssociation();
associationState.postValue(AssociationState.NONE);
}
@@ -178,11 +192,25 @@
/** Removes the association of the given device. */
public void removeDevice(@NonNull AssociatedDevice device) {
+ if (!connector.isConnected()) {
+ loge(
+ TAG,
+ "Failed to remove device " + device.getDeviceId() + " , connector is not connected.");
+ return;
+ }
connector.removeAssociatedDevice(device.getDeviceId());
}
/** Toggles connection of the given associated device. */
public void toggleConnectionStatusForDevice(@NonNull AssociatedDevice device) {
+ if (!connector.isConnected()) {
+ loge(
+ TAG,
+ "Failed to change connection on device "
+ + device.getDeviceId()
+ + " , connector is not connected.");
+ return;
+ }
if (device.isConnectionEnabled()) {
connector.disableAssociatedDeviceConnection(device.getDeviceId());
} else {
@@ -192,11 +220,24 @@
/** Mark the given device as belonging to the active driver. */
public void claimDevice(@NonNull AssociatedDevice device) {
+ if (!connector.isConnected()) {
+ loge(
+ TAG, "Failed to claim device " + device.getDeviceId() + " , connector is not connected.");
+ return;
+ }
connector.claimAssociatedDevice(device.getDeviceId());
}
/** Mark the given device as unclaimed by any user. */
public void removeClaimOnDevice(@NonNull AssociatedDevice device) {
+ if (!connector.isConnected()) {
+ loge(
+ TAG,
+ "Failed to remove claim on device "
+ + device.getDeviceId()
+ + " , connector is not connected.");
+ return;
+ }
connector.removeAssociatedDeviceClaim(device.getDeviceId());
}
@@ -292,6 +333,10 @@
getApplication().startActivity(discoverableIntent);
}
+ if (!connector.isConnected()) {
+ loge(TAG, "Failed to start association, connector is not connected.");
+ return;
+ }
if (associationIdentifier != null) {
connector.startAssociation(associationIdentifier, associationCallback);
} else {
@@ -385,11 +430,22 @@
}
}
+ private void connect() {
+ handler.postDelayed(connectTimeoutRunnable, CONNECT_TIME_OUT_DURATION.toMillis());
+ connector.connect();
+ }
+
+ private void onConnectTimeout() {
+ logd(TAG, "Connector failed to connect in " + CONNECT_TIME_OUT_DURATION);
+ connector.disconnect();
+ }
+
private final Connector.Callback connectorCallback =
new Connector.Callback() {
@Override
public void onConnected() {
logd(TAG, "Connected to platform.");
+ handler.removeCallbacks(connectTimeoutRunnable);
isServiceConnected.postValue(true);
if (isPassengerEnabled) {
@@ -405,7 +461,7 @@
public void onDisconnected() {
logd(TAG, "Disconnected from the platform.");
isServiceConnected.postValue(false);
- connector.connect();
+ connect();
}
@Override
@@ -444,6 +500,11 @@
logd(TAG, "Device " + device.getDeviceId() + " has established a secure channel.");
updateDeviceDetails();
}
+
+ @Override
+ public void onFailedToConnect() {
+ loge(TAG, "Connector failed to connect.");
+ }
};
private final IAssociationCallback associationCallback =
diff --git a/libs/connecteddevice/src/com/google/android/connecteddevice/util/DirectExecutor.kt b/libs/connecteddevice/src/com/google/android/connecteddevice/util/DirectExecutor.kt
new file mode 100644
index 0000000..bd71117
--- /dev/null
+++ b/libs/connecteddevice/src/com/google/android/connecteddevice/util/DirectExecutor.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.util
+
+import java.util.concurrent.Executor
+
+/** Runs commands in current thread. */
+class DirectExecutor : Executor {
+ override fun execute(r: Runnable) {
+ r.run()
+ }
+}
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 11a20e3..d2c4db0 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
@@ -41,7 +41,6 @@
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.spy
@@ -70,8 +69,7 @@
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 val mockOobRunner = mock<OobRunner>()
private var mockEncryptionRunner = mock<FakeEncryptionRunner>()
private val testDevice1 = ProtocolDevice(testProtocol1, TEST_PROTOCOL_ID_1)
private val testDevice2 = ProtocolDevice(testProtocol2, TEST_PROTOCOL_ID_2)
@@ -146,7 +144,7 @@
}
@Test
- fun receivedSupportedVersion_sendVersionMessageAndSendOobData() {
+ fun receivedSupportedVersion_sendVersionMessage() {
channelResolver.resolveAssociation(mockOobRunner)
argumentCaptor<IDataReceivedListener>().apply {
verify(testProtocol1).registerDataReceivedListener(eq(TEST_PROTOCOL_ID_1), capture())
@@ -157,9 +155,8 @@
verify(testProtocol1).sendData(eq(TEST_PROTOCOL_ID_1), capture(), anyOrNull())
assertThat(firstValue).isEqualTo(expectedVersion)
}
-
- verify(mockOobRunner).sendOobData(any())
}
+
@Test
fun receivedInvalidChallenge_invokeOnError() {
channelResolver.resolveReconnect(TEST_DEVICE_ID, TEST_CHALLENGE)
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
deleted file mode 100644
index f0a275a..0000000
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/connection/MultiProtocolSecureChannelV4Test.kt
+++ /dev/null
@@ -1,259 +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 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.ShowVerificationCodeListener
-import com.google.android.connecteddevice.model.DeviceMessage
-import com.google.android.connecteddevice.model.DeviceMessage.OperationType
-import com.google.android.connecteddevice.oob.OobRunner
-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
-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.mock
-import com.nhaarman.mockitokotlin2.spy
-import com.nhaarman.mockitokotlin2.times
-import com.nhaarman.mockitokotlin2.verify
-import com.nhaarman.mockitokotlin2.whenever
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-
-private const val PROTOCOL_ID = "testProtocol"
-
-@RunWith(AndroidJUnit4::class)
-class MultiProtocolSecureChannelV4Test {
- private val context = ApplicationProvider.getApplicationContext<Context>()
- private val stream1 = spy(ProtocolStream(ProtocolDevice(TestProtocolV4(), PROTOCOL_ID)))
- private lateinit var storage: ConnectedDeviceStorage
- private val mockOobRunner: OobRunner = mock()
- private val mockShowVerificationCodeListener: ShowVerificationCodeListener = mock()
- private val mockCallback: MultiProtocolSecureChannel.Callback = mock()
-
- @Before
- fun setUp() {
- val database =
- Room.inMemoryDatabaseBuilder(context, ConnectedDeviceDatabase::class.java)
- .allowMainThreadQueries()
- .setQueryExecutor(directExecutor())
- .build()
- .associatedDeviceDao()
- storage = ConnectedDeviceStorage(context, Base64CryptoHelperV4(), database, directExecutor())
- }
- @Test
- fun processVerificationCodeMessage_oobVerification_verifyOobCode() {
- val secureChannel = setupSecureChannel(false)
- initHandshakeMessage(secureChannel)
- respondToContinueMessage(secureChannel)
- val testPayload = "testPayload".toByteArray()
- val testVerificationCodeMessage =
- VerificationCode.newBuilder().run {
- state = VerificationCodeState.OOB_VERIFICATION
- payload = 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 createOobResponse_oobCodeMatch_sendCorrectMessage() {
- val testPayload = "testPayload".toByteArray()
- whenever(mockOobRunner.decryptData(testPayload))
- .thenReturn(FakeEncryptionRunner.VERIFICATION_CODE)
- whenever(mockOobRunner.encryptData(FakeEncryptionRunner.VERIFICATION_CODE))
- .thenReturn(testPayload)
- val secureChannel = setupSecureChannel(false)
- initHandshakeMessage(secureChannel)
- respondToContinueMessage(secureChannel)
- val testVerificationCodeMessage =
- VerificationCode.newBuilder().run {
- state = VerificationCodeState.OOB_VERIFICATION
- payload = ByteString.copyFrom(testPayload)
- build()
- }
- val deviceMessage =
- DeviceMessage.createOutgoingMessage(
- /* recipient= */ null,
- /* isMessageEncrypted= */ false,
- OperationType.ENCRYPTION_HANDSHAKE,
- testVerificationCodeMessage.toByteArray()
- )
- secureChannel.onDeviceMessageReceived(deviceMessage)
-
- 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 processVerificationCodeMessage_visualVerification_invokeListener() {
- val secureChannel = setupSecureChannel(false)
- initHandshakeMessage(secureChannel)
- respondToContinueMessage(secureChannel)
- 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())
- }
-
- @Test
- fun onVisualVerificationCodeConfirmed_sendConfirmationMessage() {
- val secureChannel = setupSecureChannel(false)
- initHandshakeMessage(secureChannel)
- respondToContinueMessage(secureChannel)
- 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
- ): MultiProtocolSecureChannelV4 {
- val encryptionRunner = EncryptionRunnerFactory.newFakeRunner()
- encryptionRunner.setIsReconnect(isReconnect)
- return MultiProtocolSecureChannelV4(
- stream1,
- storage,
- encryptionRunner,
- deviceId = deviceId,
- oobRunner = mockOobRunner
- )
- .apply {
- callback = mockCallback
- showVerificationCodeListener = mockShowVerificationCodeListener
- }
- }
-
- private fun initHandshakeMessage(
- channel: MultiProtocolSecureChannelV4,
- message: ByteArray = FakeEncryptionRunner.INIT_MESSAGE
- ) {
- val deviceMessage =
- DeviceMessage.createOutgoingMessage(
- /* recipient= */ null,
- /* isMessageEncrypted= */ false,
- OperationType.ENCRYPTION_HANDSHAKE,
- message
- )
- channel.onDeviceMessageReceived(deviceMessage)
- }
-
- private fun respondToContinueMessage(
- channel: MultiProtocolSecureChannelV4,
- message: ByteArray = FakeEncryptionRunner.CLIENT_RESPONSE
- ) {
- val deviceMessage =
- DeviceMessage.createOutgoingMessage(
- /* recipient= */ null,
- /* isMessageEncrypted= */ false,
- OperationType.ENCRYPTION_HANDSHAKE,
- message
- )
- channel.onDeviceMessageReceived(deviceMessage)
- }
-}
-
-private class TestProtocolV4 : ConnectionProtocol() {
- override fun isDeviceVerificationRequired() = false
-
- override fun startAssociationDiscovery(
- name: String,
- identifier: ParcelUuid,
- callback: IDiscoveryCallback
- ) {}
-
- override fun startConnectionDiscovery(
- id: ParcelUuid,
- challenge: ConnectChallenge,
- callback: IDiscoveryCallback
- ) {}
-
- override fun stopAssociationDiscovery() {}
-
- override fun stopConnectionDiscovery(id: ParcelUuid) {}
-
- override fun sendData(protocolId: String, data: ByteArray, callback: IDataSendCallback?) {}
-
- override fun disconnectDevice(protocolId: String) {}
-
- override fun reset() {}
-
- override fun getMaxWriteSize(protocolId: String): Int {
- return 0
- }
-}
-
-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/core/MultiProtocolDeviceControllerTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/core/MultiProtocolDeviceControllerTest.kt
index fbde8ce..6263a43 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
@@ -78,7 +78,7 @@
private val testConnectionProtocol: TestConnectionProtocol = spy(TestConnectionProtocol())
private val mockCallback = mock<Callback>()
private val mockStream = mock<ProtocolStream>()
- private val mockOobRunner = mock<OobRunner> { on { generateOobData() } doReturn TEST_OOB_DATA }
+ private val mockOobRunner = mock<OobRunner> { on { sendOobData() } doReturn TEST_OOB_DATA }
private val mockAssociationCallback = mockToBeAlive<IAssociationCallback>()
private val mockDeadAssociationCallback = mockToBeDead<IAssociationCallback>()
private val protocolDelegate = ProtocolDelegate().apply { addProtocol(testConnectionProtocol) }
@@ -251,7 +251,7 @@
firstValue.onDiscoveryStartedSuccessfully()
}
- verify(mockOobRunner).generateOobData()
+ verify(mockOobRunner).sendOobData()
val response =
argumentCaptor<StartAssociationResponse>().run {
verify(mockAssociationCallback).onAssociationStartSuccess(capture())
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
deleted file mode 100644
index 7239564..0000000
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/BluetoothRfcommChannelTest.java
+++ /dev/null
@@ -1,230 +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.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doThrow;
-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.ParcelUuid;
-import android.os.RemoteException;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-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.common.collect.ImmutableSet;
-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.invocation.InvocationOnMock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-import org.mockito.stubbing.Answer;
-import org.robolectric.shadows.ShadowLooper;
-
-@RunWith(AndroidJUnit4.class)
-public class BluetoothRfcommChannelTest {
- private static final String DEVICE_ADDRESS = "00:11:22:33:AA:BB";
- private static final byte[] TEST_MESSAGE = "someData".getBytes(UTF_8);
- private static final BluetoothDevice TEST_BLUETOOTH_DEVICE =
- ApplicationProvider.getApplicationContext()
- .getSystemService(BluetoothManager.class)
- .getAdapter()
- .getRemoteDevice(DEVICE_ADDRESS);
-
- @Rule public final MockitoRule mockito = MockitoJUnit.rule();
-
- @Mock private BluetoothRfcommChannel bluetoothRfcommChannel;
- @Mock private TestConnectionProtocol mockValidProtocol;
- @Mock private ConnectionProtocol mockInvalidProtocol;
- @Mock private ConnectedDeviceSppDelegateBinder mockSppDelegateBinder;
-
- @Before
- public void setUp() {
- bluetoothRfcommChannel = new BluetoothRfcommChannel(mockSppDelegateBinder);
- }
-
- @Test
- public void completeOobExchange_createRfcommSocketFails_doNotSendData() throws Exception {
- doThrow(RemoteException.class)
- .when(mockSppDelegateBinder)
- .connectAsClient(any(), any(), anyBoolean());
-
- bluetoothRfcommChannel.completeOobDataExchange(
- TEST_BLUETOOTH_DEVICE, () -> ImmutableSet.of(TEST_BLUETOOTH_DEVICE), TEST_MESSAGE);
- verify(mockSppDelegateBinder, never()).sendMessage(any(), any());
- }
-
- @Test
- public void completeOobExchange_noBondedDevices_doNotInitConnection() throws Exception {
- bluetoothRfcommChannel.completeOobDataExchange(
- TEST_BLUETOOTH_DEVICE, ImmutableSet::of, TEST_MESSAGE);
-
- verify(mockSppDelegateBinder, never()).connectAsClient(any(), any(), anyBoolean());
- }
-
- @Test
- public void completeOobExchange_bondedToTheWrongDevice_doNotInitConnection() throws Exception {
- BluetoothDevice otherBtDevice =
- ApplicationProvider.getApplicationContext()
- .getSystemService(BluetoothManager.class)
- .getAdapter()
- .getRemoteDevice("BB:AA:33:22:11:00");
- bluetoothRfcommChannel.completeOobDataExchange(
- TEST_BLUETOOTH_DEVICE, () -> ImmutableSet.of(otherBtDevice), TEST_MESSAGE);
-
- verify(mockSppDelegateBinder, never()).connectAsClient(any(), any(), anyBoolean());
- }
-
- @Test
- public void completeOobExchange_timeout_cancelsConnectionAndCallsOnFailed() throws Exception {
- PendingConnection pendingConnection = requestConnection();
-
- // Simulate the timeout
- ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
-
- verify(mockSppDelegateBinder).cancelConnectionAttempt(pendingConnection);
- }
-
- @Test
- public void interrupt_disconnects() throws Exception {
- PendingConnection connection = establishConnection();
- bluetoothRfcommChannel.interrupt();
- verify(mockSppDelegateBinder).disconnect(connection.toConnection(TEST_BLUETOOTH_DEVICE));
- }
-
- @Test
- public void interrupt_cancelConnectionAttempt() throws Exception {
- PendingConnection connection = establishConnection();
- bluetoothRfcommChannel.interrupt();
- verify(mockSppDelegateBinder).disconnect(connection.toConnection(TEST_BLUETOOTH_DEVICE));
- }
-
- @Test
- public void completeOobDataExchange_supportedProtocol_returnTrue() {
- String protocolId = "testProtocolId";
- when(mockValidProtocol.getBluetoothDeviceById(protocolId)).thenReturn(TEST_BLUETOOTH_DEVICE);
-
- assertThat(
- bluetoothRfcommChannel.completeOobDataExchange(
- new ProtocolDevice(mockValidProtocol, protocolId), TEST_MESSAGE))
- .isTrue();
- }
-
- @Test
- public void completeOobDataExchange_unsupportedProtocol_returnFalse() {
- String protocolId = "testProtocolId";
-
- assertThat(
- bluetoothRfcommChannel.completeOobDataExchange(
- new ProtocolDevice(mockInvalidProtocol, protocolId), TEST_MESSAGE))
- .isFalse();
- }
-
- private PendingConnection establishConnection() throws Exception {
- PendingConnection connection = requestConnection();
- connection.notifyConnected(TEST_BLUETOOTH_DEVICE, TEST_BLUETOOTH_DEVICE.getName());
- verify(mockSppDelegateBinder).sendMessage(any(), eq(TEST_MESSAGE));
-
- return connection;
- }
-
- private PendingConnection requestConnection() throws Exception {
- ConnectionResultCaptor connectionCaptor = new ConnectionResultCaptor();
- doAnswer(connectionCaptor)
- .when(mockSppDelegateBinder)
- .connectAsClient(any(), any(), anyBoolean());
-
- bluetoothRfcommChannel.completeOobDataExchange(
- TEST_BLUETOOTH_DEVICE, () -> ImmutableSet.of(TEST_BLUETOOTH_DEVICE), TEST_MESSAGE);
- verify(mockSppDelegateBinder).connectAsClient(any(), any(), anyBoolean());
-
- return connectionCaptor.getResult();
- }
-
- private static class ConnectionResultCaptor implements Answer<PendingConnection> {
- private PendingConnection result = null;
-
- public PendingConnection getResult() {
- return result;
- }
-
- @Override
- public PendingConnection answer(InvocationOnMock invocationOnMock) {
- UUID uuid = invocationOnMock.getArgument(0);
- boolean isSecure = invocationOnMock.getArgument(2);
- result = new PendingConnection(uuid, isSecure);
- return result;
- }
- }
-
- private static class TestConnectionProtocol extends ConnectionProtocol
- implements BluetoothDeviceProvider {
- @Override
- public BluetoothDevice getBluetoothDeviceById(String protocolId) {
- return null;
- }
-
- @Override
- public boolean isDeviceVerificationRequired() {
- return false;
- }
-
- @Override
- public void startAssociationDiscovery(
- String name, ParcelUuid identifier, IDiscoveryCallback callback) {}
-
- @Override
- public void startConnectionDiscovery(
- ParcelUuid id, ConnectChallenge challenge, IDiscoveryCallback callback) {}
-
- @Override
- public void stopAssociationDiscovery() {}
-
- @Override
- public void stopConnectionDiscovery(ParcelUuid id) {}
-
- @Override
- public void sendData(String protocolId, byte[] data, IDataSendCallback callback) {}
-
- @Override
- public void disconnectDevice(String protocolId) {}
-
- @Override
- public int getMaxWriteSize(String protocolId) {
- return 0;
- }
- }
-}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/OobChannelFactoryTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/OobChannelFactoryTest.kt
deleted file mode 100644
index d647f72..0000000
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/OobChannelFactoryTest.kt
+++ /dev/null
@@ -1,54 +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.oob
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.android.connecteddevice.transport.spp.ConnectedDeviceSppDelegateBinder
-import com.google.common.truth.Truth.assertThat
-import com.nhaarman.mockitokotlin2.mock
-import org.junit.Assert.assertThrows
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class OobChannelFactoryTest {
- private val mockSppBinder: ConnectedDeviceSppDelegateBinder = mock()
- private val factory = OobChannelFactory(mockSppBinder)
-
- @Test
- fun createOobChannel_supportedType_returnCorrectChannelType() {
- val supportedType = OobChannelFactory.BT_RFCOMM
- val oobChannel = factory.createOobChannel(supportedType)
-
- assertThat(oobChannel).isInstanceOf(BluetoothRfcommChannel::class.java)
- }
-
- @Test
- fun createOobChannel_supportedType_returnPassThroughChannel() {
- val supportedType = OobChannelFactory.PRE_ASSOCIATION
- val oobChannel = factory.createOobChannel(supportedType)
-
- assertThat(oobChannel).isInstanceOf(PassThroughChannel::class.java)
- }
-
- @Test
- fun createOobChannel_unsupportedChannelType_throwException() {
- assertThrows(IllegalArgumentException::class.java) {
- factory.createOobChannel("Unknown channel")
- }
- }
-}
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 5e997d8..88e6934 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,21 +16,12 @@
package com.google.android.connecteddevice.oob
-import android.os.ParcelUuid
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.android.connecteddevice.core.util.mockToBeAlive
import com.google.android.connecteddevice.model.OobData
-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.IConnectionProtocol
+import com.google.android.connecteddevice.transport.ProtocolDelegate
import com.google.common.truth.Truth.assertThat
-import com.nhaarman.mockitokotlin2.any
-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 org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
@@ -38,51 +29,37 @@
@RunWith(AndroidJUnit4::class)
class OobRunnerTest {
- private val mockOobChannelFactory = mock<OobChannelFactory>()
- private val oobRunner = OobRunner(mockOobChannelFactory, SUPPORTED_TYPES_SINGLE)
- private lateinit var testOobChannel: TestOobChannel
- private lateinit var testProtocolDevice: ProtocolDevice
+ private val testProtocolName = "testProtocolName"
+ private val testConnectionProtocol = mockToBeAlive<IConnectionProtocol>()
+ private val testProtocolDelegate = ProtocolDelegate()
+ private lateinit var oobRunner: OobRunner
@Before
fun setUp() {
- testOobChannel = spy(TestOobChannel())
- whenever(mockOobChannelFactory.createOobChannel(any())).thenReturn(testOobChannel)
- testProtocolDevice = ProtocolDevice(TestConnectionProtocol(), "testProtocolId")
+ oobRunner = OobRunner(testProtocolDelegate, testProtocolName)
}
@Test
fun sendOobData_startOobDataExchangeSuccessfully_addToEatablishedOobChannels() {
- oobRunner.generateOobData()
- testOobChannel.oobDataExchangeResult = true
- oobRunner.sendOobData(testProtocolDevice)
+ testProtocolDelegate.addOobProtocol(testConnectionProtocol)
+
+ oobRunner.sendOobData()
assertThat(oobRunner.establishedOobChannels).hasSize(1)
}
@Test
fun sendOobData_startOobDataExchangeFailed() {
- testOobChannel.oobDataExchangeResult = false
- oobRunner.sendOobData(testProtocolDevice)
+ oobRunner.sendOobData()
assertThat(oobRunner.establishedOobChannels).isEmpty()
}
@Test
- fun sendOobData_tryAllSupportedChannel() {
- val oobRunner = OobRunner(mockOobChannelFactory, SUPPORTED_TYPES_TWO)
- oobRunner.generateOobData()
- oobRunner.sendOobData(testProtocolDevice)
-
- verify(mockOobChannelFactory, times(2)).createOobChannel(any())
- }
-
- @Test
fun reset_resetAllStatus() {
- testOobChannel.oobDataExchangeResult = true
- oobRunner.generateOobData()
- oobRunner.sendOobData(testProtocolDevice)
+ testProtocolDelegate.addOobProtocol(testConnectionProtocol)
+ oobRunner.sendOobData()
oobRunner.reset()
- assertThat(testOobChannel.isInterrupted).isTrue()
assertThat(oobRunner.encryptionKey).isNull()
assertThat(oobRunner.establishedOobChannels).isEmpty()
}
@@ -94,14 +71,13 @@
@Test
fun generateOobData_algorithmNotSupported_throwException() {
- val oobRunner =
- OobRunner(mockOobChannelFactory, SUPPORTED_TYPES_SINGLE, keyAlgorithm = "UNKNOWN")
- assertThrows(IllegalStateException::class.java) { oobRunner.generateOobData() }
+ val oobRunner = OobRunner(testProtocolDelegate, testProtocolName, keyAlgorithm = "UNKNOWN")
+ assertThrows(IllegalStateException::class.java) { oobRunner.sendOobData() }
}
@Test
fun generateOobData_keyAndNoncesAreNonNullAndOobDataIsSetCorrectly() {
- val oobData = oobRunner.generateOobData()
+ val oobData = oobRunner.sendOobData()
assertThat(oobRunner.encryptionKey).isNotNull()
assertThat(oobRunner.ihuIv).isNotNull()
assertThat(oobRunner.mobileIv).isNotNull()
@@ -114,7 +90,7 @@
@Throws(Exception::class)
fun serverEncryptAndClientDecrypt() {
val testMessage = "testMessage".toByteArray()
- oobRunner.generateOobData()
+ oobRunner.sendOobData()
val encryptedTestMessage = oobRunner.encryptData(testMessage)
switchClientAndServerRole()
@@ -127,14 +103,14 @@
@Throws(Exception::class)
fun encryptAndDecryptWithDifferentNonces_throwsException() {
val testMessage = "testMessage".toByteArray()
- oobRunner.generateOobData()
+ oobRunner.sendOobData()
val encryptedMessage = oobRunner.encryptData(testMessage)
assertThrows(IllegalStateException::class.java) { oobRunner.decryptData(encryptedMessage) }
}
@Test
fun decryptWithShortMessage_throwsException() {
- oobRunner.generateOobData()
+ oobRunner.sendOobData()
assertThrows(IllegalStateException::class.java) { oobRunner.decryptData("short".toByteArray()) }
}
@@ -159,10 +135,7 @@
var oobDataExchangeResult = false
var isInterrupted = false
- override fun completeOobDataExchange(
- protocolDevice: ProtocolDevice,
- oobData: ByteArray,
- ): Boolean {
+ override fun completeOobDataExchange(oobData: ByteArray): Boolean {
return oobDataExchangeResult
}
@@ -170,33 +143,4 @@
isInterrupted = true
}
}
-
- private class TestConnectionProtocol : ConnectionProtocol() {
- override fun isDeviceVerificationRequired() = false
-
- override fun startAssociationDiscovery(
- name: String,
- identifier: ParcelUuid,
- callback: IDiscoveryCallback,
- ) {}
- override fun startConnectionDiscovery(
- id: ParcelUuid,
- challenge: ConnectChallenge,
- callback: IDiscoveryCallback,
- ) {}
-
- override fun stopAssociationDiscovery() {}
- 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
- }
- }
-
- companion object {
- private val SUPPORTED_TYPES_SINGLE = listOf(OobChannelFactory.BT_RFCOMM)
- private val SUPPORTED_TYPES_TWO =
- listOf(OobChannelFactory.BT_RFCOMM, OobChannelFactory.PRE_ASSOCIATION)
- }
}
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
deleted file mode 100644
index 0af4f23..0000000
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/PassThroughChannelTest.kt
+++ /dev/null
@@ -1,63 +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.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 org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class PassThroughChannelTest {
- private val channel = PassThroughChannel()
- private val testProtocolDevice = ProtocolDevice(TestConnectionProtocol(), "testProtocolId")
-
- @Test
- fun startOobExchange_directlyPassThrough_invokeCallback() {
- assertThat(channel.completeOobDataExchange(testProtocolDevice, "testMessage".toByteArray()))
- .isTrue()
- }
-}
-
-private class TestConnectionProtocol : ConnectionProtocol() {
- override fun isDeviceVerificationRequired() = false
-
- override fun startAssociationDiscovery(
- name: String,
- identifier: ParcelUuid,
- callback: IDiscoveryCallback
- ) {}
- override fun startConnectionDiscovery(
- id: ParcelUuid,
- challenge: ConnectChallenge,
- callback: IDiscoveryCallback
- ) {}
-
- override fun stopAssociationDiscovery() {}
- 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/TransportOobChannelTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/TransportOobChannelTest.kt
new file mode 100644
index 0000000..551ce7e
--- /dev/null
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/oob/TransportOobChannelTest.kt
@@ -0,0 +1,96 @@
+/*
+ * 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.oob
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.android.connecteddevice.core.util.mockToBeAlive
+import com.google.android.connecteddevice.transport.IConnectionProtocol
+import com.google.android.connecteddevice.transport.IDataSendCallback
+import com.google.android.connecteddevice.transport.IDiscoveryCallback
+import com.google.android.connecteddevice.transport.ProtocolDelegate
+import com.nhaarman.mockitokotlin2.any
+import com.nhaarman.mockitokotlin2.argumentCaptor
+import com.nhaarman.mockitokotlin2.eq
+import com.nhaarman.mockitokotlin2.verify
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class TransportOobChannelTest {
+ private val testConnectionProtocol = mockToBeAlive<IConnectionProtocol>()
+ private lateinit var transportOobChannel: OobChannel
+ private val protocolDelegate = ProtocolDelegate().apply { addOobProtocol(testConnectionProtocol) }
+
+ @Before
+ fun setUp() {
+ transportOobChannel = TransportOobChannel(protocolDelegate, TEST_PROTOCOL_NAME)
+ }
+
+ @Test
+ fun completeOobDataExchange_startProtocolDiscovery() {
+ transportOobChannel.completeOobDataExchange(testMessage)
+
+ verify(testConnectionProtocol).startAssociationDiscovery(eq(TEST_PROTOCOL_NAME), any(), any())
+ }
+
+ @Test
+ fun onDeviceConnected_sendOobData() {
+ transportOobChannel.completeOobDataExchange(testMessage)
+ with(argumentCaptor<IDiscoveryCallback>()) {
+ verify(testConnectionProtocol)
+ .startAssociationDiscovery(eq(TEST_PROTOCOL_NAME), any(), capture())
+ firstValue.onDeviceConnected(TEST_PROTOCOL_ID)
+ }
+ verify(testConnectionProtocol).sendData(eq(TEST_PROTOCOL_ID), eq(testMessage), any())
+ }
+
+ @Test
+ fun onDataFailedToSend_disconnectDevice() {
+ transportOobChannel.completeOobDataExchange(testMessage)
+ with(argumentCaptor<IDiscoveryCallback>()) {
+ verify(testConnectionProtocol)
+ .startAssociationDiscovery(eq(TEST_PROTOCOL_NAME), any(), capture())
+ firstValue.onDeviceConnected(TEST_PROTOCOL_ID)
+ }
+ with(argumentCaptor<IDataSendCallback>()) {
+ verify(testConnectionProtocol).sendData(eq(TEST_PROTOCOL_ID), eq(testMessage), capture())
+ firstValue.onDataFailedToSend()
+ }
+ verify(testConnectionProtocol).disconnectDevice(TEST_PROTOCOL_ID)
+ }
+
+ @Test
+ fun interrupt_disconnectOngoingConnection() {
+ transportOobChannel.completeOobDataExchange(testMessage)
+ with(argumentCaptor<IDiscoveryCallback>()) {
+ verify(testConnectionProtocol)
+ .startAssociationDiscovery(eq(TEST_PROTOCOL_NAME), any(), capture())
+ firstValue.onDeviceConnected(TEST_PROTOCOL_ID)
+ }
+
+ transportOobChannel.interrupt()
+
+ verify(testConnectionProtocol).disconnectDevice(TEST_PROTOCOL_ID)
+ }
+
+ companion object {
+ private const val TEST_PROTOCOL_ID = "testProtocolId"
+ private const val TEST_PROTOCOL_NAME = "testProtocolName"
+ private val testMessage = "testMessage".toByteArray()
+ }
+}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/service/TransportServiceTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/service/TransportServiceTest.kt
index a5b956b..d63f7e4 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/service/TransportServiceTest.kt
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/service/TransportServiceTest.kt
@@ -7,10 +7,12 @@
import android.content.Intent
import android.content.IntentFilter
import android.content.ServiceConnection
+import android.content.res.Resources
import android.os.Bundle
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.android.connecteddevice.core.util.mockToBeAlive
+import com.google.android.connecteddevice.model.TransportProtocols
import com.google.android.connecteddevice.transport.IProtocolDelegate
import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.any
@@ -22,6 +24,9 @@
import org.junit.runner.RunWith
import org.robolectric.Robolectric
+private const val OOB_PROTOCOL_CHANNELS_RESOURCE_ID = 0
+private const val PROTOCOL_CHANNELS_RESOURCE_ID = 1
+
@RunWith(AndroidJUnit4::class)
class TransportServiceTest {
@@ -61,7 +66,7 @@
verify(mockDelegate).removeProtocol(any())
assertThat(service.boundService).isNull()
assertThat(service.receiver).isNull()
- assertThat(service.supportedProtocols).isEmpty()
+ assertThat(service.initializedProtocols).isEmpty()
}
@Test
@@ -83,7 +88,7 @@
assertThat(service.delegate).isNull()
assertThat(service.boundService).isNotNull()
- assertThat(service.supportedProtocols).isEmpty()
+ assertThat(service.initializedProtocols).isEmpty()
}
@Test
@@ -118,7 +123,7 @@
issueBluetoothChangedBroadcast(BluetoothAdapter.STATE_OFF)
verify(mockDelegate).removeProtocol(any())
- assertThat(service.supportedProtocols).isEmpty()
+ assertThat(service.initializedProtocols).isEmpty()
}
@Test
@@ -140,6 +145,7 @@
}
class TestTransportService : TransportService() {
+ private val context = ApplicationProvider.getApplicationContext<Context>()
var boundService: ServiceConnection? = null
@@ -171,6 +177,29 @@
}
override fun retrieveMetaDataBundle(): Bundle {
- return Bundle()
+ val bundle = Bundle()
+ bundle.putInt(META_SUPPORTED_TRANSPORT_PROTOCOLS, PROTOCOL_CHANNELS_RESOURCE_ID)
+ bundle.putInt(META_SUPPORTED_OOB_CHANNELS, OOB_PROTOCOL_CHANNELS_RESOURCE_ID)
+ return bundle
+ }
+
+ override fun getResources(): Resources {
+ return object :
+ Resources(
+ context.getResources().getAssets(),
+ context.getResources().getDisplayMetrics(),
+ context.getResources().getConfiguration()
+ ) {
+
+ override fun getStringArray(id: Int): Array<String> {
+ when (id) {
+ OOB_PROTOCOL_CHANNELS_RESOURCE_ID ->
+ return arrayOf(TransportProtocols.PROTOCOL_EAP, TransportProtocols.PROTOCOL_SPP)
+ PROTOCOL_CHANNELS_RESOURCE_ID ->
+ return arrayOf(TransportProtocols.PROTOCOL_BLE_PERIPHERAL)
+ else -> throw NotFoundException()
+ }
+ }
+ }
}
}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/ProtocolDelegateTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/ProtocolDelegateTest.kt
index 1799746..9def0be 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/ProtocolDelegateTest.kt
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/ProtocolDelegateTest.kt
@@ -37,6 +37,25 @@
}
@Test
+ fun addOobProtocol_addedToOobList() {
+ val protocol = mockToBeAlive<IConnectionProtocol>()
+
+ delegate.addOobProtocol(protocol)
+
+ assertThat(delegate.oobProtocols).containsExactly(protocol)
+ }
+
+ @Test
+ fun removeOobProtocol_removeFromOobList() {
+ val protocol = mockToBeAlive<IConnectionProtocol>()
+
+ delegate.addOobProtocol(protocol)
+ delegate.removeOobProtocol(protocol)
+
+ assertThat(delegate.oobProtocols).isEmpty()
+ }
+
+ @Test
fun removeProtocol_unrecognizedProtocolDoesNotInvokeCallback() {
val protocol = mockToBeAlive<IConnectionProtocol>()
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/eap/EapProtocolTest.kt b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/eap/EapProtocolTest.kt
new file mode 100644
index 0000000..3d4b51a
--- /dev/null
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/eap/EapProtocolTest.kt
@@ -0,0 +1,202 @@
+/*
+ * 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.eap
+
+import android.os.IBinder
+import android.os.ParcelUuid
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.android.connecteddevice.core.util.mockToBeAlive
+import com.google.android.connecteddevice.transport.IDataReceivedListener
+import com.google.android.connecteddevice.transport.IDeviceDisconnectedListener
+import com.google.android.connecteddevice.transport.IDiscoveryCallback
+import com.google.common.truth.Truth.assertThat
+import com.nhaarman.mockitokotlin2.any
+import com.nhaarman.mockitokotlin2.argumentCaptor
+import com.nhaarman.mockitokotlin2.eq
+import com.nhaarman.mockitokotlin2.mock
+import com.nhaarman.mockitokotlin2.never
+import com.nhaarman.mockitokotlin2.verify
+import com.panasonic.iapx.IDeviceConnection
+import com.panasonic.iapx.IDeviceConnectionDelegate
+import com.panasonic.iapx.IServiceConnector
+import java.util.UUID
+import kotlin.random.Random
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class EapProtocolTest {
+ private val mockServiceConnector = mock<IServiceConnector>()
+ private val mockDeviceConnection = mock<IDeviceConnection>()
+ private val mockDataReceivedListener = mockToBeAlive<IDataReceivedListener>()
+ private val mockDiscoveryCallback = mockToBeAlive<IDiscoveryCallback>()
+ private val mockDisconnectedListener = mockToBeAlive<IDeviceDisconnectedListener>()
+ private val eapProtocol =
+ EapProtocol(TEST_EAP_CLIENT_NAME, TEST_EAP_SERVICE_NAME, TEST_MAX_WRITE_SIZE)
+
+ @Test
+ fun OnEAPSessionStartWithValidProtocolName_invokeCallback() {
+ val deviceConnectionDelegate = captureDeviceCallback()
+ eapProtocol.startAssociationDiscovery(
+ TEST_EAP_PROTOCOL_NAME,
+ testIdentifier,
+ mockDiscoveryCallback
+ )
+
+ deviceConnectionDelegate.OnEAPSessionStart(
+ mockDeviceConnection,
+ testProtocolId,
+ TEST_EAP_PROTOCOL_NAME
+ )
+
+ verify(mockDiscoveryCallback).onDeviceConnected(testProtocolId.toString())
+ }
+
+ @Test
+ fun OnEAPSessionStartWithInValidProtocolName_doesNotInvokeCallback() {
+ val deviceConnectionDelegate = captureDeviceCallback()
+ eapProtocol.startAssociationDiscovery(
+ TEST_EAP_PROTOCOL_NAME,
+ testIdentifier,
+ mockDiscoveryCallback
+ )
+
+ deviceConnectionDelegate.OnEAPSessionStart(
+ mockDeviceConnection,
+ testProtocolId,
+ "invalidProtocolName"
+ )
+
+ verify(mockDiscoveryCallback, never()).onDeviceConnected(testProtocolId.toString())
+ }
+
+ @Test
+ fun OnEAPSessionStop_invokeCallback() {
+ val deviceConnectionDelegate = captureDeviceCallback()
+ eapProtocol.startAssociationDiscovery(
+ TEST_EAP_PROTOCOL_NAME,
+ testIdentifier,
+ mockDiscoveryCallback
+ )
+ deviceConnectionDelegate.OnEAPSessionStart(
+ mockDeviceConnection,
+ testProtocolId,
+ TEST_EAP_PROTOCOL_NAME
+ )
+ eapProtocol.registerDeviceDisconnectedListener(
+ testProtocolId.toString(),
+ mockDisconnectedListener
+ )
+
+ deviceConnectionDelegate.OnEAPSessionStop(mockDeviceConnection, testProtocolId)
+
+ verify(mockDisconnectedListener).onDeviceDisconnected(testProtocolId.toString())
+ }
+
+ @Test
+ fun OnEAPData_invokeCallback() {
+ val deviceConnectionDelegate = captureDeviceCallback()
+ eapProtocol.registerDataReceivedListener(testProtocolId.toString(), mockDataReceivedListener)
+
+ deviceConnectionDelegate.OnEAPData(mockDeviceConnection, testProtocolId, testMessage)
+
+ verify(mockDataReceivedListener).onDataReceived(testProtocolId.toString(), testMessage)
+ }
+
+ @Test
+ fun sendData_sendDataViaConnection() {
+ val deviceConnectionDelegate = captureDeviceCallback()
+ eapProtocol.startAssociationDiscovery(
+ TEST_EAP_PROTOCOL_NAME,
+ testIdentifier,
+ mockDiscoveryCallback
+ )
+ deviceConnectionDelegate.OnEAPSessionStart(
+ mockDeviceConnection,
+ testProtocolId,
+ TEST_EAP_PROTOCOL_NAME
+ )
+
+ eapProtocol.sendData(testProtocolId.toString(), testMessage, callback = null)
+
+ verify(mockDeviceConnection).SendEAPData(testProtocolId, testMessage)
+ }
+
+ @Test
+ fun stopAssociationDiscovery_doesNotInvokeCallbackOnDeviceConnected() {
+ val deviceConnectionDelegate = captureDeviceCallback()
+ eapProtocol.startAssociationDiscovery(
+ TEST_EAP_PROTOCOL_NAME,
+ testIdentifier,
+ mockDiscoveryCallback
+ )
+
+ eapProtocol.stopAssociationDiscovery()
+
+ deviceConnectionDelegate.OnEAPSessionStart(
+ mockDeviceConnection,
+ testProtocolId,
+ TEST_EAP_PROTOCOL_NAME
+ )
+ verify(mockDiscoveryCallback, never()).onDeviceConnected(testProtocolId.toString())
+ }
+
+ @Test
+ fun reset_clearDiscoveries() {
+ val deviceConnectionDelegate = captureDeviceCallback()
+ eapProtocol.startAssociationDiscovery(
+ TEST_EAP_PROTOCOL_NAME,
+ testIdentifier,
+ mockDiscoveryCallback
+ )
+
+ eapProtocol.reset()
+
+ deviceConnectionDelegate.OnEAPSessionStart(
+ mockDeviceConnection,
+ testProtocolId,
+ TEST_EAP_PROTOCOL_NAME
+ )
+ verify(mockDiscoveryCallback, never()).onDeviceConnected(testProtocolId.toString())
+ }
+
+ @Test
+ fun getMaxWriteSize_returnCorrectValue() {
+ assertThat(eapProtocol.getMaxWriteSize(testProtocolId.toString()))
+ .isEqualTo(TEST_MAX_WRITE_SIZE)
+ }
+
+ private fun captureDeviceCallback(): IDeviceConnectionDelegate {
+ eapProtocol.connectClient(mockServiceConnector)
+ val deviceDelegateBinder =
+ argumentCaptor<IBinder>()
+ .apply {
+ verify(mockServiceConnector).ConnectClient(eq(TEST_EAP_CLIENT_NAME), any(), capture())
+ }
+ .firstValue
+ return IDeviceConnectionDelegate.Stub.asInterface(deviceDelegateBinder)
+ }
+ companion object {
+ private const val TEST_EAP_CLIENT_NAME = "eapClientName"
+ private const val TEST_EAP_SERVICE_NAME = "eapServiceName"
+ private const val TEST_EAP_PROTOCOL_NAME = "eapProtocolName"
+ private const val TEST_MAX_WRITE_SIZE = 100
+ private val testIdentifier = ParcelUuid(UUID.randomUUID())
+ private val testProtocolId = Random.nextLong()
+ private val testMessage = "TestMessage".toByteArray()
+ }
+}
diff --git a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/spp/SppManagerTest.java b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/spp/SppManagerTest.java
index 950d92d..e77a089 100644
--- a/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/spp/SppManagerTest.java
+++ b/libs/connecteddevice/tests/unit/src/com/google/android/connecteddevice/transport/spp/SppManagerTest.java
@@ -107,6 +107,16 @@
}
@Test
+ public void
+ testReadMessageTaskCallback_onMessageReceivedWithoutConnection_doNotCallOnMessageReceivedListener()
+ throws InterruptedException {
+ sppManager.addOnMessageReceivedListener(mockListener, callbackExecutor);
+ sppManager.connectedSocket = null;
+ sppManager.readMessageTaskCallback.onMessageReceived(testData);
+ verify(mockListener, never()).onMessageReceived(any(), eq(testData));
+ }
+
+ @Test
public void testReadMessageTaskCallback_onMessageReadError_disconnectRemoteDevice()
throws InterruptedException {
sppManager.readMessageTaskCallback.onMessageReadError();
@@ -146,6 +156,15 @@
}
@Test
+ public void testCleanUp_doNotIssueCallbackWithoutConnection() throws IOException {
+ sppManager.connectedSocket = null;
+
+ sppManager.cleanup();
+
+ verify(mockConnectionCallback, never()).onRemoteDeviceDisconnected(any());
+ }
+
+ @Test
public void testCleanup_clearConnectTask() {
sppManager.connect(testBluetoothDevice, TEST_SERVICE_UUID);
@@ -171,4 +190,14 @@
sppManager.addOnMessageReceivedListener(mockListener, callbackExecutor);
verify(mockListener).onMessageReceived(any(), eq(testData));
}
+
+ @Test
+ public void testAddOnMessageReceivedListenerWithoutConnection_doNotInvokeCallback() {
+ sppManager.readMessageTaskCallback.onMessageReceived(testData);
+ verify(mockListener, never()).onMessageReceived(any(), eq(testData));
+
+ sppManager.connectedSocket = null;
+ sppManager.addOnMessageReceivedListener(mockListener, callbackExecutor);
+ verify(mockListener, never()).onMessageReceived(any(), eq(testData));
+ }
}
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 e2d5c89..04c0ad9 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
@@ -16,14 +16,17 @@
package com.google.android.connecteddevice.ui;
+import static android.os.Looper.getMainLooper;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
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 static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
import android.app.Application;
import android.bluetooth.BluetoothAdapter;
@@ -34,6 +37,7 @@
import androidx.lifecycle.Observer;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.connecteddevice.api.Connector;
import com.google.android.connecteddevice.api.FakeConnector;
import com.google.android.connecteddevice.api.IAssociationCallback;
import com.google.android.connecteddevice.model.AssociatedDevice;
@@ -76,6 +80,8 @@
application.getSystemService(BluetoothManager.class).getAdapter();
private final FakeConnector fakeConnector = spy(new FakeConnector());
+ private final NeverConnectFakeConnector neverConnectFakeConnector =
+ spy(new NeverConnectFakeConnector());
@Mock private Observer<AssociationState> mockAssociationStateObserver;
@Mock private Observer<List<AssociatedDeviceDetails>> mockDeviceDetailsObserver;
@@ -89,13 +95,7 @@
@Before
public void setUp() throws RemoteException {
- viewModel =
- new AssociatedDeviceViewModel(
- application,
- /* isSppEnabled= */ false,
- TEST_BLE_DEVICE_NAME_PREFIX,
- /* isPassengerEnabled= */ false,
- fakeConnector);
+ viewModel = createViewModel(fakeConnector);
adapter.enable();
}
@@ -106,6 +106,15 @@
}
@Test
+ public void acceptVerification_notInvokedIfServiceBotConnected() {
+ viewModel = createViewModel(neverConnectFakeConnector);
+
+ viewModel.acceptVerification();
+
+ verify(neverConnectFakeConnector, never()).acceptVerification();
+ }
+
+ @Test
public void removeDevice() {
AssociatedDevice device = createAssociatedDevice(/* isConnectionEnabled= */ true);
fakeConnector.addAssociatedDevice(device);
@@ -114,6 +123,16 @@
}
@Test
+ public void removeDevice_notInvokedIfServiceBotConnected() {
+ viewModel = createViewModel(neverConnectFakeConnector);
+ AssociatedDevice device = createAssociatedDevice(/* isConnectionEnabled= */ true);
+
+ viewModel.removeDevice(device);
+
+ verify(neverConnectFakeConnector, never()).removeAssociatedDevice(anyString());
+ }
+
+ @Test
public void toggleConnectionStatusForDevice_disableDevice() {
AssociatedDevice device = createAssociatedDevice(/* isConnectionEnabled= */ true);
fakeConnector.addAssociatedDevice(device);
@@ -130,6 +149,19 @@
}
@Test
+ public void toggleConnectionStatusForDevice_notInvokedIfServiceBotConnected() {
+ viewModel = createViewModel(neverConnectFakeConnector);
+ AssociatedDevice device1 = createAssociatedDevice(/* isConnectionEnabled= */ false);
+ AssociatedDevice device2 = createAssociatedDevice(/* isConnectionEnabled= */ true);
+
+ viewModel.toggleConnectionStatusForDevice(device1);
+ viewModel.toggleConnectionStatusForDevice(device2);
+
+ verify(neverConnectFakeConnector, never()).enableAssociatedDeviceConnection(anyString());
+ verify(neverConnectFakeConnector, never()).disableAssociatedDeviceConnection(anyString());
+ }
+
+ @Test
public void startAssociation_startWithIdentifier() {
ParcelUuid testIdentifier = new ParcelUuid(UUID.randomUUID());
viewModel.startAssociation(testIdentifier);
@@ -137,6 +169,14 @@
}
@Test
+ public void startAssociation_notInvokedIfServiceBotConnected() {
+ viewModel = createViewModel(neverConnectFakeConnector);
+
+ viewModel.startAssociation();
+ verify(neverConnectFakeConnector, never()).startAssociation(any());
+ }
+
+ @Test
public void startAssociation_deviceNotReady() {
adapter.disable();
viewModel.startAssociation();
@@ -385,6 +425,16 @@
}
@Test
+ public void claimDevice_notInvokedIfServiceBotConnected() {
+ viewModel = createViewModel(neverConnectFakeConnector);
+ AssociatedDevice device = createAssociatedDevice(/* isConnectionEnabled= */ true);
+
+ viewModel.claimDevice(device);
+
+ verify(neverConnectFakeConnector, never()).claimAssociatedDevice(anyString());
+ }
+
+ @Test
public void removeClaimOnDevice_removesAssociatedDeviceClaim() {
AssociatedDevice device = createAssociatedDevice(/* isConnectionEnabled= */ true);
fakeConnector.addAssociatedDevice(device);
@@ -394,6 +444,25 @@
verify(fakeConnector).removeAssociatedDeviceClaim(device.getDeviceId());
}
+ @Test
+ public void removeClaimOnDevice_notInvokedIfServiceBotConnected() {
+ viewModel = createViewModel(neverConnectFakeConnector);
+ AssociatedDevice device = createAssociatedDevice(/* isConnectionEnabled= */ true);
+
+ viewModel.claimDevice(device);
+
+ verify(neverConnectFakeConnector, never()).claimAssociatedDevice(anyString());
+ }
+
+ @Test
+ public void connectTimeout() {
+ viewModel = createViewModel(neverConnectFakeConnector);
+
+ shadowOf(getMainLooper()).runToEndOfTasks();
+
+ verify(neverConnectFakeConnector).disconnect();
+ }
+
private void captureAssociationCallback() {
ArgumentCaptor<IAssociationCallback> associationCallbackCaptor =
ArgumentCaptor.forClass(IAssociationCallback.class);
@@ -424,4 +493,21 @@
/* belongsToDriver= */ true,
/* hasSecureChannel= */ false);
}
+
+ private AssociatedDeviceViewModel createViewModel(Connector connector) {
+ return new AssociatedDeviceViewModel(
+ application,
+ /* isSppEnabled= */ false,
+ TEST_BLE_DEVICE_NAME_PREFIX,
+ /* isPassengerEnabled= */ false,
+ connector);
+ }
+
+ /** Fake connector that never connects. */
+ private static class NeverConnectFakeConnector extends FakeConnector {
+ @Override
+ public void connect() {
+ // Do nothing
+ }
+ }
}