RemoteAuth discovery implementation
Ignore-AOSP-First: Dependent changes in CDM are not available in aosp-main. Bug: 290264664 Test: atest RemoteAuthUnitTest Change-Id: I4e33178313d2add09436bf01ce127b3df152974e
This commit is contained in:
parent
ad35f0926c
commit
ba35a14236
16 changed files with 809 additions and 23 deletions
|
|
@ -32,6 +32,7 @@ java_library {
|
|||
libs: [
|
||||
"androidx.annotation_annotation",
|
||||
"framework-bluetooth",
|
||||
"framework-connectivity",
|
||||
"error_prone_annotations",
|
||||
"framework-configinfrastructure",
|
||||
"framework-connectivity-pre-jarjar",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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() {}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {}
|
||||
}
|
||||
Loading…
Reference in a new issue