Merge "RemoteAuth discovery implementation" into udc-dev-plus-aosp

This commit is contained in:
Treehugger Robot 2023-09-11 19:05:08 +00:00 • committed by Android (Google) Code Review
commit de39966e73
16 changed files with 809 additions and 23 deletions

View file

@ -33,6 +33,7 @@ java_library {
libs: [
"androidx.annotation_annotation",
"framework-bluetooth",
"framework-connectivity",
"error_prone_annotations",
"framework-configinfrastructure",
"framework-connectivity-pre-jarjar",

View file

@ -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<AssociationInfo> {
@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;
}
}

View file

@ -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<DiscoveredDeviceReceiver, Future> 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<AssociationInfo> 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<Void> 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);
}
}

View file

@ -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<AssociationInfo> 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;
}
}

View file

@ -25,7 +25,6 @@ import java.lang.annotation.RetentionPolicy;
* A connection with the peer device.
*
* <p>Connections are used to exchange data with the peer device.
*
*/
public interface Connection {
/** Unknown error. */

View file

@ -28,9 +28,10 @@ import java.util.Objects;
* <p>Connection information captures the details of underlying connection such as connection id,
* type of connection and peer device mac address.
*
* @param <T> connection params per connection type.
*/
// TODO(b/295407748) Change to use @DataClass.
public abstract class ConnectionInfo implements Parcelable {
public abstract class ConnectionInfo<T> 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();
}

View file

@ -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 {}
/**

View file

@ -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() {}
}

View file

@ -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;
}

View file

@ -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();
}

View file

@ -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);
}

View file

@ -32,17 +32,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",

View file

@ -31,5 +31,6 @@
<instrumentation
android:name="androidx.test.runner.AndroidJUnitRunner"
android:targetPackage="android.remoteauth.test"
android:label="RemoteAuth Mainline Module Tests" />
android:label="RemoteAuth Mainline Module Tests"
android:directBootAware="true"/>
</manifest>

View file

@ -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<Boolean> 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<Boolean> 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<Boolean> 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<Boolean> 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<Boolean> future1 = SettableFuture.create();
SettableFuture<Boolean> 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<Boolean> 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();
}
}

View file

@ -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();
}
}

View file

@ -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.
*
* <p> 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<AssociationInfo> getFakeAssociationInfoList(int associationsSize) {
if (associationsSize > 0) {
List<AssociationInfo> 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() {}
}