Merge changes from topic "tc_change_23050307_56" into udc-dev-plus-aosp
* changes:
[automerged blank] Import translations. DO NOT MERGE ANYWHERE 2p: 8ecc8e79bf 2p: 315d19d852
[automerged blank] Import translations. DO NOT MERGE ANYWHERE 2p: 8ecc8e79bf
Import translations. DO NOT MERGE ANYWHERE
diff --git a/Tethering/tests/integration/base/android/net/TetheringTester.java b/Tethering/tests/integration/base/android/net/TetheringTester.java
index ae39b24..1c0803e 100644
--- a/Tethering/tests/integration/base/android/net/TetheringTester.java
+++ b/Tethering/tests/integration/base/android/net/TetheringTester.java
@@ -628,7 +628,7 @@
return false;
}
- private void sendUploadPacket(ByteBuffer packet) throws Exception {
+ public void sendUploadPacket(ByteBuffer packet) throws Exception {
mDownstreamReader.sendResponse(packet);
}
@@ -680,4 +680,12 @@
return verifyPacketNotNull("Download fail", getDownloadPacket(filter));
}
+
+ // Send DHCPDISCOVER to DHCP server to see if DHCP server is still alive to handle
+ // the upcoming DHCP packets. This method should be only used when we know the DHCP
+ // server has been created successfully before.
+ public boolean testDhcpServerAlive(final MacAddress mac) throws Exception {
+ sendDhcpDiscover(mac.toByteArray());
+ return getNextDhcpPacket() != null;
+ }
}
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index 55854e2..21927df 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -39,6 +39,7 @@
import static com.android.net.module.util.NetworkStackConstants.IPV4_LENGTH_OFFSET;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -839,4 +840,41 @@
REMOTE_NAT64_ADDR /* downloadSrcIp */, clatIp6 /* downloadDstIp */,
tester, true /* isClat */);
}
+
+ private static final byte[] ZeroLengthDhcpPacket = new byte[] {
+ // scapy.Ether(
+ // dst="ff:ff:ff:ff:ff:ff")
+ // scapy.IP(
+ // dst="255.255.255.255")
+ // scapy.UDP(sport=68, dport=67)
+ /* Ethernet Header */
+ (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+ (byte) 0xe0, (byte) 0x4f, (byte) 0x43, (byte) 0xe6, (byte) 0xfb, (byte) 0xd2,
+ (byte) 0x08, (byte) 0x00,
+ /* Ip header */
+ (byte) 0x45, (byte) 0x00, (byte) 0x00, (byte) 0x1c, (byte) 0x00, (byte) 0x01,
+ (byte) 0x00, (byte) 0x00, (byte) 0x40, (byte) 0x11, (byte) 0xb6, (byte) 0x58,
+ (byte) 0x64, (byte) 0x4f, (byte) 0x60, (byte) 0x29, (byte) 0xff, (byte) 0xff,
+ (byte) 0xff, (byte) 0xff,
+ /* UDP header */
+ (byte) 0x00, (byte) 0x44, (byte) 0x00, (byte) 0x43,
+ (byte) 0x00, (byte) 0x08, (byte) 0x3a, (byte) 0xdf
+ };
+
+ @Test
+ public void testTetherZeroLengthDhcpPacket() throws Exception {
+ final TetheringTester tester = initTetheringTester(toList(TEST_IP4_ADDR),
+ toList(TEST_IP4_DNS));
+ tester.createTetheredDevice(TEST_MAC, false /* hasIpv6 */);
+
+ // Send a zero-length DHCP packet to upstream DHCP server.
+ final ByteBuffer packet = ByteBuffer.wrap(ZeroLengthDhcpPacket);
+ tester.sendUploadPacket(packet);
+
+ // Send DHCPDISCOVER packet from another downstream tethered device to verify that upstream
+ // DHCP server has closed the listening socket and stopped reading, then we will not receive
+ // any DHCPOFFER in this case.
+ final MacAddress macAddress = MacAddress.fromString("11:22:33:44:55:66");
+ assertFalse(tester.testDhcpServerAlive(macAddress));
+ }
}
diff --git a/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java b/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
index 07e6f0c..0c10af2 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
@@ -22,6 +22,7 @@
import androidx.annotation.NonNull;
import com.android.internal.annotations.GuardedBy;
+import com.android.server.nearby.managers.DiscoveryProviderManager;
import java.util.concurrent.Executors;
@@ -46,6 +47,12 @@
*/
public static final String NEARBY_SUPPORT_TEST_APP = "nearby_support_test_app";
+ /**
+ * Flag to control which version of DiscoveryProviderManager should be used.
+ */
+ public static final String NEARBY_REFACTOR_DISCOVERY_MANAGER =
+ "nearby_refactor_discovery_manager";
+
private static final boolean IS_USER_BUILD = "user".equals(Build.TYPE);
private final DeviceConfigListener mDeviceConfigListener = new DeviceConfigListener();
@@ -57,11 +64,27 @@
private int mNanoAppMinVersion;
@GuardedBy("mDeviceConfigLock")
private boolean mSupportTestApp;
+ @GuardedBy("mDeviceConfigLock")
+ private boolean mRefactorDiscoveryManager;
public NearbyConfiguration() {
mDeviceConfigListener.start();
}
+ private static boolean getDeviceConfigBoolean(final String name, final boolean defaultValue) {
+ final String value = getDeviceConfigProperty(name);
+ return value != null ? Boolean.parseBoolean(value) : defaultValue;
+ }
+
+ private static int getDeviceConfigInt(final String name, final int defaultValue) {
+ final String value = getDeviceConfigProperty(name);
+ return value != null ? Integer.parseInt(value) : defaultValue;
+ }
+
+ private static String getDeviceConfigProperty(String name) {
+ return DeviceConfig.getProperty(DeviceConfig.NAMESPACE_NEARBY, name);
+ }
+
/**
* Returns whether broadcasting legacy presence spec is enabled.
*/
@@ -86,6 +109,16 @@
}
}
+ /**
+ * @return {@code true} if use {@link DiscoveryProviderManager} or use
+ * DiscoveryProviderManagerLegacy if {@code false}.
+ */
+ public boolean refactorDiscoveryManager() {
+ synchronized (mDeviceConfigLock) {
+ return mRefactorDiscoveryManager;
+ }
+ }
+
private class DeviceConfigListener implements DeviceConfig.OnPropertiesChangedListener {
public void start() {
DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_NEARBY,
@@ -102,21 +135,9 @@
NEARBY_MAINLINE_NANO_APP_MIN_VERSION, 0 /* defaultValue */);
mSupportTestApp = !IS_USER_BUILD && getDeviceConfigBoolean(
NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
+ mRefactorDiscoveryManager = getDeviceConfigBoolean(
+ NEARBY_REFACTOR_DISCOVERY_MANAGER, false /* defaultValue */);
}
}
}
-
- private static boolean getDeviceConfigBoolean(final String name, final boolean defaultValue) {
- final String value = getDeviceConfigProperty(name);
- return value != null ? Boolean.parseBoolean(value) : defaultValue;
- }
-
- private static int getDeviceConfigInt(final String name, final int defaultValue) {
- final String value = getDeviceConfigProperty(name);
- return value != null ? Integer.parseInt(value) : defaultValue;
- }
-
- private static String getDeviceConfigProperty(String name) {
- return DeviceConfig.getProperty(DeviceConfig.NAMESPACE_NEARBY, name);
- }
}
diff --git a/nearby/service/java/com/android/server/nearby/NearbyService.java b/nearby/service/java/com/android/server/nearby/NearbyService.java
index 8d35e08..2ac2b00 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyService.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyService.java
@@ -45,7 +45,9 @@
import com.android.server.nearby.fastpair.FastPairManager;
import com.android.server.nearby.injector.Injector;
import com.android.server.nearby.managers.BroadcastProviderManager;
+import com.android.server.nearby.managers.DiscoveryManager;
import com.android.server.nearby.managers.DiscoveryProviderManager;
+import com.android.server.nearby.managers.DiscoveryProviderManagerLegacy;
import com.android.server.nearby.presence.PresenceManager;
import com.android.server.nearby.provider.FastPairDataProvider;
import com.android.server.nearby.util.identity.CallerIdentity;
@@ -80,18 +82,21 @@
}
}
};
- private final DiscoveryProviderManager mProviderManager;
+ private final DiscoveryManager mDiscoveryProviderManager;
private final BroadcastProviderManager mBroadcastProviderManager;
public NearbyService(Context context) {
mContext = context;
mInjector = new SystemInjector(context);
- mProviderManager = new DiscoveryProviderManager(context, mInjector);
mBroadcastProviderManager = new BroadcastProviderManager(context, mInjector);
final LocatorContextWrapper lcw = new LocatorContextWrapper(context, null);
mFastPairManager = new FastPairManager(lcw);
mPresenceManager = new PresenceManager(lcw);
mNearbyConfiguration = new NearbyConfiguration();
+ mDiscoveryProviderManager =
+ mNearbyConfiguration.refactorDiscoveryManager()
+ ? new DiscoveryProviderManager(context, mInjector)
+ : new DiscoveryProviderManagerLegacy(context, mInjector);
}
@VisibleForTesting
@@ -108,7 +113,7 @@
CallerIdentity identity = CallerIdentity.fromBinder(mContext, packageName, attributionTag);
DiscoveryPermissions.enforceDiscoveryPermission(mContext, identity);
- return mProviderManager.registerScanListener(scanRequest, listener, identity);
+ return mDiscoveryProviderManager.registerScanListener(scanRequest, listener, identity);
}
@Override
@@ -119,7 +124,7 @@
CallerIdentity identity = CallerIdentity.fromBinder(mContext, packageName, attributionTag);
DiscoveryPermissions.enforceDiscoveryPermission(mContext, identity);
- mProviderManager.unregisterScanListener(listener);
+ mDiscoveryProviderManager.unregisterScanListener(listener);
}
@Override
@@ -147,7 +152,7 @@
@Override
public void queryOffloadCapability(IOffloadCallback callback) {
- mProviderManager.queryOffloadCapability(callback);
+ mDiscoveryProviderManager.queryOffloadCapability(callback);
}
/**
@@ -174,7 +179,7 @@
// Initialize ContextManager for CHRE scan.
((SystemInjector) mInjector).initializeContextHubManager();
}
- mProviderManager.init();
+ mDiscoveryProviderManager.init();
mContext.registerReceiver(
mBluetoothReceiver,
new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
diff --git a/nearby/service/java/com/android/server/nearby/managers/DiscoveryManager.java b/nearby/service/java/com/android/server/nearby/managers/DiscoveryManager.java
new file mode 100644
index 0000000..c9b9a43
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/DiscoveryManager.java
@@ -0,0 +1,49 @@
+/*
+ * 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.nearby.IScanListener;
+import android.nearby.NearbyManager;
+import android.nearby.ScanRequest;
+import android.nearby.aidl.IOffloadCallback;
+
+import com.android.server.nearby.util.identity.CallerIdentity;
+
+/**
+ * Interface added for flagging DiscoveryProviderManager refactor. After the
+ * nearby_refactor_discovery_manager flag is fully rolled out, this can be deleted.
+ */
+public interface DiscoveryManager {
+
+ /**
+ * Registers the listener in the manager and starts scan according to the requested scan mode.
+ */
+ @NearbyManager.ScanStatus
+ int registerScanListener(ScanRequest scanRequest, IScanListener listener,
+ CallerIdentity callerIdentity);
+
+ /**
+ * Unregisters the listener in the manager and adjusts the scan mode if necessary afterwards.
+ */
+ void unregisterScanListener(IScanListener listener);
+
+ /** Query offload capability in a device. */
+ void queryOffloadCapability(IOffloadCallback callback);
+
+ /** Called after boot completed. */
+ void init();
+}
diff --git a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java
index 29b18f3..4cda93a 100644
--- a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java
+++ b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java
@@ -21,320 +21,74 @@
import static com.android.server.nearby.NearbyService.TAG;
import android.annotation.Nullable;
-import android.app.AppOpsManager;
import android.content.Context;
import android.nearby.DataElement;
import android.nearby.IScanListener;
import android.nearby.NearbyDeviceParcelable;
import android.nearby.NearbyManager;
import android.nearby.PresenceScanFilter;
-import android.nearby.ScanCallback;
import android.nearby.ScanFilter;
import android.nearby.ScanRequest;
import android.nearby.aidl.IOffloadCallback;
-import android.os.IBinder;
-import android.os.RemoteException;
import android.util.Log;
-import com.android.internal.annotations.GuardedBy;
+import androidx.annotation.NonNull;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.nearby.injector.Injector;
-import com.android.server.nearby.metrics.NearbyMetrics;
-import com.android.server.nearby.presence.PresenceDiscoveryResult;
+import com.android.server.nearby.managers.registration.DiscoveryRegistration;
import com.android.server.nearby.provider.AbstractDiscoveryProvider;
import com.android.server.nearby.provider.BleDiscoveryProvider;
import com.android.server.nearby.provider.ChreCommunication;
import com.android.server.nearby.provider.ChreDiscoveryProvider;
-import com.android.server.nearby.provider.PrivacyFilter;
import com.android.server.nearby.util.identity.CallerIdentity;
-import com.android.server.nearby.util.permissions.DiscoveryPermissions;
import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.Collection;
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.stream.Collectors;
+
+import javax.annotation.concurrent.GuardedBy;
/** Manages all aspects of discovery providers. */
-public class DiscoveryProviderManager implements AbstractDiscoveryProvider.Listener {
+public class DiscoveryProviderManager extends
+ ListenerMultiplexer<IScanListener, DiscoveryRegistration, MergedDiscoveryRequest> implements
+ AbstractDiscoveryProvider.Listener,
+ DiscoveryManager {
protected final Object mLock = new Object();
- private final Context mContext;
- private final BleDiscoveryProvider mBleDiscoveryProvider;
@VisibleForTesting
@Nullable
final ChreDiscoveryProvider mChreDiscoveryProvider;
- private @ScanRequest.ScanMode int mScanMode;
+ private final Context mContext;
+ private final BleDiscoveryProvider mBleDiscoveryProvider;
private final Injector mInjector;
-
- @GuardedBy("mLock")
- private Map<IBinder, ScanListenerRecord> mScanTypeScanListenerRecordMap;
-
- @Override
- public void onNearbyDeviceDiscovered(NearbyDeviceParcelable nearbyDevice) {
- synchronized (mLock) {
- AppOpsManager appOpsManager = Objects.requireNonNull(mInjector.getAppOpsManager());
- for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
- ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
- if (record == null) {
- Log.w(TAG, "DiscoveryProviderManager cannot find the scan record.");
- continue;
- }
- CallerIdentity callerIdentity = record.getCallerIdentity();
- if (!DiscoveryPermissions.noteDiscoveryResultDelivery(
- appOpsManager, callerIdentity)) {
- Log.w(TAG, "[DiscoveryProviderManager] scan permission revoked "
- + "- not forwarding results");
- try {
- record.getScanListener().onError(ScanCallback.ERROR_PERMISSION_DENIED);
- } catch (RemoteException e) {
- Log.w(TAG, "DiscoveryProviderManager failed to report error.", e);
- }
- return;
- }
-
- if (nearbyDevice.getScanType() == SCAN_TYPE_NEARBY_PRESENCE) {
- List<ScanFilter> presenceFilters =
- record.getScanRequest().getScanFilters().stream()
- .filter(
- scanFilter ->
- scanFilter.getType()
- == SCAN_TYPE_NEARBY_PRESENCE)
- .collect(Collectors.toList());
- if (!presenceFilterMatches(nearbyDevice, presenceFilters)) {
- Log.d(TAG, "presence filter does not match for "
- + "the scanned Presence Device");
- continue;
- }
- }
- try {
- record.getScanListener()
- .onDiscovered(
- PrivacyFilter.filter(
- record.getScanRequest().getScanType(), nearbyDevice));
- NearbyMetrics.logScanDeviceDiscovered(
- record.hashCode(), record.getScanRequest(), nearbyDevice);
- } catch (RemoteException e) {
- Log.w(TAG, "DiscoveryProviderManager failed to report onDiscovered.", e);
- }
- }
- }
- }
-
- @Override
- public void onError(int errorCode) {
- synchronized (mLock) {
- AppOpsManager appOpsManager = Objects.requireNonNull(mInjector.getAppOpsManager());
- for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
- ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
- if (record == null) {
- Log.w(TAG, "DiscoveryProviderManager cannot find the scan record.");
- continue;
- }
- CallerIdentity callerIdentity = record.getCallerIdentity();
- if (!DiscoveryPermissions.noteDiscoveryResultDelivery(
- appOpsManager, callerIdentity)) {
- Log.w(TAG, "[DiscoveryProviderManager] scan permission revoked "
- + "- not forwarding results");
- try {
- record.getScanListener().onError(ScanCallback.ERROR_PERMISSION_DENIED);
- } catch (RemoteException e) {
- Log.w(TAG, "DiscoveryProviderManager failed to report error.", e);
- }
- return;
- }
-
- try {
- record.getScanListener().onError(errorCode);
- } catch (RemoteException e) {
- Log.w(TAG, "DiscoveryProviderManager failed to report onError.", e);
- }
- }
- }
- }
+ private final Executor mExecutor;
public DiscoveryProviderManager(Context context, Injector injector) {
+ Log.v(TAG, "DiscoveryProviderManager: ");
mContext = context;
mBleDiscoveryProvider = new BleDiscoveryProvider(mContext, injector);
- Executor executor = Executors.newSingleThreadExecutor();
- mChreDiscoveryProvider =
- new ChreDiscoveryProvider(
- mContext, new ChreCommunication(injector, mContext, executor), executor);
- mScanTypeScanListenerRecordMap = new HashMap<>();
+ mExecutor = Executors.newSingleThreadExecutor();
+ mChreDiscoveryProvider = new ChreDiscoveryProvider(mContext,
+ new ChreCommunication(injector, mContext, mExecutor), mExecutor);
mInjector = injector;
}
@VisibleForTesting
- DiscoveryProviderManager(Context context, Injector injector,
+ DiscoveryProviderManager(Context context, Executor executor, Injector injector,
BleDiscoveryProvider bleDiscoveryProvider,
- ChreDiscoveryProvider chreDiscoveryProvider,
- Map<IBinder, ScanListenerRecord> scanTypeScanListenerRecordMap) {
+ ChreDiscoveryProvider chreDiscoveryProvider) {
mContext = context;
+ mExecutor = executor;
mInjector = injector;
mBleDiscoveryProvider = bleDiscoveryProvider;
mChreDiscoveryProvider = chreDiscoveryProvider;
- mScanTypeScanListenerRecordMap = scanTypeScanListenerRecordMap;
}
- /** Called after boot completed. */
- public void init() {
- if (mInjector.getContextHubManager() != null) {
- mChreDiscoveryProvider.init();
- }
- mChreDiscoveryProvider.getController().setListener(this);
- }
-
- /**
- * Registers the listener in the manager and starts scan according to the requested scan mode.
- */
- @NearbyManager.ScanStatus
- public int registerScanListener(ScanRequest scanRequest, IScanListener listener,
- CallerIdentity callerIdentity) {
- synchronized (mLock) {
- ScanListenerDeathRecipient deathRecipient = (listener != null)
- ? new ScanListenerDeathRecipient(listener) : null;
- IBinder listenerBinder = listener.asBinder();
- if (listenerBinder != null && deathRecipient != null) {
- try {
- listenerBinder.linkToDeath(deathRecipient, 0);
- } catch (RemoteException e) {
- throw new IllegalArgumentException("Can't link to scan listener's death");
- }
- }
- if (mScanTypeScanListenerRecordMap.containsKey(listener.asBinder())) {
- ScanRequest savedScanRequest =
- mScanTypeScanListenerRecordMap.get(listenerBinder).getScanRequest();
- if (scanRequest.equals(savedScanRequest)) {
- Log.d(TAG, "Already registered the scanRequest: " + scanRequest);
- return NearbyManager.ScanStatus.SUCCESS;
- }
- }
- ScanListenerRecord scanListenerRecord =
- new ScanListenerRecord(scanRequest, listener, callerIdentity, deathRecipient);
-
- Boolean started = startProviders(scanRequest);
- if (started == null) {
- return NearbyManager.ScanStatus.UNKNOWN;
- }
- if (!started) {
- return NearbyManager.ScanStatus.ERROR;
- }
- mScanTypeScanListenerRecordMap.put(listenerBinder, scanListenerRecord);
- NearbyMetrics.logScanStarted(scanListenerRecord.hashCode(), scanRequest);
- if (mScanMode < scanRequest.getScanMode()) {
- mScanMode = scanRequest.getScanMode();
- invalidateProviderScanMode();
- }
- return NearbyManager.ScanStatus.SUCCESS;
- }
- }
-
- /**
- * Unregisters the listener in the manager and adjusts the scan mode if necessary afterwards.
- */
- public void unregisterScanListener(IScanListener listener) {
- IBinder listenerBinder = listener.asBinder();
- synchronized (mLock) {
- if (!mScanTypeScanListenerRecordMap.containsKey(listenerBinder)) {
- Log.w(
- TAG,
- "Cannot unregister the scanRequest because the request is never "
- + "registered.");
- return;
- }
-
- ScanListenerRecord removedRecord =
- mScanTypeScanListenerRecordMap.remove(listenerBinder);
- ScanListenerDeathRecipient deathRecipient = removedRecord.getDeathRecipient();
- if (listenerBinder != null && deathRecipient != null) {
- listenerBinder.unlinkToDeath(removedRecord.getDeathRecipient(), 0);
- }
- Log.v(TAG, "DiscoveryProviderManager unregistered scan listener.");
- NearbyMetrics.logScanStopped(removedRecord.hashCode(), removedRecord.getScanRequest());
- if (mScanTypeScanListenerRecordMap.isEmpty()) {
- Log.v(TAG, "DiscoveryProviderManager stops provider because there is no "
- + "scan listener registered.");
- stopProviders();
- return;
- }
-
- // TODO(b/221082271): updates the scan with reduced filters.
-
- // Removes current highest scan mode requested and sets the next highest scan mode.
- if (removedRecord.getScanRequest().getScanMode() == mScanMode) {
- Log.v(TAG, "DiscoveryProviderManager starts to find the new highest scan mode "
- + "because the highest scan mode listener was unregistered.");
- @ScanRequest.ScanMode int highestScanModeRequested = ScanRequest.SCAN_MODE_NO_POWER;
- // find the next highest scan mode;
- for (ScanListenerRecord record : mScanTypeScanListenerRecordMap.values()) {
- @ScanRequest.ScanMode int scanMode = record.getScanRequest().getScanMode();
- if (scanMode > highestScanModeRequested) {
- highestScanModeRequested = scanMode;
- }
- }
- if (mScanMode != highestScanModeRequested) {
- mScanMode = highestScanModeRequested;
- invalidateProviderScanMode();
- }
- }
- }
- }
-
- /**
- * Query offload capability in a device.
- */
- public void queryOffloadCapability(IOffloadCallback callback) {
- mChreDiscoveryProvider.queryOffloadCapability(callback);
- }
-
- /**
- * @return {@code null} when all providers are initializing
- * {@code false} when fail to start all the providers
- * {@code true} when any one of the provider starts successfully
- */
- @VisibleForTesting
- @Nullable
- Boolean startProviders(ScanRequest scanRequest) {
- if (!scanRequest.isBleEnabled()) {
- Log.w(TAG, "failed to start any provider because client disabled BLE");
- return false;
- }
- List<ScanFilter> scanFilters = getPresenceScanFilters();
- boolean chreOnly = isChreOnly(scanFilters);
- Boolean chreAvailable = mChreDiscoveryProvider.available();
- if (chreAvailable == null) {
- if (chreOnly) {
- Log.w(TAG, "client wants CHRE only and Nearby service is still querying CHRE"
- + " status");
- return null;
- }
- startBleProvider(scanFilters);
- return true;
- }
-
- if (!chreAvailable) {
- if (chreOnly) {
- Log.w(TAG, "failed to start any provider because client wants CHRE only and CHRE"
- + " is not available");
- return false;
- }
- startBleProvider(scanFilters);
- return true;
- }
-
- if (scanRequest.getScanType() == SCAN_TYPE_NEARBY_PRESENCE) {
- startChreProvider(scanFilters);
- return true;
- }
-
- startBleProvider(scanFilters);
- return true;
- }
-
- private static boolean isChreOnly(List<ScanFilter> scanFilters) {
+ private static boolean isChreOnly(Set<ScanFilter> scanFilters) {
for (ScanFilter scanFilter : scanFilters) {
List<DataElement> dataElements =
((PresenceScanFilter) scanFilter).getExtendedProperties();
@@ -355,41 +109,160 @@
return false;
}
- private void startBleProvider(List<ScanFilter> scanFilters) {
+ @Override
+ public void onNearbyDeviceDiscovered(NearbyDeviceParcelable nearbyDevice) {
+ synchronized (mMultiplexerLock) {
+ Log.d(TAG, "Found device" + nearbyDevice);
+ deliverToListeners(registration -> {
+ try {
+ return registration.onNearbyDeviceDiscovered(nearbyDevice);
+ } catch (Exception e) {
+ Log.w(TAG, "DiscoveryProviderManager failed to report callback.", e);
+ return null;
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onError(int errorCode) {
+ synchronized (mMultiplexerLock) {
+ Log.e(TAG, "Error found during scanning.");
+ deliverToListeners(registration -> {
+ try {
+ return registration.reportError(errorCode);
+ } catch (Exception e) {
+ Log.w(TAG, "DiscoveryProviderManager failed to report error.", e);
+ return null;
+ }
+ });
+ }
+ }
+
+ /** Called after boot completed. */
+ public void init() {
+ if (mInjector.getContextHubManager() != null) {
+ mChreDiscoveryProvider.init();
+ }
+ mChreDiscoveryProvider.getController().setListener(this);
+ }
+
+ /**
+ * Registers the listener in the manager and starts scan according to the requested scan mode.
+ */
+ @NearbyManager.ScanStatus
+ public int registerScanListener(ScanRequest scanRequest, IScanListener listener,
+ CallerIdentity callerIdentity) {
+ DiscoveryRegistration registration = new DiscoveryRegistration(this, scanRequest, listener,
+ mExecutor, callerIdentity, mMultiplexerLock, mInjector.getAppOpsManager());
+ synchronized (mMultiplexerLock) {
+ putRegistration(listener.asBinder(), registration);
+ return NearbyManager.ScanStatus.SUCCESS;
+ }
+ }
+
+ @Override
+ public void onRegister() {
+ Log.v(TAG, "Registering the DiscoveryProviderManager.");
+ startProviders();
+ }
+
+ @Override
+ public void onUnregister() {
+ Log.v(TAG, "Unregistering the DiscoveryProviderManager.");
+ stopProviders();
+ }
+
+ /**
+ * Unregisters the listener in the manager and adjusts the scan mode if necessary afterwards.
+ */
+ public void unregisterScanListener(IScanListener listener) {
+ Log.v(TAG, "Unregister scan listener");
+ synchronized (mMultiplexerLock) {
+ removeRegistration(listener.asBinder());
+ }
+ // TODO(b/221082271): updates the scan with reduced filters.
+ }
+
+ /**
+ * Query offload capability in a device.
+ */
+ public void queryOffloadCapability(IOffloadCallback callback) {
+ mChreDiscoveryProvider.queryOffloadCapability(callback);
+ }
+
+ /**
+ * @return {@code null} when all providers are initializing
+ * {@code false} when fail to start all the providers
+ * {@code true} when any one of the provider starts successfully
+ */
+ @VisibleForTesting
+ @Nullable
+ Boolean startProviders() {
+ synchronized (mMultiplexerLock) {
+ if (!mMerged.getMediums().contains(MergedDiscoveryRequest.Medium.BLE)) {
+ Log.w(TAG, "failed to start any provider because client disabled BLE");
+ return false;
+ }
+ Set<ScanFilter> scanFilters = mMerged.getScanFilters();
+ boolean chreOnly = isChreOnly(scanFilters);
+ Boolean chreAvailable = mChreDiscoveryProvider.available();
+ Log.v(TAG, "startProviders: chreOnly " + chreOnly + " chreAvailable " + chreAvailable);
+ if (chreAvailable == null) {
+ if (chreOnly) {
+ Log.w(TAG, "client wants CHRE only and Nearby service is still querying CHRE"
+ + " status");
+ return null;
+ }
+ startBleProvider(scanFilters);
+ return true;
+ }
+
+ if (!chreAvailable) {
+ if (chreOnly) {
+ Log.w(TAG,
+ "failed to start any provider because client wants CHRE only and CHRE"
+ + " is not available");
+ return false;
+ }
+ startBleProvider(scanFilters);
+ return true;
+ }
+
+ if (mMerged.getScanTypes().contains(SCAN_TYPE_NEARBY_PRESENCE)) {
+ startChreProvider(scanFilters);
+ Log.d(TAG, "startProviders: heeey");
+ return true;
+ }
+
+ Log.d(TAG, "startProviders: geagewage");
+
+ startBleProvider(scanFilters);
+ return true;
+ }
+ }
+
+ @GuardedBy("mMultiplexerLock")
+ private void startBleProvider(Set<ScanFilter> scanFilters) {
if (!mBleDiscoveryProvider.getController().isStarted()) {
Log.d(TAG, "DiscoveryProviderManager starts Ble scanning.");
mBleDiscoveryProvider.getController().setListener(this);
- mBleDiscoveryProvider.getController().setProviderScanMode(mScanMode);
- mBleDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
+ mBleDiscoveryProvider.getController().setProviderScanMode(mMerged.getScanMode());
+ mBleDiscoveryProvider.getController().setProviderScanFilters(
+ new ArrayList<>(scanFilters));
mBleDiscoveryProvider.getController().start();
}
}
@VisibleForTesting
- void startChreProvider(List<ScanFilter> scanFilters) {
- Log.d(TAG, "DiscoveryProviderManager starts CHRE scanning.");
- mChreDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
- mChreDiscoveryProvider.getController().setProviderScanMode(mScanMode);
+ @GuardedBy("mMultiplexerLock")
+ void startChreProvider(Collection<ScanFilter> scanFilters) {
+ Log.d(TAG, "DiscoveryProviderManager starts CHRE scanning. " + mMerged);
+ mChreDiscoveryProvider.getController().setProviderScanFilters(new ArrayList<>(scanFilters));
+ mChreDiscoveryProvider.getController().setProviderScanMode(mMerged.getScanMode());
mChreDiscoveryProvider.getController().start();
}
- private List<ScanFilter> getPresenceScanFilters() {
- synchronized (mLock) {
- List<ScanFilter> scanFilters = new ArrayList();
- for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
- ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
- List<ScanFilter> presenceFilters =
- record.getScanRequest().getScanFilters().stream()
- .filter(
- scanFilter ->
- scanFilter.getType() == SCAN_TYPE_NEARBY_PRESENCE)
- .collect(Collectors.toList());
- scanFilters.addAll(presenceFilters);
- }
- return scanFilters;
- }
- }
-
private void stopProviders() {
stopBleProvider();
stopChreProvider();
@@ -407,96 +280,40 @@
@VisibleForTesting
void invalidateProviderScanMode() {
if (mBleDiscoveryProvider.getController().isStarted()) {
- mBleDiscoveryProvider.getController().setProviderScanMode(mScanMode);
+ synchronized (mMultiplexerLock) {
+ mBleDiscoveryProvider.getController().setProviderScanMode(mMerged.getScanMode());
+ }
} else {
- Log.d(
- TAG,
- "Skip invalidating BleDiscoveryProvider scan mode because the provider not "
- + "started.");
+ Log.d(TAG, "Skip invalidating BleDiscoveryProvider scan mode because the provider not "
+ + "started.");
}
}
- @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;
+ @Override
+ public MergedDiscoveryRequest mergeRegistrations(
+ @NonNull Collection<DiscoveryRegistration> registrations) {
+ MergedDiscoveryRequest.Builder builder = new MergedDiscoveryRequest.Builder();
+ int scanMode = ScanRequest.SCAN_MODE_NO_POWER;
+ for (DiscoveryRegistration registration : registrations) {
+ builder.addActions(registration.getActions());
+ builder.addScanFilters(registration.getPresenceScanFilters());
+ Log.d(TAG,
+ "mergeRegistrations: type is " + registration.getScanRequest().getScanType());
+ builder.addScanType(registration.getScanRequest().getScanType());
+ if (registration.getScanRequest().isBleEnabled()) {
+ builder.addMedium(MergedDiscoveryRequest.Medium.BLE);
+ }
+ int requestScanMode = registration.getScanRequest().getScanMode();
+ if (scanMode < requestScanMode) {
+ scanMode = requestScanMode;
}
}
- return false;
+ builder.setScanMode(scanMode);
+ return builder.build();
}
- /**
- * Class to make listener unregister after the binder is dead.
- */
- public class ScanListenerDeathRecipient implements IBinder.DeathRecipient {
- public IScanListener listener;
-
- ScanListenerDeathRecipient(IScanListener listener) {
- this.listener = listener;
- }
-
- @Override
- public void binderDied() {
- Log.d(TAG, "Binder is dead - unregistering scan listener");
- unregisterScanListener(listener);
- }
- }
-
- @VisibleForTesting
- static class ScanListenerRecord {
-
- private final ScanRequest mScanRequest;
-
- private final IScanListener mScanListener;
-
- private final CallerIdentity mCallerIdentity;
-
- private final ScanListenerDeathRecipient mDeathRecipient;
-
- ScanListenerRecord(ScanRequest scanRequest, IScanListener iScanListener,
- CallerIdentity callerIdentity, ScanListenerDeathRecipient deathRecipient) {
- mScanListener = iScanListener;
- mScanRequest = scanRequest;
- mCallerIdentity = callerIdentity;
- mDeathRecipient = deathRecipient;
- }
-
- IScanListener getScanListener() {
- return mScanListener;
- }
-
- ScanRequest getScanRequest() {
- return mScanRequest;
- }
-
- CallerIdentity getCallerIdentity() {
- return mCallerIdentity;
- }
-
- ScanListenerDeathRecipient getDeathRecipient() {
- return mDeathRecipient;
- }
-
- @Override
- public boolean equals(Object other) {
- if (other instanceof ScanListenerRecord) {
- ScanListenerRecord otherScanListenerRecord = (ScanListenerRecord) other;
- return Objects.equals(mScanRequest, otherScanListenerRecord.mScanRequest)
- && Objects.equals(mScanListener, otherScanListenerRecord.mScanListener);
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(mScanListener, mScanRequest);
- }
+ @Override
+ public void onMergedRegistrationsUpdated() {
+ invalidateProviderScanMode();
}
}
diff --git a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java
new file mode 100644
index 0000000..e68d22a
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java
@@ -0,0 +1,504 @@
+/*
+ * 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.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.Nullable;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.nearby.DataElement;
+import android.nearby.IScanListener;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.NearbyManager;
+import android.nearby.PresenceScanFilter;
+import android.nearby.ScanCallback;
+import android.nearby.ScanFilter;
+import android.nearby.ScanRequest;
+import android.nearby.aidl.IOffloadCallback;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.metrics.NearbyMetrics;
+import com.android.server.nearby.presence.PresenceDiscoveryResult;
+import com.android.server.nearby.provider.AbstractDiscoveryProvider;
+import com.android.server.nearby.provider.BleDiscoveryProvider;
+import com.android.server.nearby.provider.ChreCommunication;
+import com.android.server.nearby.provider.ChreDiscoveryProvider;
+import com.android.server.nearby.provider.PrivacyFilter;
+import com.android.server.nearby.util.identity.CallerIdentity;
+import com.android.server.nearby.util.permissions.DiscoveryPermissions;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
+/** Manages all aspects of discovery providers. */
+public class DiscoveryProviderManagerLegacy implements AbstractDiscoveryProvider.Listener,
+ DiscoveryManager {
+
+ protected final Object mLock = new Object();
+ @VisibleForTesting
+ @Nullable
+ final ChreDiscoveryProvider mChreDiscoveryProvider;
+ private final Context mContext;
+ private final BleDiscoveryProvider mBleDiscoveryProvider;
+ private final Injector mInjector;
+ @ScanRequest.ScanMode
+ private int mScanMode;
+ @GuardedBy("mLock")
+ private final Map<IBinder, ScanListenerRecord> mScanTypeScanListenerRecordMap;
+
+ public DiscoveryProviderManagerLegacy(Context context, Injector injector) {
+ mContext = context;
+ mBleDiscoveryProvider = new BleDiscoveryProvider(mContext, injector);
+ Executor executor = Executors.newSingleThreadExecutor();
+ mChreDiscoveryProvider =
+ new ChreDiscoveryProvider(
+ mContext, new ChreCommunication(injector, mContext, executor), executor);
+ mScanTypeScanListenerRecordMap = new HashMap<>();
+ mInjector = injector;
+ Log.v(TAG, "DiscoveryProviderManagerLegacy: ");
+ }
+
+ @VisibleForTesting
+ DiscoveryProviderManagerLegacy(Context context, Injector injector,
+ BleDiscoveryProvider bleDiscoveryProvider,
+ ChreDiscoveryProvider chreDiscoveryProvider,
+ Map<IBinder, ScanListenerRecord> scanTypeScanListenerRecordMap) {
+ mContext = context;
+ mInjector = injector;
+ mBleDiscoveryProvider = bleDiscoveryProvider;
+ mChreDiscoveryProvider = chreDiscoveryProvider;
+ mScanTypeScanListenerRecordMap = scanTypeScanListenerRecordMap;
+ }
+
+ private static boolean isChreOnly(List<ScanFilter> scanFilters) {
+ for (ScanFilter scanFilter : scanFilters) {
+ List<DataElement> dataElements =
+ ((PresenceScanFilter) scanFilter).getExtendedProperties();
+ for (DataElement dataElement : dataElements) {
+ if (dataElement.getKey() != DataElement.DataType.SCAN_MODE) {
+ continue;
+ }
+ byte[] scanModeValue = dataElement.getValue();
+ if (scanModeValue == null || scanModeValue.length == 0) {
+ break;
+ }
+ if (Byte.toUnsignedInt(scanModeValue[0]) == ScanRequest.SCAN_MODE_CHRE_ONLY) {
+ return true;
+ }
+ }
+
+ }
+ return false;
+ }
+
+ @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;
+ }
+
+ @Override
+ public void onNearbyDeviceDiscovered(NearbyDeviceParcelable nearbyDevice) {
+ synchronized (mLock) {
+ AppOpsManager appOpsManager = Objects.requireNonNull(mInjector.getAppOpsManager());
+ for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
+ ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
+ if (record == null) {
+ Log.w(TAG, "DiscoveryProviderManager cannot find the scan record.");
+ continue;
+ }
+ CallerIdentity callerIdentity = record.getCallerIdentity();
+ if (!DiscoveryPermissions.noteDiscoveryResultDelivery(
+ appOpsManager, callerIdentity)) {
+ Log.w(TAG, "[DiscoveryProviderManager] scan permission revoked "
+ + "- not forwarding results");
+ try {
+ record.getScanListener().onError(ScanCallback.ERROR_PERMISSION_DENIED);
+ } catch (RemoteException e) {
+ Log.w(TAG, "DiscoveryProviderManager failed to report error.", e);
+ }
+ return;
+ }
+
+ if (nearbyDevice.getScanType() == SCAN_TYPE_NEARBY_PRESENCE) {
+ List<ScanFilter> presenceFilters =
+ record.getScanRequest().getScanFilters().stream()
+ .filter(
+ scanFilter ->
+ scanFilter.getType()
+ == SCAN_TYPE_NEARBY_PRESENCE)
+ .collect(Collectors.toList());
+ if (!presenceFilterMatches(nearbyDevice, presenceFilters)) {
+ Log.d(TAG, "presence filter does not match for "
+ + "the scanned Presence Device");
+ continue;
+ }
+ }
+ try {
+ record.getScanListener()
+ .onDiscovered(
+ PrivacyFilter.filter(
+ record.getScanRequest().getScanType(), nearbyDevice));
+ NearbyMetrics.logScanDeviceDiscovered(
+ record.hashCode(), record.getScanRequest(), nearbyDevice);
+ } catch (RemoteException e) {
+ Log.w(TAG, "DiscoveryProviderManager failed to report onDiscovered.", e);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onError(int errorCode) {
+ synchronized (mLock) {
+ AppOpsManager appOpsManager = Objects.requireNonNull(mInjector.getAppOpsManager());
+ for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
+ ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
+ if (record == null) {
+ Log.w(TAG, "DiscoveryProviderManager cannot find the scan record.");
+ continue;
+ }
+ CallerIdentity callerIdentity = record.getCallerIdentity();
+ if (!DiscoveryPermissions.noteDiscoveryResultDelivery(
+ appOpsManager, callerIdentity)) {
+ Log.w(TAG, "[DiscoveryProviderManager] scan permission revoked "
+ + "- not forwarding results");
+ try {
+ record.getScanListener().onError(ScanCallback.ERROR_PERMISSION_DENIED);
+ } catch (RemoteException e) {
+ Log.w(TAG, "DiscoveryProviderManager failed to report error.", e);
+ }
+ return;
+ }
+
+ try {
+ record.getScanListener().onError(errorCode);
+ } catch (RemoteException e) {
+ Log.w(TAG, "DiscoveryProviderManager failed to report onError.", e);
+ }
+ }
+ }
+ }
+
+ /** Called after boot completed. */
+ public void init() {
+ if (mInjector.getContextHubManager() != null) {
+ mChreDiscoveryProvider.init();
+ }
+ mChreDiscoveryProvider.getController().setListener(this);
+ }
+
+ /**
+ * Registers the listener in the manager and starts scan according to the requested scan mode.
+ */
+ @NearbyManager.ScanStatus
+ public int registerScanListener(ScanRequest scanRequest, IScanListener listener,
+ CallerIdentity callerIdentity) {
+ synchronized (mLock) {
+ ScanListenerDeathRecipient deathRecipient = (listener != null)
+ ? new ScanListenerDeathRecipient(listener) : null;
+ IBinder listenerBinder = listener.asBinder();
+ if (listenerBinder != null && deathRecipient != null) {
+ try {
+ listenerBinder.linkToDeath(deathRecipient, 0);
+ } catch (RemoteException e) {
+ throw new IllegalArgumentException("Can't link to scan listener's death");
+ }
+ }
+ if (mScanTypeScanListenerRecordMap.containsKey(listener.asBinder())) {
+ ScanRequest savedScanRequest =
+ mScanTypeScanListenerRecordMap.get(listenerBinder).getScanRequest();
+ if (scanRequest.equals(savedScanRequest)) {
+ Log.d(TAG, "Already registered the scanRequest: " + scanRequest);
+ return NearbyManager.ScanStatus.SUCCESS;
+ }
+ }
+ ScanListenerRecord scanListenerRecord =
+ new ScanListenerRecord(scanRequest, listener, callerIdentity, deathRecipient);
+
+ Boolean started = startProviders(scanRequest);
+ if (started == null) {
+ return NearbyManager.ScanStatus.UNKNOWN;
+ }
+ if (!started) {
+ return NearbyManager.ScanStatus.ERROR;
+ }
+ mScanTypeScanListenerRecordMap.put(listenerBinder, scanListenerRecord);
+ NearbyMetrics.logScanStarted(scanListenerRecord.hashCode(), scanRequest);
+ if (mScanMode < scanRequest.getScanMode()) {
+ mScanMode = scanRequest.getScanMode();
+ invalidateProviderScanMode();
+ }
+ return NearbyManager.ScanStatus.SUCCESS;
+ }
+ }
+
+ /**
+ * Unregisters the listener in the manager and adjusts the scan mode if necessary afterwards.
+ */
+ public void unregisterScanListener(IScanListener listener) {
+ IBinder listenerBinder = listener.asBinder();
+ synchronized (mLock) {
+ if (!mScanTypeScanListenerRecordMap.containsKey(listenerBinder)) {
+ Log.w(
+ TAG,
+ "Cannot unregister the scanRequest because the request is never "
+ + "registered.");
+ return;
+ }
+
+ ScanListenerRecord removedRecord =
+ mScanTypeScanListenerRecordMap.remove(listenerBinder);
+ ScanListenerDeathRecipient deathRecipient = removedRecord.getDeathRecipient();
+ if (listenerBinder != null && deathRecipient != null) {
+ listenerBinder.unlinkToDeath(removedRecord.getDeathRecipient(), 0);
+ }
+ Log.v(TAG, "DiscoveryProviderManager unregistered scan listener.");
+ NearbyMetrics.logScanStopped(removedRecord.hashCode(), removedRecord.getScanRequest());
+ if (mScanTypeScanListenerRecordMap.isEmpty()) {
+ Log.v(TAG, "DiscoveryProviderManager stops provider because there is no "
+ + "scan listener registered.");
+ stopProviders();
+ return;
+ }
+
+ // TODO(b/221082271): updates the scan with reduced filters.
+
+ // Removes current highest scan mode requested and sets the next highest scan mode.
+ if (removedRecord.getScanRequest().getScanMode() == mScanMode) {
+ Log.v(TAG, "DiscoveryProviderManager starts to find the new highest scan mode "
+ + "because the highest scan mode listener was unregistered.");
+ @ScanRequest.ScanMode int highestScanModeRequested = ScanRequest.SCAN_MODE_NO_POWER;
+ // find the next highest scan mode;
+ for (ScanListenerRecord record : mScanTypeScanListenerRecordMap.values()) {
+ @ScanRequest.ScanMode int scanMode = record.getScanRequest().getScanMode();
+ if (scanMode > highestScanModeRequested) {
+ highestScanModeRequested = scanMode;
+ }
+ }
+ if (mScanMode != highestScanModeRequested) {
+ mScanMode = highestScanModeRequested;
+ invalidateProviderScanMode();
+ }
+ }
+ }
+ }
+
+ /**
+ * Query offload capability in a device.
+ */
+ public void queryOffloadCapability(IOffloadCallback callback) {
+ mChreDiscoveryProvider.queryOffloadCapability(callback);
+ }
+
+ /**
+ * @return {@code null} when all providers are initializing
+ * {@code false} when fail to start all the providers
+ * {@code true} when any one of the provider starts successfully
+ */
+ @VisibleForTesting
+ @Nullable
+ Boolean startProviders(ScanRequest scanRequest) {
+ if (!scanRequest.isBleEnabled()) {
+ Log.w(TAG, "failed to start any provider because client disabled BLE");
+ return false;
+ }
+ List<ScanFilter> scanFilters = getPresenceScanFilters();
+ boolean chreOnly = isChreOnly(scanFilters);
+ Boolean chreAvailable = mChreDiscoveryProvider.available();
+ if (chreAvailable == null) {
+ if (chreOnly) {
+ Log.w(TAG, "client wants CHRE only and Nearby service is still querying CHRE"
+ + " status");
+ return null;
+ }
+ startBleProvider(scanFilters);
+ return true;
+ }
+
+ if (!chreAvailable) {
+ if (chreOnly) {
+ Log.w(TAG, "failed to start any provider because client wants CHRE only and CHRE"
+ + " is not available");
+ return false;
+ }
+ startBleProvider(scanFilters);
+ return true;
+ }
+
+ if (scanRequest.getScanType() == SCAN_TYPE_NEARBY_PRESENCE) {
+ startChreProvider(scanFilters);
+ return true;
+ }
+
+ startBleProvider(scanFilters);
+ return true;
+ }
+
+ private void startBleProvider(List<ScanFilter> scanFilters) {
+ if (!mBleDiscoveryProvider.getController().isStarted()) {
+ Log.d(TAG, "DiscoveryProviderManager starts Ble scanning.");
+ mBleDiscoveryProvider.getController().setListener(this);
+ mBleDiscoveryProvider.getController().setProviderScanMode(mScanMode);
+ mBleDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
+ mBleDiscoveryProvider.getController().start();
+ }
+ }
+
+ @VisibleForTesting
+ void startChreProvider(List<ScanFilter> scanFilters) {
+ Log.d(TAG, "DiscoveryProviderManager starts CHRE scanning.");
+ mChreDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
+ mChreDiscoveryProvider.getController().setProviderScanMode(mScanMode);
+ mChreDiscoveryProvider.getController().start();
+ }
+
+ private List<ScanFilter> getPresenceScanFilters() {
+ synchronized (mLock) {
+ List<ScanFilter> scanFilters = new ArrayList();
+ for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
+ ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
+ List<ScanFilter> presenceFilters =
+ record.getScanRequest().getScanFilters().stream()
+ .filter(
+ scanFilter ->
+ scanFilter.getType() == SCAN_TYPE_NEARBY_PRESENCE)
+ .collect(Collectors.toList());
+ scanFilters.addAll(presenceFilters);
+ }
+ return scanFilters;
+ }
+ }
+
+ private void stopProviders() {
+ stopBleProvider();
+ stopChreProvider();
+ }
+
+ private void stopBleProvider() {
+ mBleDiscoveryProvider.getController().stop();
+ }
+
+ @VisibleForTesting
+ protected void stopChreProvider() {
+ mChreDiscoveryProvider.getController().stop();
+ }
+
+ @VisibleForTesting
+ void invalidateProviderScanMode() {
+ if (mBleDiscoveryProvider.getController().isStarted()) {
+ mBleDiscoveryProvider.getController().setProviderScanMode(mScanMode);
+ } else {
+ Log.d(
+ TAG,
+ "Skip invalidating BleDiscoveryProvider scan mode because the provider not "
+ + "started.");
+ }
+ }
+
+ @VisibleForTesting
+ static class ScanListenerRecord {
+
+ private final ScanRequest mScanRequest;
+
+ private final IScanListener mScanListener;
+
+ private final CallerIdentity mCallerIdentity;
+
+ private final ScanListenerDeathRecipient mDeathRecipient;
+
+ ScanListenerRecord(ScanRequest scanRequest, IScanListener iScanListener,
+ CallerIdentity callerIdentity, ScanListenerDeathRecipient deathRecipient) {
+ mScanListener = iScanListener;
+ mScanRequest = scanRequest;
+ mCallerIdentity = callerIdentity;
+ mDeathRecipient = deathRecipient;
+ }
+
+ IScanListener getScanListener() {
+ return mScanListener;
+ }
+
+ ScanRequest getScanRequest() {
+ return mScanRequest;
+ }
+
+ CallerIdentity getCallerIdentity() {
+ return mCallerIdentity;
+ }
+
+ ScanListenerDeathRecipient getDeathRecipient() {
+ return mDeathRecipient;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof ScanListenerRecord) {
+ ScanListenerRecord otherScanListenerRecord = (ScanListenerRecord) other;
+ return Objects.equals(mScanRequest, otherScanListenerRecord.mScanRequest)
+ && Objects.equals(mScanListener, otherScanListenerRecord.mScanListener);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mScanListener, mScanRequest);
+ }
+ }
+
+ /**
+ * Class to make listener unregister after the binder is dead.
+ */
+ public class ScanListenerDeathRecipient implements IBinder.DeathRecipient {
+ public IScanListener listener;
+
+ ScanListenerDeathRecipient(IScanListener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public void binderDied() {
+ Log.d(TAG, "Binder is dead - unregistering scan listener");
+ unregisterScanListener(listener);
+ }
+ }
+}
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..bef319ec
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/registration/DiscoveryRegistration.java
@@ -0,0 +1,363 @@
+/*
+ * 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);
+ }
+ });
+
+ }
+
+ /** Reports an error to the client. */
+ public ListenerOperation<IScanListener> reportError(@ScanCallback.ErrorCode int errorCode) {
+ return listener -> listener.onError(errorCode);
+ }
+
+ @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 reportError(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/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
index 2cb9d38..c9098a6 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
@@ -44,12 +44,14 @@
import com.android.server.nearby.injector.Injector;
import com.android.server.nearby.presence.ExtendedAdvertisement;
import com.android.server.nearby.presence.PresenceConstants;
+import com.android.server.nearby.util.ArrayUtils;
import com.android.server.nearby.util.ForegroundThread;
import com.android.server.nearby.util.encryption.CryptorImpIdentityV1;
import com.google.common.annotations.VisibleForTesting;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
@@ -86,10 +88,12 @@
@Override
public void onScanResult(int callbackType, ScanResult scanResult) {
NearbyDeviceParcelable.Builder builder = new NearbyDeviceParcelable.Builder();
- builder.setMedium(NearbyDevice.Medium.BLE)
+ String bleAddress = scanResult.getDevice().getAddress();
+ builder.setDeviceId(bleAddress.hashCode())
+ .setMedium(NearbyDevice.Medium.BLE)
.setRssi(scanResult.getRssi())
.setTxPower(scanResult.getTxPower())
- .setBluetoothAddress(scanResult.getDevice().getAddress());
+ .setBluetoothAddress(bleAddress);
ScanRecord record = scanResult.getScanRecord();
if (record != null) {
@@ -300,6 +304,9 @@
builder.setPresenceDevice(getPresenceDevice(advertisement, deviceName,
rssi));
builder.setEncryptionKeyTag(credential.getEncryptedMetadataKeyTag());
+ if (!ArrayUtils.isEmpty(credential.getSecretId())) {
+ builder.setDeviceId(Arrays.hashCode(credential.getSecretId()));
+ }
return;
}
}
diff --git a/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
index aa18517..7ab0523 100644
--- a/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
@@ -44,6 +44,7 @@
import com.google.protobuf.ByteString;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
@@ -362,6 +363,7 @@
NearbyDeviceParcelable device =
new NearbyDeviceParcelable.Builder()
+ .setDeviceId(Arrays.hashCode(secretId))
.setScanType(SCAN_TYPE_NEARBY_PRESENCE)
.setMedium(NearbyDevice.Medium.BLE)
.setTxPower(filterResult.getTxPower())
diff --git a/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java b/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java
index 0d46d3e..90c85ab 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java
@@ -36,6 +36,7 @@
import android.content.Context;
import android.nearby.IScanListener;
import android.nearby.ScanRequest;
+import android.os.IBinder;
import android.provider.DeviceConfig;
import androidx.test.platform.app.InstrumentationRegistry;
@@ -61,10 +62,14 @@
private IScanListener mScanListener;
@Mock
private AppOpsManager mMockAppOpsManager;
+ @Mock
+ private IBinder mIBinder;
@Before
public void setUp() {
initMocks(this);
+ when(mScanListener.asBinder()).thenReturn(mIBinder);
+
mUiAutomation.adoptShellPermissionIdentity(
READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG, BLUETOOTH_PRIVILEGED);
mContext = InstrumentationRegistry.getInstrumentation().getContext();
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/DiscoveryProviderManagerLegacyTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java
new file mode 100644
index 0000000..aa0dad3
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java
@@ -0,0 +1,378 @@
+/*
+ * 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 android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.nearby.DataElement;
+import android.nearby.IScanListener;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+import android.nearby.ScanRequest;
+import android.os.IBinder;
+
+import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.provider.BleDiscoveryProvider;
+import com.android.server.nearby.provider.ChreCommunication;
+import com.android.server.nearby.provider.ChreDiscoveryProvider;
+import com.android.server.nearby.provider.DiscoveryProviderController;
+import com.android.server.nearby.util.identity.CallerIdentity;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Unit test for {@link DiscoveryProviderManagerLegacy} class.
+ */
+public class DiscoveryProviderManagerLegacyTest {
+ private static final int SCAN_MODE_CHRE_ONLY = 3;
+ private static final int DATA_TYPE_SCAN_MODE = 102;
+ private static final int UID = 1234;
+ private static final int PID = 5678;
+ private static final String PACKAGE_NAME = "android.nearby.test";
+ private static final int RSSI = -60;
+ @Mock
+ Injector mInjector;
+ @Mock
+ Context mContext;
+ @Mock
+ AppOpsManager mAppOpsManager;
+ @Mock
+ BleDiscoveryProvider mBleDiscoveryProvider;
+ @Mock
+ ChreDiscoveryProvider mChreDiscoveryProvider;
+ @Mock
+ DiscoveryProviderController mBluetoothController;
+ @Mock
+ DiscoveryProviderController mChreController;
+ @Mock
+ IScanListener mScanListener;
+ @Mock
+ CallerIdentity mCallerIdentity;
+ @Mock
+ DiscoveryProviderManagerLegacy.ScanListenerDeathRecipient mScanListenerDeathRecipient;
+ @Mock
+ IBinder mIBinder;
+ private DiscoveryProviderManagerLegacy mDiscoveryProviderManager;
+ private Map<IBinder, DiscoveryProviderManagerLegacy.ScanListenerRecord>
+ mScanTypeScanListenerRecordMap;
+
+ private static PresenceScanFilter getPresenceScanFilter() {
+ final byte[] secretId = 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};
+
+ PublicCredential credential = new PublicCredential.Builder(
+ secretId, authenticityKey, publicKey, encryptedMetadata, metadataEncryptionKeyTag)
+ .setIdentityType(IDENTITY_TYPE_PRIVATE)
+ .build();
+
+ final int action = 123;
+ return new PresenceScanFilter.Builder()
+ .addCredential(credential)
+ .setMaxPathLoss(RSSI)
+ .addPresenceAction(action)
+ .build();
+ }
+
+ private static PresenceScanFilter getChreOnlyPresenceScanFilter() {
+ final byte[] secretId = 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};
+
+ PublicCredential credential = new PublicCredential.Builder(
+ secretId, authenticityKey, publicKey, encryptedMetadata, metadataEncryptionKeyTag)
+ .setIdentityType(IDENTITY_TYPE_PRIVATE)
+ .build();
+
+ final int action = 123;
+ DataElement scanModeElement = new DataElement(DATA_TYPE_SCAN_MODE,
+ new byte[]{SCAN_MODE_CHRE_ONLY});
+ return new PresenceScanFilter.Builder()
+ .addCredential(credential)
+ .setMaxPathLoss(RSSI)
+ .addPresenceAction(action)
+ .addExtendedProperty(scanModeElement)
+ .build();
+ }
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ when(mInjector.getAppOpsManager()).thenReturn(mAppOpsManager);
+ when(mBleDiscoveryProvider.getController()).thenReturn(mBluetoothController);
+ when(mChreDiscoveryProvider.getController()).thenReturn(mChreController);
+
+ mScanTypeScanListenerRecordMap = new HashMap<>();
+ mDiscoveryProviderManager =
+ new DiscoveryProviderManagerLegacy(mContext, mInjector,
+ mBleDiscoveryProvider,
+ mChreDiscoveryProvider,
+ mScanTypeScanListenerRecordMap);
+ mCallerIdentity = CallerIdentity
+ .forTest(UID, PID, PACKAGE_NAME, /* attributionTag= */ null);
+ }
+
+ @Test
+ public void testOnNearbyDeviceDiscovered() {
+ NearbyDeviceParcelable nearbyDeviceParcelable = new NearbyDeviceParcelable.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .build();
+ mDiscoveryProviderManager.onNearbyDeviceDiscovered(nearbyDeviceParcelable);
+ }
+
+ @Test
+ public void testInvalidateProviderScanMode() {
+ mDiscoveryProviderManager.invalidateProviderScanMode();
+ }
+
+ @Test
+ public void testStartProviders_chreOnlyChreAvailable_bleProviderNotStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(true);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+ DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+ new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+ scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+ Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void testStartProviders_chreOnlyChreAvailable_multipleFilters_bleProviderNotStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(true);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getChreOnlyPresenceScanFilter())
+ .addScanFilter(getPresenceScanFilter()).build();
+ DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+ new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+ scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+ Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void testStartProviders_chreOnlyChreUnavailable_bleProviderNotStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(false);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+ DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+ new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+ scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+ Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isFalse();
+ }
+
+ @Test
+ public void testStartProviders_notChreOnlyChreAvailable_bleProviderNotStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(true);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+ new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+ scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+ Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void testStartProviders_notChreOnlyChreUnavailable_bleProviderStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(false);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+ new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+ scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+ Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ verify(mBluetoothController, atLeastOnce()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void testStartProviders_chreOnlyChreUndetermined_bleProviderNotStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(null);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+ DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+ new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+ scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+ Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isNull();
+ }
+
+ @Test
+ public void testStartProviders_notChreOnlyChreUndetermined_bleProviderStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(null);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+ new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+ scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+ Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ verify(mBluetoothController, atLeastOnce()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void test_stopChreProvider_clearFilters() throws Exception {
+ // Cannot use mocked ChreDiscoveryProvider,
+ // so we cannot use class variable mDiscoveryProviderManager
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ DiscoveryProviderManagerLegacy manager =
+ new DiscoveryProviderManagerLegacy(mContext, mInjector,
+ mBleDiscoveryProvider,
+ new ChreDiscoveryProvider(
+ mContext,
+ new ChreCommunication(mInjector, mContext, executor), executor),
+ mScanTypeScanListenerRecordMap);
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+ new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+ scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+ manager.startChreProvider(List.of(getPresenceScanFilter()));
+ // This is an asynchronized process. The filters will be set in executor thread. So we need
+ // to wait for some time to get the correct result.
+ Thread.sleep(200);
+
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isTrue();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+
+ manager.stopChreProvider();
+ Thread.sleep(200);
+ // The filters should be cleared right after.
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isFalse();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isEmpty();
+ }
+
+ @Test
+ public void test_restartChreProvider() throws Exception {
+ // Cannot use mocked ChreDiscoveryProvider,
+ // so we cannot use class variable mDiscoveryProviderManager
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ DiscoveryProviderManagerLegacy manager =
+ new DiscoveryProviderManagerLegacy(mContext, mInjector,
+ mBleDiscoveryProvider,
+ new ChreDiscoveryProvider(
+ mContext,
+ new ChreCommunication(mInjector, mContext, executor), executor),
+ mScanTypeScanListenerRecordMap);
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+ new DiscoveryProviderManagerLegacy.ScanListenerRecord(scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+ manager.startChreProvider(List.of(getPresenceScanFilter()));
+ // This is an asynchronized process. The filters will be set in executor thread. So we need
+ // to wait for some time to get the correct result.
+ Thread.sleep(200);
+
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isTrue();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+
+ // We want to make sure quickly restart the provider the filters should
+ // be reset correctly.
+ // See b/255922206, there can be a race condition that filters get cleared because onStop()
+ // get executed after onStart() if they are called from different threads.
+ manager.stopChreProvider();
+ manager.mChreDiscoveryProvider.getController().setProviderScanFilters(
+ List.of(getPresenceScanFilter()));
+ manager.startChreProvider(List.of(getPresenceScanFilter()));
+ Thread.sleep(200);
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isTrue();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+
+ // Wait for enough time
+ Thread.sleep(1000);
+
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isTrue();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java
index 542ceed..7ecf631 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java
@@ -23,6 +23,7 @@
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -48,10 +49,8 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class DiscoveryProviderManagerTest {
@@ -80,12 +79,9 @@
@Mock
CallerIdentity mCallerIdentity;
@Mock
- DiscoveryProviderManager.ScanListenerDeathRecipient mScanListenerDeathRecipient;
- @Mock
IBinder mIBinder;
+ private Executor mExecutor;
private DiscoveryProviderManager mDiscoveryProviderManager;
- private Map<IBinder, DiscoveryProviderManager.ScanListenerRecord>
- mScanTypeScanListenerRecordMap;
private static PresenceScanFilter getPresenceScanFilter() {
final byte[] secretId = new byte[]{1, 2, 3, 4};
@@ -133,16 +129,16 @@
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
+ mExecutor = Executors.newSingleThreadExecutor();
when(mInjector.getAppOpsManager()).thenReturn(mAppOpsManager);
when(mBleDiscoveryProvider.getController()).thenReturn(mBluetoothController);
when(mChreDiscoveryProvider.getController()).thenReturn(mChreController);
+ when(mScanListener.asBinder()).thenReturn(mIBinder);
- mScanTypeScanListenerRecordMap = new HashMap<>();
mDiscoveryProviderManager =
- new DiscoveryProviderManager(mContext, mInjector,
+ new DiscoveryProviderManager(mContext, mExecutor, mInjector,
mBleDiscoveryProvider,
- mChreDiscoveryProvider,
- mScanTypeScanListenerRecordMap);
+ mChreDiscoveryProvider);
mCallerIdentity = CallerIdentity
.forTest(UID, PID, PACKAGE_NAME, /* attributionTag= */ null);
}
@@ -162,55 +158,45 @@
@Test
public void testStartProviders_chreOnlyChreAvailable_bleProviderNotStarted() {
+ reset(mBluetoothController);
when(mChreDiscoveryProvider.available()).thenReturn(true);
ScanRequest scanRequest = new ScanRequest.Builder()
.setScanType(SCAN_TYPE_NEARBY_PRESENCE)
.addScanFilter(getChreOnlyPresenceScanFilter()).build();
- DiscoveryProviderManager.ScanListenerRecord record =
- new DiscoveryProviderManager.ScanListenerRecord(
- scanRequest, mScanListener,
- mCallerIdentity, mScanListenerDeathRecipient);
- mScanTypeScanListenerRecordMap.put(mIBinder, record);
+ mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
- Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ Boolean start = mDiscoveryProviderManager.startProviders();
verify(mBluetoothController, never()).start();
assertThat(start).isTrue();
}
@Test
public void testStartProviders_chreOnlyChreAvailable_multipleFilters_bleProviderNotStarted() {
+ reset(mBluetoothController);
when(mChreDiscoveryProvider.available()).thenReturn(true);
ScanRequest scanRequest = new ScanRequest.Builder()
.setScanType(SCAN_TYPE_NEARBY_PRESENCE)
- .addScanFilter(getChreOnlyPresenceScanFilter())
- .addScanFilter(getPresenceScanFilter()).build();
- DiscoveryProviderManager.ScanListenerRecord record =
- new DiscoveryProviderManager.ScanListenerRecord(
- scanRequest, mScanListener,
- mCallerIdentity, mScanListenerDeathRecipient);
- mScanTypeScanListenerRecordMap.put(mIBinder, record);
+ .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+ mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
- Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ Boolean start = mDiscoveryProviderManager.startProviders();
verify(mBluetoothController, never()).start();
assertThat(start).isTrue();
}
@Test
public void testStartProviders_chreOnlyChreUnavailable_bleProviderNotStarted() {
+ reset(mBluetoothController);
when(mChreDiscoveryProvider.available()).thenReturn(false);
ScanRequest scanRequest = new ScanRequest.Builder()
.setScanType(SCAN_TYPE_NEARBY_PRESENCE)
.addScanFilter(getChreOnlyPresenceScanFilter()).build();
- DiscoveryProviderManager.ScanListenerRecord record =
- new DiscoveryProviderManager.ScanListenerRecord(
- scanRequest, mScanListener,
- mCallerIdentity, mScanListenerDeathRecipient);
- mScanTypeScanListenerRecordMap.put(mIBinder, record);
+ mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
- Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ Boolean start = mDiscoveryProviderManager.startProviders();
verify(mBluetoothController, never()).start();
assertThat(start).isFalse();
}
@@ -218,17 +204,14 @@
@Test
public void testStartProviders_notChreOnlyChreAvailable_bleProviderNotStarted() {
when(mChreDiscoveryProvider.available()).thenReturn(true);
+ reset(mBluetoothController);
ScanRequest scanRequest = new ScanRequest.Builder()
.setScanType(SCAN_TYPE_NEARBY_PRESENCE)
.addScanFilter(getPresenceScanFilter()).build();
- DiscoveryProviderManager.ScanListenerRecord record =
- new DiscoveryProviderManager.ScanListenerRecord(
- scanRequest, mScanListener,
- mCallerIdentity, mScanListenerDeathRecipient);
- mScanTypeScanListenerRecordMap.put(mIBinder, record);
+ mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
- Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ Boolean start = mDiscoveryProviderManager.startProviders();
verify(mBluetoothController, never()).start();
assertThat(start).isTrue();
}
@@ -236,17 +219,14 @@
@Test
public void testStartProviders_notChreOnlyChreUnavailable_bleProviderStarted() {
when(mChreDiscoveryProvider.available()).thenReturn(false);
+ reset(mBluetoothController);
ScanRequest scanRequest = new ScanRequest.Builder()
.setScanType(SCAN_TYPE_NEARBY_PRESENCE)
.addScanFilter(getPresenceScanFilter()).build();
- DiscoveryProviderManager.ScanListenerRecord record =
- new DiscoveryProviderManager.ScanListenerRecord(
- scanRequest, mScanListener,
- mCallerIdentity, mScanListenerDeathRecipient);
- mScanTypeScanListenerRecordMap.put(mIBinder, record);
+ mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
- Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ Boolean start = mDiscoveryProviderManager.startProviders();
verify(mBluetoothController, atLeastOnce()).start();
assertThat(start).isTrue();
}
@@ -254,17 +234,14 @@
@Test
public void testStartProviders_chreOnlyChreUndetermined_bleProviderNotStarted() {
when(mChreDiscoveryProvider.available()).thenReturn(null);
+ reset(mBluetoothController);
ScanRequest scanRequest = new ScanRequest.Builder()
.setScanType(SCAN_TYPE_NEARBY_PRESENCE)
.addScanFilter(getChreOnlyPresenceScanFilter()).build();
- DiscoveryProviderManager.ScanListenerRecord record =
- new DiscoveryProviderManager.ScanListenerRecord(
- scanRequest, mScanListener,
- mCallerIdentity, mScanListenerDeathRecipient);
- mScanTypeScanListenerRecordMap.put(mIBinder, record);
+ mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
- Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ Boolean start = mDiscoveryProviderManager.startProviders();
verify(mBluetoothController, never()).start();
assertThat(start).isNull();
}
@@ -272,17 +249,14 @@
@Test
public void testStartProviders_notChreOnlyChreUndetermined_bleProviderStarted() {
when(mChreDiscoveryProvider.available()).thenReturn(null);
+ reset(mBluetoothController);
ScanRequest scanRequest = new ScanRequest.Builder()
.setScanType(SCAN_TYPE_NEARBY_PRESENCE)
.addScanFilter(getPresenceScanFilter()).build();
- DiscoveryProviderManager.ScanListenerRecord record =
- new DiscoveryProviderManager.ScanListenerRecord(
- scanRequest, mScanListener,
- mCallerIdentity, mScanListenerDeathRecipient);
- mScanTypeScanListenerRecordMap.put(mIBinder, record);
+ mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
- Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ Boolean start = mDiscoveryProviderManager.startProviders();
verify(mBluetoothController, atLeastOnce()).start();
assertThat(start).isTrue();
}
@@ -291,22 +265,16 @@
public void test_stopChreProvider_clearFilters() throws Exception {
// Cannot use mocked ChreDiscoveryProvider,
// so we cannot use class variable mDiscoveryProviderManager
- ExecutorService executor = Executors.newSingleThreadExecutor();
DiscoveryProviderManager manager =
- new DiscoveryProviderManager(mContext, mInjector,
+ new DiscoveryProviderManager(mContext, mExecutor, mInjector,
mBleDiscoveryProvider,
new ChreDiscoveryProvider(
mContext,
- new ChreCommunication(mInjector, mContext, executor), executor),
- mScanTypeScanListenerRecordMap);
+ new ChreCommunication(mInjector, mContext, mExecutor), mExecutor));
ScanRequest scanRequest = new ScanRequest.Builder()
.setScanType(SCAN_TYPE_NEARBY_PRESENCE)
.addScanFilter(getPresenceScanFilter()).build();
- DiscoveryProviderManager.ScanListenerRecord record =
- new DiscoveryProviderManager.ScanListenerRecord(
- scanRequest, mScanListener,
- mCallerIdentity, mScanListenerDeathRecipient);
- mScanTypeScanListenerRecordMap.put(mIBinder, record);
+ manager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
manager.startChreProvider(List.of(getPresenceScanFilter()));
// This is an asynchronized process. The filters will be set in executor thread. So we need
// to wait for some time to get the correct result.
@@ -328,21 +296,17 @@
public void test_restartChreProvider() throws Exception {
// Cannot use mocked ChreDiscoveryProvider,
// so we cannot use class variable mDiscoveryProviderManager
- ExecutorService executor = Executors.newSingleThreadExecutor();
DiscoveryProviderManager manager =
- new DiscoveryProviderManager(mContext, mInjector,
+ new DiscoveryProviderManager(mContext, mExecutor, mInjector,
mBleDiscoveryProvider,
new ChreDiscoveryProvider(
mContext,
- new ChreCommunication(mInjector, mContext, executor), executor),
- mScanTypeScanListenerRecordMap);
+ new ChreCommunication(mInjector, mContext, mExecutor), mExecutor));
ScanRequest scanRequest = new ScanRequest.Builder()
.setScanType(SCAN_TYPE_NEARBY_PRESENCE)
.addScanFilter(getPresenceScanFilter()).build();
- DiscoveryProviderManager.ScanListenerRecord record =
- new DiscoveryProviderManager.ScanListenerRecord(scanRequest, mScanListener,
- mCallerIdentity, mScanListenerDeathRecipient);
- mScanTypeScanListenerRecordMap.put(mIBinder, record);
+ manager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
+
manager.startChreProvider(List.of(getPresenceScanFilter()));
// This is an asynchronized process. The filters will be set in executor thread. So we need
// to wait for some time to get the correct result.
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() {
+
+ }
+ }
+}
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index a62ee7f..1d99190 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -23,6 +23,7 @@
import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
import static com.android.modules.utils.build.SdkLevel.isAtLeastU;
+import static com.android.server.connectivity.mdns.MdnsRecord.MAX_LABEL_LENGTH;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -73,6 +74,7 @@
import com.android.server.connectivity.mdns.MdnsServiceInfo;
import com.android.server.connectivity.mdns.MdnsSocketClientBase;
import com.android.server.connectivity.mdns.MdnsSocketProvider;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -81,11 +83,6 @@
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
-import java.nio.ByteBuffer;
-import java.nio.CharBuffer;
-import java.nio.charset.Charset;
-import java.nio.charset.CharsetEncoder;
-import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -108,8 +105,6 @@
*/
private static final String MDNS_DISCOVERY_MANAGER_VERSION = "mdns_discovery_manager_version";
private static final String LOCAL_DOMAIN_NAME = "local";
- // Max label length as per RFC 1034/1035
- private static final int MAX_LABEL_LENGTH = 63;
/**
* Enable advertising using the Java MdnsAdvertiser, instead of the legacy mdnsresponder
@@ -570,18 +565,7 @@
*/
@NonNull
private String truncateServiceName(@NonNull String originalName) {
- // UTF-8 is at most 4 bytes per character; return early in the common case where
- // the name can't possibly be over the limit given its string length.
- if (originalName.length() <= MAX_LABEL_LENGTH / 4) return originalName;
-
- final Charset utf8 = StandardCharsets.UTF_8;
- final CharsetEncoder encoder = utf8.newEncoder();
- final ByteBuffer out = ByteBuffer.allocate(MAX_LABEL_LENGTH);
- // encode will write as many characters as possible to the out buffer, and just
- // return an overflow code if there were too many characters (no need to check the
- // return code here, this method truncates the name on purpose).
- encoder.encode(CharBuffer.wrap(originalName), out, true /* endOfInput */);
- return new String(out.array(), 0, out.position(), utf8);
+ return MdnsUtils.truncateServiceName(originalName, MAX_LABEL_LENGTH);
}
private void stopDiscoveryManagerRequest(ClientRequest request, int clientId, int id,
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
index 33fef9d..a332da7 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -16,6 +16,8 @@
package com.android.server.connectivity.mdns;
+import static com.android.server.connectivity.mdns.MdnsRecord.MAX_LABEL_LENGTH;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.LinkAddress;
@@ -29,6 +31,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.SharedLog;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.PrintWriter;
import java.util.List;
@@ -359,7 +362,7 @@
// "Name (2)", then "Name (3)" etc.
// TODO: use a hidden method in NsdServiceInfo once MdnsAdvertiser is moved to service-t
final NsdServiceInfo newInfo = new NsdServiceInfo();
- newInfo.setServiceName(mOriginalName + " (" + (mConflictCount + renameCount + 1) + ")");
+ newInfo.setServiceName(getUpdatedServiceName(renameCount));
newInfo.setServiceType(mServiceInfo.getServiceType());
for (Map.Entry<String, byte[]> attr : mServiceInfo.getAttributes().entrySet()) {
newInfo.setAttribute(attr.getKey(),
@@ -372,6 +375,13 @@
return newInfo;
}
+ private String getUpdatedServiceName(int renameCount) {
+ final String suffix = " (" + (mConflictCount + renameCount + 1) + ")";
+ final String truncatedServiceName = MdnsUtils.truncateServiceName(mOriginalName,
+ MAX_LABEL_LENGTH - suffix.length());
+ return truncatedServiceName + suffix;
+ }
+
@NonNull
public NsdServiceInfo getServiceInfo() {
return mServiceInfo;
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
index bcee9d1..19630e3 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
@@ -46,6 +46,8 @@
public static final long RECEIPT_TIME_NOT_SENT = 0L;
public static final int CLASS_ANY = 0x00ff;
+ /** Max label length as per RFC 1034/1035 */
+ public static final int MAX_LABEL_LENGTH = 63;
/** Status indicating that the record is current. */
public static final int STATUS_OK = 0;
diff --git a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
index 4b9759d..5cc789f 100644
--- a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
+++ b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
@@ -21,6 +21,12 @@
import android.net.Network;
import android.os.Handler;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.StandardCharsets;
+
/**
* Mdns utility functions.
*/
@@ -73,4 +79,22 @@
@Nullable Network currentNetwork) {
return targetNetwork == null || targetNetwork.equals(currentNetwork);
}
-}
+
+ /**
+ * Truncate a service name to up to maxLength UTF-8 bytes.
+ */
+ public static String truncateServiceName(@NonNull String originalName, int maxLength) {
+ // UTF-8 is at most 4 bytes per character; return early in the common case where
+ // the name can't possibly be over the limit given its string length.
+ if (originalName.length() <= maxLength / 4) return originalName;
+
+ final Charset utf8 = StandardCharsets.UTF_8;
+ final CharsetEncoder encoder = utf8.newEncoder();
+ final ByteBuffer out = ByteBuffer.allocate(maxLength);
+ // encode will write as many characters as possible to the out buffer, and just
+ // return an overflow code if there were too many characters (no need to check the
+ // return code here, this method truncates the name on purpose).
+ encoder.encode(CharBuffer.wrap(originalName), out, true /* endOfInput */);
+ return new String(out.array(), 0, out.position(), utf8);
+ }
+}
\ No newline at end of file
diff --git a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
index 8d909ed..d966070 100644
--- a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
+++ b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
@@ -90,11 +90,6 @@
#undef ALOGF
-bool isGsiImage() {
- // this implementation matches 2 other places in the codebase (same function name too)
- return !access("/system/system_ext/etc/init/init.gsi.rc", F_OK);
-}
-
static const char* kClatdDir = "/apex/com.android.tethering/bin/for-system";
static const char* kClatdBin = "/apex/com.android.tethering/bin/for-system/clatd";
@@ -135,14 +130,6 @@
#undef V2
- // HACK: Some old vendor kernels lack ~5.10 backport of 'bpffs selinux genfscon' support.
- // This is *NOT* supported, but let's allow, at least for now, U+ GSI to boot on them.
- // (without this hack pixel5 R vendor + U gsi breaks)
- if (isGsiImage() && !bpf::isAtLeastKernelVersion(5, 10, 0)) {
- ALOGE("GSI with *BAD* pre-5.10 kernel lacking bpffs selinux genfscon support.");
- return;
- }
-
if (fatal) abort();
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
index cdfa0d0..106a49c 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
@@ -719,12 +719,13 @@
Log.i(TAG, "Setting Battery Saver Mode to " + enabled);
if (enabled) {
turnBatteryOn();
+ AmUtils.waitForBroadcastBarrier();
executeSilentShellCommand("cmd power set-mode 1");
} else {
executeSilentShellCommand("cmd power set-mode 0");
turnBatteryOff();
+ AmUtils.waitForBroadcastBarrier();
}
- AmUtils.waitForBroadcastBarrier();
}
protected void setDozeMode(boolean enabled) throws Exception {
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
index a917361..4b495cd 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
@@ -47,6 +47,8 @@
private const val SERVICE_ID_1 = 1
private const val SERVICE_ID_2 = 2
+private const val LONG_SERVICE_ID_1 = 3
+private const val LONG_SERVICE_ID_2 = 4
private const val TIMEOUT_MS = 10_000L
private val TEST_ADDR = parseNumericAddress("2001:db8::123")
private val TEST_LINKADDR = LinkAddress(TEST_ADDR, 64 /* prefixLength */)
@@ -56,16 +58,30 @@
private val SERVICE_1 = NsdServiceInfo("TestServiceName", "_advertisertest._tcp").apply {
port = 12345
- host = TEST_ADDR
+ hostAddresses = listOf(TEST_ADDR)
network = TEST_NETWORK_1
}
+private val LONG_SERVICE_1 =
+ NsdServiceInfo("a".repeat(48) + "TestServiceName", "_longadvertisertest._tcp").apply {
+ port = 12345
+ hostAddresses = listOf(TEST_ADDR)
+ network = TEST_NETWORK_1
+ }
+
private val ALL_NETWORKS_SERVICE = NsdServiceInfo("TestServiceName", "_advertisertest._tcp").apply {
port = 12345
- host = TEST_ADDR
+ hostAddresses = listOf(TEST_ADDR)
network = null
}
+private val LONG_ALL_NETWORKS_SERVICE =
+ NsdServiceInfo("a".repeat(48) + "TestServiceName", "_longadvertisertest._tcp").apply {
+ port = 12345
+ hostAddresses = listOf(TEST_ADDR)
+ network = null
+ }
+
@RunWith(DevSdkIgnoreRunner::class)
@IgnoreUpTo(Build.VERSION_CODES.S_V2)
class MdnsAdvertiserTest {
@@ -191,6 +207,9 @@
verify(socketProvider).requestSocket(eq(null), allNetSocketCbCaptor.capture())
val allNetSocketCb = allNetSocketCbCaptor.value
+ postSync { advertiser.addService(LONG_SERVICE_ID_1, LONG_SERVICE_1) }
+ postSync { advertiser.addService(LONG_SERVICE_ID_2, LONG_ALL_NETWORKS_SERVICE) }
+
// Callbacks for matching network and all networks both get the socket
postSync {
oneNetSocketCb.onSocketCreated(TEST_NETWORK_1, mockSocket1, listOf(TEST_LINKADDR))
@@ -200,10 +219,18 @@
val expectedRenamed = NsdServiceInfo(
"${ALL_NETWORKS_SERVICE.serviceName} (2)", ALL_NETWORKS_SERVICE.serviceType).apply {
port = ALL_NETWORKS_SERVICE.port
- host = ALL_NETWORKS_SERVICE.host
+ hostAddresses = ALL_NETWORKS_SERVICE.hostAddresses
network = ALL_NETWORKS_SERVICE.network
}
+ val expectedLongRenamed = NsdServiceInfo(
+ "${LONG_ALL_NETWORKS_SERVICE.serviceName.dropLast(4)} (2)",
+ LONG_ALL_NETWORKS_SERVICE.serviceType).apply {
+ port = LONG_ALL_NETWORKS_SERVICE.port
+ hostAddresses = LONG_ALL_NETWORKS_SERVICE.hostAddresses
+ network = LONG_ALL_NETWORKS_SERVICE.network
+ }
+
val intAdvCbCaptor = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
verify(mockDeps).makeAdvertiser(eq(mockSocket1), eq(listOf(TEST_LINKADDR)),
eq(thread.looper), any(), intAdvCbCaptor.capture(), eq(TEST_HOSTNAME)
@@ -212,6 +239,10 @@
argThat { it.matches(SERVICE_1) })
verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_2),
argThat { it.matches(expectedRenamed) })
+ verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_1),
+ argThat { it.matches(LONG_SERVICE_1) })
+ verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_2),
+ argThat { it.matches(expectedLongRenamed) })
doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
postSync { intAdvCbCaptor.value.onRegisterServiceSucceeded(
@@ -251,7 +282,7 @@
return Objects.equals(serviceName, other.serviceName) &&
Objects.equals(serviceType, other.serviceType) &&
Objects.equals(attributes, other.attributes) &&
- Objects.equals(host, other.host) &&
+ Objects.equals(hostAddresses, other.hostAddresses) &&
port == other.port &&
Objects.equals(network, other.network)
}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
index f584ed5..61c3a70 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
@@ -19,6 +19,7 @@
import android.os.Build
import com.android.server.connectivity.mdns.util.MdnsUtils.equalsIgnoreDnsCase
import com.android.server.connectivity.mdns.util.MdnsUtils.toDnsLowerCase
+import com.android.server.connectivity.mdns.util.MdnsUtils.truncateServiceName
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
import org.junit.Assert.assertEquals
@@ -65,4 +66,10 @@
"Test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
"\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<"))
}
+
+ @Test
+ fun testTruncateServiceName() {
+ assertEquals(truncateServiceName("测试abcde", 7), "测试a")
+ assertEquals(truncateServiceName("测试abcde", 100), "测试abcde")
+ }
}