From ba35a142361417ef19be622ced41632cf936ba88 Mon Sep 17 00:00:00 2001 From: Salil Rajadhyaksha Date: Mon, 14 Aug 2023 22:21:52 +0000 Subject: [PATCH] RemoteAuth discovery implementation Ignore-AOSP-First: Dependent changes in CDM are not available in aosp-main. Bug: 290264664 Test: atest RemoteAuthUnitTest Change-Id: I4e33178313d2add09436bf01ce127b3df152974e --- remoteauth/service/Android.bp | 1 + .../connectivity/CdmConnectionInfo.java | 15 +- .../connectivity/CdmConnectivityManager.java | 201 ++++++++++++ .../CompanionDeviceManagerWrapper.java | 77 +++++ .../remoteauth/connectivity/Connection.java | 1 - .../connectivity/ConnectionInfo.java | 8 +- .../connectivity/ConnectivityManager.java | 12 +- .../ConnectivityManagerFactory.java | 51 +++ .../connectivity/DiscoveredDevice.java | 3 +- .../connectivity/DiscoveryFilter.java | 3 +- .../connectivity/EventListener.java | 6 +- remoteauth/tests/unit/Android.bp | 3 + remoteauth/tests/unit/AndroidManifest.xml | 3 +- .../CdmConnectivityManagerTest.java | 304 ++++++++++++++++++ .../ConnectivityManagerFactoryTest.java | 45 +++ .../server/remoteauth/connectivity/Utils.java | 99 ++++++ 16 files changed, 809 insertions(+), 23 deletions(-) create mode 100644 remoteauth/service/java/com/android/server/remoteauth/connectivity/CdmConnectivityManager.java create mode 100644 remoteauth/service/java/com/android/server/remoteauth/connectivity/CompanionDeviceManagerWrapper.java create mode 100644 remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectivityManagerFactory.java create mode 100644 remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/CdmConnectivityManagerTest.java create mode 100644 remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/ConnectivityManagerFactoryTest.java create mode 100644 remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/Utils.java diff --git a/remoteauth/service/Android.bp b/remoteauth/service/Android.bp index dba8b75812..215a5911fb 100644 --- a/remoteauth/service/Android.bp +++ b/remoteauth/service/Android.bp @@ -32,6 +32,7 @@ java_library { libs: [ "androidx.annotation_annotation", "framework-bluetooth", + "framework-connectivity", "error_prone_annotations", "framework-configinfrastructure", "framework-connectivity-pre-jarjar", diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/CdmConnectionInfo.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/CdmConnectionInfo.java index 8bfdd3615c..d2a828cd87 100644 --- a/remoteauth/service/java/com/android/server/remoteauth/connectivity/CdmConnectionInfo.java +++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/CdmConnectionInfo.java @@ -34,11 +34,11 @@ import java.util.Objects; // TODO(b/295407748): Change to use @DataClass. // TODO(b/296625303): Change to VANILLA_ICE_CREAM when AssociationInfo is available in V. @TargetApi(Build.VERSION_CODES.TIRAMISU) -public final class CdmConnectionInfo extends ConnectionInfo { +public final class CdmConnectionInfo extends ConnectionInfo { @NonNull private final AssociationInfo mAssociationInfo; public CdmConnectionInfo(int connectionId, @NonNull AssociationInfo associationInfo) { - super(connectionId); + super(connectionId); mAssociationInfo = associationInfo; } @@ -78,10 +78,6 @@ public final class CdmConnectionInfo extends ConnectionInfo { out.writeTypedObject(mAssociationInfo, 0); } - public AssociationInfo getAssociationInfo() { - return mAssociationInfo; - } - /** Returns a string representation of ConnectionInfo. */ @Override public String toString() { @@ -98,11 +94,16 @@ public final class CdmConnectionInfo extends ConnectionInfo { } CdmConnectionInfo other = (CdmConnectionInfo) o; - return mAssociationInfo.equals(other.getAssociationInfo()); + return super.equals(o) && mAssociationInfo.equals(other.getConnectionParams()); } @Override public int hashCode() { return Objects.hash(mAssociationInfo); } + + @Override + public AssociationInfo getConnectionParams() { + return mAssociationInfo; + } } diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/CdmConnectivityManager.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/CdmConnectivityManager.java new file mode 100644 index 0000000000..49745c003d --- /dev/null +++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/CdmConnectivityManager.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2023 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.android.server.remoteauth.connectivity; + +import static com.android.server.remoteauth.connectivity.DiscoveryFilter.DeviceType; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.TargetApi; +import android.companion.AssociationInfo; +import android.companion.AssociationRequest; +import android.os.Build; +import android.util.Log; + + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.Preconditions; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; + +/** + * Discovers devices associated with the companion device manager. + * + * TODO(b/296625303): Change to VANILLA_ICE_CREAM when AssociationInfo is available in V. + */ +@TargetApi(Build.VERSION_CODES.TIRAMISU) +public class CdmConnectivityManager implements ConnectivityManager { + private static final String TAG = "CdmConnectivityManager"; + + private final CompanionDeviceManagerWrapper mCompanionDeviceManagerWrapper; + + private ExecutorService mExecutor; + private Map mPendingDiscoveryCallbacks = + new ConcurrentHashMap<>(); + + public CdmConnectivityManager( + @NonNull ExecutorService executor, + @NonNull CompanionDeviceManagerWrapper companionDeviceManagerWrapper) { + mExecutor = executor; + mCompanionDeviceManagerWrapper = companionDeviceManagerWrapper; + } + + /** + * Runs associated discovery callbacks for discovered devices. + * + * @param discoveredDeviceReceiver callback. + * @param device discovered device. + */ + private void notifyOnDiscovered( + @NonNull DiscoveredDeviceReceiver discoveredDeviceReceiver, + @NonNull DiscoveredDevice device) { + Preconditions.checkNotNull(discoveredDeviceReceiver); + Preconditions.checkNotNull(device); + + Log.i(TAG, "Notifying discovered device"); + discoveredDeviceReceiver.onDiscovered(device); + } + + /** + * Posts an async task to discover CDM associations and run callback if device is discovered. + * + * @param discoveryFilter filter for association. + * @param discoveredDeviceReceiver callback. + */ + private void startDiscoveryAsync(@NonNull DiscoveryFilter discoveryFilter, + @NonNull DiscoveredDeviceReceiver discoveredDeviceReceiver) { + Preconditions.checkNotNull(discoveryFilter); + Preconditions.checkNotNull(discoveredDeviceReceiver); + + List associations = mCompanionDeviceManagerWrapper.getAllAssociations(); + Log.i(TAG, "Found associations: " + associations.size()); + for (AssociationInfo association : associations) { + String deviceProfile = getDeviceProfileFromType(discoveryFilter.getDeviceType()); + // TODO(b/297574984): Include device presence signal before notifying discovery result. + if (mCompanionDeviceManagerWrapper.getDeviceProfile(association) + .equals(deviceProfile)) { + notifyOnDiscovered( + discoveredDeviceReceiver, + createDiscoveredDeviceFromAssociation(association)); + } + } + } + + /** + * Returns the device profile from association info. + * + * @param deviceType Discovery filter device type. + * @return Device profile string defined in {@link AssociationRequest}. + * @throws AssertionError if type cannot be mapped. + */ + private String getDeviceProfileFromType(@DeviceType int deviceType) { + if (deviceType == DiscoveryFilter.WATCH) { + return AssociationRequest.DEVICE_PROFILE_WATCH; + } else { + // Should not reach here. + throw new AssertionError(deviceType); + } + } + + /** + * Creates discovered device from association info. + * + * @param info Association info. + * @return discovered device object. + */ + private @NonNull DiscoveredDevice createDiscoveredDeviceFromAssociation( + @NonNull AssociationInfo info) { + return new DiscoveredDevice( + new CdmConnectionInfo(info.getId(), info), + info.getDisplayName() == null ? "" : info.getDisplayName().toString()); + } + + /** + * Triggers the discovery for CDM associations. + * + * Runs discovery only if a callback has not been previously registered. + * + * @param discoveryFilter filter for associations. + * @param discoveredDeviceReceiver callback to be run on discovery result. + */ + @Override + public void startDiscovery( + @NonNull DiscoveryFilter discoveryFilter, + @NonNull DiscoveredDeviceReceiver discoveredDeviceReceiver) { + Preconditions.checkNotNull(mCompanionDeviceManagerWrapper); + Preconditions.checkNotNull(discoveryFilter); + Preconditions.checkNotNull(discoveredDeviceReceiver); + + try { + mPendingDiscoveryCallbacks.computeIfAbsent( + discoveredDeviceReceiver, + discoveryFuture -> mExecutor.submit( + () -> startDiscoveryAsync(discoveryFilter, discoveryFuture), + /* result= */ null)); + } catch (RejectedExecutionException | NullPointerException e) { + Log.e(TAG, "Failed to start async discovery: " + e.getMessage()); + } + } + + /** Stops discovery. */ + @Override + public void stopDiscovery( + @NonNull DiscoveryFilter discoveryFilter, + @NonNull DiscoveredDeviceReceiver discoveredDeviceReceiver) { + Preconditions.checkNotNull(discoveryFilter); + Preconditions.checkNotNull(discoveredDeviceReceiver); + + Future discoveryFuture = mPendingDiscoveryCallbacks.remove(discoveredDeviceReceiver); + if (null != discoveryFuture && !discoveryFuture.cancel(/* mayInterruptIfRunning= */ true)) { + Log.d(TAG, "Discovery was possibly completed."); + } + } + + @Nullable + @Override + public Connection connect(@NonNull ConnectionInfo connectionInfo, + @NonNull EventListener eventListener) { + // Not implemented. + return null; + } + + @Override + public void startListening(MessageReceiver messageReceiver) { + // Not implemented. + } + + @Override + public void stopListening(MessageReceiver messageReceiver) { + // Not implemented. + } + + /** + * Returns whether the callback is already registered and pending. + * + * @param discoveredDeviceReceiver callback + * @return true if the callback is pending, false otherwise. + */ + @VisibleForTesting + boolean hasPendingCallbacks(@NonNull DiscoveredDeviceReceiver discoveredDeviceReceiver) { + return mPendingDiscoveryCallbacks.containsKey(discoveredDeviceReceiver); + } +} diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/CompanionDeviceManagerWrapper.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/CompanionDeviceManagerWrapper.java new file mode 100644 index 0000000000..eaf3edb8a0 --- /dev/null +++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/CompanionDeviceManagerWrapper.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2023 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.android.server.remoteauth.connectivity; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.TargetApi; +import android.companion.AssociationInfo; +import android.companion.CompanionDeviceManager; +import android.content.Context; +import android.os.Build; +import android.util.Log; + +import java.util.List; + +/** Wraps {@link android.companion.CompanionDeviceManager} for easier testing. */ +// TODO(b/296625303): Change to VANILLA_ICE_CREAM when AssociationInfo is available in V. +@TargetApi(Build.VERSION_CODES.TIRAMISU) +public class CompanionDeviceManagerWrapper { + private static final String TAG = "CompanionDeviceManagerWrapper"; + + private Context mContext; + private CompanionDeviceManager mCompanionDeviceManager; + + public CompanionDeviceManagerWrapper(@NonNull Context context) { + mContext = context; + } + + /** + * Returns device profile string from the association info. + * + * @param associationInfo the association info. + * @return String indicating device profile + */ + @Nullable + public String getDeviceProfile(@NonNull AssociationInfo associationInfo) { + return associationInfo.getDeviceProfile(); + } + + /** + * Returns all associations. + * + * @return associations or null if no associated devices present. + */ + @Nullable + public List getAllAssociations() { + if (mCompanionDeviceManager == null) { + try { + mCompanionDeviceManager = mContext.getSystemService(CompanionDeviceManager.class); + } catch (NullPointerException e) { + Log.e(TAG, "CompanionDeviceManager service does not exist: " + e); + return null; + } + } + + try { + return mCompanionDeviceManager.getAllAssociations(); + } catch (RuntimeException e) { + Log.e(TAG, "Failed to get CompanionDeviceManager associations: " + e.getMessage()); + } + return null; + } +} diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/Connection.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/Connection.java index eb5458d315..dca8b9ff36 100644 --- a/remoteauth/service/java/com/android/server/remoteauth/connectivity/Connection.java +++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/Connection.java @@ -25,7 +25,6 @@ import java.lang.annotation.RetentionPolicy; * A connection with the peer device. * *

Connections are used to exchange data with the peer device. - * */ public interface Connection { /** Unknown error. */ diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectionInfo.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectionInfo.java index 39bfa8d81c..6e0edb4b90 100644 --- a/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectionInfo.java +++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectionInfo.java @@ -28,9 +28,10 @@ import java.util.Objects; *

Connection information captures the details of underlying connection such as connection id, * type of connection and peer device mac address. * + * @param connection params per connection type. */ // TODO(b/295407748) Change to use @DataClass. -public abstract class ConnectionInfo implements Parcelable { +public abstract class ConnectionInfo implements Parcelable { int mConnectionId; public ConnectionInfo(int connectionId) { @@ -85,4 +86,9 @@ public abstract class ConnectionInfo implements Parcelable { public int hashCode() { return Objects.hash(mConnectionId); } + + /** + * Returns connection related parameters. + */ + public abstract T getConnectionParams(); } diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectivityManager.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectivityManager.java index bc0d77e830..7b3028596d 100644 --- a/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectivityManager.java +++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectivityManager.java @@ -23,9 +23,7 @@ import android.annotation.Nullable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -/** - * Performs discovery and triggers a connection to an associated device. - */ +/** Performs discovery and triggers a connection to an associated device. */ public interface ConnectivityManager { /** * Starts device discovery. @@ -63,8 +61,12 @@ public interface ConnectivityManager { /** Reason codes for connect failure. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({ERROR_REASON_UNKNOWN, ERROR_CONNECTION_TIMED_OUT, ERROR_CONNECTION_REFUSED, - ERROR_DEVICE_UNAVAILABLE}) + @IntDef({ + ERROR_REASON_UNKNOWN, + ERROR_CONNECTION_TIMED_OUT, + ERROR_CONNECTION_REFUSED, + ERROR_DEVICE_UNAVAILABLE + }) @interface ReasonCode {} /** diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectivityManagerFactory.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectivityManagerFactory.java new file mode 100644 index 0000000000..3407fca779 --- /dev/null +++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/ConnectivityManagerFactory.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 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.android.server.remoteauth.connectivity; + +import android.annotation.NonNull; +import android.content.Context; + +import com.android.internal.util.Preconditions; + +import java.lang.ref.WeakReference; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Factory class to create different types of connectivity managers based on the underlying + * network transports (for example CompanionDeviceManager). + */ +public final class ConnectivityManagerFactory { + private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor(); + + /** + * Creates and returns a ConnectivityManager object depending on connection type. + * + * @param context of the caller. + * @return ConnectivityManager object. + */ + public static ConnectivityManager getConnectivityManager(@NonNull Context context) { + Preconditions.checkNotNull(context); + + // For now, we only have one case, but ideally this should create a new type based on some + // feature flag. + return new CdmConnectivityManager(EXECUTOR, new CompanionDeviceManagerWrapper( + new WeakReference<>(context.getApplicationContext()).get())); + } + + private ConnectivityManagerFactory() {} +} diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveredDevice.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveredDevice.java index a3e1e589f1..8cad3eb387 100644 --- a/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveredDevice.java +++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveredDevice.java @@ -25,8 +25,7 @@ public final class DiscoveredDevice { private @NonNull ConnectionInfo mConnectionInfo; private @Nullable String mDisplayName; - public DiscoveredDevice( - @NonNull ConnectionInfo connectionInfo, @Nullable String displayName) { + public DiscoveredDevice(@NonNull ConnectionInfo connectionInfo, @Nullable String displayName) { this.mConnectionInfo = connectionInfo; this.mDisplayName = displayName; } diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveryFilter.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveryFilter.java index 36c4b60a71..3590f2056a 100644 --- a/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveryFilter.java +++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/DiscoveryFilter.java @@ -31,7 +31,6 @@ import java.lang.annotation.RetentionPolicy; * the filter criteria (device type, name or peer address). */ public final class DiscoveryFilter { - /** Device type WATCH. */ public static final int WATCH = 0; @@ -86,7 +85,7 @@ public final class DiscoveryFilter { private Builder() {} /** Static method to create a new builder */ - public static Builder newInstance() { + public static Builder builder() { return new Builder(); } diff --git a/remoteauth/service/java/com/android/server/remoteauth/connectivity/EventListener.java b/remoteauth/service/java/com/android/server/remoteauth/connectivity/EventListener.java index d07adb1f23..eaf57e0d3a 100644 --- a/remoteauth/service/java/com/android/server/remoteauth/connectivity/EventListener.java +++ b/remoteauth/service/java/com/android/server/remoteauth/connectivity/EventListener.java @@ -18,10 +18,8 @@ package com.android.server.remoteauth.connectivity; import android.annotation.NonNull; -/** - * Listens to the events from underlying transport. - */ -interface EventListener { +/** Listens to the events from underlying transport. */ +public interface EventListener { /** Called when remote device is disconnected from the underlying transport. */ void onDisconnect(@NonNull ConnectionInfo connectionInfo); } diff --git a/remoteauth/tests/unit/Android.bp b/remoteauth/tests/unit/Android.bp index 629d3605e5..ccfa20e8d8 100644 --- a/remoteauth/tests/unit/Android.bp +++ b/remoteauth/tests/unit/Android.bp @@ -29,17 +29,20 @@ android_test { "android.test.base", "android.test.mock", "android.test.runner", + "framework-annotations-lib", ], compile_multilib: "both", static_libs: [ "androidx.test.ext.junit", + "androidx.test.ext.truth", "androidx.test.rules", "com.uwb.support.generic", "framework-remoteauth-static", "junit", "libprotobuf-java-lite", "mockito-target-extended-minus-junit4", + "mockito-target-minus-junit4", "platform-test-annotations", "service-remoteauth-pre-jarjar", "truth-prebuilt", diff --git a/remoteauth/tests/unit/AndroidManifest.xml b/remoteauth/tests/unit/AndroidManifest.xml index 0449409119..a5294c878b 100644 --- a/remoteauth/tests/unit/AndroidManifest.xml +++ b/remoteauth/tests/unit/AndroidManifest.xml @@ -31,5 +31,6 @@ + android:label="RemoteAuth Mainline Module Tests" + android:directBootAware="true"/> diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/CdmConnectivityManagerTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/CdmConnectivityManagerTest.java new file mode 100644 index 0000000000..824344ace1 --- /dev/null +++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/CdmConnectivityManagerTest.java @@ -0,0 +1,304 @@ +/* + * Copyright (C) 2023 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.android.server.remoteauth.connectivity; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.companion.AssociationInfo; +import android.companion.AssociationRequest; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.google.common.util.concurrent.SettableFuture; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +/** Unit tests for {@link CdmConnectivityManager}. */ +@RunWith(AndroidJUnit4.class) +public class CdmConnectivityManagerTest { + @Rule public final MockitoRule mocks = MockitoJUnit.rule(); + + @Mock CompanionDeviceManagerWrapper mCompanionDeviceManagerWrapper; + + private CdmConnectivityManager mCdmConnectivityManager; + private ExecutorService mTestExecutor = Executors.newSingleThreadExecutor(); + + @Before + public void setUp() { + mCdmConnectivityManager = + new CdmConnectivityManager(mTestExecutor, mCompanionDeviceManagerWrapper); + } + + @After + public void tearDown() { + mTestExecutor.shutdown(); + } + + @Test + public void testStartDiscovery_callsGetAllAssociationsOnce() throws InterruptedException { + mCdmConnectivityManager.startDiscovery( + Utils.getFakeDiscoveryFilter(), Utils.getFakeDiscoveredDeviceReceiver()); + + mTestExecutor.awaitTermination(1, TimeUnit.SECONDS); + + verify(mCompanionDeviceManagerWrapper, times(1)).getAllAssociations(); + } + + @Test + public void testStartDiscovery_fetchesNoAssociations() { + SettableFuture future = SettableFuture.create(); + + when(mCompanionDeviceManagerWrapper.getAllAssociations()) + .thenReturn(Utils.getFakeAssociationInfoList(0)); + + DiscoveredDeviceReceiver discoveredDeviceReceiver = + new DiscoveredDeviceReceiver() { + @Override + public void onDiscovered(DiscoveredDevice unused) { + future.set(true); + } + + @Override + public void onLost(DiscoveredDevice unused) { + future.set(true); + } + }; + + mCdmConnectivityManager.startDiscovery( + Utils.getFakeDiscoveryFilter(), discoveredDeviceReceiver); + + assertThat(future.isDone()).isFalse(); + } + + @Test + public void testStartDiscovery_DoesNotReturnNonWatchAssociations() throws InterruptedException { + SettableFuture future = SettableFuture.create(); + DiscoveredDeviceReceiver discoveredDeviceReceiver = + new DiscoveredDeviceReceiver() { + @Override + public void onDiscovered(DiscoveredDevice unused) { + future.set(true); + } + + @Override + public void onLost(DiscoveredDevice unused) { + future.set(true); + } + }; + + when(mCompanionDeviceManagerWrapper.getDeviceProfile(any(AssociationInfo.class))) + .thenReturn(Utils.FAKE_DEVICE_PROFILE); + + mCdmConnectivityManager.startDiscovery( + Utils.getFakeDiscoveryFilter(), discoveredDeviceReceiver); + + mTestExecutor.awaitTermination(1, TimeUnit.SECONDS); + + verify(mCompanionDeviceManagerWrapper, times(1)).getAllAssociations(); + verify(mCompanionDeviceManagerWrapper, times(0)) + .getDeviceProfile(any(AssociationInfo.class)); + assertThat(future.isDone()).isFalse(); + } + + @Test + public void testStartDiscovery_returnsOneWatchAssociation() throws InterruptedException { + SettableFuture future = SettableFuture.create(); + DiscoveredDeviceReceiver discoveredDeviceReceiver = + new DiscoveredDeviceReceiver() { + @Override + public void onDiscovered(DiscoveredDevice unused) { + future.set(true); + } + }; + + when(mCompanionDeviceManagerWrapper.getAllAssociations()) + .thenReturn(Utils.getFakeAssociationInfoList(1)); + when(mCompanionDeviceManagerWrapper.getDeviceProfile(any(AssociationInfo.class))) + .thenReturn(AssociationRequest.DEVICE_PROFILE_WATCH); + + mCdmConnectivityManager.startDiscovery( + Utils.getFakeDiscoveryFilter(), discoveredDeviceReceiver); + + mTestExecutor.awaitTermination(1, TimeUnit.SECONDS); + + verify(mCompanionDeviceManagerWrapper, times(1)).getAllAssociations(); + verify(mCompanionDeviceManagerWrapper, times(1)) + .getDeviceProfile(any(AssociationInfo.class)); + assertThat(future.isDone()).isTrue(); + } + + @Test + public void testStartDiscovery_returnsMultipleWatchAssociations() throws InterruptedException { + int numAssociations = 3; + SettableFuture future = SettableFuture.create(); + DiscoveredDeviceReceiver discoveredDeviceReceiver = + new DiscoveredDeviceReceiver() { + int mNumCallbacks = 0; + + @Override + public void onDiscovered(DiscoveredDevice unused) { + ++mNumCallbacks; + if (mNumCallbacks == numAssociations) { + future.set(true); + } + } + }; + + when(mCompanionDeviceManagerWrapper.getAllAssociations()) + .thenReturn(Utils.getFakeAssociationInfoList(numAssociations)); + when(mCompanionDeviceManagerWrapper.getDeviceProfile(any(AssociationInfo.class))) + .thenReturn(AssociationRequest.DEVICE_PROFILE_WATCH); + + mCdmConnectivityManager.startDiscovery( + Utils.getFakeDiscoveryFilter(), discoveredDeviceReceiver); + + mTestExecutor.awaitTermination(1, TimeUnit.SECONDS); + + verify(mCompanionDeviceManagerWrapper, times(1)).getAllAssociations(); + verify(mCompanionDeviceManagerWrapper, times(numAssociations)) + .getDeviceProfile(any(AssociationInfo.class)); + assertThat(future.isDone()).isTrue(); + } + + @Test + public void testMultipleStartDiscovery_runsAllCallbacks() throws InterruptedException { + SettableFuture future1 = SettableFuture.create(); + SettableFuture future2 = SettableFuture.create(); + DiscoveredDeviceReceiver discoveredDeviceReceiver1 = + new DiscoveredDeviceReceiver() { + @Override + public void onDiscovered(DiscoveredDevice unused) { + future1.set(true); + } + }; + DiscoveredDeviceReceiver discoveredDeviceReceiver2 = + new DiscoveredDeviceReceiver() { + @Override + public void onDiscovered(DiscoveredDevice unused) { + future2.set(true); + } + }; + + when(mCompanionDeviceManagerWrapper.getAllAssociations()) + .thenReturn(Utils.getFakeAssociationInfoList(1)); + when(mCompanionDeviceManagerWrapper.getDeviceProfile(any(AssociationInfo.class))) + .thenReturn(AssociationRequest.DEVICE_PROFILE_WATCH); + + // Start discovery twice with different callbacks. + mCdmConnectivityManager.startDiscovery( + Utils.getFakeDiscoveryFilter(), discoveredDeviceReceiver1); + mCdmConnectivityManager.startDiscovery( + Utils.getFakeDiscoveryFilter(), discoveredDeviceReceiver2); + + mTestExecutor.awaitTermination(1, TimeUnit.SECONDS); + + verify(mCompanionDeviceManagerWrapper, times(2)).getAllAssociations(); + verify(mCompanionDeviceManagerWrapper, times(2)) + .getDeviceProfile(any(AssociationInfo.class)); + assertThat(future1.isDone()).isTrue(); + assertThat(future2.isDone()).isTrue(); + } + + @Test + public void testStartDiscovery_returnsExpectedDiscoveredDevice() throws InterruptedException { + SettableFuture future = SettableFuture.create(); + DiscoveredDeviceReceiver discoveredDeviceReceiver = + new DiscoveredDeviceReceiver() { + @Override + public void onDiscovered(DiscoveredDevice device) { + assertThat(device.getConnectionInfo() instanceof CdmConnectionInfo) + .isTrue(); + + CdmConnectionInfo connectionInfo = + (CdmConnectionInfo) device.getConnectionInfo(); + if (connectionInfo.getConnectionParams().getDeviceMacAddress().toString() + .equals(Utils.FAKE_PEER_ADDRESS) + && connectionInfo.getConnectionId() == Utils.FAKE_CONNECTION_ID) { + future.set(true); + } + } + }; + + when(mCompanionDeviceManagerWrapper.getAllAssociations()) + .thenReturn(Utils.getFakeAssociationInfoList(1)); + when(mCompanionDeviceManagerWrapper.getDeviceProfile(any(AssociationInfo.class))) + .thenReturn(AssociationRequest.DEVICE_PROFILE_WATCH); + + mCdmConnectivityManager.startDiscovery( + Utils.getFakeDiscoveryFilter(), discoveredDeviceReceiver); + + mTestExecutor.awaitTermination(1, TimeUnit.SECONDS); + + verify(mCompanionDeviceManagerWrapper, times(1)).getAllAssociations(); + verify(mCompanionDeviceManagerWrapper, times(1)) + .getDeviceProfile(any(AssociationInfo.class)); + assertThat(future.isDone()).isTrue(); + } + + @Test + public void testStopDiscovery_removesCallback() { + DiscoveredDeviceReceiver discoveredDeviceReceiver = + new DiscoveredDeviceReceiver() { + @Override + public void onDiscovered(DiscoveredDevice unused) {} + }; + + DiscoveryFilter discoveryFilter = Utils.getFakeDiscoveryFilter(); + mCdmConnectivityManager.startDiscovery(discoveryFilter, discoveredDeviceReceiver); + + assertThat(mCdmConnectivityManager.hasPendingCallbacks(discoveredDeviceReceiver)).isTrue(); + + mCdmConnectivityManager.stopDiscovery(discoveryFilter, discoveredDeviceReceiver); + + assertThat(mCdmConnectivityManager.hasPendingCallbacks(discoveredDeviceReceiver)).isFalse(); + } + + @Test + public void testStopDiscovery_DoesNotRunCallback() { + SettableFuture future = SettableFuture.create(); + DiscoveredDeviceReceiver discoveredDeviceReceiver = + new DiscoveredDeviceReceiver() { + @Override + public void onDiscovered(DiscoveredDevice unused) { + future.set(true); + } + }; + + DiscoveryFilter discoveryFilter = Utils.getFakeDiscoveryFilter(); + mCdmConnectivityManager.startDiscovery(discoveryFilter, discoveredDeviceReceiver); + mCdmConnectivityManager.stopDiscovery(discoveryFilter, discoveredDeviceReceiver); + + assertThat(future.isDone()).isFalse(); + } +} + diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/ConnectivityManagerFactoryTest.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/ConnectivityManagerFactoryTest.java new file mode 100644 index 0000000000..42eff90561 --- /dev/null +++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/ConnectivityManagerFactoryTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2023 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.android.server.remoteauth.connectivity; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link CdmConnectivityManager}. */ +@RunWith(AndroidJUnit4.class) +public class ConnectivityManagerFactoryTest { + + @Before + public void setUp() {} + + @Test + public void testFactory_returnsConnectivityManager() { + final Context context = InstrumentationRegistry.getInstrumentation().getContext(); + ConnectivityManager connectivityManager = + ConnectivityManagerFactory.getConnectivityManager(context); + + assertThat(connectivityManager).isNotNull(); + } +} diff --git a/remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/Utils.java b/remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/Utils.java new file mode 100644 index 0000000000..a5c992a1f2 --- /dev/null +++ b/remoteauth/tests/unit/src/com/android/server/remoteauth/connectivity/Utils.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2023 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.android.server.remoteauth.connectivity; + +import android.companion.AssociationInfo; +import android.companion.AssociationRequest; +import android.content.pm.PackageManager; +import android.net.MacAddress; + +import java.util.ArrayList; +import java.util.List; + +public final class Utils { + public static final int FAKE_CONNECTION_ID = 1; + public static final int FAKE_USER_ID = 0; + public static final String FAKE_DISPLAY_NAME = "FAKE-DISPLAY-NAME"; + public static final String FAKE_PEER_ADDRESS = "ff:ff:ff:ff:ff:ff"; + public static final String FAKE_DEVICE_PROFILE = "FAKE-DEVICE-PROFILE"; + public static final String FAKE_PACKAGE_NAME = "FAKE-PACKAGE-NAME"; + + public static DiscoveryFilter getFakeDiscoveryFilter() { + return DiscoveryFilter.Builder.builder() + .setDeviceName(FAKE_DISPLAY_NAME) + .setPeerAddress("FAKE-PEER-ADDRESS") + .setDeviceType(DiscoveryFilter.WATCH) + .build(); + } + + public static DiscoveredDeviceReceiver getFakeDiscoveredDeviceReceiver() { + return new DiscoveredDeviceReceiver() { + @Override + public void onDiscovered(DiscoveredDevice unused) {} + + @Override + public void onLost(DiscoveredDevice unused) {} + }; + } + + /** + * Returns a fake CDM connection info. + * + * @return connection info. + */ + public static CdmConnectionInfo getFakeCdmConnectionInfo() + throws PackageManager.NameNotFoundException { + return new CdmConnectionInfo(FAKE_CONNECTION_ID, getFakeAssociationInfoList(1).get(0)); + } + + /** + * Returns a fake discovered device. + * + * @return discovered device. + */ + public static DiscoveredDevice getFakeCdmDiscoveredDevice() + throws PackageManager.NameNotFoundException { + return new DiscoveredDevice(getFakeCdmConnectionInfo(), FAKE_DISPLAY_NAME); + } + + /** + * Returns fake association info array. + * + *

Creates an AssociationInfo object with fake values. + * + * @param associationsSize number of fake association info entries to return. + * @return list of {@link AssociationInfo} or null. + */ + public static List getFakeAssociationInfoList(int associationsSize) { + if (associationsSize > 0) { + List associations = new ArrayList<>(); + // Association id starts from 1. + for (int i = 1; i <= associationsSize; ++i) { + associations.add( + (new AssociationInfo.Builder(i, FAKE_USER_ID, FAKE_PACKAGE_NAME)) + .setDeviceProfile(AssociationRequest.DEVICE_PROFILE_WATCH) + .setDisplayName(FAKE_DISPLAY_NAME) + .setDeviceMacAddress(MacAddress.fromString(FAKE_PEER_ADDRESS)) + .build()); + } + return associations; + } + return new ArrayList<>(); + } + + private Utils() {} +}