Merge changes from topic "fixTetheringRevert" into main
* changes:
Validate packageName with AppOpsManager#checkPackage
Revert^2 "Add UID and package name to TetheringRequest"
diff --git a/bpf/loader/NetBpfLoad.cpp b/bpf/loader/NetBpfLoad.cpp
index 9a049c7..c2a1d6e 100644
--- a/bpf/loader/NetBpfLoad.cpp
+++ b/bpf/loader/NetBpfLoad.cpp
@@ -1288,6 +1288,8 @@
#define APEX_MOUNT_POINT "/apex/com.android.tethering"
const char * const platformBpfLoader = "/system/bin/bpfloader";
+const char *const uprobestatsBpfLoader =
+ "/apex/com.android.uprobestats/bin/uprobestatsbpfload";
static int logTetheringApexVersion(void) {
char * found_blockdev = NULL;
@@ -1657,8 +1659,17 @@
}
// unreachable before U QPR3
- ALOGI("done, transferring control to platform bpfloader.");
+ {
+ ALOGI("done, transferring control to uprobestatsbpfload.");
+ const char *args[] = {
+ uprobestatsBpfLoader,
+ NULL,
+ };
+ execve(args[0], (char **)args, envp);
+ }
+ ALOGI("unable to execute uprobestatsbpfload, transferring control to "
+ "platform bpfloader.");
// platform BpfLoader *needs* to run as root
const char * args[] = { platformBpfLoader, NULL, };
execve(args[0], (char**)args, envp);
diff --git a/bpf/netd/BpfHandler.cpp b/bpf/netd/BpfHandler.cpp
index 8e4c2c6..50e0329 100644
--- a/bpf/netd/BpfHandler.cpp
+++ b/bpf/netd/BpfHandler.cpp
@@ -120,18 +120,22 @@
}
if (modules::sdklevel::IsAtLeastV()) {
- RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_CONNECT4_PROG_PATH,
- cg_fd, BPF_CGROUP_INET4_CONNECT));
- RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_CONNECT6_PROG_PATH,
- cg_fd, BPF_CGROUP_INET6_CONNECT));
- RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_UDP4_RECVMSG_PROG_PATH,
- cg_fd, BPF_CGROUP_UDP4_RECVMSG));
- RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_UDP6_RECVMSG_PROG_PATH,
- cg_fd, BPF_CGROUP_UDP6_RECVMSG));
- RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_UDP4_SENDMSG_PROG_PATH,
- cg_fd, BPF_CGROUP_UDP4_SENDMSG));
- RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_UDP6_SENDMSG_PROG_PATH,
- cg_fd, BPF_CGROUP_UDP6_SENDMSG));
+ // V requires 4.19+, so technically this 2nd 'if' is not required, but it
+ // doesn't hurt us to try to support AOSP forks that try to support older kernels.
+ if (bpf::isAtLeastKernelVersion(4, 19, 0)) {
+ RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_CONNECT4_PROG_PATH,
+ cg_fd, BPF_CGROUP_INET4_CONNECT));
+ RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_CONNECT6_PROG_PATH,
+ cg_fd, BPF_CGROUP_INET6_CONNECT));
+ RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_UDP4_RECVMSG_PROG_PATH,
+ cg_fd, BPF_CGROUP_UDP4_RECVMSG));
+ RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_UDP6_RECVMSG_PROG_PATH,
+ cg_fd, BPF_CGROUP_UDP6_RECVMSG));
+ RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_UDP4_SENDMSG_PROG_PATH,
+ cg_fd, BPF_CGROUP_UDP4_SENDMSG));
+ RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_UDP6_SENDMSG_PROG_PATH,
+ cg_fd, BPF_CGROUP_UDP6_SENDMSG));
+ }
if (bpf::isAtLeastKernelVersion(5, 4, 0)) {
RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_GETSOCKOPT_PROG_PATH,
@@ -161,12 +165,16 @@
}
if (modules::sdklevel::IsAtLeastV()) {
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET4_CONNECT) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET6_CONNECT) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP4_RECVMSG) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP6_RECVMSG) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP4_SENDMSG) <= 0) abort();
- if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP6_SENDMSG) <= 0) abort();
+ // V requires 4.19+, so technically this 2nd 'if' is not required, but it
+ // doesn't hurt us to try to support AOSP forks that try to support older kernels.
+ if (bpf::isAtLeastKernelVersion(4, 19, 0)) {
+ if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET4_CONNECT) <= 0) abort();
+ if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET6_CONNECT) <= 0) abort();
+ if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP4_RECVMSG) <= 0) abort();
+ if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP6_RECVMSG) <= 0) abort();
+ if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP4_SENDMSG) <= 0) abort();
+ if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP6_SENDMSG) <= 0) abort();
+ }
if (bpf::isAtLeastKernelVersion(5, 4, 0)) {
if (bpf::queryProgram(cg_fd, BPF_CGROUP_GETSOCKOPT) <= 0) abort();
diff --git a/bpf/progs/netd.c b/bpf/progs/netd.c
index cbe856d..ed0eed5 100644
--- a/bpf/progs/netd.c
+++ b/bpf/progs/netd.c
@@ -709,32 +709,32 @@
return block_port(ctx);
}
-DEFINE_NETD_V_BPF_PROG_KVER("connect4/inet4_connect", AID_ROOT, AID_ROOT, inet4_connect, KVER_4_14)
+DEFINE_NETD_V_BPF_PROG_KVER("connect4/inet4_connect", AID_ROOT, AID_ROOT, inet4_connect, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return check_localhost(ctx);
}
-DEFINE_NETD_V_BPF_PROG_KVER("connect6/inet6_connect", AID_ROOT, AID_ROOT, inet6_connect, KVER_4_14)
+DEFINE_NETD_V_BPF_PROG_KVER("connect6/inet6_connect", AID_ROOT, AID_ROOT, inet6_connect, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return check_localhost(ctx);
}
-DEFINE_NETD_V_BPF_PROG_KVER("recvmsg4/udp4_recvmsg", AID_ROOT, AID_ROOT, udp4_recvmsg, KVER_4_14)
+DEFINE_NETD_V_BPF_PROG_KVER("recvmsg4/udp4_recvmsg", AID_ROOT, AID_ROOT, udp4_recvmsg, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return check_localhost(ctx);
}
-DEFINE_NETD_V_BPF_PROG_KVER("recvmsg6/udp6_recvmsg", AID_ROOT, AID_ROOT, udp6_recvmsg, KVER_4_14)
+DEFINE_NETD_V_BPF_PROG_KVER("recvmsg6/udp6_recvmsg", AID_ROOT, AID_ROOT, udp6_recvmsg, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return check_localhost(ctx);
}
-DEFINE_NETD_V_BPF_PROG_KVER("sendmsg4/udp4_sendmsg", AID_ROOT, AID_ROOT, udp4_sendmsg, KVER_4_14)
+DEFINE_NETD_V_BPF_PROG_KVER("sendmsg4/udp4_sendmsg", AID_ROOT, AID_ROOT, udp4_sendmsg, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return check_localhost(ctx);
}
-DEFINE_NETD_V_BPF_PROG_KVER("sendmsg6/udp6_sendmsg", AID_ROOT, AID_ROOT, udp6_sendmsg, KVER_4_14)
+DEFINE_NETD_V_BPF_PROG_KVER("sendmsg6/udp6_sendmsg", AID_ROOT, AID_ROOT, udp6_sendmsg, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return check_localhost(ctx);
}
diff --git a/framework-t/src/android/net/TrafficStats.java b/framework-t/src/android/net/TrafficStats.java
index 1294b3e..caf3152 100644
--- a/framework-t/src/android/net/TrafficStats.java
+++ b/framework-t/src/android/net/TrafficStats.java
@@ -17,8 +17,6 @@
package android.net;
import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
-import static android.net.NetworkStats.UID_ALL;
-import static android.os.Process.SYSTEM_UID;
import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
@@ -188,8 +186,6 @@
private static INetworkStatsService sStatsService;
@GuardedBy("TrafficStats.class")
private static INetworkStatsService sStatsServiceForTest = null;
- @GuardedBy("TrafficStats.class")
- private static int sMyUidForTest = UID_ALL;
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
private synchronized static INetworkStatsService getStatsService() {
@@ -202,12 +198,7 @@
}
/** @hide */
- protected static int getMyUid() {
- synchronized (TrafficStats.class) {
- if (sMyUidForTest != UID_ALL) {
- return sMyUidForTest;
- }
- }
+ private static int getMyUid() {
return android.os.Process.myUid();
}
@@ -224,18 +215,6 @@
}
/**
- * Set myUid for test, or UID_ALL to reset.
- *
- * @hide
- */
- @VisibleForTesting(visibility = PRIVATE)
- public static void setMyUidForTest(int myUid) {
- synchronized (TrafficStats.class) {
- sMyUidForTest = myUid;
- }
- }
-
- /**
* Snapshot of {@link NetworkStats} when the currently active profiling
* session started, or {@code null} if no session active.
*
@@ -289,8 +268,8 @@
private static class SocketTagger extends dalvik.system.SocketTagger {
- // TODO: set to false
- private static final boolean LOGD = true;
+ // Enable log with `setprop log.tag.TrafficStats DEBUG` and restart the module.
+ private static final boolean LOGD = Log.isLoggable(TAG, Log.DEBUG);
SocketTagger() {
}
@@ -1006,13 +985,6 @@
/** @hide */
public static long getUidStats(int uid, int type) {
- // Perform a quick check on the UID to avoid unnecessary work.
- // This mirrors a similar check on the service side, but is primarily for
- // efficiency rather than security, as user-space checks can be bypassed.
- final int myUid = getMyUid();
- if (!isEntryValueTypeValid(type) || (myUid != SYSTEM_UID && myUid != uid)) {
- return UNSUPPORTED;
- }
final StatsResult stats;
try {
stats = getStatsService().getUidStats(uid);
@@ -1024,9 +996,6 @@
/** @hide */
public static long getTotalStats(int type) {
- if (!isEntryValueTypeValid(type)) {
- return UNSUPPORTED;
- }
final StatsResult stats;
try {
stats = getStatsService().getTotalStats();
@@ -1038,9 +1007,6 @@
/** @hide */
public static long getIfaceStats(String iface, int type) {
- if (!isEntryValueTypeValid(type)) {
- return UNSUPPORTED;
- }
final StatsResult stats;
try {
stats = getStatsService().getIfaceStats(iface);
diff --git a/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
index ca97d07..667aad1 100644
--- a/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
+++ b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
@@ -19,9 +19,8 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.NetworkStats;
-import android.util.LruCache;
-import com.android.internal.annotations.GuardedBy;
+import com.android.net.module.util.LruCacheWithExpiry;
import java.time.Clock;
import java.util.Objects;
@@ -31,9 +30,8 @@
* A thread-safe cache for storing and retrieving {@link NetworkStats.Entry} objects,
* with an adjustable expiry duration to manage data freshness.
*/
-class TrafficStatsRateLimitCache {
- private final Clock mClock;
- private final long mExpiryDurationMs;
+class TrafficStatsRateLimitCache extends
+ LruCacheWithExpiry<TrafficStatsRateLimitCache.TrafficStatsCacheKey, NetworkStats.Entry> {
/**
* Constructs a new {@link TrafficStatsRateLimitCache} with the specified expiry duration.
@@ -43,19 +41,17 @@
* @param maxSize Maximum number of entries.
*/
TrafficStatsRateLimitCache(@NonNull Clock clock, long expiryDurationMs, int maxSize) {
- mClock = clock;
- mExpiryDurationMs = expiryDurationMs;
- mMap = new LruCache<>(maxSize);
+ super(clock, expiryDurationMs, maxSize, it -> !it.isEmpty());
}
- private static class TrafficStatsCacheKey {
+ public static class TrafficStatsCacheKey {
@Nullable
- public final String iface;
- public final int uid;
+ private final String mIface;
+ private final int mUid;
TrafficStatsCacheKey(@Nullable String iface, int uid) {
- this.iface = iface;
- this.uid = uid;
+ this.mIface = iface;
+ this.mUid = uid;
}
@Override
@@ -63,29 +59,15 @@
if (this == o) return true;
if (!(o instanceof TrafficStatsCacheKey)) return false;
TrafficStatsCacheKey that = (TrafficStatsCacheKey) o;
- return uid == that.uid && Objects.equals(iface, that.iface);
+ return mUid == that.mUid && Objects.equals(mIface, that.mIface);
}
@Override
public int hashCode() {
- return Objects.hash(iface, uid);
+ return Objects.hash(mIface, mUid);
}
}
- private static class TrafficStatsCacheValue {
- public final long timestamp;
- @NonNull
- public final NetworkStats.Entry entry;
-
- TrafficStatsCacheValue(long timestamp, NetworkStats.Entry entry) {
- this.timestamp = timestamp;
- this.entry = entry;
- }
- }
-
- @GuardedBy("mMap")
- private final LruCache<TrafficStatsCacheKey, TrafficStatsCacheValue> mMap;
-
/**
* Retrieves a {@link NetworkStats.Entry} from the cache, associated with the given key.
*
@@ -95,16 +77,7 @@
*/
@Nullable
NetworkStats.Entry get(String iface, int uid) {
- final TrafficStatsCacheKey key = new TrafficStatsCacheKey(iface, uid);
- synchronized (mMap) { // Synchronize for thread-safety
- final TrafficStatsCacheValue value = mMap.get(key);
- if (value != null && !isExpired(value.timestamp)) {
- return value.entry;
- } else {
- mMap.remove(key); // Remove expired entries
- return null;
- }
- }
+ return super.get(new TrafficStatsCacheKey(iface, uid));
}
/**
@@ -122,19 +95,7 @@
@Nullable
NetworkStats.Entry getOrCompute(String iface, int uid,
@NonNull Supplier<NetworkStats.Entry> supplier) {
- synchronized (mMap) {
- final NetworkStats.Entry cachedValue = get(iface, uid);
- if (cachedValue != null) {
- return cachedValue;
- }
-
- // Entry not found or expired, compute it
- final NetworkStats.Entry computedEntry = supplier.get();
- if (computedEntry != null && !computedEntry.isEmpty()) {
- put(iface, uid, computedEntry);
- }
- return computedEntry;
- }
+ return super.getOrCompute(new TrafficStatsCacheKey(iface, uid), supplier);
}
/**
@@ -145,23 +106,7 @@
* @param entry The {@link NetworkStats.Entry} to store in the cache.
*/
void put(String iface, int uid, @NonNull final NetworkStats.Entry entry) {
- Objects.requireNonNull(entry);
- final TrafficStatsCacheKey key = new TrafficStatsCacheKey(iface, uid);
- synchronized (mMap) { // Synchronize for thread-safety
- mMap.put(key, new TrafficStatsCacheValue(mClock.millis(), entry));
- }
+ super.put(new TrafficStatsCacheKey(iface, uid), entry);
}
- /**
- * Clear the cache.
- */
- void clear() {
- synchronized (mMap) {
- mMap.evictAll();
- }
- }
-
- private boolean isExpired(long timestamp) {
- return mClock.millis() > timestamp + mExpiryDurationMs;
- }
}
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index e503312..9015434 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -121,6 +121,7 @@
import static android.net.connectivity.ConnectivityCompatChanges.NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION;
import static android.os.Process.INVALID_UID;
import static android.os.Process.VPN_UID;
+import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
import static android.system.OsConstants.ETH_P_ALL;
import static android.system.OsConstants.IPPROTO_TCP;
import static android.system.OsConstants.IPPROTO_UDP;
@@ -145,10 +146,12 @@
import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermissionOr;
import static com.android.net.module.util.PermissionUtils.hasAnyPermissionOf;
import static com.android.server.ConnectivityStatsLog.CONNECTIVITY_STATE_SAMPLE;
+import static com.android.server.connectivity.ConnectivityFlags.CELLULAR_DATA_INACTIVITY_TIMEOUT;
import static com.android.server.connectivity.ConnectivityFlags.DELAY_DESTROY_SOCKETS;
import static com.android.server.connectivity.ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING;
import static com.android.server.connectivity.ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS;
import static com.android.server.connectivity.ConnectivityFlags.REQUEST_RESTRICTED_WIFI;
+import static com.android.server.connectivity.ConnectivityFlags.WIFI_DATA_INACTIVITY_TIMEOUT;
import android.Manifest;
import android.annotation.CheckResult;
@@ -1610,6 +1613,18 @@
connectivityServiceInternalHandler);
}
+ /** Returns the data inactivity timeout to be used for cellular networks */
+ public int getDefaultCellularDataInactivityTimeout() {
+ return DeviceConfigUtils.getDeviceConfigPropertyInt(NAMESPACE_TETHERING,
+ CELLULAR_DATA_INACTIVITY_TIMEOUT, 10);
+ }
+
+ /** Returns the data inactivity timeout to be used for WiFi networks */
+ public int getDefaultWifiDataInactivityTimeout() {
+ return DeviceConfigUtils.getDeviceConfigPropertyInt(NAMESPACE_TETHERING,
+ WIFI_DATA_INACTIVITY_TIMEOUT, 15);
+ }
+
/**
* @see DeviceConfigUtils#isTetheringFeatureEnabled
*/
@@ -1958,8 +1973,13 @@
// But reading the trunk stable flags from mainline modules is not supported yet.
// So enabling this feature on V+ release.
mTrackMultiNetworkActivities = mDeps.isAtLeastV();
+ final int defaultCellularDataInactivityTimeout =
+ mDeps.getDefaultCellularDataInactivityTimeout();
+ final int defaultWifiDataInactivityTimeout =
+ mDeps.getDefaultWifiDataInactivityTimeout();
mNetworkActivityTracker = new LegacyNetworkActivityTracker(mContext, mNetd, mHandler,
- mTrackMultiNetworkActivities);
+ mTrackMultiNetworkActivities, defaultCellularDataInactivityTimeout,
+ defaultWifiDataInactivityTimeout);
final NetdCallback netdCallback = new NetdCallback();
try {
@@ -13027,6 +13047,8 @@
// Key is netId. Value is configured idle timer information.
private final SparseArray<IdleTimerParams> mActiveIdleTimers = new SparseArray<>();
private final boolean mTrackMultiNetworkActivities;
+ private final int mDefaultCellularDataInactivityTimeout;
+ private final int mDefaultWifiDataInactivityTimeout;
// Store netIds of Wi-Fi networks whose idletimers report that they are active
private final Set<Integer> mActiveWifiNetworks = new ArraySet<>();
// Store netIds of cellular networks whose idletimers report that they are active
@@ -13043,11 +13065,14 @@
}
LegacyNetworkActivityTracker(@NonNull Context context, @NonNull INetd netd,
- @NonNull Handler handler, boolean trackMultiNetworkActivities) {
+ @NonNull Handler handler, boolean trackMultiNetworkActivities,
+ int defaultCellularDataInactivityTimeout, int defaultWifiDataInactivityTimeout) {
mContext = context;
mNetd = netd;
mHandler = handler;
mTrackMultiNetworkActivities = trackMultiNetworkActivities;
+ mDefaultCellularDataInactivityTimeout = defaultCellularDataInactivityTimeout;
+ mDefaultWifiDataInactivityTimeout = defaultWifiDataInactivityTimeout;
}
private void ensureRunningOnConnectivityServiceThread() {
@@ -13247,13 +13272,13 @@
NetworkCapabilities.TRANSPORT_CELLULAR)) {
timeout = Settings.Global.getInt(mContext.getContentResolver(),
ConnectivitySettingsManager.DATA_ACTIVITY_TIMEOUT_MOBILE,
- 10);
+ mDefaultCellularDataInactivityTimeout);
type = NetworkCapabilities.TRANSPORT_CELLULAR;
} else if (networkAgent.networkCapabilities.hasTransport(
NetworkCapabilities.TRANSPORT_WIFI)) {
timeout = Settings.Global.getInt(mContext.getContentResolver(),
ConnectivitySettingsManager.DATA_ACTIVITY_TIMEOUT_WIFI,
- 15);
+ mDefaultWifiDataInactivityTimeout);
type = NetworkCapabilities.TRANSPORT_WIFI;
} else {
return false; // do not track any other networks
@@ -13377,6 +13402,12 @@
public void dump(IndentingPrintWriter pw) {
pw.print("mTrackMultiNetworkActivities="); pw.println(mTrackMultiNetworkActivities);
+
+ pw.print("mDefaultCellularDataInactivityTimeout=");
+ pw.println(mDefaultCellularDataInactivityTimeout);
+ pw.print("mDefaultWifiDataInactivityTimeout=");
+ pw.println(mDefaultWifiDataInactivityTimeout);
+
pw.print("mIsDefaultNetworkActive="); pw.println(mIsDefaultNetworkActive);
pw.print("mDefaultNetwork="); pw.println(mDefaultNetwork);
pw.println("Idle timers:");
diff --git a/service/src/com/android/server/connectivity/ConnectivityFlags.java b/service/src/com/android/server/connectivity/ConnectivityFlags.java
index df87316..93335f1 100644
--- a/service/src/com/android/server/connectivity/ConnectivityFlags.java
+++ b/service/src/com/android/server/connectivity/ConnectivityFlags.java
@@ -44,6 +44,11 @@
public static final String BACKGROUND_FIREWALL_CHAIN = "background_firewall_chain";
+ public static final String CELLULAR_DATA_INACTIVITY_TIMEOUT =
+ "cellular_data_inactivity_timeout";
+
+ public static final String WIFI_DATA_INACTIVITY_TIMEOUT = "wifi_data_inactivity_timeout";
+
public static final String DELAY_DESTROY_SOCKETS = "delay_destroy_sockets";
public static final String USE_DECLARED_METHODS_FOR_CALLBACKS =
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index a825b87..f484027 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -671,11 +671,12 @@
visibility: ["//visibility:private"],
}
+// Sources outside of com.android.net.module.util should not be added because many modules depend on
+// them and need jarjar rules
filegroup {
name: "net-utils-all-srcs",
srcs: [
"device/**/*.java",
- ":framework-connectivity-shared-srcs",
":net-utils-framework-common-srcs",
],
visibility: ["//visibility:private"],
diff --git a/staticlibs/device/com/android/net/module/util/PrivateAddressCoordinator.java b/staticlibs/device/com/android/net/module/util/PrivateAddressCoordinator.java
index 7fcbd4e..bb95585 100644
--- a/staticlibs/device/com/android/net/module/util/PrivateAddressCoordinator.java
+++ b/staticlibs/device/com/android/net/module/util/PrivateAddressCoordinator.java
@@ -35,12 +35,12 @@
import android.net.NetworkCapabilities;
import android.os.RemoteException;
import android.util.ArrayMap;
-import android.util.IndentingPrintWriter;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
+import java.io.PrintWriter;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.util.ArrayList;
@@ -459,34 +459,27 @@
}
}
- // TODO: dump PrivateAddressCoordinator when dumping RoutingCoordinatorService.
- void dump(final IndentingPrintWriter pw) {
+ // TODO: dump PrivateAddressCoordinator when dumping RoutingCoordinatorService and apply
+ // indentation.
+ void dump(final PrintWriter pw) {
pw.println("mTetheringPrefixes:");
- pw.increaseIndent();
for (IpPrefix prefix : mTetheringPrefixes) {
pw.println(prefix);
}
- pw.decreaseIndent();
pw.println("mUpstreamPrefixMap:");
- pw.increaseIndent();
for (int i = 0; i < mUpstreamPrefixMap.size(); i++) {
pw.println(mUpstreamPrefixMap.keyAt(i) + " - " + mUpstreamPrefixMap.valueAt(i));
}
- pw.decreaseIndent();
pw.println("mDownstreams:");
- pw.increaseIndent();
for (LinkAddress downstream : mDownstreams.values()) {
pw.println(downstream);
}
- pw.decreaseIndent();
pw.println("mCachedAddresses:");
- pw.increaseIndent();
for (int i = 0; i < mCachedAddresses.size(); i++) {
pw.println(mCachedAddresses.keyAt(i) + " - " + mCachedAddresses.valueAt(i));
}
- pw.decreaseIndent();
}
}
diff --git a/staticlibs/framework/com/android/net/module/util/LruCacheWithExpiry.java b/staticlibs/framework/com/android/net/module/util/LruCacheWithExpiry.java
new file mode 100644
index 0000000..80088b9
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/LruCacheWithExpiry.java
@@ -0,0 +1,149 @@
+/*
+ * 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.net.module.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.LruCache;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.time.Clock;
+import java.util.Objects;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+/**
+ * An LRU cache that stores key-value pairs with an expiry time.
+ *
+ * <p>This cache uses an {@link LruCache} to store entries and evicts the least
+ * recently used entries when the cache reaches its maximum capacity. It also
+ * supports an expiry time for each entry, allowing entries to be automatically
+ * removed from the cache after a certain duration.
+ *
+ * @param <K> The type of keys used to identify cached entries.
+ * @param <V> The type of values stored in the cache.
+ *
+ * @hide
+ */
+public class LruCacheWithExpiry<K, V> {
+ private final Clock mClock;
+ private final long mExpiryDurationMs;
+ @GuardedBy("mMap")
+ private final LruCache<K, CacheValue<V>> mMap;
+ private final Predicate<V> mShouldCacheValue;
+
+ /**
+ * Constructs a new {@link LruCacheWithExpiry} with the specified parameters.
+ *
+ * @param clock The {@link Clock} to use for determining timestamps.
+ * @param expiryDurationMs The expiry duration for cached entries in milliseconds.
+ * @param maxSize The maximum number of entries to hold in the cache.
+ * @param shouldCacheValue A {@link Predicate} that determines whether a given value should be
+ * cached. This can be used to filter out certain values from being
+ * stored in the cache.
+ */
+ public LruCacheWithExpiry(@NonNull Clock clock, long expiryDurationMs, int maxSize,
+ Predicate<V> shouldCacheValue) {
+ mClock = clock;
+ mExpiryDurationMs = expiryDurationMs;
+ mMap = new LruCache<>(maxSize);
+ mShouldCacheValue = shouldCacheValue;
+ }
+
+ /**
+ * Retrieves a value from the cache, associated with the given key.
+ *
+ * @param key The key to look up in the cache.
+ * @return The cached value, or {@code null} if not found or expired.
+ */
+ @Nullable
+ public V get(@NonNull K key) {
+ synchronized (mMap) {
+ final CacheValue<V> value = mMap.get(key);
+ if (value != null && !isExpired(value.timestamp)) {
+ return value.entry;
+ } else {
+ mMap.remove(key); // Remove expired entries
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Retrieves a value from the cache, associated with the given key.
+ * If the entry is not found in the cache or has expired, computes it using the provided
+ * {@code supplier} and stores the result in the cache.
+ *
+ * @param key The key to look up in the cache.
+ * @param supplier The {@link Supplier} to compute the value if not found or expired.
+ * @return The cached or computed value, or {@code null} if the {@code supplier} returns null.
+ */
+ @Nullable
+ public V getOrCompute(@NonNull K key, @NonNull Supplier<V> supplier) {
+ synchronized (mMap) {
+ final V cachedValue = get(key);
+ if (cachedValue != null) {
+ return cachedValue;
+ }
+
+ // Entry not found or expired, compute it
+ final V computedValue = supplier.get();
+ if (computedValue != null && mShouldCacheValue.test(computedValue)) {
+ put(key, computedValue);
+ }
+ return computedValue;
+ }
+ }
+
+ /**
+ * Stores a value in the cache, associated with the given key.
+ *
+ * @param key The key to associate with the value.
+ * @param value The value to store in the cache.
+ */
+ public void put(@NonNull K key, @NonNull V value) {
+ Objects.requireNonNull(value);
+ synchronized (mMap) {
+ mMap.put(key, new CacheValue<>(mClock.millis(), value));
+ }
+ }
+
+ /**
+ * Clear the cache.
+ */
+ public void clear() {
+ synchronized (mMap) {
+ mMap.evictAll();
+ }
+ }
+
+ private boolean isExpired(long timestamp) {
+ return mClock.millis() > timestamp + mExpiryDurationMs;
+ }
+
+ private static class CacheValue<V> {
+ public final long timestamp;
+ @NonNull
+ public final V entry;
+
+ CacheValue(long timestamp, V entry) {
+ this.timestamp = timestamp;
+ this.entry = entry;
+ }
+ }
+}
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index a5ad7f2..9e57f69 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -70,6 +70,14 @@
":ConnectivityTestPreparer",
":CtsCarrierServicePackage",
],
+ errorprone: {
+ enabled: true,
+ // Error-prone checking only warns of problems when building. To make the build fail with
+ // these errors, list the specific error-prone problems below.
+ javacflags: [
+ "-Xep:NullablePrimitive:ERROR",
+ ],
+ },
}
// Networking CTS tests for development and release. These tests always target the platform SDK
diff --git a/tests/cts/net/AndroidTestTemplate.xml b/tests/cts/net/AndroidTestTemplate.xml
index 7590a2b..965d1f6 100644
--- a/tests/cts/net/AndroidTestTemplate.xml
+++ b/tests/cts/net/AndroidTestTemplate.xml
@@ -19,6 +19,7 @@
<option name="config-descriptor:metadata" key="parameter" value="instant_app" />
<option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
<option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user_on_secondary_display" />
<option name="config-descriptor:metadata" key="mainline-param" value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex" />
<option name="config-descriptor:metadata" key="mainline-param" value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk" />
diff --git a/tests/cts/netpermission/internetpermission/Android.bp b/tests/cts/netpermission/internetpermission/Android.bp
index e0424ac..71d2b6e 100644
--- a/tests/cts/netpermission/internetpermission/Android.bp
+++ b/tests/cts/netpermission/internetpermission/Android.bp
@@ -32,4 +32,7 @@
],
host_required: ["net-tests-utils-host-common"],
sdk_version: "test_current",
+ data: [
+ ":ConnectivityTestPreparer",
+ ],
}
diff --git a/tests/cts/netpermission/internetpermission/AndroidTest.xml b/tests/cts/netpermission/internetpermission/AndroidTest.xml
index ad9a731..13deb82 100644
--- a/tests/cts/netpermission/internetpermission/AndroidTest.xml
+++ b/tests/cts/netpermission/internetpermission/AndroidTest.xml
@@ -20,6 +20,7 @@
<option name="config-descriptor:metadata" key="parameter" value="instant_app" />
<option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
<option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user_on_secondary_display" />
<option name="not-shardable" value="true" />
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="cleanup-apks" value="true" />
diff --git a/tests/cts/netpermission/updatestatspermission/Android.bp b/tests/cts/netpermission/updatestatspermission/Android.bp
index 689ce74..b324dc8 100644
--- a/tests/cts/netpermission/updatestatspermission/Android.bp
+++ b/tests/cts/netpermission/updatestatspermission/Android.bp
@@ -36,5 +36,6 @@
"cts",
"general-tests",
],
+ data: [":ConnectivityTestPreparer"],
host_required: ["net-tests-utils-host-common"],
}
diff --git a/tests/unit/java/android/net/TrafficStatsTest.kt b/tests/unit/java/android/net/TrafficStatsTest.kt
deleted file mode 100644
index 6e8f3db..0000000
--- a/tests/unit/java/android/net/TrafficStatsTest.kt
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * 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 android.net
-
-import android.net.NetworkStats.UID_ALL
-import android.net.TrafficStats.UNSUPPORTED
-import android.net.netstats.StatsResult
-import android.os.Build
-import android.os.Process.SYSTEM_UID
-import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.DevSdkIgnoreRunner
-import org.junit.After
-import org.junit.Assert.assertEquals
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.Mockito.doReturn
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-
-@RunWith(DevSdkIgnoreRunner::class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
-class TrafficStatsTest {
- private val binder = mock(INetworkStatsService::class.java)
- private val myUid = android.os.Process.myUid()
- private val notMyUid = myUid + 1
- private val mockSystemUidStatsResult = StatsResult(1L, 2L, 3L, 4L)
- private val mockMyUidStatsResult = StatsResult(5L, 6L, 7L, 8L)
- private val mockNotMyUidStatsResult = StatsResult(9L, 10L, 11L, 12L)
- private val unsupportedStatsResult =
- StatsResult(UNSUPPORTED.toLong(), UNSUPPORTED.toLong(),
- UNSUPPORTED.toLong(), UNSUPPORTED.toLong())
-
- @Before
- fun setUp() {
- TrafficStats.setServiceForTest(binder)
- doReturn(mockSystemUidStatsResult).`when`(binder).getUidStats(SYSTEM_UID)
- doReturn(mockMyUidStatsResult).`when`(binder).getUidStats(myUid)
- doReturn(mockNotMyUidStatsResult).`when`(binder).getUidStats(notMyUid)
- }
-
- @After
- fun tearDown() {
- TrafficStats.setServiceForTest(null)
- TrafficStats.setMyUidForTest(UID_ALL)
- }
-
- private fun assertUidStats(uid: Int, stats: StatsResult) {
- assertEquals(stats.rxBytes, TrafficStats.getUidRxBytes(uid))
- assertEquals(stats.rxPackets, TrafficStats.getUidRxPackets(uid))
- assertEquals(stats.txBytes, TrafficStats.getUidTxBytes(uid))
- assertEquals(stats.txPackets, TrafficStats.getUidTxPackets(uid))
- }
-
- // Verify a normal caller could get a quick UNSUPPORTED result in the TrafficStats
- // without accessing the service if query stats other than itself.
- @Test
- fun testGetUidStats_appCaller() {
- assertUidStats(SYSTEM_UID, unsupportedStatsResult)
- assertUidStats(notMyUid, unsupportedStatsResult)
- verify(binder, never()).getUidStats(anyInt())
- assertUidStats(myUid, mockMyUidStatsResult)
- }
-
- // Verify that callers with SYSTEM_UID can access network
- // stats for other UIDs. While this behavior is not officially documented
- // in the API, it exists for compatibility with existing callers that may
- // rely on it.
- @Test
- fun testGetUidStats_systemCaller() {
- TrafficStats.setMyUidForTest(SYSTEM_UID)
- assertUidStats(SYSTEM_UID, mockSystemUidStatsResult)
- assertUidStats(myUid, mockMyUidStatsResult)
- assertUidStats(notMyUid, mockNotMyUidStatsResult)
- }
-}
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 999d17d..f7d7c87 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -2369,6 +2369,18 @@
mScheduledEvaluationTimeouts.add(new Pair<>(network.netId, delayMs));
super.scheduleEvaluationTimeout(handler, network, delayMs);
}
+
+ @Override
+ public int getDefaultCellularDataInactivityTimeout() {
+ // Needed to mock out the dependency on DeviceConfig
+ return 10;
+ }
+
+ @Override
+ public int getDefaultWifiDataInactivityTimeout() {
+ // Needed to mock out the dependency on DeviceConfig
+ return 15;
+ }
}
private class AutomaticOnOffKeepaliveTrackerDependencies
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt
index df0a2cc..ccbd6b3 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt
@@ -21,6 +21,7 @@
import android.net.ConnectivityManager.EXTRA_DEVICE_TYPE
import android.net.ConnectivityManager.EXTRA_IS_ACTIVE
import android.net.ConnectivityManager.EXTRA_REALTIME_NS
+import android.net.ConnectivitySettingsManager
import android.net.LinkProperties
import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.NET_CAPABILITY_IMS
@@ -41,12 +42,14 @@
import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.RecorderCallback.CallbackEntry.Lost
import com.android.testutils.TestableNetworkCallback
+import java.time.Duration
import kotlin.test.assertNotNull
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyString
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.anyLong
@@ -69,6 +72,18 @@
@IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
class CSNetworkActivityTest : CSTest() {
+ private fun setMobileDataActivityTimeout(timeoutSeconds: Int) {
+ ConnectivitySettingsManager.setMobileDataActivityTimeout(
+ context, Duration.ofSeconds(timeoutSeconds.toLong())
+ )
+ }
+
+ private fun setWifiDataActivityTimeout(timeoutSeconds: Int) {
+ ConnectivitySettingsManager.setWifiDataActivityTimeout(
+ context, Duration.ofSeconds(timeoutSeconds.toLong())
+ )
+ }
+
private fun getRegisteredNetdUnsolicitedEventListener(): BaseNetdUnsolicitedEventListener {
val captor = ArgumentCaptor.forClass(BaseNetdUnsolicitedEventListener::class.java)
verify(netd).registerUnsolicitedEventListener(captor.capture())
@@ -252,8 +267,122 @@
cm.unregisterNetworkCallback(dataNetworkCb)
cm.unregisterNetworkCallback(imsNetworkCb)
}
+
+ @Test
+ fun testCellularIdleTimerSettingsTimeout() {
+ val cellNc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build()
+ val cellLp = LinkProperties().apply {
+ interfaceName = DATA_CELL_IFNAME
+ }
+
+ val settingsTimeout: Int = deps.defaultCellDataInactivityTimeoutForTest + 432
+ // DATA_ACTIVITY_TIMEOUT_MOBILE is set, so the default should be ignored.
+ setMobileDataActivityTimeout(settingsTimeout)
+ val cellAgent = Agent(nc = cellNc, lp = cellLp)
+ cellAgent.connect()
+
+ verify(netd).idletimerAddInterface(eq(DATA_CELL_IFNAME), eq(settingsTimeout), anyString())
+ }
+
+ @Test
+ fun testCellularIdleTimerDefaultTimeout() {
+ val cellNc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build()
+ val cellLp = LinkProperties().apply {
+ interfaceName = DATA_CELL_IFNAME
+ }
+
+ val testTimeout: Int = deps.defaultCellDataInactivityTimeoutForTest
+ // DATA_ACTIVITY_TIMEOUT_MOBILE is not set, so the default should be used.
+ val cellAgent = Agent(nc = cellNc, lp = cellLp)
+ cellAgent.connect()
+
+ verify(netd).idletimerAddInterface(eq(DATA_CELL_IFNAME), eq(testTimeout), anyString())
+ }
+
+ @Test
+ fun testCellularIdleTimerDisabled() {
+ val cellNc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build()
+ val cellLp = LinkProperties().apply {
+ interfaceName = DATA_CELL_IFNAME
+ }
+ setMobileDataActivityTimeout(0)
+ val cellAgent = Agent(nc = cellNc, lp = cellLp)
+ cellAgent.connect()
+
+ verify(netd, never()).idletimerAddInterface(eq(DATA_CELL_IFNAME), anyInt(), anyString())
+ }
+
+ @Test
+ fun testWifiIdleTimerSettingsTimeout() {
+ val wifiNc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build()
+ val wifiLp = LinkProperties().apply {
+ interfaceName = WIFI_IFNAME
+ }
+ val settingsTimeout: Int = deps.defaultWifiDataInactivityTimeout + 435
+ setWifiDataActivityTimeout(settingsTimeout)
+ // DATA_ACTIVITY_TIMEOUT_MOBILE is set, so the default should be ignored.
+ val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+ wifiAgent.connect()
+
+ verify(netd).idletimerAddInterface(eq(WIFI_IFNAME), eq(settingsTimeout), anyString())
+ }
+
+ @Test
+ fun testWifiIdleTimerDefaultTimeout() {
+ val wifiNc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build()
+ val wifiLp = LinkProperties().apply {
+ interfaceName = WIFI_IFNAME
+ }
+ val testTimeout: Int = deps.defaultWifiDataInactivityTimeoutForTest
+ // DATA_ACTIVITY_TIMEOUT_WIFI is not set, so the default should be used.
+ val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+ wifiAgent.connect()
+
+ verify(netd).idletimerAddInterface(eq(WIFI_IFNAME), eq(testTimeout), anyString())
+ }
+
+ @Test
+ fun testWifiIdleTimerDisabled() {
+ val wifiNc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build()
+ val wifiLp = LinkProperties().apply {
+ interfaceName = WIFI_IFNAME
+ }
+ setWifiDataActivityTimeout(0)
+ val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+ wifiAgent.connect()
+
+ verify(netd, never()).idletimerAddInterface(eq(WIFI_IFNAME), anyInt(), anyString())
+ }
}
+
internal fun CSContext.expectDataActivityBroadcast(
deviceType: Int,
isActive: Boolean,
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
index 46c25d2..ae196a6 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -341,6 +341,18 @@
}
}
+ // Need a non-zero value to avoid disarming the timer.
+ val defaultCellDataInactivityTimeoutForTest: Int = 81
+ override fun getDefaultCellularDataInactivityTimeout(): Int {
+ return defaultCellDataInactivityTimeoutForTest
+ }
+
+ // Need a non-zero value to avoid disarming the timer.
+ val defaultWifiDataInactivityTimeoutForTest: Int = 121
+ override fun getDefaultWifiDataInactivityTimeout(): Int {
+ return defaultWifiDataInactivityTimeoutForTest
+ }
+
override fun isChangeEnabled(changeId: Long, pkg: String, user: UserHandle) =
changeId in enabledChangeIds
override fun isChangeEnabled(changeId: Long, uid: Int) =
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkSpecifier.java b/thread/framework/java/android/net/thread/ThreadNetworkSpecifier.java
new file mode 100644
index 0000000..205c16e
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ThreadNetworkSpecifier.java
@@ -0,0 +1,227 @@
+/*
+ * 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 android.net.thread;
+
+import static android.net.thread.ActiveOperationalDataset.LENGTH_EXTENDED_PAN_ID;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.NetworkSpecifier;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.net.module.util.HexDump;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Represents and identifies a Thread network.
+ *
+ * @hide
+ */
+public final class ThreadNetworkSpecifier extends NetworkSpecifier implements Parcelable {
+ /** The Extended PAN ID of a Thread network. */
+ @NonNull private final byte[] mExtendedPanId;
+
+ /** The Active Timestamp of a Thread network. */
+ @Nullable private final OperationalDatasetTimestamp mActiveTimestamp;
+
+ private final boolean mRouterEligibleForLeader;
+
+ private ThreadNetworkSpecifier(@NonNull Builder builder) {
+ mExtendedPanId = builder.mExtendedPanId.clone();
+ mActiveTimestamp = builder.mActiveTimestamp;
+ mRouterEligibleForLeader = builder.mRouterEligibleForLeader;
+ }
+
+ /** Returns the Extended PAN ID of the Thread network this specifier refers to. */
+ @NonNull
+ public byte[] getExtendedPanId() {
+ return mExtendedPanId.clone();
+ }
+
+ /**
+ * Returns the Active Timestamp of the Thread network this specifier refers to, or {@code null}
+ * if not specified.
+ */
+ @Nullable
+ public OperationalDatasetTimestamp getActiveTimestamp() {
+ return mActiveTimestamp;
+ }
+
+ /**
+ * Returns {@code true} if this device can be a leader during attachment when there are no
+ * nearby routers.
+ */
+ public boolean isRouterEligibleForLeader() {
+ return mRouterEligibleForLeader;
+ }
+
+ /**
+ * Returns {@code true} if both {@link #getExtendedPanId()} and {@link #getActiveTimestamp()}
+ * (if not {@code null}) of the two {@link ThreadNetworkSpecifier} objects are equal.
+ *
+ * <p>Note value of {@link #isRouterEligibleForLeader()} is expiclitly excluded because this is
+ * not part of the identifier.
+ *
+ * @hide
+ */
+ @Override
+ public boolean canBeSatisfiedBy(@Nullable NetworkSpecifier other) {
+ if (!(other instanceof ThreadNetworkSpecifier)) {
+ return false;
+ }
+ ThreadNetworkSpecifier otherSpecifier = (ThreadNetworkSpecifier) other;
+
+ if (mActiveTimestamp != null && !mActiveTimestamp.equals(otherSpecifier.mActiveTimestamp)) {
+ return false;
+ }
+
+ return Arrays.equals(mExtendedPanId, otherSpecifier.mExtendedPanId);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (!(other instanceof ThreadNetworkSpecifier)) {
+ return false;
+ } else if (this == other) {
+ return true;
+ }
+
+ ThreadNetworkSpecifier otherSpecifier = (ThreadNetworkSpecifier) other;
+
+ return Arrays.equals(mExtendedPanId, otherSpecifier.mExtendedPanId)
+ && Objects.equals(mActiveTimestamp, otherSpecifier.mActiveTimestamp)
+ && mRouterEligibleForLeader == otherSpecifier.mRouterEligibleForLeader;
+ }
+
+ @Override
+ public int hashCode() {
+ return deepHashCode(mExtendedPanId, mActiveTimestamp, mRouterEligibleForLeader);
+ }
+
+ /** An easy-to-use wrapper of {@link Arrays#deepHashCode}. */
+ private static int deepHashCode(Object... values) {
+ return Arrays.deepHashCode(values);
+ }
+
+ @Override
+ public String toString() {
+ return "ThreadNetworkSpecifier{extendedPanId="
+ + HexDump.toHexString(mExtendedPanId)
+ + ", activeTimestamp="
+ + mActiveTimestamp
+ + ", routerEligibleForLeader="
+ + mRouterEligibleForLeader
+ + "}";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeByteArray(mExtendedPanId);
+ dest.writeByteArray(mActiveTimestamp != null ? mActiveTimestamp.toTlvValue() : null);
+ dest.writeBoolean(mRouterEligibleForLeader);
+ }
+
+ public static final @NonNull Parcelable.Creator<ThreadNetworkSpecifier> CREATOR =
+ new Parcelable.Creator<ThreadNetworkSpecifier>() {
+ @Override
+ public ThreadNetworkSpecifier createFromParcel(Parcel in) {
+ byte[] extendedPanId = in.createByteArray();
+ byte[] activeTimestampBytes = in.createByteArray();
+ OperationalDatasetTimestamp activeTimestamp =
+ (activeTimestampBytes != null)
+ ? OperationalDatasetTimestamp.fromTlvValue(activeTimestampBytes)
+ : null;
+ boolean routerEligibleForLeader = in.readBoolean();
+
+ return new Builder(extendedPanId)
+ .setActiveTimestamp(activeTimestamp)
+ .setRouterEligibleForLeader(routerEligibleForLeader)
+ .build();
+ }
+
+ @Override
+ public ThreadNetworkSpecifier[] newArray(int size) {
+ return new ThreadNetworkSpecifier[size];
+ }
+ };
+
+ /** The builder for creating {@link ActiveOperationalDataset} objects. */
+ public static final class Builder {
+ @NonNull private final byte[] mExtendedPanId;
+ @Nullable private OperationalDatasetTimestamp mActiveTimestamp;
+ private boolean mRouterEligibleForLeader;
+
+ /**
+ * Creates a new {@link Builder} object with given Extended PAN ID.
+ *
+ * @throws IllegalArgumentException if {@code extendedPanId} is {@code null} or the length
+ * is not {@link ActiveOperationalDataset#LENGTH_EXTENDED_PAN_ID}
+ */
+ public Builder(@NonNull byte[] extendedPanId) {
+ if (extendedPanId == null || extendedPanId.length != LENGTH_EXTENDED_PAN_ID) {
+ throw new IllegalArgumentException(
+ "extendedPanId is null or length is not "
+ + LENGTH_EXTENDED_PAN_ID
+ + ": "
+ + Arrays.toString(extendedPanId));
+ }
+ mExtendedPanId = extendedPanId.clone();
+ mRouterEligibleForLeader = false;
+ }
+
+ /**
+ * Creates a new {@link Builder} object by copying the data in the given {@code specifier}
+ * object.
+ */
+ public Builder(@NonNull ThreadNetworkSpecifier specifier) {
+ this(specifier.getExtendedPanId());
+ setActiveTimestamp(specifier.getActiveTimestamp());
+ setRouterEligibleForLeader(specifier.isRouterEligibleForLeader());
+ }
+
+ /** Sets the Active Timestamp of the Thread network. */
+ @NonNull
+ public Builder setActiveTimestamp(@Nullable OperationalDatasetTimestamp activeTimestamp) {
+ mActiveTimestamp = activeTimestamp;
+ return this;
+ }
+
+ /**
+ * Sets whether this device should be a leader during attachment when there are no nearby
+ * routers.
+ */
+ @NonNull
+ public Builder setRouterEligibleForLeader(boolean eligible) {
+ mRouterEligibleForLeader = eligible;
+ return this;
+ }
+
+ /** Creates a new {@link ThreadNetworkSpecifier} object from values set so far. */
+ @NonNull
+ public ThreadNetworkSpecifier build() {
+ return new ThreadNetworkSpecifier(this);
+ }
+ }
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index e3c2a28..8747b44 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -1405,9 +1405,10 @@
}
private void setInfraLinkState(InfraLinkState newInfraLinkState) {
- if (!Objects.equals(mInfraLinkState, newInfraLinkState)) {
- LOG.i("Infra link state changed: " + mInfraLinkState + " -> " + newInfraLinkState);
+ if (Objects.equals(mInfraLinkState, newInfraLinkState)) {
+ return ;
}
+ LOG.i("Infra link state changed: " + mInfraLinkState + " -> " + newInfraLinkState);
setInfraLinkInterfaceName(newInfraLinkState.interfaceName);
setInfraLinkNat64Prefix(newInfraLinkState.nat64Prefix);
setInfraLinkDnsServers(newInfraLinkState.dnsServers);
@@ -1415,6 +1416,9 @@
}
private void setInfraLinkInterfaceName(String newInfraLinkInterfaceName) {
+ if (Objects.equals(mInfraLinkState.interfaceName, newInfraLinkInterfaceName)) {
+ return ;
+ }
ParcelFileDescriptor infraIcmp6Socket = null;
if (newInfraLinkInterfaceName != null) {
try {
@@ -1435,6 +1439,9 @@
}
private void setInfraLinkNat64Prefix(@Nullable String newNat64Prefix) {
+ if (Objects.equals(newNat64Prefix, mInfraLinkState.nat64Prefix)) {
+ return ;
+ }
try {
getOtDaemon()
.setInfraLinkNat64Prefix(
@@ -1445,6 +1452,9 @@
}
private void setInfraLinkDnsServers(List<String> newDnsServers) {
+ if (Objects.equals(newDnsServers, mInfraLinkState.dnsServers)) {
+ return ;
+ }
try {
getOtDaemon()
.setInfraLinkDnsServers(
@@ -1783,6 +1793,7 @@
// do nothing if the client is dead
}
}
+ mInfraLinkState = newInfraLinkStateBuilder().build();
}
private void onThreadEnabledChanged(int state, long listenerId) {
diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
index 1a2ff06..ddbef47 100644
--- a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
+++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
@@ -20,6 +20,7 @@
import static android.net.InetAddresses.parseNumericAddress;
import static android.net.thread.utils.IntegrationTestUtils.DEFAULT_DATASET;
import static android.net.thread.utils.IntegrationTestUtils.buildIcmpv4EchoReply;
+import static android.net.thread.utils.IntegrationTestUtils.enableThreadAndJoinNetwork;
import static android.net.thread.utils.IntegrationTestUtils.getIpv6LinkAddresses;
import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv4Packet;
import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv6Packet;
@@ -27,6 +28,7 @@
import static android.net.thread.utils.IntegrationTestUtils.isInMulticastGroup;
import static android.net.thread.utils.IntegrationTestUtils.isTo;
import static android.net.thread.utils.IntegrationTestUtils.joinNetworkAndWaitForOmr;
+import static android.net.thread.utils.IntegrationTestUtils.leaveNetworkAndDisableThread;
import static android.net.thread.utils.IntegrationTestUtils.newPacketReader;
import static android.net.thread.utils.IntegrationTestUtils.pollForPacket;
import static android.net.thread.utils.IntegrationTestUtils.sendUdpMessage;
@@ -73,7 +75,9 @@
import com.android.testutils.TestNetworkTracker;
import org.junit.After;
+import org.junit.AfterClass;
import org.junit.Before;
+import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
@@ -116,7 +120,7 @@
private final Context mContext = ApplicationProvider.getApplicationContext();
private final ThreadNetworkControllerWrapper mController =
ThreadNetworkControllerWrapper.newInstance(mContext);
- private OtDaemonController mOtCtl;
+ private final OtDaemonController mOtCtl = new OtDaemonController();
private HandlerThread mHandlerThread;
private Handler mHandler;
private TestNetworkTracker mInfraNetworkTracker;
@@ -124,20 +128,24 @@
private PollPacketReader mInfraNetworkReader;
private InfraNetworkDevice mInfraDevice;
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ enableThreadAndJoinNetwork(DEFAULT_DATASET);
+ }
+
+ @AfterClass
+ public static void afterClass() throws Exception {
+ leaveNetworkAndDisableThread();
+ }
+
@Before
public void setUp() throws Exception {
- // TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network
- mOtCtl = new OtDaemonController();
- mOtCtl.factoryReset();
-
mHandlerThread = new HandlerThread(getClass().getSimpleName());
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
mFtds = new ArrayList<>();
setUpInfraNetwork();
- mController.setEnabledAndWait(true);
- mController.joinAndWait(DEFAULT_DATASET);
// Creates a infra network device.
mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
@@ -152,8 +160,6 @@
@After
public void tearDown() throws Exception {
mController.setTestNetworkAsUpstreamAndWait(null);
- mController.leaveAndWait();
- tearDownInfraNetwork();
mHandlerThread.quitSafely();
mHandlerThread.join();
diff --git a/thread/tests/integration/src/android/net/thread/InternetAccessTest.kt b/thread/tests/integration/src/android/net/thread/InternetAccessTest.kt
index 9b787f5..162f58e 100644
--- a/thread/tests/integration/src/android/net/thread/InternetAccessTest.kt
+++ b/thread/tests/integration/src/android/net/thread/InternetAccessTest.kt
@@ -24,7 +24,9 @@
import android.net.thread.utils.FullThreadDevice
import android.net.thread.utils.InfraNetworkDevice
import android.net.thread.utils.IntegrationTestUtils.DEFAULT_DATASET
+import android.net.thread.utils.IntegrationTestUtils.enableThreadAndJoinNetwork
import android.net.thread.utils.IntegrationTestUtils.joinNetworkAndWaitForOmr
+import android.net.thread.utils.IntegrationTestUtils.leaveNetworkAndDisableThread
import android.net.thread.utils.IntegrationTestUtils.newPacketReader
import android.net.thread.utils.IntegrationTestUtils.setUpInfraNetwork
import android.net.thread.utils.IntegrationTestUtils.startInfraDeviceAndWaitForOnLinkAddr
@@ -32,6 +34,7 @@
import android.net.thread.utils.IntegrationTestUtils.waitFor
import android.net.thread.utils.OtDaemonController
import android.net.thread.utils.TestDnsServer
+import android.net.thread.utils.TestUdpEchoServer
import android.net.thread.utils.ThreadFeatureCheckerRule
import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresSimulationThreadDevice
import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature
@@ -48,9 +51,12 @@
import com.google.common.truth.Truth.assertThat
import java.net.Inet4Address
import java.net.InetAddress
+import java.net.InetSocketAddress
import java.time.Duration
import org.junit.After
+import org.junit.AfterClass
import org.junit.Before
+import org.junit.BeforeClass
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -61,26 +67,42 @@
@RequiresSimulationThreadDevice
@LargeTest
class InternetAccessTest {
- private val TAG = BorderRoutingTest::class.java.simpleName
- private val NUM_FTD = 1
- private val DNS_SERVER_ADDR = parseNumericAddress("8.8.8.8") as Inet4Address
- private val ANSWER_RECORDS =
- listOf(
- DnsPacket.DnsRecord.makeAOrAAAARecord(
- ANSECTION,
- "google.com",
- CLASS_IN,
- 30 /* ttl */,
- parseNumericAddress("1.2.3.4"),
- ),
- DnsPacket.DnsRecord.makeAOrAAAARecord(
- ANSECTION,
- "google.com",
- CLASS_IN,
- 30 /* ttl */,
- parseNumericAddress("2001::234"),
- ),
- )
+ companion object {
+ private val TAG = BorderRoutingTest::class.java.simpleName
+ private val NUM_FTD = 1
+ private val DNS_SERVER_ADDR = parseNumericAddress("8.8.8.8") as Inet4Address
+ private val UDP_ECHO_SERVER_ADDRESS =
+ InetSocketAddress(parseNumericAddress("1.2.3.4"), 12345)
+ private val ANSWER_RECORDS =
+ listOf(
+ DnsPacket.DnsRecord.makeAOrAAAARecord(
+ ANSECTION,
+ "google.com",
+ CLASS_IN,
+ 30 /* ttl */,
+ parseNumericAddress("1.2.3.4"),
+ ),
+ DnsPacket.DnsRecord.makeAOrAAAARecord(
+ ANSECTION,
+ "google.com",
+ CLASS_IN,
+ 30 /* ttl */,
+ parseNumericAddress("2001::234"),
+ ),
+ )
+
+ @BeforeClass
+ @JvmStatic
+ fun beforeClass() {
+ enableThreadAndJoinNetwork(DEFAULT_DATASET)
+ }
+
+ @AfterClass
+ @JvmStatic
+ fun afterClass() {
+ leaveNetworkAndDisableThread()
+ }
+ }
@get:Rule val threadRule = ThreadFeatureCheckerRule()
@@ -94,13 +116,12 @@
private lateinit var infraNetworkReader: PollPacketReader
private lateinit var infraDevice: InfraNetworkDevice
private lateinit var dnsServer: TestDnsServer
+ private lateinit var udpEchoServer: TestUdpEchoServer
@Before
@Throws(Exception::class)
fun setUp() {
- // TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network
otCtl = OtDaemonController()
- otCtl.factoryReset()
handlerThread = HandlerThread(javaClass.simpleName)
handlerThread.start()
@@ -108,16 +129,17 @@
ftds = ArrayList()
infraNetworkTracker = setUpInfraNetwork(context, controller)
- controller.setEnabledAndWait(true)
- controller.joinAndWait(DEFAULT_DATASET)
- // Creates a infra network device.
+ // Create an infra network device.
infraNetworkReader = newPacketReader(infraNetworkTracker.testIface, handler)
infraDevice = startInfraDeviceAndWaitForOnLinkAddr(infraNetworkReader)
// Create a DNS server
dnsServer = TestDnsServer(infraNetworkReader, DNS_SERVER_ADDR, ANSWER_RECORDS)
+ // Create a UDP echo server
+ udpEchoServer = TestUdpEchoServer(infraNetworkReader, UDP_ECHO_SERVER_ADDRESS)
+
// Create Ftds
for (i in 0 until NUM_FTD) {
ftds.add(FullThreadDevice(15 + i /* node ID */))
@@ -128,10 +150,10 @@
@Throws(Exception::class)
fun tearDown() {
controller.setTestNetworkAsUpstreamAndWait(null)
- controller.leaveAndWait()
tearDownInfraNetwork(infraNetworkTracker)
dnsServer.stop()
+ udpEchoServer.stop()
handlerThread.quitSafely()
handlerThread.join()
@@ -166,6 +188,20 @@
assertThat(ftd.resolveHost("google.com", TYPE_AAAA)).isEmpty()
}
+ @Test
+ fun nat64Enabled_threadDeviceSendsUdpToEchoServer_replyIsReceived() {
+ controller.setNat64EnabledAndWait(true)
+ waitFor({ otCtl.hasNat64PrefixInNetdata() }, Duration.ofSeconds(10))
+ val ftd = ftds[0]
+ joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET)
+ udpEchoServer.start()
+
+ ftd.udpOpen()
+ ftd.udpSend("Hello,Thread", UDP_ECHO_SERVER_ADDRESS.address, UDP_ECHO_SERVER_ADDRESS.port)
+ val reply = ftd.udpReceive()
+ assertThat(reply).isEqualTo("Hello,Thread")
+ }
+
private fun extractIpv4AddressFromMappedAddress(address: InetAddress): Inet4Address {
return InetAddress.getByAddress(address.address.slice(12 until 16).toByteArray())
as Inet4Address
diff --git a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
index 5613454..d41550b 100644
--- a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
+++ b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
@@ -29,6 +29,7 @@
import static android.net.thread.utils.IntegrationTestUtils.getThreadNetwork;
import static android.net.thread.utils.IntegrationTestUtils.isInMulticastGroup;
import static android.net.thread.utils.IntegrationTestUtils.waitFor;
+import static android.net.thread.utils.ThreadNetworkControllerWrapper.JOIN_TIMEOUT;
import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
@@ -55,6 +56,7 @@
import android.net.thread.utils.ThreadFeatureCheckerRule;
import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
import android.net.thread.utils.ThreadNetworkControllerWrapper;
+import android.net.thread.utils.ThreadStateListener;
import android.os.SystemClock;
import androidx.test.core.app.ApplicationProvider;
@@ -138,6 +140,8 @@
@After
public void tearDown() throws Exception {
+ ThreadStateListener.stopAllListeners();
+
mController.setTestNetworkAsUpstreamAndWait(null);
mController.leaveAndWait();
mController.setConfigurationAndWait(DEFAULT_CONFIG);
@@ -268,6 +272,20 @@
}
@Test
+ public void joinNetwork_joinTheSameNetworkTwice_neverDetached() throws Exception {
+ mController.joinAndWait(DEFAULT_DATASET);
+ mController.waitForRole(DEVICE_ROLE_LEADER, JOIN_TIMEOUT);
+
+ var listener = ThreadStateListener.startListener(mController.get());
+ mController.joinAndWait(DEFAULT_DATASET);
+
+ assertThat(
+ listener.pollForAnyRoleOf(
+ List.of(DEVICE_ROLE_DETACHED, DEVICE_ROLE_STOPPED), JOIN_TIMEOUT))
+ .isNull();
+ }
+
+ @Test
public void edPingsMeshLocalAddresses_oneReplyPerRequest() throws Exception {
mController.joinAndWait(DEFAULT_DATASET);
startFtdChild(mFtd, DEFAULT_DATASET);
diff --git a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
index 5b532c7..209eed6 100644
--- a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
+++ b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
@@ -234,8 +234,8 @@
return matcher.group(4);
}
- /** Sends a UDP message to given IPv6 address and port. */
- public void udpSend(String message, Inet6Address serverAddr, int serverPort) {
+ /** Sends a UDP message to given IP address and port. */
+ public void udpSend(String message, InetAddress serverAddr, int serverPort) {
executeCommand("udp send %s %d %s", serverAddr.getHostAddress(), serverPort, message);
}
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
index dc2a9c9..c3859c1 100644
--- a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
@@ -590,6 +590,27 @@
return ftd.omrAddress
}
+ /** Enables Thread and joins the specified Thread network. */
+ @JvmStatic
+ fun enableThreadAndJoinNetwork(dataset: ActiveOperationalDataset) {
+ // TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network
+ OtDaemonController().factoryReset();
+
+ val context: Context = requireNotNull(ApplicationProvider.getApplicationContext());
+ val controller = requireNotNull(ThreadNetworkControllerWrapper.newInstance(context));
+ controller.setEnabledAndWait(true);
+ controller.joinAndWait(dataset);
+ }
+
+ /** Leaves the Thread network and disables Thread. */
+ @JvmStatic
+ fun leaveNetworkAndDisableThread() {
+ val context: Context = requireNotNull(ApplicationProvider.getApplicationContext());
+ val controller = requireNotNull(ThreadNetworkControllerWrapper.newInstance(context));
+ controller.leaveAndWait();
+ controller.setEnabledAndWait(false);
+ }
+
private open class DefaultDiscoveryListener : NsdManager.DiscoveryListener {
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {}
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {}
diff --git a/thread/tests/integration/src/android/net/thread/utils/TestDnsServer.kt b/thread/tests/integration/src/android/net/thread/utils/TestDnsServer.kt
index c52fc49..f97c0f2 100644
--- a/thread/tests/integration/src/android/net/thread/utils/TestDnsServer.kt
+++ b/thread/tests/integration/src/android/net/thread/utils/TestDnsServer.kt
@@ -16,18 +16,16 @@
package android.net.thread.utils
-import android.net.thread.utils.IntegrationTestUtils.pollForPacket
import android.system.OsConstants.IPPROTO_IP
import android.system.OsConstants.IPPROTO_UDP
import com.android.net.module.util.DnsPacket
import com.android.net.module.util.PacketBuilder
-import com.android.net.module.util.Struct
import com.android.net.module.util.structs.Ipv4Header
import com.android.net.module.util.structs.UdpHeader
import com.android.testutils.PollPacketReader
import java.net.InetAddress
+import java.net.InetSocketAddress
import java.nio.ByteBuffer
-import kotlin.concurrent.thread
/**
* A class that simulates a DNS server.
@@ -41,11 +39,12 @@
class TestDnsServer(
private val packetReader: PollPacketReader,
private val serverAddress: InetAddress,
- private val answerRecords: List<DnsPacket.DnsRecord>,
-) {
- private val TAG = TestDnsServer::class.java.simpleName
- private val DNS_UDP_PORT = 53
- private var workerThread: Thread? = null
+ private val serverAnswers: List<DnsPacket.DnsRecord>,
+) : TestUdpServer(packetReader, InetSocketAddress(serverAddress, DNS_UDP_PORT)) {
+ companion object {
+ private val TAG = TestDnsServer::class.java.simpleName
+ private const val DNS_UDP_PORT = 53
+ }
private class TestDnsPacket : DnsPacket {
@@ -61,49 +60,12 @@
val records = super.mRecords
}
- /**
- * Starts the DNS server to respond to DNS requests.
- *
- * <p> The server polls the DNS requests from the {@code packetReader} and responds with the
- * {@code answerRecords}. The server will automatically stop when it fails to poll a DNS request
- * within the timeout (3000 ms, as defined in IntegrationTestUtils).
- */
- fun start() {
- workerThread = thread {
- var requestPacket: ByteArray
- while (true) {
- requestPacket = pollForDnsPacket() ?: break
- val buf = ByteBuffer.wrap(requestPacket)
- packetReader.sendResponse(buildDnsResponse(buf, answerRecords))
- }
- }
- }
-
- /** Stops the DNS server. */
- fun stop() {
- workerThread?.join()
- }
-
- private fun pollForDnsPacket(): ByteArray? {
- val filter =
- fun(packet: ByteArray): Boolean {
- val buf = ByteBuffer.wrap(packet)
- val ipv4Header = Struct.parse(Ipv4Header::class.java, buf) ?: return false
- val udpHeader = Struct.parse(UdpHeader::class.java, buf) ?: return false
- return ipv4Header.dstIp == serverAddress && udpHeader.dstPort == DNS_UDP_PORT
- }
- return pollForPacket(packetReader, filter)
- }
-
- private fun buildDnsResponse(
- requestPacket: ByteBuffer,
- serverAnswers: List<DnsPacket.DnsRecord>,
+ override fun buildResponse(
+ requestIpv4Header: Ipv4Header,
+ requestUdpHeader: UdpHeader,
+ requestUdpPayload: ByteArray,
): ByteBuffer? {
- val requestIpv4Header = Struct.parse(Ipv4Header::class.java, requestPacket) ?: return null
- val requestUdpHeader = Struct.parse(UdpHeader::class.java, requestPacket) ?: return null
- val remainingRequestPacket = ByteArray(requestPacket.remaining())
- requestPacket.get(remainingRequestPacket)
- val requestDnsPacket = TestDnsPacket(remainingRequestPacket)
+ val requestDnsPacket = TestDnsPacket(requestUdpPayload)
val requestDnsHeader = requestDnsPacket.header
val answerRecords =
diff --git a/thread/tests/integration/src/android/net/thread/utils/TestUdpEchoServer.kt b/thread/tests/integration/src/android/net/thread/utils/TestUdpEchoServer.kt
new file mode 100644
index 0000000..9fcd6a4
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/utils/TestUdpEchoServer.kt
@@ -0,0 +1,74 @@
+/*
+ * 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 android.net.thread.utils
+
+import android.system.OsConstants.IPPROTO_IP
+import android.system.OsConstants.IPPROTO_UDP
+import com.android.net.module.util.PacketBuilder
+import com.android.net.module.util.structs.Ipv4Header
+import com.android.net.module.util.structs.UdpHeader
+import com.android.testutils.PollPacketReader
+import java.net.InetSocketAddress
+import java.nio.ByteBuffer
+
+/**
+ * A class that simulates a UDP echo server that replies to incoming UDP message with the same
+ * payload.
+ *
+ * @param packetReader the packet reader to poll UDP requests from
+ * @param serverAddress the address and port of the UDP server
+ */
+class TestUdpEchoServer(
+ private val packetReader: PollPacketReader,
+ private val serverAddress: InetSocketAddress,
+) : TestUdpServer(packetReader, serverAddress) {
+ companion object {
+ private val TAG = TestUdpEchoServer::class.java.simpleName
+ }
+
+ override fun buildResponse(
+ requestIpv4Header: Ipv4Header,
+ requestUdpHeader: UdpHeader,
+ requestUdpPayload: ByteArray,
+ ): ByteBuffer? {
+ val buf =
+ PacketBuilder.allocate(
+ false /* hasEther */,
+ IPPROTO_IP,
+ IPPROTO_UDP,
+ requestUdpPayload.size,
+ )
+
+ val packetBuilder = PacketBuilder(buf)
+ packetBuilder.writeIpv4Header(
+ requestIpv4Header.tos,
+ requestIpv4Header.id,
+ requestIpv4Header.flagsAndFragmentOffset,
+ 0x40 /* ttl */,
+ IPPROTO_UDP.toByte(),
+ requestIpv4Header.dstIp, /* srcIp */
+ requestIpv4Header.srcIp, /* dstIp */
+ )
+ packetBuilder.writeUdpHeader(
+ requestUdpHeader.dstPort.toShort() /* srcPort */,
+ requestUdpHeader.srcPort.toShort(), /* dstPort */
+ )
+ buf.put(requestUdpPayload)
+
+ return packetBuilder.finalizePacket()
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/utils/TestUdpServer.kt b/thread/tests/integration/src/android/net/thread/utils/TestUdpServer.kt
new file mode 100644
index 0000000..fb0942e
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/utils/TestUdpServer.kt
@@ -0,0 +1,98 @@
+/*
+ * 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 android.net.thread.utils
+
+import android.net.thread.utils.IntegrationTestUtils.pollForPacket
+import com.android.net.module.util.Struct
+import com.android.net.module.util.structs.Ipv4Header
+import com.android.net.module.util.structs.UdpHeader
+import com.android.testutils.PollPacketReader
+import java.net.InetSocketAddress
+import java.nio.ByteBuffer
+import kotlin.concurrent.thread
+
+/**
+ * A class that simulates a UDP server that replies to incoming UDP messages.
+ *
+ * @param packetReader the packet reader to poll UDP requests from
+ * @param serverAddress the address and port of the UDP server
+ */
+abstract class TestUdpServer(
+ private val packetReader: PollPacketReader,
+ private val serverAddress: InetSocketAddress,
+) {
+ private val TAG = TestUdpServer::class.java.simpleName
+ private var workerThread: Thread? = null
+
+ /**
+ * Starts the UDP server to respond to UDP messages.
+ *
+ * <p> The server polls the UDP messages from the {@code packetReader} and responds with a
+ * message built by {@code buildResponse}. The server will automatically stop when it fails to
+ * poll a UDP request within the timeout (3000 ms, as defined in IntegrationTestUtils).
+ */
+ fun start() {
+ workerThread = thread {
+ var requestPacket: ByteArray
+ while (true) {
+ requestPacket = pollForUdpPacket() ?: break
+ val buf = ByteBuffer.wrap(requestPacket)
+ packetReader.sendResponse(buildResponse(buf) ?: break)
+ }
+ }
+ }
+
+ /** Stops the UDP server. */
+ fun stop() {
+ workerThread?.join()
+ }
+
+ /**
+ * Builds the UDP response for the given UDP request.
+ *
+ * @param ipv4Header the IPv4 header of the UDP request
+ * @param udpHeader the UDP header of the UDP request
+ * @param udpPayload the payload of the UDP request
+ * @return the UDP response
+ */
+ abstract fun buildResponse(
+ requestIpv4Header: Ipv4Header,
+ requestUdpHeader: UdpHeader,
+ requestUdpPayload: ByteArray,
+ ): ByteBuffer?
+
+ private fun pollForUdpPacket(): ByteArray? {
+ val filter =
+ fun(packet: ByteArray): Boolean {
+ val buf = ByteBuffer.wrap(packet)
+ val ipv4Header = Struct.parse(Ipv4Header::class.java, buf) ?: return false
+ val udpHeader = Struct.parse(UdpHeader::class.java, buf) ?: return false
+ return ipv4Header.dstIp == serverAddress.address &&
+ udpHeader.dstPort == serverAddress.port
+ }
+ return pollForPacket(packetReader, filter)
+ }
+
+ private fun buildResponse(requestPacket: ByteBuffer): ByteBuffer? {
+ val requestIpv4Header = Struct.parse(Ipv4Header::class.java, requestPacket) ?: return null
+ val requestUdpHeader = Struct.parse(UdpHeader::class.java, requestPacket) ?: return null
+ val remainingRequestPacket = ByteArray(requestPacket.remaining())
+ requestPacket.get(remainingRequestPacket)
+
+ return buildResponse(requestIpv4Header, requestUdpHeader, remainingRequestPacket)
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java b/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java
index 4354702..b6114f3 100644
--- a/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java
+++ b/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java
@@ -37,6 +37,7 @@
import android.os.OutcomeReceiver;
import java.time.Duration;
+import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@@ -53,6 +54,9 @@
private final ThreadNetworkController mController;
+ private final List<Integer> mDeviceRoleUpdates = new ArrayList<>();
+ @Nullable private StateCallback mStateCallback;
+
/**
* Returns a new {@link ThreadNetworkControllerWrapper} instance or {@code null} if Thread
* feature is not supported on this device.
@@ -71,6 +75,15 @@
}
/**
+ * Returns the underlying {@link ThreadNetworkController} object or {@code null} if the current
+ * platform doesn't support it.
+ */
+ @Nullable
+ public ThreadNetworkController get() {
+ return mController;
+ }
+
+ /**
* Returns the Thread enabled state.
*
* <p>The value can be one of {@code ThreadNetworkController#STATE_*}.
diff --git a/thread/tests/unit/src/android/net/thread/ThreadNetworkSpecifierTest.java b/thread/tests/unit/src/android/net/thread/ThreadNetworkSpecifierTest.java
new file mode 100644
index 0000000..c83cb7a
--- /dev/null
+++ b/thread/tests/unit/src/android/net/thread/ThreadNetworkSpecifierTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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 android.net.thread;
+
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+
+import com.google.common.testing.EqualsTester;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collection;
+
+/** Tests for {@link ThreadNetworkSpecifier}. */
+@SmallTest
+@RunWith(Parameterized.class)
+public final class ThreadNetworkSpecifierTest {
+ public final byte[] mExtendedPanId;
+ public final OperationalDatasetTimestamp mActiveTimestamp;
+ public final boolean mRouterEligibleForLeader;
+
+ @Parameterized.Parameters
+ public static Collection specifierArguments() {
+ var timestampNow = OperationalDatasetTimestamp.fromInstant(Instant.now());
+ return Arrays.asList(
+ new Object[][] {
+ {new byte[] {0, 1, 2, 3, 4, 5, 6, 7}, null, false},
+ {new byte[] {1, 1, 1, 1, 2, 2, 2, 2}, timestampNow, true},
+ {new byte[] {1, 1, 1, 1, 2, 2, 2, 2}, timestampNow, false},
+ });
+ }
+
+ public ThreadNetworkSpecifierTest(
+ byte[] extendedPanId,
+ OperationalDatasetTimestamp activeTimestamp,
+ boolean routerEligibleForLeader) {
+ mExtendedPanId = extendedPanId.clone();
+ mActiveTimestamp = activeTimestamp;
+ mRouterEligibleForLeader = routerEligibleForLeader;
+ }
+
+ @Test
+ public void parcelable_parcelingIsLossLess() {
+ ThreadNetworkSpecifier specifier =
+ new ThreadNetworkSpecifier.Builder(mExtendedPanId)
+ .setActiveTimestamp(mActiveTimestamp)
+ .setRouterEligibleForLeader(mRouterEligibleForLeader)
+ .build();
+ assertParcelingIsLossless(specifier);
+ }
+
+ @Test
+ public void builder_correctValuesAreSet() {
+ ThreadNetworkSpecifier specifier =
+ new ThreadNetworkSpecifier.Builder(mExtendedPanId)
+ .setActiveTimestamp(mActiveTimestamp)
+ .setRouterEligibleForLeader(mRouterEligibleForLeader)
+ .build();
+
+ assertThat(specifier.getExtendedPanId()).isEqualTo(mExtendedPanId);
+ assertThat(specifier.getActiveTimestamp()).isEqualTo(mActiveTimestamp);
+ assertThat(specifier.isRouterEligibleForLeader()).isEqualTo(mRouterEligibleForLeader);
+ }
+
+ @Test
+ public void builderConstructor_specifiersAreEqual() {
+ ThreadNetworkSpecifier specifier1 =
+ new ThreadNetworkSpecifier.Builder(mExtendedPanId)
+ .setActiveTimestamp(mActiveTimestamp)
+ .setRouterEligibleForLeader(mRouterEligibleForLeader)
+ .build();
+
+ ThreadNetworkSpecifier specifier2 = new ThreadNetworkSpecifier.Builder(specifier1).build();
+
+ assertThat(specifier1).isEqualTo(specifier2);
+ }
+
+ @Test
+ public void equalsTester() {
+ var timestampNow = OperationalDatasetTimestamp.fromInstant(Instant.now());
+ new EqualsTester()
+ .addEqualityGroup(
+ new ThreadNetworkSpecifier.Builder(new byte[] {0, 1, 2, 3, 4, 5, 6, 7})
+ .setActiveTimestamp(timestampNow)
+ .setRouterEligibleForLeader(true)
+ .build(),
+ new ThreadNetworkSpecifier.Builder(new byte[] {0, 1, 2, 3, 4, 5, 6, 7})
+ .setActiveTimestamp(timestampNow)
+ .setRouterEligibleForLeader(true)
+ .build())
+ .addEqualityGroup(
+ new ThreadNetworkSpecifier.Builder(new byte[] {0, 1, 2, 3, 4, 5, 6, 7})
+ .setActiveTimestamp(null)
+ .setRouterEligibleForLeader(false)
+ .build(),
+ new ThreadNetworkSpecifier.Builder(new byte[] {0, 1, 2, 3, 4, 5, 6, 7})
+ .setActiveTimestamp(null)
+ .setRouterEligibleForLeader(false)
+ .build())
+ .addEqualityGroup(
+ new ThreadNetworkSpecifier.Builder(new byte[] {1, 1, 1, 1, 2, 2, 2, 2})
+ .setActiveTimestamp(null)
+ .setRouterEligibleForLeader(false)
+ .build(),
+ new ThreadNetworkSpecifier.Builder(new byte[] {1, 1, 1, 1, 2, 2, 2, 2})
+ .setActiveTimestamp(null)
+ .setRouterEligibleForLeader(false)
+ .build())
+ .testEquals();
+ }
+}
diff --git a/thread/tests/utils/src/android/net/thread/utils/ThreadStateListener.java b/thread/tests/utils/src/android/net/thread/utils/ThreadStateListener.java
new file mode 100644
index 0000000..21eb7d9
--- /dev/null
+++ b/thread/tests/utils/src/android/net/thread/utils/ThreadStateListener.java
@@ -0,0 +1,96 @@
+/*
+ * 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 android.net.thread.utils;
+
+import static android.Manifest.permission.ACCESS_NETWORK_STATE;
+
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+import android.annotation.Nullable;
+import android.net.thread.ThreadNetworkController;
+import android.net.thread.ThreadNetworkController.StateCallback;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.ArrayTrackRecord;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A listener for sequential Thread state updates.
+ *
+ * <p>This is a wrapper around {@link ThreadNetworkController#registerStateCallback} to make
+ * synchronized access to Thread state updates easier.
+ */
+@VisibleForTesting
+public final class ThreadStateListener {
+ private static final List<ThreadStateListener> sListeners = new ArrayList<>();
+ private final ArrayTrackRecord<Integer> mDeviceRoleUpdates = new ArrayTrackRecord<>();
+ private final ArrayTrackRecord<Integer>.ReadHead mReadHead = mDeviceRoleUpdates.newReadHead();
+ private final ThreadNetworkController mController;
+ private final StateCallback mCallback =
+ new ThreadNetworkController.StateCallback() {
+ @Override
+ public void onDeviceRoleChanged(int newRole) {
+ mDeviceRoleUpdates.add(newRole);
+ }
+ // Add more state update trackers here
+ };
+
+ /** Creates a new {@link ThreadStateListener} object and starts listening for state updates. */
+ public static ThreadStateListener startListener(ThreadNetworkController controller) {
+ var listener = new ThreadStateListener(controller);
+ sListeners.add(listener);
+ listener.start();
+ return listener;
+ }
+
+ /** Stops all listeners created by {@link #startListener}. */
+ public static void stopAllListeners() {
+ for (var listener : sListeners) {
+ listener.stop();
+ }
+ sListeners.clear();
+ }
+
+ private ThreadStateListener(ThreadNetworkController controller) {
+ mController = controller;
+ }
+
+ private void start() {
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ () -> mController.registerStateCallback(directExecutor(), mCallback));
+ }
+
+ private void stop() {
+ runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(mCallback));
+ }
+
+ /**
+ * Polls for any role in {@code roles} starting after call to {@link #startListener}.
+ *
+ * <p>Returns the matched device role or {@code null} if timeout.
+ */
+ @Nullable
+ public Integer pollForAnyRoleOf(List<Integer> roles, Duration timeout) {
+ return mReadHead.poll(timeout.toMillis(), newRole -> (roles.contains(newRole)));
+ }
+}