Merge changes I6d0e5d34,I55af0796 into main
* changes:
Inline tetherInterface and untetherInterface
Separate route modification and update linkproperties
diff --git a/bpf/loader/Android.bp b/bpf/loader/Android.bp
index 780fe20..345e92b 100644
--- a/bpf/loader/Android.bp
+++ b/bpf/loader/Android.bp
@@ -42,6 +42,7 @@
shared_libs: [
"libbase",
"liblog",
+ "libbpf",
],
srcs: ["NetBpfLoad.cpp"],
apex_available: [
diff --git a/bpf/loader/NetBpfLoad.cpp b/bpf/loader/NetBpfLoad.cpp
index bad506f..b9ef766 100644
--- a/bpf/loader/NetBpfLoad.cpp
+++ b/bpf/loader/NetBpfLoad.cpp
@@ -17,6 +17,7 @@
#define LOG_TAG "NetBpfLoad"
#include <arpa/inet.h>
+#include <bpf/libbpf.h>
#include <dirent.h>
#include <elf.h>
#include <errno.h>
@@ -1438,10 +1439,12 @@
if (isAtLeastV) ++bpfloader_ver; // [46] BPFLOADER_MAINLINE_V_VERSION
if (isAtLeast25Q2) ++bpfloader_ver; // [47] BPFLOADER_MAINLINE_25Q2_VERSION
- ALOGI("NetBpfLoad v0.%u (%s) api:%d/%d kver:%07x (%s) uid:%d rc:%d%d",
+ ALOGI("NetBpfLoad v0.%u (%s) api:%d/%d kver:%07x (%s) libbpf: v%u.%u "
+ "uid:%d rc:%d%d",
bpfloader_ver, argv[0], android_get_device_api_level(), api_level,
- kernelVersion(), describeArch(), getuid(),
- has_platform_bpfloader_rc, has_platform_netbpfload_rc);
+ kernelVersion(), describeArch(), libbpf_major_version(),
+ libbpf_minor_version(), getuid(), has_platform_bpfloader_rc,
+ has_platform_netbpfload_rc);
if (!has_platform_bpfloader_rc && !has_platform_netbpfload_rc) {
ALOGE("Unable to find platform's bpfloader & netbpfload init scripts.");
diff --git a/service-t/src/com/android/server/connectivity/mdns/DiscoveryExecutor.java b/service-t/src/com/android/server/connectivity/mdns/DiscoveryExecutor.java
new file mode 100644
index 0000000..21af1a1
--- /dev/null
+++ b/service-t/src/com/android/server/connectivity/mdns/DiscoveryExecutor.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2024 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.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.util.Log;
+import android.util.Pair;
+
+import androidx.annotation.GuardedBy;
+
+import com.android.net.module.util.HandlerUtils;
+
+import java.util.ArrayList;
+import java.util.concurrent.Executor;
+
+/**
+ * A utility class to generate a handler, optionally with a looper, and to run functions on the
+ * newly created handler.
+ */
+public class DiscoveryExecutor implements Executor {
+ private static final String TAG = DiscoveryExecutor.class.getSimpleName();
+ @Nullable
+ private final HandlerThread mHandlerThread;
+
+ @GuardedBy("mPendingTasks")
+ @Nullable
+ private Handler mHandler;
+ // Store pending tasks and associated delay time. Each Pair represents a pending task
+ // (first) and its delay time (second).
+ @GuardedBy("mPendingTasks")
+ @NonNull
+ private final ArrayList<Pair<Runnable, Long>> mPendingTasks = new ArrayList<>();
+
+ DiscoveryExecutor(@Nullable Looper defaultLooper) {
+ if (defaultLooper != null) {
+ this.mHandlerThread = null;
+ synchronized (mPendingTasks) {
+ this.mHandler = new Handler(defaultLooper);
+ }
+ } else {
+ this.mHandlerThread = new HandlerThread(MdnsDiscoveryManager.class.getSimpleName()) {
+ @Override
+ protected void onLooperPrepared() {
+ synchronized (mPendingTasks) {
+ mHandler = new Handler(getLooper());
+ for (Pair<Runnable, Long> pendingTask : mPendingTasks) {
+ mHandler.postDelayed(pendingTask.first, pendingTask.second);
+ }
+ mPendingTasks.clear();
+ }
+ }
+ };
+ this.mHandlerThread.start();
+ }
+ }
+
+ /**
+ * Check if the current thread is the expected thread. If it is, run the given function.
+ * Otherwise, execute it using the handler.
+ */
+ public void checkAndRunOnHandlerThread(@NonNull Runnable function) {
+ if (this.mHandlerThread == null) {
+ // Callers are expected to already be running on the handler when a defaultLooper
+ // was provided
+ function.run();
+ } else {
+ execute(function);
+ }
+ }
+
+ /** Execute the given function */
+ @Override
+ public void execute(Runnable function) {
+ executeDelayed(function, 0L /* delayMillis */);
+ }
+
+ /** Execute the given function after the specified amount of time elapses. */
+ public void executeDelayed(Runnable function, long delayMillis) {
+ final Handler handler;
+ synchronized (mPendingTasks) {
+ if (this.mHandler == null) {
+ mPendingTasks.add(Pair.create(function, delayMillis));
+ return;
+ } else {
+ handler = this.mHandler;
+ }
+ }
+ handler.postDelayed(function, delayMillis);
+ }
+
+ /** Shutdown the thread if necessary. */
+ public void shutDown() {
+ if (this.mHandlerThread != null) {
+ this.mHandlerThread.quitSafely();
+ }
+ }
+
+ /**
+ * Ensures that the current running thread is the same as the handler thread.
+ */
+ public void ensureRunningOnHandlerThread() {
+ synchronized (mPendingTasks) {
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
+ }
+ }
+
+ /**
+ * Runs the specified task synchronously for dump method.
+ */
+ public void runWithScissorsForDumpIfReady(@NonNull Runnable function) {
+ final Handler handler;
+ synchronized (mPendingTasks) {
+ if (this.mHandler == null) {
+ Log.d(TAG, "The handler is not ready. Ignore the DiscoveryManager dump");
+ return;
+ } else {
+ handler = this.mHandler;
+ }
+ }
+ HandlerUtils.runWithScissorsForDump(handler, function, 10_000);
+ }
+}
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 c3306bd..bd00b70 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -35,6 +35,7 @@
import android.os.Looper;
import android.text.TextUtils;
import android.util.ArrayMap;
+import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
@@ -93,6 +94,7 @@
new ArrayMap<>();
private final MdnsFeatureFlags mMdnsFeatureFlags;
private final Map<String, Integer> mServiceTypeToOffloadPriority;
+ private final ArraySet<String> mOffloadServiceTypeDenyList;
/**
* Dependencies for {@link MdnsAdvertiser}, useful for testing.
@@ -160,6 +162,16 @@
return mInterfaceOffloadServices.getOrDefault(interfaceName, Collections.emptyList());
}
+ private boolean isInOffloadDenyList(@NonNull String serviceType) {
+ for (int i = 0; i < mOffloadServiceTypeDenyList.size(); ++i) {
+ final String denyListServiceType = mOffloadServiceTypeDenyList.valueAt(i);
+ if (DnsUtils.equalsIgnoreDnsCase(serviceType, denyListServiceType)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private final MdnsInterfaceAdvertiser.Callback mInterfaceAdvertiserCb =
new MdnsInterfaceAdvertiser.Callback() {
@Override
@@ -173,19 +185,25 @@
if (mMdnsFeatureFlags.mIsMdnsOffloadFeatureEnabled
// TODO: Enable offload when the serviceInfo contains a custom host.
&& TextUtils.isEmpty(registration.getServiceInfo().getHostname())) {
- final String interfaceName = advertiser.getSocketInterfaceName();
- final List<OffloadServiceInfoWrapper> existingOffloadServiceInfoWrappers =
- mInterfaceOffloadServices.computeIfAbsent(interfaceName,
- k -> new ArrayList<>());
- // Remove existing offload services from cache for update.
- existingOffloadServiceInfoWrappers.removeIf(item -> item.mServiceId == serviceId);
+ final String serviceType = registration.getServiceInfo().getServiceType();
+ if (isInOffloadDenyList(serviceType)) {
+ mSharedLog.i("Offload denied for service type: " + serviceType);
+ } else {
+ final String interfaceName = advertiser.getSocketInterfaceName();
+ final List<OffloadServiceInfoWrapper> existingOffloadServiceInfoWrappers =
+ mInterfaceOffloadServices.computeIfAbsent(interfaceName,
+ k -> new ArrayList<>());
+ // Remove existing offload services from cache for update.
+ existingOffloadServiceInfoWrappers.removeIf(
+ item -> item.mServiceId == serviceId);
- byte[] rawOffloadPacket = advertiser.getRawOffloadPayload(serviceId);
- final OffloadServiceInfoWrapper newOffloadServiceInfoWrapper = createOffloadService(
- serviceId, registration, rawOffloadPacket);
- existingOffloadServiceInfoWrappers.add(newOffloadServiceInfoWrapper);
- mCb.onOffloadStartOrUpdate(interfaceName,
- newOffloadServiceInfoWrapper.mOffloadServiceInfo);
+ byte[] rawOffloadPacket = advertiser.getRawOffloadPayload(serviceId);
+ final OffloadServiceInfoWrapper newOffloadServiceInfoWrapper =
+ createOffloadService(serviceId, registration, rawOffloadPacket);
+ existingOffloadServiceInfoWrappers.add(newOffloadServiceInfoWrapper);
+ mCb.onOffloadStartOrUpdate(interfaceName,
+ newOffloadServiceInfoWrapper.mOffloadServiceInfo);
+ }
}
// Wait for all current interfaces to be done probing before notifying of success.
@@ -846,6 +864,8 @@
final ConnectivityResources res = new ConnectivityResources(context);
mServiceTypeToOffloadPriority = parseOffloadPriorityList(
res.get().getStringArray(R.array.config_nsdOffloadServicesPriority), sharedLog);
+ mOffloadServiceTypeDenyList = new ArraySet<>(
+ res.get().getStringArray(R.array.config_nsdOffloadServicesDenyList));
}
private static Map<String, Integer> parseOffloadPriorityList(
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
index c833422..33bcb70 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -16,24 +16,17 @@
package com.android.server.connectivity.mdns;
-import static com.android.internal.annotations.VisibleForTesting.Visibility;
-
import android.Manifest.permission;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
-import android.os.Handler;
-import android.os.HandlerThread;
import android.os.Looper;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
-import androidx.annotation.GuardedBy;
-
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.DnsUtils;
-import com.android.net.module.util.HandlerUtils;
import com.android.net.module.util.SharedLog;
import java.io.IOException;
@@ -41,7 +34,6 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
-import java.util.concurrent.Executor;
/**
* This class keeps tracking the set of registered {@link MdnsServiceBrowserListener} instances, and
@@ -137,98 +129,6 @@
}
/**
- * A utility class to generate a handler, optionally with a looper, and to run functions on the
- * newly created handler.
- */
- @VisibleForTesting(visibility = Visibility.PRIVATE)
- static class DiscoveryExecutor implements Executor {
- private final HandlerThread handlerThread;
-
- @GuardedBy("pendingTasks")
- @Nullable private Handler handler;
- // Store pending tasks and associated delay time. Each Pair represents a pending task
- // (first) and its delay time (second).
- @GuardedBy("pendingTasks")
- @NonNull private final ArrayList<Pair<Runnable, Long>> pendingTasks = new ArrayList<>();
-
- DiscoveryExecutor(@Nullable Looper defaultLooper) {
- if (defaultLooper != null) {
- this.handlerThread = null;
- synchronized (pendingTasks) {
- this.handler = new Handler(defaultLooper);
- }
- } else {
- this.handlerThread = new HandlerThread(MdnsDiscoveryManager.class.getSimpleName()) {
- @Override
- protected void onLooperPrepared() {
- synchronized (pendingTasks) {
- handler = new Handler(getLooper());
- for (Pair<Runnable, Long> pendingTask : pendingTasks) {
- handler.postDelayed(pendingTask.first, pendingTask.second);
- }
- pendingTasks.clear();
- }
- }
- };
- this.handlerThread.start();
- }
- }
-
- public void checkAndRunOnHandlerThread(@NonNull Runnable function) {
- if (this.handlerThread == null) {
- // Callers are expected to already be running on the handler when a defaultLooper
- // was provided
- function.run();
- } else {
- execute(function);
- }
- }
-
- @Override
- public void execute(Runnable function) {
- executeDelayed(function, 0L /* delayMillis */);
- }
-
- public void executeDelayed(Runnable function, long delayMillis) {
- final Handler handler;
- synchronized (pendingTasks) {
- if (this.handler == null) {
- pendingTasks.add(Pair.create(function, delayMillis));
- return;
- } else {
- handler = this.handler;
- }
- }
- handler.postDelayed(function, delayMillis);
- }
-
- void shutDown() {
- if (this.handlerThread != null) {
- this.handlerThread.quitSafely();
- }
- }
-
- void ensureRunningOnHandlerThread() {
- synchronized (pendingTasks) {
- HandlerUtils.ensureRunningOnHandlerThread(handler);
- }
- }
-
- public void runWithScissorsForDumpIfReady(@NonNull Runnable function) {
- final Handler handler;
- synchronized (pendingTasks) {
- if (this.handler == null) {
- Log.d(TAG, "The handler is not ready. Ignore the DiscoveryManager dump");
- return;
- } else {
- handler = this.handler;
- }
- }
- HandlerUtils.runWithScissorsForDump(handler, function, 10_000);
- }
- }
-
- /**
* Do the cleanup of the MdnsDiscoveryManager
*/
public void shutDown() {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
index 2f3bdc5..1cf5e4d 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -95,6 +95,11 @@
"nsd_cached_services_retention_time";
public static final int DEFAULT_CACHED_SERVICES_RETENTION_TIME_MILLISECONDS = 10000;
+ /**
+ * A feature flag to control whether the accurate delay callback should be enabled.
+ */
+ public static final String NSD_ACCURATE_DELAY_CALLBACK = "nsd_accurate_delay_callback";
+
// Flag for offload feature
public final boolean mIsMdnsOffloadFeatureEnabled;
@@ -128,6 +133,9 @@
// Retention Time for cached services
public final long mCachedServicesRetentionTime;
+ // Flag for accurate delay callback
+ public final boolean mIsAccurateDelayCallbackEnabled;
+
// Flag to use shorter (16 characters + .local) hostnames
public final boolean mIsShortHostnamesEnabled;
@@ -231,6 +239,14 @@
}
/**
+ * Indicates whether {@link #NSD_ACCURATE_DELAY_CALLBACK} is enabled, including for testing.
+ */
+ public boolean isAccurateDelayCallbackEnabled() {
+ return mIsAccurateDelayCallbackEnabled
+ || isForceEnabledForTest(NSD_ACCURATE_DELAY_CALLBACK);
+ }
+
+ /**
* The constructor for {@link MdnsFeatureFlags}.
*/
public MdnsFeatureFlags(boolean isOffloadFeatureEnabled,
@@ -244,6 +260,7 @@
boolean avoidAdvertisingEmptyTxtRecords,
boolean isCachedServicesRemovalEnabled,
long cachedServicesRetentionTime,
+ boolean isAccurateDelayCallbackEnabled,
boolean isShortHostnamesEnabled,
@Nullable FlagOverrideProvider overrideProvider) {
mIsMdnsOffloadFeatureEnabled = isOffloadFeatureEnabled;
@@ -257,6 +274,7 @@
mAvoidAdvertisingEmptyTxtRecords = avoidAdvertisingEmptyTxtRecords;
mIsCachedServicesRemovalEnabled = isCachedServicesRemovalEnabled;
mCachedServicesRetentionTime = cachedServicesRetentionTime;
+ mIsAccurateDelayCallbackEnabled = isAccurateDelayCallbackEnabled;
mIsShortHostnamesEnabled = isShortHostnamesEnabled;
mOverrideProvider = overrideProvider;
}
@@ -281,6 +299,7 @@
private boolean mAvoidAdvertisingEmptyTxtRecords;
private boolean mIsCachedServicesRemovalEnabled;
private long mCachedServicesRetentionTime;
+ private boolean mIsAccurateDelayCallbackEnabled;
private boolean mIsShortHostnamesEnabled;
private FlagOverrideProvider mOverrideProvider;
@@ -299,6 +318,7 @@
mAvoidAdvertisingEmptyTxtRecords = true; // Default enabled.
mIsCachedServicesRemovalEnabled = false;
mCachedServicesRetentionTime = DEFAULT_CACHED_SERVICES_RETENTION_TIME_MILLISECONDS;
+ mIsAccurateDelayCallbackEnabled = false;
mIsShortHostnamesEnabled = true; // Default enabled.
mOverrideProvider = null;
}
@@ -426,6 +446,16 @@
}
/**
+ * Set whether the accurate delay callback is enabled.
+ *
+ * @see #NSD_ACCURATE_DELAY_CALLBACK
+ */
+ public Builder setIsAccurateDelayCallbackEnabled(boolean isAccurateDelayCallbackEnabled) {
+ mIsAccurateDelayCallbackEnabled = isAccurateDelayCallbackEnabled;
+ return this;
+ }
+
+ /**
* Set whether the short hostnames feature is enabled.
*
* @see #NSD_USE_SHORT_HOSTNAMES
@@ -450,6 +480,7 @@
mAvoidAdvertisingEmptyTxtRecords,
mIsCachedServicesRemovalEnabled,
mCachedServicesRetentionTime,
+ mIsAccurateDelayCallbackEnabled,
mIsShortHostnamesEnabled,
mOverrideProvider);
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
index 8c86fb8..56d4b9a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -20,6 +20,7 @@
import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE;
import static com.android.server.connectivity.mdns.MdnsServiceCache.ServiceExpiredCallback;
import static com.android.server.connectivity.mdns.MdnsServiceCache.findMatchedResponse;
+import static com.android.server.connectivity.mdns.MdnsQueryScheduler.ScheduledQueryTaskArgs;
import static com.android.server.connectivity.mdns.util.MdnsUtils.Clock;
import static com.android.server.connectivity.mdns.util.MdnsUtils.buildMdnsServiceInfoFromResponse;
@@ -36,6 +37,7 @@
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.DnsUtils;
+import com.android.net.module.util.RealtimeScheduler;
import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.mdns.util.MdnsUtils;
@@ -94,6 +96,9 @@
private final boolean removeServiceAfterTtlExpires =
MdnsConfigs.removeServiceAfterTtlExpires();
private final Clock clock;
+ // Use RealtimeScheduler for query scheduling, which allows for more accurate sending of
+ // queries.
+ @Nullable private final RealtimeScheduler realtimeScheduler;
@Nullable private MdnsSearchOptions searchOptions;
@@ -139,8 +144,7 @@
public void handleMessage(Message msg) {
switch (msg.what) {
case EVENT_START_QUERYTASK: {
- final MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs =
- (MdnsQueryScheduler.ScheduledQueryTaskArgs) msg.obj;
+ final ScheduledQueryTaskArgs taskArgs = (ScheduledQueryTaskArgs) msg.obj;
// QueryTask should be run immediately after being created (not be scheduled in
// advance). Because the result of "makeResponsesForResolve" depends on answers
// that were received before it is called, so to take into account all answers
@@ -174,7 +178,7 @@
final long now = clock.elapsedRealtime();
lastSentTime = now;
final long minRemainingTtl = getMinRemainingTtl(now);
- MdnsQueryScheduler.ScheduledQueryTaskArgs args =
+ final ScheduledQueryTaskArgs args =
mdnsQueryScheduler.scheduleNextRun(
sentResult.taskArgs.config,
minRemainingTtl,
@@ -189,10 +193,14 @@
sharedLog.log(String.format("Query sent with transactionId: %d. "
+ "Next run: sessionId: %d, in %d ms",
sentResult.transactionId, args.sessionId, timeToNextTaskMs));
- dependencies.sendMessageDelayed(
- handler,
- handler.obtainMessage(EVENT_START_QUERYTASK, args),
- timeToNextTaskMs);
+ if (realtimeScheduler != null) {
+ setDelayedTask(args, timeToNextTaskMs);
+ } else {
+ dependencies.sendMessageDelayed(
+ handler,
+ handler.obtainMessage(EVENT_START_QUERYTASK, args),
+ timeToNextTaskMs);
+ }
break;
}
default:
@@ -254,6 +262,14 @@
return List.of(new DatagramPacket(queryBuffer, 0, queryBuffer.length, address));
}
}
+
+ /**
+ * @see RealtimeScheduler
+ */
+ @Nullable
+ public RealtimeScheduler createRealtimeScheduler(@NonNull Handler handler) {
+ return new RealtimeScheduler(handler);
+ }
}
/**
@@ -301,6 +317,8 @@
this.mdnsQueryScheduler = new MdnsQueryScheduler();
this.cacheKey = new MdnsServiceCache.CacheKey(serviceType, socketKey);
this.featureFlags = featureFlags;
+ this.realtimeScheduler = featureFlags.isAccurateDelayCallbackEnabled()
+ ? dependencies.createRealtimeScheduler(handler) : null;
}
/**
@@ -310,6 +328,9 @@
removeScheduledTask();
mdnsQueryScheduler.cancelScheduledRun();
serviceCache.unregisterServiceExpiredCallback(cacheKey);
+ if (realtimeScheduler != null) {
+ realtimeScheduler.close();
+ }
}
private List<MdnsResponse> getExistingServices() {
@@ -317,6 +338,12 @@
? serviceCache.getCachedServices(cacheKey) : Collections.emptyList();
}
+ private void setDelayedTask(ScheduledQueryTaskArgs args, long timeToNextTaskMs) {
+ realtimeScheduler.removeDelayedMessage(EVENT_START_QUERYTASK);
+ realtimeScheduler.sendDelayedMessage(
+ handler.obtainMessage(EVENT_START_QUERYTASK, args), timeToNextTaskMs);
+ }
+
/**
* Registers {@code listener} for receiving discovery event of mDNS service instances, and
* starts
@@ -363,7 +390,7 @@
}
final long minRemainingTtl = getMinRemainingTtl(now);
if (hadReply) {
- MdnsQueryScheduler.ScheduledQueryTaskArgs args =
+ final ScheduledQueryTaskArgs args =
mdnsQueryScheduler.scheduleNextRun(
taskConfig,
minRemainingTtl,
@@ -377,10 +404,14 @@
final long timeToNextTaskMs = calculateTimeToNextTask(args, now);
sharedLog.log(String.format("Schedule a query. Next run: sessionId: %d, in %d ms",
args.sessionId, timeToNextTaskMs));
- dependencies.sendMessageDelayed(
- handler,
- handler.obtainMessage(EVENT_START_QUERYTASK, args),
- timeToNextTaskMs);
+ if (realtimeScheduler != null) {
+ setDelayedTask(args, timeToNextTaskMs);
+ } else {
+ dependencies.sendMessageDelayed(
+ handler,
+ handler.obtainMessage(EVENT_START_QUERYTASK, args),
+ timeToNextTaskMs);
+ }
} else {
final List<MdnsResponse> servicesToResolve = makeResponsesForResolve(socketKey);
final QueryTask queryTask = new QueryTask(
@@ -420,7 +451,11 @@
}
private void removeScheduledTask() {
- dependencies.removeMessages(handler, EVENT_START_QUERYTASK);
+ if (realtimeScheduler != null) {
+ realtimeScheduler.removeDelayedMessage(EVENT_START_QUERYTASK);
+ } else {
+ dependencies.removeMessages(handler, EVENT_START_QUERYTASK);
+ }
sharedLog.log("Remove EVENT_START_QUERYTASK"
+ ", current session: " + currentSessionId);
++currentSessionId;
@@ -506,10 +541,13 @@
}
}
}
- if (dependencies.hasMessages(handler, EVENT_START_QUERYTASK)) {
+ final boolean hasScheduledTask = realtimeScheduler != null
+ ? realtimeScheduler.hasDelayedMessage(EVENT_START_QUERYTASK)
+ : dependencies.hasMessages(handler, EVENT_START_QUERYTASK);
+ if (hasScheduledTask) {
final long now = clock.elapsedRealtime();
final long minRemainingTtl = getMinRemainingTtl(now);
- MdnsQueryScheduler.ScheduledQueryTaskArgs args =
+ final ScheduledQueryTaskArgs args =
mdnsQueryScheduler.maybeRescheduleCurrentRun(now, minRemainingTtl,
lastSentTime, currentSessionId + 1,
searchOptions.numOfQueriesBeforeBackoff());
@@ -518,10 +556,14 @@
final long timeToNextTaskMs = calculateTimeToNextTask(args, now);
sharedLog.log(String.format("Reschedule a query. Next run: sessionId: %d, in %d ms",
args.sessionId, timeToNextTaskMs));
- dependencies.sendMessageDelayed(
- handler,
- handler.obtainMessage(EVENT_START_QUERYTASK, args),
- timeToNextTaskMs);
+ if (realtimeScheduler != null) {
+ setDelayedTask(args, timeToNextTaskMs);
+ } else {
+ dependencies.sendMessageDelayed(
+ handler,
+ handler.obtainMessage(EVENT_START_QUERYTASK, args),
+ timeToNextTaskMs);
+ }
}
}
}
@@ -686,10 +728,10 @@
private static class QuerySentArguments {
private final int transactionId;
private final List<String> subTypes = new ArrayList<>();
- private final MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs;
+ private final ScheduledQueryTaskArgs taskArgs;
QuerySentArguments(int transactionId, @NonNull List<String> subTypes,
- @NonNull MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs) {
+ @NonNull ScheduledQueryTaskArgs taskArgs) {
this.transactionId = transactionId;
this.subTypes.addAll(subTypes);
this.taskArgs = taskArgs;
@@ -698,14 +740,14 @@
// A FutureTask that enqueues a single query, and schedule a new FutureTask for the next task.
private class QueryTask implements Runnable {
- private final MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs;
+ private final ScheduledQueryTaskArgs taskArgs;
private final List<MdnsResponse> servicesToResolve = new ArrayList<>();
private final List<String> subtypes = new ArrayList<>();
private final boolean sendDiscoveryQueries;
private final List<MdnsResponse> existingServices = new ArrayList<>();
private final boolean onlyUseIpv6OnIpv6OnlyNetworks;
private final SocketKey socketKey;
- QueryTask(@NonNull MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs,
+ QueryTask(@NonNull ScheduledQueryTaskArgs taskArgs,
@NonNull Collection<MdnsResponse> servicesToResolve,
@NonNull Collection<String> subtypes, boolean sendDiscoveryQueries,
@NonNull Collection<MdnsResponse> existingServices,
@@ -771,7 +813,7 @@
return minRemainingTtl == Long.MAX_VALUE ? 0 : minRemainingTtl;
}
- private static long calculateTimeToNextTask(MdnsQueryScheduler.ScheduledQueryTaskArgs args,
+ private static long calculateTimeToNextTask(ScheduledQueryTaskArgs args,
long now) {
return Math.max(args.timeToRun - now, 0);
}
diff --git a/service/ServiceConnectivityResources/res/values/config.xml b/service/ServiceConnectivityResources/res/values/config.xml
index 2d3647a..1b0f29d 100644
--- a/service/ServiceConnectivityResources/res/values/config.xml
+++ b/service/ServiceConnectivityResources/res/values/config.xml
@@ -135,6 +135,15 @@
<string-array translatable="false" name="config_nsdOffloadServicesPriority">
</string-array>
+ <!-- An array of service types that shouldn't be offloaded via NsdManager#registerOffloadEngine.
+ Format is [service type], for example: "_testservice._tcp"
+ Due to limited RAM in hardware offload, we prioritize user-impacting services.
+ _googlezone._tcp, an internal Googlecast service, was therefore blocked.
+ -->
+ <string-array name="config_nsdOffloadServicesDenyList" translatable="false">
+ <item>_googlezone._tcp</item>
+ </string-array>
+
<!-- Whether to use an ongoing notification for signing in to captive portals, instead of a
notification that can be dismissed. -->
<bool name="config_ongoingSignInNotification">false</bool>
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index 7ac86aa..5c0ba78 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -31,6 +31,7 @@
<item type="integer" name="config_networkWakeupPacketMask"/>
<item type="integer" name="config_networkNotifySwitchType"/>
<item type="array" name="config_networkNotifySwitches"/>
+ <item type="array" name="config_nsdOffloadServicesDenyList"/>
<item type="array" name="config_nsdOffloadServicesPriority"/>
<item type="bool" name="config_ongoingSignInNotification"/>
<item type="bool" name="config_autoCancelNetworkNotifications"/>
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index 0eab6e7..8034e57 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -438,7 +438,11 @@
srcs: [
"device/com/android/net/module/util/FdEventsReader.java",
"device/com/android/net/module/util/HandlerUtils.java",
+ "device/com/android/net/module/util/JniUtil.java",
+ "device/com/android/net/module/util/RealtimeScheduler.java",
"device/com/android/net/module/util/SharedLog.java",
+ "device/com/android/net/module/util/ServiceConnectivityJni.java",
+ "device/com/android/net/module/util/TimerFdUtils.java",
"framework/com/android/net/module/util/ByteUtils.java",
"framework/com/android/net/module/util/CollectionUtils.java",
"framework/com/android/net/module/util/DnsUtils.java",
diff --git a/staticlibs/device/com/android/net/module/util/BpfDump.java b/staticlibs/device/com/android/net/module/util/BpfDump.java
index 4227194..d79f52e 100644
--- a/staticlibs/device/com/android/net/module/util/BpfDump.java
+++ b/staticlibs/device/com/android/net/module/util/BpfDump.java
@@ -23,6 +23,7 @@
import android.util.Pair;
import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
import java.io.PrintWriter;
import java.nio.ByteBuffer;
@@ -132,17 +133,35 @@
}
}
+ public static class Dependencies {
+ /**
+ * Call {@link Os#access}
+ */
+ public boolean access(String path, int mode) throws ErrnoException {
+ return Os.access(path, mode);
+ }
+ }
+
/**
* Dump the BpfMap status
*/
public static <K extends Struct, V extends Struct> void dumpMapStatus(IBpfMap<K, V> map,
PrintWriter pw, String mapName, String path) {
+ dumpMapStatus(map, pw, mapName, path, new Dependencies());
+ }
+
+ /**
+ * Dump the BpfMap status. Only test should use this method directly.
+ */
+ @VisibleForTesting
+ public static <K extends Struct, V extends Struct> void dumpMapStatus(IBpfMap<K, V> map,
+ PrintWriter pw, String mapName, String path, Dependencies deps) {
if (map != null) {
pw.println(mapName + ": OK");
return;
}
try {
- Os.access(path, R_OK);
+ deps.access(path, R_OK);
pw.println(mapName + ": NULL(map is pinned to " + path + ")");
} catch (ErrnoException e) {
pw.println(mapName + ": NULL(map is not pinned to " + path + ": "
diff --git a/staticlibs/device/com/android/net/module/util/RealtimeScheduler.java b/staticlibs/device/com/android/net/module/util/RealtimeScheduler.java
index c8fdf72..2d95223 100644
--- a/staticlibs/device/com/android/net/module/util/RealtimeScheduler.java
+++ b/staticlibs/device/com/android/net/module/util/RealtimeScheduler.java
@@ -227,6 +227,13 @@
return enqueueTask(new MessageTask(msg, SystemClock.elapsedRealtime() + delayMs), delayMs);
}
+ private static boolean isMessageTask(Task task, int what) {
+ if (task instanceof MessageTask && ((MessageTask) task).mMessage.what == what) {
+ return true;
+ }
+ return false;
+ }
+
/**
* Remove a scheduled message.
*
@@ -234,8 +241,24 @@
*/
public void removeDelayedMessage(int what) {
ensureRunningOnCorrectThread();
- mTaskQueue.removeIf(task -> task instanceof MessageTask
- && ((MessageTask) task).mMessage.what == what);
+ mTaskQueue.removeIf(task -> isMessageTask(task, what));
+ }
+
+ /**
+ * Check if there is a scheduled message.
+ *
+ * @param what the message to be checked
+ * @return true if there is a target message, false otherwise.
+ */
+ public boolean hasDelayedMessage(int what) {
+ ensureRunningOnCorrectThread();
+
+ for (Task task : mTaskQueue) {
+ if (isMessageTask(task, what)) {
+ return true;
+ }
+ }
+ return false;
}
/**
diff --git a/staticlibs/tests/unit/host/python/apf_utils_test.py b/staticlibs/tests/unit/host/python/apf_utils_test.py
index 419b338..348df3b 100644
--- a/staticlibs/tests/unit/host/python/apf_utils_test.py
+++ b/staticlibs/tests/unit/host/python/apf_utils_test.py
@@ -103,8 +103,12 @@
self, mock_adb_shell: MagicMock
) -> None:
mock_adb_shell.return_value = """
+45: rmnet29: <POINTOPOINT,MULTICAST,NOARP> mtu 1500 qdisc ...
+ link/ether 73:01:23:45:df:e3 brd ff:ff:ff:ff:ff:ff
46: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq ...
link/ether 72:05:77:82:21:e0 brd ff:ff:ff:ff:ff:ff
+47: wlan1: <BROADCAST,MULTICAST> mtu 1500 qdisc ...
+ link/ether 6a:16:81:ff:82:9b brd ff:ff:ff:ff:ff:ff
"""
mac_address = get_hardware_address(self.mock_ad, "wlan0")
asserts.assert_equal(mac_address, "72:05:77:82:21:E0")
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/BpfDumpTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/BpfDumpTest.java
index a66dacd..673d9e6 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/BpfDumpTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/BpfDumpTest.java
@@ -17,18 +17,13 @@
package com.android.net.module.util;
import static android.system.OsConstants.EPERM;
-import static android.system.OsConstants.R_OK;
-
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doThrow;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
+import android.annotation.Nullable;
import android.system.ErrnoException;
-import android.system.Os;
import android.util.Pair;
import androidx.test.filters.SmallTest;
@@ -38,7 +33,6 @@
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.MockitoSession;
import java.io.PrintWriter;
import java.io.StringWriter;
@@ -129,9 +123,14 @@
assertTrue(dump.contains("key=789, val=123"));
}
- private String getDumpMapStatus(final IBpfMap<Struct.S32, Struct.S32> map) {
+ private String getDumpMapStatus(final IBpfMap<Struct.S32, Struct.S32> map,
+ @Nullable final BpfDump.Dependencies deps) {
final StringWriter sw = new StringWriter();
- BpfDump.dumpMapStatus(map, new PrintWriter(sw), "mapName", "mapPath");
+ if (deps == null) {
+ BpfDump.dumpMapStatus(map, new PrintWriter(sw), "mapName", "mapPath");
+ } else {
+ BpfDump.dumpMapStatus(map, new PrintWriter(sw), "mapName", "mapPath", deps);
+ }
return sw.toString();
}
@@ -139,25 +138,34 @@
public void testGetMapStatus() {
final IBpfMap<Struct.S32, Struct.S32> map =
new TestBpfMap<>(Struct.S32.class, Struct.S32.class);
- assertEquals("mapName: OK\n", getDumpMapStatus(map));
+ assertEquals("mapName: OK\n", getDumpMapStatus(map, null /* deps */));
}
@Test
- public void testGetMapStatusNull() {
- final MockitoSession session = mockitoSession()
- .spyStatic(Os.class)
- .startMocking();
- try {
- // Os.access succeeds
- doReturn(true).when(() -> Os.access("mapPath", R_OK));
- assertEquals("mapName: NULL(map is pinned to mapPath)\n", getDumpMapStatus(null));
+ public void testGetMapStatusNull_accessSucceed() {
+ // Os.access succeeds
+ assertEquals("mapName: NULL(map is pinned to mapPath)\n",
+ getDumpMapStatus(null /* map */,
+ new BpfDump.Dependencies() {
+ @Override
+ public boolean access(String path, int mode) {
+ return true;
+ }
+ })
+ );
+ }
- // Os.access throws EPERM
- doThrow(new ErrnoException("", EPERM)).when(() -> Os.access("mapPath", R_OK));
- assertEquals("mapName: NULL(map is not pinned to mapPath: Operation not permitted)\n",
- getDumpMapStatus(null));
- } finally {
- session.finishMocking();
- }
+ @Test
+ public void testGetMapStatusNull_accessThrow() {
+ // Os.access throws EPERM
+ assertEquals("mapName: NULL(map is not pinned to mapPath: Operation not permitted)\n",
+ getDumpMapStatus(null /* map */,
+ new BpfDump.Dependencies(){
+ @Override
+ public boolean access(String path, int mode) throws ErrnoException {
+ throw new ErrnoException("", EPERM);
+ }
+ })
+ );
}
}
diff --git a/staticlibs/testutils/host/python/apf_utils.py b/staticlibs/testutils/host/python/apf_utils.py
index 55ac860..49ffad6 100644
--- a/staticlibs/testutils/host/python/apf_utils.py
+++ b/staticlibs/testutils/host/python/apf_utils.py
@@ -162,14 +162,18 @@
"""
# Run the "ip link" command and get its output.
- ip_link_output = adb_utils.adb_shell(ad, f"ip link show {iface_name}")
+ ip_link_output = adb_utils.adb_shell(ad, f"ip link")
# Regular expression to extract the MAC address.
# Parse hardware address from ip link output like below:
+ # 45: rmnet29: <POINTOPOINT,MULTICAST,NOARP> mtu 1500 qdisc ...
+ # link/ether 73:01:23:45:df:e3 brd ff:ff:ff:ff:ff:ff
# 46: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq ...
# link/ether 72:05:77:82:21:e0 brd ff:ff:ff:ff:ff:ff
- pattern = r"link/ether (([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2})"
- match = re.search(pattern, ip_link_output)
+ # 47: wlan1: <BROADCAST,MULTICAST> mtu 1500 qdisc ...
+ # link/ether 6a:16:81:ff:82:9b brd ff:ff:ff:ff:ff:ff"
+ pattern = rf"{iface_name}:.*?link/ether (([0-9a-fA-F]{{2}}:){{5}}[0-9a-fA-F]{{2}})"
+ match = re.search(pattern, ip_link_output, re.DOTALL)
if match:
return match.group(1).upper() # Extract the MAC address string.
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/DiscoveryExecutorTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/DiscoveryExecutorTest.kt
new file mode 100644
index 0000000..51539a0
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/DiscoveryExecutorTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 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.connectivity.mdns
+
+import android.os.Build
+import android.os.HandlerThread
+import android.testing.TestableLooper
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import org.junit.After
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val DEFAULT_TIMEOUT = 2000L
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+class DiscoveryExecutorTest {
+ private val thread = HandlerThread(DiscoveryExecutorTest::class.simpleName).apply { start() }
+
+ @After
+ fun tearDown() {
+ thread.quitSafely()
+ thread.join()
+ }
+
+ @Test
+ fun testCheckAndRunOnHandlerThread() {
+ val testableLooper = TestableLooper(thread.looper)
+ val executor = DiscoveryExecutor(testableLooper.looper)
+ try {
+ val future = CompletableFuture<Boolean>()
+ executor.checkAndRunOnHandlerThread { future.complete(true) }
+ testableLooper.processAllMessages()
+ assertTrue(future.get(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS))
+ } finally {
+ testableLooper.destroy()
+ }
+
+ // Create a DiscoveryExecutor with the null defaultLooper and verify the task can execute
+ // normally.
+ val executor2 = DiscoveryExecutor(null /* defaultLooper */)
+ val future2 = CompletableFuture<Boolean>()
+ executor2.checkAndRunOnHandlerThread { future2.complete(true) }
+ assertTrue(future2.get(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS))
+ executor2.shutDown()
+ }
+
+ @Test
+ fun testExecute() {
+ val testableLooper = TestableLooper(thread.looper)
+ val executor = DiscoveryExecutor(testableLooper.looper)
+ try {
+ val future = CompletableFuture<Boolean>()
+ executor.execute { future.complete(true) }
+ assertFalse(future.isDone)
+ testableLooper.processAllMessages()
+ assertTrue(future.get(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS))
+ } finally {
+ testableLooper.destroy()
+ }
+ }
+
+ @Test
+ fun testExecuteDelayed() {
+ val testableLooper = TestableLooper(thread.looper)
+ val executor = DiscoveryExecutor(testableLooper.looper)
+ try {
+ // Verify the executeDelayed method
+ val future = CompletableFuture<Boolean>()
+ // Schedule a task with 999 ms delay
+ executor.executeDelayed({ future.complete(true) }, 999L)
+ testableLooper.processAllMessages()
+ assertFalse(future.isDone)
+
+ // 500 ms have elapsed but do not exceed the target time (999 ms)
+ // The function should not be executed.
+ testableLooper.moveTimeForward(500L)
+ testableLooper.processAllMessages()
+ assertFalse(future.isDone)
+
+ // 500 ms have elapsed again and have exceeded the target time (999 ms).
+ // The function should be executed.
+ testableLooper.moveTimeForward(500L)
+ testableLooper.processAllMessages()
+ assertTrue(future.get(500L, TimeUnit.MILLISECONDS))
+ } finally {
+ testableLooper.destroy()
+ }
+ }
+}
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 e6e6ecc..087617a 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
@@ -99,6 +99,13 @@
network = TEST_NETWORK_1
}
+private val GOOGLEZONE_SERVICE = NsdServiceInfo("TestServiceName", "_GOOglezone._tcp").apply {
+ subtypes = setOf(TEST_SUBTYPE)
+ port = 12345
+ hostAddresses = listOf(TEST_ADDR)
+ network = TEST_NETWORK_1
+}
+
private val SERVICE_1_SUBTYPE = NsdServiceInfo("TestServiceName", "_advertisertest._tcp").apply {
subtypes = setOf(TEST_SUBTYPE)
port = 12345
@@ -183,6 +190,10 @@
"5:_otherprioritytest._tcp"
)
+private val SERVICES_DENY_LIST = arrayOf(
+ "_googlezone._tcp",
+)
+
@RunWith(DevSdkIgnoreRunner::class)
@IgnoreUpTo(Build.VERSION_CODES.S_V2)
class MdnsAdvertiserTest {
@@ -247,6 +258,9 @@
doReturn(SERVICES_PRIORITY_LIST).`when`(resources).getStringArray(
R.array.config_nsdOffloadServicesPriority
)
+ doReturn(SERVICES_DENY_LIST).`when`(resources).getStringArray(
+ R.array.config_nsdOffloadServicesDenyList
+ )
ConnectivityResources.setResourcesContextForTest(context)
}
@@ -524,6 +538,44 @@
}
@Test
+ fun testAddService_NoOffloadForServiceTypeInDenyList() {
+ val advertiser =
+ MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
+ postSync {
+ advertiser.addOrUpdateService(
+ SERVICE_ID_1,
+ GOOGLEZONE_SERVICE,
+ DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1
+ )
+ }
+ val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
+ verify(socketProvider).requestSocket(eq(SERVICE_1.network), socketCbCaptor.capture())
+
+ val socketCb = socketCbCaptor.value
+ postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
+
+ val intAdvCbCaptor1 = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
+ verify(mockDeps).makeAdvertiser(
+ eq(mockSocket1),
+ eq(listOf(TEST_LINKADDR)),
+ eq(thread.looper),
+ any(),
+ intAdvCbCaptor1.capture(),
+ eq(TEST_HOSTNAME),
+ any(),
+ any()
+ )
+
+ doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
+ postSync {
+ intAdvCbCaptor1.value.onServiceProbingSucceeded(mockInterfaceAdvertiser1, SERVICE_ID_1)
+ }
+
+ verify(cb, never()).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), any())
+ }
+
+ @Test
fun testAddService_NoSubtypeForGoogleCastOffload() {
val advertiser =
MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
index 71a3274..758b822 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
@@ -19,8 +19,6 @@
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
@@ -35,12 +33,10 @@
import android.net.Network;
import android.os.Handler;
import android.os.HandlerThread;
-import android.testing.TestableLooper;
import android.text.TextUtils;
import android.util.Pair;
import com.android.net.module.util.SharedLog;
-import com.android.server.connectivity.mdns.MdnsDiscoveryManager.DiscoveryExecutor;
import com.android.server.connectivity.mdns.MdnsSocketClientBase.SocketCreationCallback;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
@@ -60,9 +56,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
-import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
/** Tests for {@link MdnsDiscoveryManager}. */
@DevSdkIgnoreRunner.MonitorThreadLeak
@@ -435,45 +429,6 @@
}
@Test
- public void testDiscoveryExecutor() throws Exception {
- final TestableLooper testableLooper = new TestableLooper(thread.getLooper());
- final DiscoveryExecutor executor = new DiscoveryExecutor(testableLooper.getLooper());
- try {
- // Verify the checkAndRunOnHandlerThread method
- final CompletableFuture<Boolean> future1 = new CompletableFuture<>();
- executor.checkAndRunOnHandlerThread(()-> future1.complete(true));
- assertTrue(future1.get(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS));
-
- // Verify the execute method
- final CompletableFuture<Boolean> future2 = new CompletableFuture<>();
- executor.execute(()-> future2.complete(true));
- testableLooper.processAllMessages();
- assertTrue(future2.get(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS));
-
- // Verify the executeDelayed method
- final CompletableFuture<Boolean> future3 = new CompletableFuture<>();
- // Schedule a task with 999 ms delay
- executor.executeDelayed(()-> future3.complete(true), 999L);
- testableLooper.processAllMessages();
- assertFalse(future3.isDone());
-
- // 500 ms have elapsed but do not exceed the target time (999 ms)
- // The function should not be executed.
- testableLooper.moveTimeForward(500L);
- testableLooper.processAllMessages();
- assertFalse(future3.isDone());
-
- // 500 ms have elapsed again and have exceeded the target time (999 ms).
- // The function should be executed.
- testableLooper.moveTimeForward(500L);
- testableLooper.processAllMessages();
- assertTrue(future3.get(500L, TimeUnit.MILLISECONDS));
- } finally {
- testableLooper.destroy();
- }
- }
-
- @Test
public void testRemoveServicesAfterAllListenersUnregistered() throws IOException {
final MdnsFeatureFlags mdnsFeatureFlags = MdnsFeatureFlags.newBuilder()
.setIsCachedServicesRemovalEnabled(true)
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
index 67f9d9c..dad03e0 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -59,6 +59,7 @@
import android.text.TextUtils;
import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.RealtimeScheduler;
import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
import com.android.server.connectivity.mdns.util.MdnsUtils;
@@ -127,6 +128,8 @@
private SharedLog mockSharedLog;
@Mock
private MdnsServiceTypeClient.Dependencies mockDeps;
+ @Mock
+ private RealtimeScheduler mockRealtimeScheduler;
@Captor
private ArgumentCaptor<MdnsServiceInfo> serviceInfoCaptor;
@@ -145,6 +148,7 @@
private Message delayMessage = null;
private Handler realHandler = null;
private MdnsFeatureFlags featureFlags = MdnsFeatureFlags.newBuilder().build();
+ private Message message = null;
@Before
@SuppressWarnings("DoNotMock")
@@ -244,10 +248,21 @@
return true;
}).when(mockDeps).sendMessage(any(Handler.class), any(Message.class));
- client = makeMdnsServiceTypeClient();
+ doAnswer(inv -> {
+ realHandler = (Handler) inv.getArguments()[0];
+ return mockRealtimeScheduler;
+ }).when(mockDeps).createRealtimeScheduler(any(Handler.class));
+
+ doAnswer(inv -> {
+ message = (Message) inv.getArguments()[0];
+ latestDelayMs = (long) inv.getArguments()[1];
+ return null;
+ }).when(mockRealtimeScheduler).sendDelayedMessage(any(), anyLong());
+
+ client = makeMdnsServiceTypeClient(featureFlags);
}
- private MdnsServiceTypeClient makeMdnsServiceTypeClient() {
+ private MdnsServiceTypeClient makeMdnsServiceTypeClient(MdnsFeatureFlags featureFlags) {
return new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
serviceCache, featureFlags);
@@ -1926,9 +1941,7 @@
@Test
public void testSendQueryWithKnownAnswers() throws Exception {
- client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
- mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
- serviceCache,
+ client = makeMdnsServiceTypeClient(
MdnsFeatureFlags.newBuilder().setIsQueryWithKnownAnswerEnabled(true).build());
doCallRealMethod().when(mockDeps).getDatagramPacketsFromMdnsPacket(
@@ -1990,9 +2003,7 @@
@Test
public void testSendQueryWithSubTypeWithKnownAnswers() throws Exception {
- client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
- mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
- serviceCache,
+ client = makeMdnsServiceTypeClient(
MdnsFeatureFlags.newBuilder().setIsQueryWithKnownAnswerEnabled(true).build());
doCallRealMethod().when(mockDeps).getDatagramPacketsFromMdnsPacket(
@@ -2114,6 +2125,73 @@
assertEquals(9680L, latestDelayMs);
}
+ @Test
+ public void sendQueries_AccurateDelayCallback() {
+ client = makeMdnsServiceTypeClient(
+ MdnsFeatureFlags.newBuilder().setIsAccurateDelayCallbackEnabled(true).build());
+
+ final int numOfQueriesBeforeBackoff = 2;
+ final MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+ .addSubtype(SUBTYPE)
+ .setQueryMode(AGGRESSIVE_QUERY_MODE)
+ .setNumOfQueriesBeforeBackoff(numOfQueriesBeforeBackoff)
+ .build();
+ startSendAndReceive(mockListenerOne, searchOptions);
+ verify(mockRealtimeScheduler, times(1)).removeDelayedMessage(EVENT_START_QUERYTASK);
+
+ // Verify that the first query has been sent.
+ verifyAndSendQuery(0 /* index */, 0 /* timeInMs */, true /* expectsUnicastResponse */,
+ true /* multipleSocketDiscovery */, 1 /* scheduledCount */,
+ 1 /* sendMessageCount */, true /* useAccurateDelayCallback */);
+
+ // Verify that the second query has been sent
+ verifyAndSendQuery(1 /* index */, 0 /* timeInMs */, false /* expectsUnicastResponse */,
+ true /* multipleSocketDiscovery */, 2 /* scheduledCount */,
+ 2 /* sendMessageCount */, true /* useAccurateDelayCallback */);
+
+ // Verify that the third query has been sent
+ verifyAndSendQuery(2 /* index */, TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS,
+ false /* expectsUnicastResponse */, true /* multipleSocketDiscovery */,
+ 3 /* scheduledCount */, 3 /* sendMessageCount */,
+ true /* useAccurateDelayCallback */);
+
+ // In backoff mode, the current scheduled task will be canceled and reschedule if the
+ // 0.8 * smallestRemainingTtl is larger than time to next run.
+ long currentTime = TEST_TTL / 2 + TEST_ELAPSED_REALTIME;
+ doReturn(currentTime).when(mockDecoderClock).elapsedRealtime();
+ doReturn(true).when(mockRealtimeScheduler).hasDelayedMessage(EVENT_START_QUERYTASK);
+ processResponse(createResponse(
+ "service-instance-1", "192.0.2.123", 5353,
+ SERVICE_TYPE_LABELS,
+ Collections.emptyMap(), TEST_TTL), socketKey);
+ // Verify that the message removal occurred.
+ verify(mockRealtimeScheduler, times(6)).removeDelayedMessage(EVENT_START_QUERYTASK);
+ assertNotNull(message);
+ verifyAndSendQuery(3 /* index */, (long) (TEST_TTL / 2 * 0.8) /* timeInMs */,
+ true /* expectsUnicastResponse */, true /* multipleSocketDiscovery */,
+ 5 /* scheduledCount */, 4 /* sendMessageCount */,
+ true /* useAccurateDelayCallback */);
+
+ // Stop sending packets.
+ stopSendAndReceive(mockListenerOne);
+ verify(mockRealtimeScheduler, times(8)).removeDelayedMessage(EVENT_START_QUERYTASK);
+ }
+
+ @Test
+ public void testTimerFdCloseProperly() {
+ client = makeMdnsServiceTypeClient(
+ MdnsFeatureFlags.newBuilder().setIsAccurateDelayCallbackEnabled(true).build());
+
+ // Start query
+ startSendAndReceive(mockListenerOne, MdnsSearchOptions.newBuilder().build());
+ verify(mockRealtimeScheduler, times(1)).removeDelayedMessage(EVENT_START_QUERYTASK);
+
+ // Stop query and verify the close() method has been called.
+ stopSendAndReceive(mockListenerOne);
+ verify(mockRealtimeScheduler, times(2)).removeDelayedMessage(EVENT_START_QUERYTASK);
+ verify(mockRealtimeScheduler).close();
+ }
+
private static MdnsServiceInfo matchServiceName(String name) {
return argThat(info -> info.getServiceInstanceName().equals(name));
}
@@ -2127,9 +2205,22 @@
private void verifyAndSendQuery(int index, long timeInMs, boolean expectsUnicastResponse,
boolean multipleSocketDiscovery, int scheduledCount) {
- // Dispatch the message
- if (delayMessage != null && realHandler != null) {
- dispatchMessage();
+ verifyAndSendQuery(index, timeInMs, expectsUnicastResponse,
+ multipleSocketDiscovery, scheduledCount, index + 1 /* sendMessageCount */,
+ false /* useAccurateDelayCallback */);
+ }
+
+ private void verifyAndSendQuery(int index, long timeInMs, boolean expectsUnicastResponse,
+ boolean multipleSocketDiscovery, int scheduledCount, int sendMessageCount,
+ boolean useAccurateDelayCallback) {
+ if (useAccurateDelayCallback && message != null && realHandler != null) {
+ runOnHandler(() -> realHandler.dispatchMessage(message));
+ message = null;
+ } else {
+ // Dispatch the message
+ if (delayMessage != null && realHandler != null) {
+ dispatchMessage();
+ }
}
assertEquals(timeInMs, latestDelayMs);
currentThreadExecutor.getAndClearLastScheduledRunnable().run();
@@ -2152,11 +2243,16 @@
eq(socketKey), eq(false));
}
}
- verify(mockDeps, times(index + 1))
+ verify(mockDeps, times(sendMessageCount))
.sendMessage(any(Handler.class), any(Message.class));
// Verify the task has been scheduled.
- verify(mockDeps, times(scheduledCount))
- .sendMessageDelayed(any(Handler.class), any(Message.class), anyLong());
+ if (useAccurateDelayCallback) {
+ verify(mockRealtimeScheduler, times(scheduledCount))
+ .sendDelayedMessage(any(), anyLong());
+ } else {
+ verify(mockDeps, times(scheduledCount))
+ .sendMessageDelayed(any(Handler.class), any(Message.class), anyLong());
+ }
}
private static String[] getTestServiceName(String instanceName) {