Merge "Add Multiplexer logic" into udc-dev
diff --git a/nearby/service/java/com/android/server/nearby/managers/ListenerMultiplexer.java b/nearby/service/java/com/android/server/nearby/managers/ListenerMultiplexer.java
new file mode 100644
index 0000000..a6a9388
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/ListenerMultiplexer.java
@@ -0,0 +1,189 @@
+/*
+ * 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.nearby.managers;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.NonNull;
+import android.os.IBinder;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.nearby.managers.registration.BinderListenerRegistration;
+import com.android.server.nearby.managers.registration.BinderListenerRegistration.ListenerOperation;
+
+import java.util.Collection;
+import java.util.Objects;
+import java.util.function.Function;
+
+/**
+ * A simplified class based on {@link com.android.server.location.listeners.ListenerMultiplexer}.
+ * It is a base class to multiplex broadcast and discovery events to multiple listener
+ * registrations. Every listener is represented by a registration object which stores all required
+ * state for a listener.
+ * Registrations will be merged to one request for the service to operate.
+ *
+ * @param <TListener> callback type for clients
+ * @param <TRegistration> child of {@link BinderListenerRegistration}
+ * @param <TMergedRegistration> merged registration type
+ */
+public abstract class ListenerMultiplexer<TListener,
+ TRegistration extends BinderListenerRegistration<TListener>, TMergedRegistration> {
+
+ /**
+ * The lock object used by the multiplexer. Acquiring this lock allows for multiple operations
+ * on the multiplexer to be completed atomically. Otherwise, it is not required to hold this
+ * lock. This lock is held while invoking all lifecycle callbacks on both the multiplexer and
+ * any registrations.
+ */
+ public final Object mMultiplexerLock = new Object();
+
+ @GuardedBy("mMultiplexerLock")
+ final ArrayMap<IBinder, TRegistration> mRegistrations = new ArrayMap<>();
+
+ // this is really @NonNull in many ways, but we explicitly null this out to allow for GC when
+ // not
+ // in use, so we can't annotate with @NonNull
+ @GuardedBy("mMultiplexerLock")
+ public TMergedRegistration mMerged;
+
+ /**
+ * Invoked when the multiplexer goes from having no registrations to having some registrations.
+ * This is a convenient entry point for registering listeners, etc, which only need to be
+ * present
+ * while there are any registrations. Invoked while holding the multiplexer's internal lock.
+ */
+ @GuardedBy("mMultiplexerLock")
+ public void onRegister() {
+ Log.v(TAG, "ListenerMultiplexer registered.");
+ }
+
+ /**
+ * Invoked when the multiplexer goes from having some registrations to having no registrations.
+ * This is a convenient entry point for unregistering listeners, etc, which only need to be
+ * present while there are any registrations. Invoked while holding the multiplexer's internal
+ * lock.
+ */
+ @GuardedBy("mMultiplexerLock")
+ public void onUnregister() {
+ Log.v(TAG, "ListenerMultiplexer unregistered.");
+ }
+
+ /**
+ * Puts a new registration with the given key, replacing any previous registration under the
+ * same key. This method cannot be called to put a registration re-entrantly.
+ */
+ public final void putRegistration(@NonNull IBinder key, @NonNull TRegistration registration) {
+ Objects.requireNonNull(key);
+ Objects.requireNonNull(registration);
+ synchronized (mMultiplexerLock) {
+ boolean wasEmpty = mRegistrations.isEmpty();
+
+ int index = mRegistrations.indexOfKey(key);
+ if (index > 0) {
+ BinderListenerRegistration<TListener> oldRegistration = mRegistrations.valueAt(
+ index);
+ oldRegistration.onUnregister();
+ mRegistrations.setValueAt(index, registration);
+ } else {
+ mRegistrations.put(key, registration);
+ }
+
+ registration.onRegister();
+ onRegistrationsUpdated();
+ if (wasEmpty) {
+ onRegister();
+ }
+ }
+ }
+
+ /**
+ * Removes the registration with the given key.
+ */
+ public final void removeRegistration(IBinder key) {
+ synchronized (mMultiplexerLock) {
+ int index = mRegistrations.indexOfKey(key);
+ if (index < 0) {
+ return;
+ }
+
+ removeRegistration(index);
+ }
+ }
+
+ @GuardedBy("mMultiplexerLock")
+ private void removeRegistration(int index) {
+ TRegistration registration = mRegistrations.valueAt(index);
+
+ registration.onUnregister();
+ mRegistrations.removeAt(index);
+
+ onRegistrationsUpdated();
+
+ if (mRegistrations.isEmpty()) {
+ onUnregister();
+ }
+ }
+
+ /**
+ * Invoked when a registration is added, removed, or replaced. Invoked while holding the
+ * multiplexer's internal lock.
+ */
+ @GuardedBy("mMultiplexerLock")
+ public final void onRegistrationsUpdated() {
+ TMergedRegistration newMerged = mergeRegistrations(mRegistrations.values());
+ if (newMerged.equals(mMerged)) {
+ return;
+ }
+ mMerged = newMerged;
+ onMergedRegistrationsUpdated();
+ }
+
+ /**
+ * Called in order to generate a merged registration from the given set of active registrations.
+ * The list of registrations will never be empty. If the resulting merged registration is equal
+ * to the currently registered merged registration, nothing further will happen. If the merged
+ * registration differs,{@link #onMergedRegistrationsUpdated()} will be invoked with the new
+ * merged registration so that the backing service can be updated.
+ */
+ @GuardedBy("mMultiplexerLock")
+ public abstract TMergedRegistration mergeRegistrations(
+ @NonNull Collection<TRegistration> registrations);
+
+ /**
+ * The operation that the manager wants to handle when there is an update for the merged
+ * registration.
+ */
+ @GuardedBy("mMultiplexerLock")
+ public abstract void onMergedRegistrationsUpdated();
+
+ protected final void deliverToListeners(
+ Function<TRegistration, ListenerOperation<TListener>> function) {
+ synchronized (mMultiplexerLock) {
+ final int size = mRegistrations.size();
+ for (int i = 0; i < size; i++) {
+ TRegistration registration = mRegistrations.valueAt(i);
+ BinderListenerRegistration.ListenerOperation<TListener> operation = function.apply(
+ registration);
+ if (operation != null) {
+ registration.executeOperation(operation);
+ }
+ }
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/managers/MergedDiscoveryRequest.java b/nearby/service/java/com/android/server/nearby/managers/MergedDiscoveryRequest.java
new file mode 100644
index 0000000..dcfb602
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/MergedDiscoveryRequest.java
@@ -0,0 +1,162 @@
+/*
+ * 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.nearby.managers;
+
+import android.annotation.IntDef;
+import android.nearby.ScanFilter;
+import android.nearby.ScanRequest;
+import android.util.ArraySet;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.Collection;
+import java.util.Set;
+
+/** Internal discovery request to {@link DiscoveryProviderManager} and providers */
+public class MergedDiscoveryRequest {
+
+ private static final MergedDiscoveryRequest EMPTY_REQUEST = new MergedDiscoveryRequest(
+ /* scanMode= */ ScanRequest.SCAN_MODE_NO_POWER,
+ /* scanTypes= */ ImmutableSet.of(),
+ /* actions= */ ImmutableSet.of(),
+ /* scanFilters= */ ImmutableSet.of(),
+ /* mediums= */ ImmutableSet.of());
+ @ScanRequest.ScanMode
+ private final int mScanMode;
+ private final Set<Integer> mScanTypes;
+ private final Set<Integer> mActions;
+ private final Set<ScanFilter> mScanFilters;
+ private final Set<Integer> mMediums;
+
+ private MergedDiscoveryRequest(@ScanRequest.ScanMode int scanMode, Set<Integer> scanTypes,
+ Set<Integer> actions, Set<ScanFilter> scanFilters, Set<Integer> mediums) {
+ mScanMode = scanMode;
+ mScanTypes = scanTypes;
+ mActions = actions;
+ mScanFilters = scanFilters;
+ mMediums = mediums;
+ }
+
+ /**
+ * Returns an empty discovery request.
+ *
+ * <p>The empty request is used as the default request when the discovery engine is enabled,
+ * but
+ * there is no request yet. It's also used to notify the discovery engine all clients have
+ * removed
+ * their requests.
+ */
+ public static MergedDiscoveryRequest empty() {
+ return EMPTY_REQUEST;
+ }
+
+ /** Returns the priority of the request */
+ @ScanRequest.ScanMode
+ public final int getScanMode() {
+ return mScanMode;
+ }
+
+ /** Returns all requested scan types. */
+ public ImmutableSet<Integer> getScanTypes() {
+ return ImmutableSet.copyOf(mScanTypes);
+ }
+
+ /** Returns the actions of the request */
+ public ImmutableSet<Integer> getActions() {
+ return ImmutableSet.copyOf(mActions);
+ }
+
+ /** Returns the scan filters of the request */
+ public ImmutableSet<ScanFilter> getScanFilters() {
+ return ImmutableSet.copyOf(mScanFilters);
+ }
+
+ /** Returns the enabled scan mediums */
+ public ImmutableSet<Integer> getMediums() {
+ return ImmutableSet.copyOf(mMediums);
+ }
+
+ /**
+ * The medium where the broadcast request should be sent.
+ *
+ * @hide
+ */
+ @IntDef({Medium.BLE})
+ public @interface Medium {
+ int BLE = 1;
+ }
+
+ /** Builder for {@link MergedDiscoveryRequest}. */
+ public static class Builder {
+ private final Set<Integer> mScanTypes;
+ private final Set<Integer> mActions;
+ private final Set<ScanFilter> mScanFilters;
+ private final Set<Integer> mMediums;
+ @ScanRequest.ScanMode
+ private int mScanMode;
+
+ public Builder() {
+ mScanMode = ScanRequest.SCAN_MODE_NO_POWER;
+ mScanTypes = new ArraySet<>();
+ mActions = new ArraySet<>();
+ mScanFilters = new ArraySet<>();
+ mMediums = new ArraySet<>();
+ }
+
+ /**
+ * Sets the priority for the engine request.
+ */
+ public Builder setScanMode(@ScanRequest.ScanMode int scanMode) {
+ mScanMode = scanMode;
+ return this;
+ }
+
+ /**
+ * Adds scan type to the request.
+ */
+ public Builder addScanType(@ScanRequest.ScanType int type) {
+ mScanTypes.add(type);
+ return this;
+ }
+
+ /** Add actions to the request. */
+ public Builder addActions(Collection<Integer> actions) {
+ mActions.addAll(actions);
+ return this;
+ }
+
+ /** Add actions to the request. */
+ public Builder addScanFilters(Collection<ScanFilter> scanFilters) {
+ mScanFilters.addAll(scanFilters);
+ return this;
+ }
+
+ /**
+ * Add mediums to the request.
+ */
+ public Builder addMedium(@Medium int medium) {
+ mMediums.add(medium);
+ return this;
+ }
+
+ /** Builds an instance of {@link MergedDiscoveryRequest}. */
+ public MergedDiscoveryRequest build() {
+ return new MergedDiscoveryRequest(mScanMode, mScanTypes, mActions, mScanFilters,
+ mMediums);
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/managers/registration/BinderListenerRegistration.java b/nearby/service/java/com/android/server/nearby/managers/registration/BinderListenerRegistration.java
new file mode 100644
index 0000000..4aaa08f
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/registration/BinderListenerRegistration.java
@@ -0,0 +1,208 @@
+/*
+ * 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.nearby.managers.registration;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.Nullable;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.server.nearby.managers.ListenerMultiplexer;
+
+import java.util.NoSuchElementException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A listener registration object which holds data associated with the listener, such as an optional
+ * request, and an executor responsible for listener invocations. Key is the IBinder.
+ *
+ * @param <TListener> listener for the callback
+ */
+public abstract class BinderListenerRegistration<TListener> implements IBinder.DeathRecipient {
+
+ private final AtomicBoolean mRemoved = new AtomicBoolean(false);
+ private final Executor mExecutor;
+ private final Object mListenerLock = new Object();
+ @Nullable
+ TListener mListener;
+ @Nullable
+ private final IBinder mKey;
+
+ public BinderListenerRegistration(IBinder key, Executor executor, TListener listener) {
+ this.mKey = key;
+ this.mExecutor = executor;
+ this.mListener = listener;
+ }
+
+ /**
+ * Must be implemented to return the
+ * {@link com.android.server.nearby.managers.ListenerMultiplexer} this registration is
+ * registered
+ * with. Often this is easiest to accomplish by defining registration subclasses as non-static
+ * inner classes of the multiplexer they are to be used with.
+ */
+ public abstract ListenerMultiplexer<TListener, ?
+ extends BinderListenerRegistration<TListener>, ?> getOwner();
+
+ public final IBinder getBinder() {
+ return mKey;
+ }
+
+ public final Executor getExecutor() {
+ return mExecutor;
+ }
+
+ /**
+ * Called when the registration is put in the Multiplexer.
+ */
+ public void onRegister() {
+ try {
+ getBinder().linkToDeath(this, 0);
+ } catch (RemoteException e) {
+ remove();
+ }
+ }
+
+ /**
+ * Called when the registration is removed in the Multiplexer.
+ */
+ public void onUnregister() {
+ this.mListener = null;
+ try {
+ getBinder().unlinkToDeath(this, 0);
+ } catch (NoSuchElementException e) {
+ Log.w(TAG, "failed to unregister binder death listener", e);
+ }
+ }
+
+ /**
+ * Removes this registration. All pending listener invocations will fail.
+ *
+ * <p>Does nothing if invoked before {@link #onRegister()} or after {@link #onUnregister()}.
+ */
+ public final void remove() {
+ IBinder key = mKey;
+ if (key != null && !mRemoved.getAndSet(true)) {
+ getOwner().removeRegistration(key);
+ }
+ }
+
+ @Override
+ public void binderDied() {
+ remove();
+ }
+
+ /**
+ * May be overridden by subclasses to handle listener operation failures. The default behavior
+ * is
+ * to further propagate any exceptions. Will always be invoked on the executor thread.
+ */
+ protected void onOperationFailure(Exception exception) {
+ throw new AssertionError(exception);
+ }
+
+ /**
+ * Executes the given listener operation on the registration executor, invoking {@link
+ * #onOperationFailure(Exception)} in case the listener operation fails. If the registration is
+ * removed prior to the operation running, the operation is considered canceled. If a null
+ * operation is supplied, nothing happens.
+ */
+ public final void executeOperation(@Nullable ListenerOperation<TListener> operation) {
+ if (operation == null) {
+ return;
+ }
+
+ synchronized (mListenerLock) {
+ if (mListener == null) {
+ return;
+ }
+
+ AtomicBoolean complete = new AtomicBoolean(false);
+ mExecutor.execute(() -> {
+ TListener listener;
+ synchronized (mListenerLock) {
+ listener = mListener;
+ }
+
+ Exception failure = null;
+ if (listener != null) {
+ try {
+ operation.operate(listener);
+ } catch (Exception e) {
+ if (e instanceof RuntimeException) {
+ throw (RuntimeException) e;
+ } else {
+ failure = e;
+ }
+ }
+ }
+
+ operation.onComplete(failure == null);
+ complete.set(true);
+
+ if (failure != null) {
+ onOperationFailure(failure);
+ }
+ });
+ operation.onScheduled(complete.get());
+ }
+ }
+
+ /**
+ * An listener operation to perform.
+ *
+ * @param <ListenerT> listener type
+ */
+ public interface ListenerOperation<ListenerT> {
+
+ /**
+ * Invoked after the operation has been scheduled for execution. The {@code complete}
+ * argument
+ * will be true if {@link #onComplete(boolean)} was invoked prior to this callback (such as
+ * if
+ * using a direct executor), or false if {@link #onComplete(boolean)} will be invoked after
+ * this
+ * callback. This method is always invoked on the calling thread.
+ */
+ default void onScheduled(boolean complete) {
+ }
+
+ /**
+ * Invoked to perform an operation on the given listener. This method is always invoked on
+ * the
+ * executor thread. If this method throws a checked exception, the operation will fail and
+ * result in {@link #onOperationFailure(Exception)} being invoked. If this method throws an
+ * unchecked exception, this propagates normally and should result in a crash.
+ */
+ void operate(ListenerT listener) throws Exception;
+
+ /**
+ * Invoked after the operation is complete. The {@code success} argument will be true if
+ * the
+ * operation completed without throwing any exceptions, and false otherwise (such as if the
+ * operation was canceled prior to executing, or if it threw an exception). This invocation
+ * may
+ * happen either before or after (but never during) the invocation of {@link
+ * #onScheduled(boolean)}. This method is always invoked on the executor thread.
+ */
+ default void onComplete(boolean success) {
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/managers/registration/DiscoveryRegistration.java b/nearby/service/java/com/android/server/nearby/managers/registration/DiscoveryRegistration.java
new file mode 100644
index 0000000..1cb70ac
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/registration/DiscoveryRegistration.java
@@ -0,0 +1,358 @@
+/*
+ * 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.nearby.managers.registration;
+
+import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.app.AppOpsManager;
+import android.nearby.IScanListener;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.PresenceScanFilter;
+import android.nearby.ScanCallback;
+import android.nearby.ScanFilter;
+import android.nearby.ScanRequest;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.common.CancelableAlarm;
+import com.android.server.nearby.managers.ListenerMultiplexer;
+import com.android.server.nearby.managers.MergedDiscoveryRequest;
+import com.android.server.nearby.presence.PresenceDiscoveryResult;
+import com.android.server.nearby.util.identity.CallerIdentity;
+import com.android.server.nearby.util.permissions.DiscoveryPermissions;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.stream.Collectors;
+
+/**
+ * Class responsible for all client based operations. Each {@link DiscoveryRegistration} is for one
+ * valid unique {@link android.nearby.NearbyManager#startScan(ScanRequest, Executor, ScanCallback)}
+ */
+public class DiscoveryRegistration extends BinderListenerRegistration<IScanListener> {
+
+ /**
+ * Timeout before a previous discovered device is reported as lost.
+ */
+ @VisibleForTesting
+ static final int ON_LOST_TIME_OUT_MS = 10000;
+ /** Lock for registration operations. */
+ final Object mMultiplexerLock;
+ private final ListenerMultiplexer<IScanListener, DiscoveryRegistration, MergedDiscoveryRequest>
+ mOwner;
+ private final AppOpsManager mAppOpsManager;
+ /** Presence devices that are currently discovered, and not lost yet. */
+ @GuardedBy("mMultiplexerLock")
+ private final Map<Long, NearbyDeviceParcelable> mDiscoveredDevices;
+ /** A map of deviceId and alarms for reporting device lost. */
+ @GuardedBy("mMultiplexerLock")
+ private final Map<Long, DeviceOnLostAlarm> mDiscoveryOnLostAlarmPerDevice = new ArrayMap<>();
+ /**
+ * The single thread executor to run {@link CancelableAlarm} to report
+ * {@link NearbyDeviceParcelable} on lost after timeout.
+ */
+ private final ScheduledExecutorService mAlarmExecutor =
+ Executors.newSingleThreadScheduledExecutor();
+ private final ScanRequest mScanRequest;
+ private final CallerIdentity mCallerIdentity;
+
+ public DiscoveryRegistration(
+ ListenerMultiplexer<IScanListener, DiscoveryRegistration, MergedDiscoveryRequest> owner,
+ ScanRequest scanRequest, IScanListener scanListener, Executor executor,
+ CallerIdentity callerIdentity, Object multiplexerLock, AppOpsManager appOpsManager) {
+ super(scanListener.asBinder(), executor, scanListener);
+ mOwner = owner;
+ mListener = scanListener;
+ mScanRequest = scanRequest;
+ mCallerIdentity = callerIdentity;
+ mMultiplexerLock = multiplexerLock;
+ mDiscoveredDevices = new ArrayMap<>();
+ mAppOpsManager = appOpsManager;
+ }
+
+ /**
+ * Gets the scan request.
+ */
+ public ScanRequest getScanRequest() {
+ return mScanRequest;
+ }
+
+ /**
+ * Gets the actions from the scan filter(s).
+ */
+ public Set<Integer> getActions() {
+ Set<Integer> result = new ArraySet<>();
+ List<ScanFilter> filters = mScanRequest.getScanFilters();
+ for (ScanFilter filter : filters) {
+ if (filter instanceof PresenceScanFilter) {
+ result.addAll(((PresenceScanFilter) filter).getPresenceActions());
+ }
+ }
+ return ImmutableSet.copyOf(result);
+ }
+
+ /**
+ * Gets all the filters that are for Nearby Presence.
+ */
+ public Set<ScanFilter> getPresenceScanFilters() {
+ Set<ScanFilter> result = new ArraySet<>();
+ List<ScanFilter> filters = mScanRequest.getScanFilters();
+ for (ScanFilter filter : filters) {
+ if (filter.getType() == SCAN_TYPE_NEARBY_PRESENCE) {
+ result.add(filter);
+ }
+ }
+ return ImmutableSet.copyOf(result);
+ }
+
+ @VisibleForTesting
+ Map<Long, DeviceOnLostAlarm> getDiscoveryOnLostAlarms() {
+ synchronized (mMultiplexerLock) {
+ return mDiscoveryOnLostAlarmPerDevice;
+ }
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof DiscoveryRegistration) {
+ DiscoveryRegistration otherRegistration = (DiscoveryRegistration) other;
+ return Objects.equals(mScanRequest, otherRegistration.mScanRequest) && Objects.equals(
+ mListener, otherRegistration.mListener);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mListener, mScanRequest);
+ }
+
+ @Override
+ public ListenerMultiplexer<
+ IScanListener, DiscoveryRegistration, MergedDiscoveryRequest> getOwner() {
+ return mOwner;
+ }
+
+ @VisibleForTesting
+ ListenerOperation<IScanListener> reportDeviceLost(NearbyDeviceParcelable device) {
+ long deviceId = device.getDeviceId();
+ return reportResult(DiscoveryResult.DEVICE_LOST, device, () -> {
+ synchronized (mMultiplexerLock) {
+ // Remove the device from reporting devices after reporting lost.
+ mDiscoveredDevices.remove(deviceId);
+ DeviceOnLostAlarm alarm = mDiscoveryOnLostAlarmPerDevice.remove(deviceId);
+ if (alarm != null) {
+ alarm.cancel();
+ }
+ }
+ });
+ }
+
+ /**
+ * Called when there is device discovered from the server.
+ */
+ public ListenerOperation<IScanListener> onNearbyDeviceDiscovered(
+ NearbyDeviceParcelable device) {
+ if (!filterCheck(device)) {
+ Log.d(TAG, "presence filter does not match for the scanned Presence Device");
+ return null;
+ }
+ synchronized (mMultiplexerLock) {
+ long deviceId = device.getDeviceId();
+ boolean deviceReported = mDiscoveredDevices.containsKey(deviceId);
+ scheduleOnLostAlarm(device);
+ if (deviceReported) {
+ NearbyDeviceParcelable oldDevice = mDiscoveredDevices.get(deviceId);
+ if (device.equals(oldDevice)) {
+ return null;
+ }
+ return reportUpdated(device);
+ }
+ return reportDiscovered(device);
+ }
+ }
+
+ @VisibleForTesting
+ static boolean presenceFilterMatches(NearbyDeviceParcelable device,
+ List<ScanFilter> scanFilters) {
+ if (scanFilters.isEmpty()) {
+ return true;
+ }
+ PresenceDiscoveryResult discoveryResult = PresenceDiscoveryResult.fromDevice(device);
+ for (ScanFilter scanFilter : scanFilters) {
+ PresenceScanFilter presenceScanFilter = (PresenceScanFilter) scanFilter;
+ if (discoveryResult.matches(presenceScanFilter)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Nullable
+ ListenerOperation<IScanListener> reportDiscovered(NearbyDeviceParcelable device) {
+ long deviceId = device.getDeviceId();
+ return reportResult(DiscoveryResult.DEVICE_DISCOVERED, device, () -> {
+ synchronized (mMultiplexerLock) {
+ // Add the device to discovered devices after reporting device is
+ // discovered.
+ mDiscoveredDevices.put(deviceId, device);
+ scheduleOnLostAlarm(device);
+ }
+ });
+ }
+
+ @Nullable
+ ListenerOperation<IScanListener> reportUpdated(NearbyDeviceParcelable device) {
+ long deviceId = device.getDeviceId();
+ return reportResult(DiscoveryResult.DEVICE_UPDATED, device, () -> {
+ synchronized (mMultiplexerLock) {
+ // Update the new device to discovered devices after reporting device is
+ // discovered.
+ mDiscoveredDevices.put(deviceId, device);
+ scheduleOnLostAlarm(device);
+ }
+ });
+
+ }
+
+ @Nullable
+ ListenerOperation<IScanListener> reportResult(@DiscoveryResult int result,
+ NearbyDeviceParcelable device, @Nullable Runnable successReportCallback) {
+ // Report the operation to AppOps.
+ // NOTE: AppOps report has to be the last operation before delivering the result. Otherwise
+ // we may over-report when the discovery result doesn't end up being delivered.
+ if (!checkIdentity()) {
+ return listener -> listener.onError(ScanCallback.ERROR_PERMISSION_DENIED);
+ }
+
+ return new ListenerOperation<>() {
+
+ @Override
+ public void operate(IScanListener listener) throws Exception {
+ switch (result) {
+ case DiscoveryResult.DEVICE_DISCOVERED:
+ listener.onDiscovered(device);
+ break;
+ case DiscoveryResult.DEVICE_UPDATED:
+ listener.onUpdated(device);
+ break;
+ case DiscoveryResult.DEVICE_LOST:
+ listener.onLost(device);
+ break;
+ }
+ }
+
+ @Override
+ public void onComplete(boolean success) {
+ if (success) {
+ if (successReportCallback != null) {
+ successReportCallback.run();
+ Log.d(TAG, "Successfully delivered result to caller.");
+ }
+ }
+ }
+ };
+ }
+
+ private boolean filterCheck(NearbyDeviceParcelable device) {
+ if (device.getScanType() != SCAN_TYPE_NEARBY_PRESENCE) {
+ return true;
+ }
+ List<ScanFilter> presenceFilters = mScanRequest.getScanFilters().stream().filter(
+ scanFilter -> scanFilter.getType() == SCAN_TYPE_NEARBY_PRESENCE).collect(
+ Collectors.toList());
+ return presenceFilterMatches(device, presenceFilters);
+ }
+
+ private boolean checkIdentity() {
+ boolean result = DiscoveryPermissions.noteDiscoveryResultDelivery(mAppOpsManager,
+ mCallerIdentity);
+ Log.v(TAG, "checkIdentity: result is " + result + " mAppOpsManager " + mAppOpsManager);
+ if (!result) {
+ Log.w(TAG, "[DiscoveryProviderManager] scan permission revoked "
+ + "- not forwarding results for the registration.");
+ }
+ return result;
+ }
+
+ @GuardedBy("mMultiplexerLock")
+ private void scheduleOnLostAlarm(NearbyDeviceParcelable device) {
+ long deviceId = device.getDeviceId();
+ DeviceOnLostAlarm alarm = mDiscoveryOnLostAlarmPerDevice.get(deviceId);
+ if (alarm == null) {
+ alarm = new DeviceOnLostAlarm(device, mAlarmExecutor);
+ mDiscoveryOnLostAlarmPerDevice.put(deviceId, alarm);
+ }
+ alarm.start();
+ Log.d(TAG, "DiscoveryProviderManager updated state for " + device.getDeviceId());
+ }
+
+ /** Status of the discovery result. */
+ @IntDef({DiscoveryResult.DEVICE_DISCOVERED, DiscoveryResult.DEVICE_UPDATED,
+ DiscoveryResult.DEVICE_LOST})
+ public @interface DiscoveryResult {
+ int DEVICE_DISCOVERED = 0;
+ int DEVICE_UPDATED = 1;
+ int DEVICE_LOST = 2;
+ }
+
+ private class DeviceOnLostAlarm {
+
+ private static final String NAME = "DeviceOnLostAlarm";
+ private final NearbyDeviceParcelable mDevice;
+ private final ScheduledExecutorService mAlarmExecutor;
+ @Nullable
+ private CancelableAlarm mTimeoutAlarm;
+
+ DeviceOnLostAlarm(NearbyDeviceParcelable device, ScheduledExecutorService alarmExecutor) {
+ mDevice = device;
+ mAlarmExecutor = alarmExecutor;
+ }
+
+ synchronized void start() {
+ cancel();
+ this.mTimeoutAlarm = CancelableAlarm.createSingleAlarm(NAME, () -> {
+ Log.d(TAG, String.format("%s timed out after %d ms. Reporting %s on lost.", NAME,
+ ON_LOST_TIME_OUT_MS, mDevice.getName()));
+ synchronized (mMultiplexerLock) {
+ executeOperation(reportDeviceLost(mDevice));
+ }
+ }, ON_LOST_TIME_OUT_MS, mAlarmExecutor);
+ }
+
+ synchronized void cancel() {
+ if (mTimeoutAlarm != null) {
+ mTimeoutAlarm.cancel();
+ mTimeoutAlarm = null;
+ }
+ }
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/CancelableAlarmTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/CancelableAlarmTest.java
index c7d3e25..719e816 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/common/CancelableAlarmTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/CancelableAlarmTest.java
@@ -80,14 +80,14 @@
@Test
public void cancelOfRunAlarmReturnsFalse() throws InterruptedException {
TestCountDownLatch latch = new TestCountDownLatch(1);
- long delayMillis = 1000;
+ long delayMillis = 500;
CancelableAlarm alarm =
CancelableAlarm.createSingleAlarm(
"cancelOfRunAlarmReturnsFalse",
new CountDownRunnable(latch),
delayMillis,
mExecutor);
- latch.awaitAndExpectDelay(delayMillis);
+ latch.awaitAndExpectDelay(delayMillis - 1);
assertThat(alarm.cancel()).isFalse();
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/ListenerMultiplexerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/ListenerMultiplexerTest.java
new file mode 100644
index 0000000..104d762
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/ListenerMultiplexerTest.java
@@ -0,0 +1,302 @@
+/*
+ * 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.nearby.managers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.os.IBinder;
+
+import androidx.annotation.NonNull;
+
+import com.android.server.nearby.managers.registration.BinderListenerRegistration;
+
+import com.google.common.util.concurrent.MoreExecutors;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Collection;
+
+public class ListenerMultiplexerTest {
+
+ @Before
+ public void setUp() {
+ initMocks(this);
+ }
+
+ @Test
+ public void testAdd() {
+ TestMultiplexer multiplexer = new TestMultiplexer();
+
+ Runnable listener = mock(Runnable.class);
+ IBinder binder = mock(IBinder.class);
+ int value = 2;
+ multiplexer.addListener(binder, listener, value);
+
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeOperationCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(1);
+ assertThat(multiplexer.mMerged).isEqualTo(value);
+ }
+ Runnable listener2 = mock(Runnable.class);
+ IBinder binder2 = mock(IBinder.class);
+ int value2 = 1;
+ multiplexer.addListener(binder2, listener2, value2);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeOperationCount).isEqualTo(2);
+ assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(1);
+ assertThat(multiplexer.mMerged).isEqualTo(value);
+ }
+ }
+
+ @Test
+ public void testReplace() {
+ TestMultiplexer multiplexer = new TestMultiplexer();
+ Runnable listener = mock(Runnable.class);
+ IBinder binder = mock(IBinder.class);
+ int value = 2;
+ multiplexer.addListener(binder, listener, value);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMerged).isEqualTo(value);
+ }
+ multiplexer.notifyListeners();
+ verify(listener, times(1)).run();
+ reset(listener);
+
+ // Same key, different value
+ Runnable listener2 = mock(Runnable.class);
+ int value2 = 1;
+ multiplexer.addListener(binder, listener2, value2);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ // Should not be called again
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mOnUnregisterCalledCount).isEqualTo(0);
+ assertThat(multiplexer.mMerged).isEqualTo(value2);
+ }
+ // Run on the new listener
+ multiplexer.notifyListeners();
+ verify(listener, never()).run();
+ verify(listener2, times(1)).run();
+
+ multiplexer.removeRegistration(binder);
+
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isFalse();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mOnUnregisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMerged).isEqualTo(Integer.MIN_VALUE);
+ }
+ }
+
+ @Test
+ public void testRemove() {
+ TestMultiplexer multiplexer = new TestMultiplexer();
+ Runnable listener = mock(Runnable.class);
+ IBinder binder = mock(IBinder.class);
+ int value = 2;
+ multiplexer.addListener(binder, listener, value);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ assertThat(multiplexer.mMerged).isEqualTo(value);
+ }
+ multiplexer.notifyListeners();
+ verify(listener, times(1)).run();
+ reset(listener);
+
+ multiplexer.removeRegistration(binder);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isFalse();
+ assertThat(multiplexer.mMerged).isEqualTo(Integer.MIN_VALUE);
+ }
+ multiplexer.notifyListeners();
+ verify(listener, never()).run();
+ }
+
+ @Test
+ public void testMergeMultiple() {
+ TestMultiplexer multiplexer = new TestMultiplexer();
+
+ Runnable listener = mock(Runnable.class);
+ IBinder binder = mock(IBinder.class);
+ int value = 2;
+
+ Runnable listener2 = mock(Runnable.class);
+ IBinder binder2 = mock(IBinder.class);
+ int value2 = 1;
+
+ Runnable listener3 = mock(Runnable.class);
+ IBinder binder3 = mock(IBinder.class);
+ int value3 = 5;
+
+ multiplexer.addListener(binder, listener, value);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeOperationCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(1);
+ assertThat(multiplexer.mMerged).isEqualTo(value);
+ }
+ multiplexer.notifyListeners();
+ verify(listener, times(1)).run();
+ verify(listener2, never()).run();
+ verify(listener3, never()).run();
+
+ multiplexer.addListener(binder2, listener2, value2);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeOperationCount).isEqualTo(2);
+ assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(1);
+ assertThat(multiplexer.mMerged).isEqualTo(value);
+ }
+ multiplexer.notifyListeners();
+ verify(listener, times(2)).run();
+ verify(listener2, times(1)).run();
+ verify(listener3, never()).run();
+
+ multiplexer.addListener(binder3, listener3, value3);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeOperationCount).isEqualTo(3);
+ assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(2);
+ assertThat(multiplexer.mMerged).isEqualTo(value3);
+ }
+ multiplexer.notifyListeners();
+ verify(listener, times(3)).run();
+ verify(listener2, times(2)).run();
+ verify(listener3, times(1)).run();
+
+ multiplexer.removeRegistration(binder);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeOperationCount).isEqualTo(4);
+ assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(2);
+ assertThat(multiplexer.mMerged).isEqualTo(value3);
+ }
+ multiplexer.notifyListeners();
+ verify(listener, times(3)).run();
+ verify(listener2, times(3)).run();
+ verify(listener3, times(2)).run();
+
+ multiplexer.removeRegistration(binder3);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeOperationCount).isEqualTo(5);
+ assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(3);
+ assertThat(multiplexer.mMerged).isEqualTo(value2);
+ }
+ multiplexer.notifyListeners();
+ verify(listener, times(3)).run();
+ verify(listener2, times(4)).run();
+ verify(listener3, times(2)).run();
+
+ multiplexer.removeRegistration(binder2);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isFalse();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeOperationCount).isEqualTo(6);
+ assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(4);
+ assertThat(multiplexer.mMerged).isEqualTo(Integer.MIN_VALUE);
+ }
+ multiplexer.notifyListeners();
+ verify(listener, times(3)).run();
+ verify(listener2, times(4)).run();
+ verify(listener3, times(2)).run();
+ }
+
+ private class TestMultiplexer extends
+ ListenerMultiplexer<Runnable, TestMultiplexer.TestListenerRegistration, Integer> {
+ int mOnRegisterCalledCount;
+ int mOnUnregisterCalledCount;
+ boolean mRegistered;
+ private int mMergeOperationCount;
+ private int mMergeUpdatedCount;
+
+ @Override
+ public void onRegister() {
+ mOnRegisterCalledCount++;
+ mRegistered = true;
+ }
+
+ @Override
+ public void onUnregister() {
+ mOnUnregisterCalledCount++;
+ mRegistered = false;
+ }
+
+ @Override
+ public Integer mergeRegistrations(
+ @NonNull Collection<TestListenerRegistration> testListenerRegistrations) {
+ mMergeOperationCount++;
+ int max = Integer.MIN_VALUE;
+ for (TestListenerRegistration registration : testListenerRegistrations) {
+ max = Math.max(max, registration.getValue());
+ }
+ return max;
+ }
+
+ @Override
+ public void onMergedRegistrationsUpdated() {
+ mMergeUpdatedCount++;
+ }
+
+ public void addListener(IBinder binder, Runnable runnable, int value) {
+ TestListenerRegistration registration = new TestListenerRegistration(binder, runnable,
+ value);
+ putRegistration(binder, registration);
+ }
+
+ public void notifyListeners() {
+ deliverToListeners(registration -> Runnable::run);
+ }
+
+ private class TestListenerRegistration extends BinderListenerRegistration<Runnable> {
+ private final int mValue;
+
+ protected TestListenerRegistration(IBinder binder, Runnable runnable, int value) {
+ super(binder, MoreExecutors.directExecutor(), runnable);
+ mValue = value;
+ }
+
+ @Override
+ public TestMultiplexer getOwner() {
+ return TestMultiplexer.this;
+ }
+
+ public int getValue() {
+ return mValue;
+ }
+ }
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/MergedDiscoveryRequestTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/MergedDiscoveryRequestTest.java
new file mode 100644
index 0000000..9281e42
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/MergedDiscoveryRequestTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.nearby.managers;
+
+import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.nearby.DataElement;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+import android.nearby.ScanFilter;
+import android.nearby.ScanRequest;
+import android.util.ArraySet;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Unit test for {@link MergedDiscoveryRequest} class.
+ */
+public class MergedDiscoveryRequestTest {
+
+ @Test
+ public void test_addScanType() {
+ MergedDiscoveryRequest.Builder builder = new MergedDiscoveryRequest.Builder();
+ builder.addScanType(ScanRequest.SCAN_TYPE_FAST_PAIR);
+ builder.addScanType(ScanRequest.SCAN_TYPE_NEARBY_PRESENCE);
+ MergedDiscoveryRequest request = builder.build();
+
+ assertThat(request.getScanTypes()).isEqualTo(new ArraySet<>(
+ Arrays.asList(ScanRequest.SCAN_TYPE_FAST_PAIR,
+ ScanRequest.SCAN_TYPE_NEARBY_PRESENCE)));
+ }
+
+ @Test
+ public void test_addActions() {
+ MergedDiscoveryRequest.Builder builder = new MergedDiscoveryRequest.Builder();
+ builder.addActions(new ArrayList<>(Arrays.asList(1, 2, 3)));
+ builder.addActions(new ArraySet<>(Arrays.asList(2, 3, 4)));
+ builder.addActions(new ArraySet<>(Collections.singletonList(5)));
+
+ MergedDiscoveryRequest request = builder.build();
+ assertThat(request.getActions()).isEqualTo(new ArraySet<>(new Integer[]{1, 2, 3, 4, 5}));
+ }
+
+ @Test
+ public void test_addFilters() {
+ final int rssi = -40;
+ final int action = 123;
+ final byte[] secreteId = new byte[]{1, 2, 3, 4};
+ final byte[] authenticityKey = new byte[]{0, 1, 1, 1};
+ final byte[] publicKey = new byte[]{1, 1, 2, 2};
+ final byte[] encryptedMetadata = new byte[]{1, 2, 3, 4, 5};
+ final byte[] metadataEncryptionKeyTag = new byte[]{1, 1, 3, 4, 5};
+ final int key = 3;
+ final byte[] value = new byte[]{1, 1, 1, 1};
+
+ PublicCredential mPublicCredential = new PublicCredential.Builder(secreteId,
+ authenticityKey, publicKey, encryptedMetadata,
+ metadataEncryptionKeyTag).setIdentityType(IDENTITY_TYPE_PRIVATE).build();
+ PresenceScanFilter scanFilterBuilder = new PresenceScanFilter.Builder().setMaxPathLoss(
+ rssi).addCredential(mPublicCredential).addPresenceAction(
+ action).addExtendedProperty(new DataElement(key, value)).build();
+
+ MergedDiscoveryRequest.Builder builder = new MergedDiscoveryRequest.Builder();
+ builder.addScanFilters(Collections.singleton(scanFilterBuilder));
+ MergedDiscoveryRequest request = builder.build();
+
+ Set<ScanFilter> expectedResult = new ArraySet<>();
+ expectedResult.add(scanFilterBuilder);
+ assertThat(request.getScanFilters()).isEqualTo(expectedResult);
+ }
+
+ @Test
+ public void test_addMedium() {
+ MergedDiscoveryRequest.Builder builder = new MergedDiscoveryRequest.Builder();
+ builder.addMedium(MergedDiscoveryRequest.Medium.BLE);
+ builder.addMedium(MergedDiscoveryRequest.Medium.BLE);
+ MergedDiscoveryRequest request = builder.build();
+
+ Set<Integer> expectedResult = new ArraySet<>();
+ expectedResult.add(MergedDiscoveryRequest.Medium.BLE);
+ assertThat(request.getMediums()).isEqualTo(expectedResult);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/registration/BinderListenerRegistrationTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/registration/BinderListenerRegistrationTest.java
new file mode 100644
index 0000000..8814190
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/registration/BinderListenerRegistrationTest.java
@@ -0,0 +1,163 @@
+/*
+ * 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.nearby.managers.registration;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import androidx.annotation.NonNull;
+
+import com.android.server.nearby.managers.ListenerMultiplexer;
+
+import com.google.common.util.concurrent.MoreExecutors;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Collection;
+
+/**
+ * Unit test for {@link BinderListenerRegistration} class.
+ */
+public class BinderListenerRegistrationTest {
+ private TestMultiplexer mMultiplexer;
+ private boolean mOnRegisterCalled;
+ private boolean mOnUnRegisterCalled;
+
+ @Before
+ public void setUp() {
+ mMultiplexer = new TestMultiplexer();
+ }
+
+ @Test
+ public void test_addAndRemove() throws RemoteException {
+ Runnable listener = mock(Runnable.class);
+ IBinder binder = mock(IBinder.class);
+ int value = 2;
+ BinderListenerRegistration<Runnable> registration = mMultiplexer.addListener(binder,
+ listener, value);
+ // First element, onRegister should be called
+ assertThat(mOnRegisterCalled).isTrue();
+ verify(binder, times(1)).linkToDeath(any(), anyInt());
+ mMultiplexer.notifyListeners();
+ verify(listener, times(1)).run();
+ synchronized (mMultiplexer.mMultiplexerLock) {
+ assertThat(mMultiplexer.mMerged).isEqualTo(value);
+ }
+ reset(listener);
+
+ Runnable listener2 = mock(Runnable.class);
+ IBinder binder2 = mock(IBinder.class);
+ int value2 = 1;
+ BinderListenerRegistration<Runnable> registration2 = mMultiplexer.addListener(binder2,
+ listener2, value2);
+ verify(binder2, times(1)).linkToDeath(any(), anyInt());
+ mMultiplexer.notifyListeners();
+ verify(listener2, times(1)).run();
+ synchronized (mMultiplexer.mMultiplexerLock) {
+ assertThat(mMultiplexer.mMerged).isEqualTo(value);
+ }
+ reset(listener);
+ reset(listener2);
+
+ registration2.remove();
+ verify(binder2, times(1)).unlinkToDeath(any(), anyInt());
+ // Remove one element, onUnregister should NOT be called
+ assertThat(mOnUnRegisterCalled).isFalse();
+ mMultiplexer.notifyListeners();
+ verify(listener, times(1)).run();
+ synchronized (mMultiplexer.mMultiplexerLock) {
+ assertThat(mMultiplexer.mMerged).isEqualTo(value);
+ }
+ reset(listener);
+ reset(listener2);
+
+ registration.remove();
+ verify(binder, times(1)).unlinkToDeath(any(), anyInt());
+ // Remove all elements, onUnregister should NOT be called
+ assertThat(mOnUnRegisterCalled).isTrue();
+ synchronized (mMultiplexer.mMultiplexerLock) {
+ assertThat(mMultiplexer.mMerged).isEqualTo(Integer.MIN_VALUE);
+ }
+ }
+
+ private class TestMultiplexer extends
+ ListenerMultiplexer<Runnable, TestMultiplexer.TestListenerRegistration, Integer> {
+ @Override
+ public void onRegister() {
+ mOnRegisterCalled = true;
+ }
+
+ @Override
+ public void onUnregister() {
+ mOnUnRegisterCalled = true;
+ }
+
+ @Override
+ public Integer mergeRegistrations(
+ @NonNull Collection<TestListenerRegistration> testListenerRegistrations) {
+ int max = Integer.MIN_VALUE;
+ for (TestListenerRegistration registration : testListenerRegistrations) {
+ max = Math.max(max, registration.getValue());
+ }
+ return max;
+ }
+
+ @Override
+ public void onMergedRegistrationsUpdated() {
+ }
+
+ public BinderListenerRegistration<Runnable> addListener(IBinder binder, Runnable runnable,
+ int value) {
+ TestListenerRegistration registration = new TestListenerRegistration(binder, runnable,
+ value);
+ putRegistration(binder, registration);
+ return registration;
+ }
+
+ public void notifyListeners() {
+ deliverToListeners(registration -> Runnable::run);
+ }
+
+ private class TestListenerRegistration extends BinderListenerRegistration<Runnable> {
+ private final int mValue;
+
+ protected TestListenerRegistration(IBinder binder, Runnable runnable, int value) {
+ super(binder, MoreExecutors.directExecutor(), runnable);
+ mValue = value;
+ }
+
+ @Override
+ public TestMultiplexer getOwner() {
+ return TestMultiplexer.this;
+ }
+
+ public int getValue() {
+ return mValue;
+ }
+ }
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/registration/DiscoveryRegistrationTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/registration/DiscoveryRegistrationTest.java
new file mode 100644
index 0000000..03c4f75
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/registration/DiscoveryRegistrationTest.java
@@ -0,0 +1,252 @@
+/*
+ * 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.nearby.managers.registration;
+
+import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.app.AppOpsManager;
+import android.nearby.DataElement;
+import android.nearby.IScanListener;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+import android.nearby.ScanCallback;
+import android.nearby.ScanFilter;
+import android.nearby.ScanRequest;
+import android.os.IBinder;
+import android.util.ArraySet;
+
+import androidx.annotation.NonNull;
+
+import com.android.server.nearby.managers.ListenerMultiplexer;
+import com.android.server.nearby.managers.MergedDiscoveryRequest;
+import com.android.server.nearby.util.identity.CallerIdentity;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executors;
+
+/**
+ * Unit test for {@link DiscoveryRegistration} class.
+ */
+public class DiscoveryRegistrationTest {
+ private static final int RSSI = -40;
+ private static final int ACTION = 123;
+ private static final byte[] SECRETE_ID = new byte[]{1, 2, 3, 4};
+ private static final byte[] AUTHENTICITY_KEY = new byte[]{0, 1, 1, 1};
+ private static final byte[] PUBLIC_KEY = new byte[]{1, 1, 2, 2};
+ private static final byte[] ENCRYPTED_METADATA = new byte[]{1, 2, 3, 4, 5};
+ private static final byte[] METADATA_ENCRYPTION_KEY_TAG = new byte[]{1, 1, 3, 4, 5};
+ private static final int KEY = 3;
+ private static final byte[] VALUE = new byte[]{1, 1, 1, 1};
+ private final PublicCredential mPublicCredential = new PublicCredential.Builder(SECRETE_ID,
+ AUTHENTICITY_KEY, PUBLIC_KEY, ENCRYPTED_METADATA,
+ METADATA_ENCRYPTION_KEY_TAG).setIdentityType(IDENTITY_TYPE_PRIVATE).build();
+ private final PresenceScanFilter mFilter = new PresenceScanFilter.Builder().setMaxPathLoss(
+ 50).addCredential(mPublicCredential).addPresenceAction(ACTION).addExtendedProperty(
+ new DataElement(KEY, VALUE)).build();
+ private DiscoveryRegistration mDiscoveryRegistration;
+ private ScanRequest mScanRequest;
+ private TestDiscoveryManager mOwner;
+ private Object mMultiplexLock;
+ @Mock
+ private IScanListener mCallback;
+ @Mock
+ private CallerIdentity mIdentity;
+ @Mock
+ private AppOpsManager mAppOpsManager;
+ @Mock
+ private IBinder mBinder;
+
+ @Before
+ public void setUp() {
+ initMocks(this);
+ when(mCallback.asBinder()).thenReturn(mBinder);
+ when(mAppOpsManager.noteOp(eq("android:bluetooth_scan"), eq(0), eq(null), eq(null),
+ eq(null))).thenReturn(AppOpsManager.MODE_ALLOWED);
+
+ mOwner = new TestDiscoveryManager();
+ mMultiplexLock = new Object();
+ mScanRequest = new ScanRequest.Builder().setScanType(
+ ScanRequest.SCAN_TYPE_NEARBY_PRESENCE).addScanFilter(mFilter).build();
+ mDiscoveryRegistration = new DiscoveryRegistration(mOwner, mScanRequest, mCallback,
+ Executors.newSingleThreadExecutor(), mIdentity, mMultiplexLock, mAppOpsManager);
+ }
+
+ @Test
+ public void test_getScanRequest() {
+ assertThat(mDiscoveryRegistration.getScanRequest()).isEqualTo(mScanRequest);
+ }
+
+ @Test
+ public void test_getActions() {
+ Set<Integer> result = new ArraySet<>();
+ result.add(ACTION);
+ assertThat(mDiscoveryRegistration.getActions()).isEqualTo(result);
+ }
+
+ @Test
+ public void test_getOwner() {
+ assertThat(mDiscoveryRegistration.getOwner()).isEqualTo(mOwner);
+ }
+
+ @Test
+ public void test_getPresenceScanFilters() {
+ Set<ScanFilter> result = new ArraySet<>();
+ result.add(mFilter);
+ assertThat(mDiscoveryRegistration.getPresenceScanFilters()).isEqualTo(result);
+ }
+
+ @Test
+ public void test_presenceFilterMatches_match() {
+ NearbyDeviceParcelable device = new NearbyDeviceParcelable.Builder().setDeviceId(
+ 123).setName("test").setTxPower(RSSI + 1).setRssi(RSSI).setScanType(
+ ScanRequest.SCAN_TYPE_NEARBY_PRESENCE).setAction(ACTION).setEncryptionKeyTag(
+ METADATA_ENCRYPTION_KEY_TAG).build();
+ assertThat(DiscoveryRegistration.presenceFilterMatches(device, List.of(mFilter))).isTrue();
+ }
+
+ @Test
+ public void test_presenceFilterMatches_emptyFilter() {
+ NearbyDeviceParcelable device = new NearbyDeviceParcelable.Builder().setDeviceId(
+ 123).setName("test").setScanType(ScanRequest.SCAN_TYPE_NEARBY_PRESENCE).build();
+ assertThat(DiscoveryRegistration.presenceFilterMatches(device, List.of())).isTrue();
+ }
+
+ @Test
+ public void test_presenceFilterMatches_actionNotMatch() {
+ NearbyDeviceParcelable device = new NearbyDeviceParcelable.Builder().setDeviceId(
+ 12).setName("test").setRssi(RSSI).setScanType(
+ ScanRequest.SCAN_TYPE_NEARBY_PRESENCE).setAction(5).setEncryptionKeyTag(
+ METADATA_ENCRYPTION_KEY_TAG).build();
+ assertThat(DiscoveryRegistration.presenceFilterMatches(device, List.of(mFilter))).isFalse();
+ }
+
+ @Test
+ public void test_onDiscoveredOnUpdatedCalled() throws Exception {
+ final long deviceId = 122;
+ NearbyDeviceParcelable.Builder builder = new NearbyDeviceParcelable.Builder().setDeviceId(
+ deviceId).setName("test").setTxPower(RSSI + 1).setRssi(RSSI).setScanType(
+ ScanRequest.SCAN_TYPE_NEARBY_PRESENCE).setAction(ACTION).setEncryptionKeyTag(
+ METADATA_ENCRYPTION_KEY_TAG);
+ runOperation(mDiscoveryRegistration.onNearbyDeviceDiscovered(builder.build()));
+
+ verify(mCallback, times(1)).onDiscovered(eq(builder.build()));
+ verify(mCallback, never()).onUpdated(any());
+ verify(mCallback, never()).onLost(any());
+ verify(mCallback, never()).onError(anyInt());
+ assertThat(mDiscoveryRegistration.getDiscoveryOnLostAlarms().get(deviceId)).isNotNull();
+ reset(mCallback);
+
+ // Update RSSI
+ runOperation(
+ mDiscoveryRegistration.onNearbyDeviceDiscovered(builder.setRssi(RSSI - 1).build()));
+ verify(mCallback, never()).onDiscovered(any());
+ verify(mCallback, times(1)).onUpdated(eq(builder.build()));
+ verify(mCallback, never()).onLost(any());
+ verify(mCallback, never()).onError(anyInt());
+ assertThat(mDiscoveryRegistration.getDiscoveryOnLostAlarms().get(deviceId)).isNotNull();
+ }
+
+ @Test
+ public void test_onLost() throws Exception {
+ final long deviceId = 123;
+ NearbyDeviceParcelable device = new NearbyDeviceParcelable.Builder().setDeviceId(
+ deviceId).setName("test").setTxPower(RSSI + 1).setRssi(RSSI).setScanType(
+ ScanRequest.SCAN_TYPE_NEARBY_PRESENCE).setAction(ACTION).setEncryptionKeyTag(
+ METADATA_ENCRYPTION_KEY_TAG).build();
+ runOperation(mDiscoveryRegistration.onNearbyDeviceDiscovered(device));
+ assertThat(mDiscoveryRegistration.getDiscoveryOnLostAlarms().get(deviceId)).isNotNull();
+ verify(mCallback, times(1)).onDiscovered(eq(device));
+ verify(mCallback, never()).onUpdated(any());
+ verify(mCallback, never()).onError(anyInt());
+ verify(mCallback, never()).onLost(any());
+ reset(mCallback);
+
+ runOperation(mDiscoveryRegistration.reportDeviceLost(device));
+
+ assertThat(mDiscoveryRegistration.getDiscoveryOnLostAlarms().get(deviceId)).isNull();
+ verify(mCallback, never()).onDiscovered(eq(device));
+ verify(mCallback, never()).onUpdated(any());
+ verify(mCallback, never()).onError(anyInt());
+ verify(mCallback, times(1)).onLost(eq(device));
+ }
+
+ @Test
+ public void test_onError() throws Exception {
+ AppOpsManager manager = mock(AppOpsManager.class);
+ when(manager.noteOp(eq("android:bluetooth_scan"), eq(0), eq(null), eq(null),
+ eq(null))).thenReturn(AppOpsManager.MODE_IGNORED);
+
+ DiscoveryRegistration r = new DiscoveryRegistration(mOwner, mScanRequest, mCallback,
+ Executors.newSingleThreadExecutor(), mIdentity, mMultiplexLock, manager);
+
+ NearbyDeviceParcelable device = new NearbyDeviceParcelable.Builder().setDeviceId(
+ 123).setName("test").setTxPower(RSSI + 1).setRssi(RSSI).setScanType(
+ ScanRequest.SCAN_TYPE_NEARBY_PRESENCE).setAction(ACTION).setEncryptionKeyTag(
+ METADATA_ENCRYPTION_KEY_TAG).build();
+ runOperation(r.onNearbyDeviceDiscovered(device));
+
+ verify(mCallback, never()).onDiscovered(any());
+ verify(mCallback, never()).onUpdated(any());
+ verify(mCallback, never()).onLost(any());
+ verify(mCallback, times(1)).onError(eq(ScanCallback.ERROR_PERMISSION_DENIED));
+ }
+
+ private void runOperation(BinderListenerRegistration.ListenerOperation<IScanListener> operation)
+ throws Exception {
+ if (operation == null) {
+ return;
+ }
+ operation.onScheduled(false);
+ operation.operate(mCallback);
+ operation.onComplete(/* success= */ true);
+ }
+
+ private static class TestDiscoveryManager extends
+ ListenerMultiplexer<IScanListener, DiscoveryRegistration, MergedDiscoveryRequest> {
+
+ @Override
+ public MergedDiscoveryRequest mergeRegistrations(
+ @NonNull Collection<DiscoveryRegistration> discoveryRegistrations) {
+ return null;
+ }
+
+ @Override
+ public void onMergedRegistrationsUpdated() {
+
+ }
+ }
+}