Merge "Ignore BPF tethering offload test if tether config is disabled"
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 4eeaf51..4774866 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -171,23 +171,6 @@
]
}
],
- "auto-postsubmit": [
- // Test tag for automotive targets. These are only running in postsubmit so as to harden the
- // automotive targets to avoid introducing additional test flake and build time. The plan for
- // presubmit testing for auto is to augment the existing tests to cover auto use cases as well.
- // Additionally, this tag is used in targeted test suites to limit resource usage on the test
- // infra during the hardening phase.
- // TODO: this tag to be removed once the above is no longer an issue.
- {
- "name": "FrameworksNetTests"
- },
- {
- "name": "FrameworksNetIntegrationTests"
- },
- {
- "name": "FrameworksNetDeflakeTest"
- }
- ],
"imports": [
{
"path": "frameworks/base/core/java/android/net"
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 19d0d5f..28edc8a 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -134,7 +134,7 @@
"-Wthread-safety",
],
- ldflags: ["-Wl,--exclude-libs=ALL,-error-limit=0"],
+ ldflags: ["-Wl,--exclude-libs=ALL,--error-limit=0"],
}
// Common defaults for compiling the actual APK.
@@ -231,4 +231,4 @@
cmd: "$(location stats-log-api-gen) --java $(out) --module network_tethering" +
" --javaPackage com.android.networkstack.tethering.metrics --javaClass TetheringStatsLog",
out: ["com/android/networkstack/tethering/metrics/TetheringStatsLog.java"],
-}
\ No newline at end of file
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index af017f3..f613b73 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -1406,7 +1406,9 @@
private void enableIpServing(int tetheringType, String ifname, int ipServingMode,
boolean isNcm) {
ensureIpServerStarted(ifname, tetheringType, isNcm);
- changeInterfaceState(ifname, ipServingMode);
+ if (tether(ifname, ipServingMode) != TETHER_ERROR_NO_ERROR) {
+ Log.e(TAG, "unable start tethering on iface " + ifname);
+ }
}
private void disableWifiIpServingCommon(int tetheringType, String ifname) {
@@ -1551,27 +1553,6 @@
}
}
- private void changeInterfaceState(String ifname, int requestedState) {
- final int result;
- switch (requestedState) {
- case IpServer.STATE_UNAVAILABLE:
- case IpServer.STATE_AVAILABLE:
- result = untether(ifname);
- break;
- case IpServer.STATE_TETHERED:
- case IpServer.STATE_LOCAL_ONLY:
- result = tether(ifname, requestedState);
- break;
- default:
- Log.wtf(TAG, "Unknown interface state: " + requestedState);
- return;
- }
- if (result != TETHER_ERROR_NO_ERROR) {
- Log.e(TAG, "unable start or stop tethering on iface " + ifname);
- return;
- }
- }
-
TetheringConfiguration getTetheringConfiguration() {
return mConfig;
}
diff --git a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
index e25f2ae..d8e631e 100644
--- a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
+++ b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
@@ -69,7 +69,6 @@
/** Update Tethering stats about caller's package name and downstream type. */
public void createBuilder(final int downstreamType, final String callerPkg) {
- mBuilderMap.clear();
NetworkTetheringReported.Builder statsBuilder =
NetworkTetheringReported.newBuilder();
statsBuilder.setDownstreamType(downstreamTypeToEnum(downstreamType))
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
index c34cf5f..6a85718 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
@@ -81,6 +81,22 @@
mTetheringMetrics = spy(new MockTetheringMetrics());
}
+ private void verifyReport(DownstreamType downstream, ErrorCode error, UserType user)
+ throws Exception {
+ final NetworkTetheringReported expectedReport =
+ mStatsBuilder.setDownstreamType(downstream)
+ .setUserType(user)
+ .setUpstreamType(UpstreamType.UT_UNKNOWN)
+ .setErrorCode(error)
+ .build();
+ verify(mTetheringMetrics).write(expectedReport);
+ }
+
+ private void updateErrorAndSendReport(int downstream, int error) {
+ mTetheringMetrics.updateErrorCode(downstream, error);
+ mTetheringMetrics.sendReport(downstream);
+ }
+
private void runDownstreamTypesTest(final Pair<Integer, DownstreamType>... testPairs)
throws Exception {
for (Pair<Integer, DownstreamType> testPair : testPairs) {
@@ -88,15 +104,8 @@
final DownstreamType expectedResult = testPair.second;
mTetheringMetrics.createBuilder(type, TEST_CALLER_PKG);
- mTetheringMetrics.updateErrorCode(type, TETHER_ERROR_NO_ERROR);
- mTetheringMetrics.sendReport(type);
- NetworkTetheringReported expectedReport =
- mStatsBuilder.setDownstreamType(expectedResult)
- .setUserType(UserType.USER_UNKNOWN)
- .setUpstreamType(UpstreamType.UT_UNKNOWN)
- .setErrorCode(ErrorCode.EC_NO_ERROR)
- .build();
- verify(mTetheringMetrics).write(expectedReport);
+ updateErrorAndSendReport(type, TETHER_ERROR_NO_ERROR);
+ verifyReport(expectedResult, ErrorCode.EC_NO_ERROR, UserType.USER_UNKNOWN);
reset(mTetheringMetrics);
}
}
@@ -118,15 +127,8 @@
final ErrorCode expectedResult = testPair.second;
mTetheringMetrics.createBuilder(TETHERING_WIFI, TEST_CALLER_PKG);
- mTetheringMetrics.updateErrorCode(TETHERING_WIFI, errorCode);
- mTetheringMetrics.sendReport(TETHERING_WIFI);
- NetworkTetheringReported expectedReport =
- mStatsBuilder.setDownstreamType(DownstreamType.DS_TETHERING_WIFI)
- .setUserType(UserType.USER_UNKNOWN)
- .setUpstreamType(UpstreamType.UT_UNKNOWN)
- .setErrorCode(expectedResult)
- .build();
- verify(mTetheringMetrics).write(expectedReport);
+ updateErrorAndSendReport(TETHERING_WIFI, errorCode);
+ verifyReport(DownstreamType.DS_TETHERING_WIFI, expectedResult, UserType.USER_UNKNOWN);
reset(mTetheringMetrics);
}
}
@@ -163,15 +165,8 @@
final UserType expectedResult = testPair.second;
mTetheringMetrics.createBuilder(TETHERING_WIFI, callerPkg);
- mTetheringMetrics.updateErrorCode(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
- mTetheringMetrics.sendReport(TETHERING_WIFI);
- NetworkTetheringReported expectedReport =
- mStatsBuilder.setDownstreamType(DownstreamType.DS_TETHERING_WIFI)
- .setUserType(expectedResult)
- .setUpstreamType(UpstreamType.UT_UNKNOWN)
- .setErrorCode(ErrorCode.EC_NO_ERROR)
- .build();
- verify(mTetheringMetrics).write(expectedReport);
+ updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
+ verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_NO_ERROR, expectedResult);
reset(mTetheringMetrics);
}
}
@@ -183,4 +178,23 @@
new Pair<>(SYSTEMUI_PKG, UserType.USER_SYSTEMUI),
new Pair<>(GMS_PKG, UserType.USER_GMS));
}
+
+ @Test
+ public void testMultiBuildersCreatedBeforeSendReport() throws Exception {
+ mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG);
+ mTetheringMetrics.createBuilder(TETHERING_USB, SYSTEMUI_PKG);
+ mTetheringMetrics.createBuilder(TETHERING_BLUETOOTH, GMS_PKG);
+
+ updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_DHCPSERVER_ERROR);
+ verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_DHCPSERVER_ERROR,
+ UserType.USER_SETTINGS);
+
+ updateErrorAndSendReport(TETHERING_USB, TETHER_ERROR_ENABLE_FORWARDING_ERROR);
+ verifyReport(DownstreamType.DS_TETHERING_USB, ErrorCode.EC_ENABLE_FORWARDING_ERROR,
+ UserType.USER_SYSTEMUI);
+
+ updateErrorAndSendReport(TETHERING_BLUETOOTH, TETHER_ERROR_TETHER_IFACE_ERROR);
+ verifyReport(DownstreamType.DS_TETHERING_BLUETOOTH, ErrorCode.EC_TETHER_IFACE_ERROR,
+ UserType.USER_GMS);
+ }
}
diff --git a/bpf_progs/bpf_shared.h b/bpf_progs/bpf_shared.h
index fd449a3..85b9f86 100644
--- a/bpf_progs/bpf_shared.h
+++ b/bpf_progs/bpf_shared.h
@@ -148,6 +148,7 @@
#endif // __cplusplus
+// LINT.IfChange(match_type)
enum UidOwnerMatchType {
NO_MATCH = 0,
HAPPY_BOX_MATCH = (1 << 0),
@@ -163,6 +164,7 @@
OEM_DENY_2_MATCH = (1 << 10),
OEM_DENY_3_MATCH = (1 << 11),
};
+// LINT.ThenChange(packages/modules/Connectivity/service/src/com/android/server/BpfNetMaps.java)
enum BpfPermissionMatch {
BPF_PERMISSION_INTERNET = 1 << 2,
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index 17c18c9..44f76de 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -53,15 +53,18 @@
// For maps netd does not need to access
#define DEFINE_BPF_MAP_NO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
- DEFINE_BPF_MAP_UGM(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, AID_ROOT, AID_NET_BW_ACCT, 0060)
+ DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, \
+ AID_ROOT, AID_NET_BW_ACCT, 0060, "fs_bpf_net_shared", "", false)
// For maps netd only needs read only access to
#define DEFINE_BPF_MAP_RO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
- DEFINE_BPF_MAP_UGM(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, AID_ROOT, AID_NET_BW_ACCT, 0460)
+ DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, \
+ AID_ROOT, AID_NET_BW_ACCT, 0460, "fs_bpf_netd_readonly", "", false)
// For maps netd needs to be able to read and write
#define DEFINE_BPF_MAP_RW_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
- DEFINE_BPF_MAP_UGM(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, AID_ROOT, AID_NET_BW_ACCT, 0660)
+ DEFINE_BPF_MAP_UGM(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, \
+ AID_ROOT, AID_NET_BW_ACCT, 0660)
// Bpf map arrays on creation are preinitialized to 0 and do not support deletion of a key,
// see: kernel/bpf/arraymap.c array_map_delete_elem() returns -EINVAL (from both syscall and ebpf)
@@ -81,6 +84,20 @@
/* never actually used from ebpf */
DEFINE_BPF_MAP_NO_NETD(iface_index_name_map, HASH, uint32_t, IfaceValue, IFACE_INDEX_NAME_MAP_SIZE)
+// iptables xt_bpf programs need to be usable by both netd and netutils_wrappers
+#define DEFINE_XTBPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
+ DEFINE_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog)
+
+// programs that need to be usable by netd, but not by netutils_wrappers
+#define DEFINE_NETD_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
+ DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, \
+ KVER_NONE, KVER_INF, false, "fs_bpf_netd_readonly", "")
+
+// programs that only need to be usable by the system server
+#define DEFINE_SYS_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
+ DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, \
+ KVER_NONE, KVER_INF, false, "fs_bpf_net_shared", "")
+
static __always_inline int is_system_uid(uint32_t uid) {
// MIN_SYSTEM_UID is AID_ROOT == 0, so uint32_t is *always* >= 0
// MAX_SYSTEM_UID is AID_NOBODY == 9999, while AID_APP_START == 10000
@@ -313,18 +330,18 @@
return match;
}
-DEFINE_BPF_PROG("cgroupskb/ingress/stats", AID_ROOT, AID_SYSTEM, bpf_cgroup_ingress)
+DEFINE_NETD_BPF_PROG("cgroupskb/ingress/stats", AID_ROOT, AID_SYSTEM, bpf_cgroup_ingress)
(struct __sk_buff* skb) {
return bpf_traffic_account(skb, BPF_INGRESS);
}
-DEFINE_BPF_PROG("cgroupskb/egress/stats", AID_ROOT, AID_SYSTEM, bpf_cgroup_egress)
+DEFINE_NETD_BPF_PROG("cgroupskb/egress/stats", AID_ROOT, AID_SYSTEM, bpf_cgroup_egress)
(struct __sk_buff* skb) {
return bpf_traffic_account(skb, BPF_EGRESS);
}
// WARNING: Android T's non-updatable netd depends on the name of this program.
-DEFINE_BPF_PROG("skfilter/egress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_egress_prog)
+DEFINE_XTBPF_PROG("skfilter/egress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_egress_prog)
(struct __sk_buff* skb) {
// Clat daemon does not generate new traffic, all its traffic is accounted for already
// on the v4-* interfaces (except for the 20 (or 28) extra bytes of IPv6 vs IPv4 overhead,
@@ -344,7 +361,7 @@
}
// WARNING: Android T's non-updatable netd depends on the name of this program.
-DEFINE_BPF_PROG("skfilter/ingress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_ingress_prog)
+DEFINE_XTBPF_PROG("skfilter/ingress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_ingress_prog)
(struct __sk_buff* skb) {
// Clat daemon traffic is not accounted by virtue of iptables raw prerouting drop rule
// (in clat_raw_PREROUTING chain), which triggers before this (in bw_raw_PREROUTING chain).
@@ -356,7 +373,8 @@
return BPF_MATCH;
}
-DEFINE_BPF_PROG("schedact/ingress/account", AID_ROOT, AID_NET_ADMIN, tc_bpf_ingress_account_prog)
+DEFINE_SYS_BPF_PROG("schedact/ingress/account", AID_ROOT, AID_NET_ADMIN,
+ tc_bpf_ingress_account_prog)
(struct __sk_buff* skb) {
if (is_received_skb(skb)) {
// Account for ingress traffic before tc drops it.
@@ -367,7 +385,7 @@
}
// WARNING: Android T's non-updatable netd depends on the name of this program.
-DEFINE_BPF_PROG("skfilter/allowlist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_allowlist_prog)
+DEFINE_XTBPF_PROG("skfilter/allowlist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_allowlist_prog)
(struct __sk_buff* skb) {
uint32_t sock_uid = bpf_get_socket_uid(skb);
if (is_system_uid(sock_uid)) return BPF_MATCH;
@@ -385,7 +403,7 @@
}
// WARNING: Android T's non-updatable netd depends on the name of this program.
-DEFINE_BPF_PROG("skfilter/denylist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_denylist_prog)
+DEFINE_XTBPF_PROG("skfilter/denylist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_denylist_prog)
(struct __sk_buff* skb) {
uint32_t sock_uid = bpf_get_socket_uid(skb);
UidOwnerValue* denylistMatch = bpf_uid_owner_map_lookup_elem(&sock_uid);
@@ -393,7 +411,7 @@
return BPF_NOMATCH;
}
-DEFINE_BPF_PROG("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create)
+DEFINE_NETD_BPF_PROG("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create)
(struct bpf_sock* sk) {
uint64_t gid_uid = bpf_get_current_uid_gid();
/*
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 39cd7f3..02083ff 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -5903,6 +5903,7 @@
*
* @param chain target chain.
* @param enable whether the chain should be enabled.
+ * @throws UnsupportedOperationException if called on pre-T devices.
* @throws IllegalStateException if enabling or disabling the firewall chain failed.
* @hide
*/
@@ -5921,6 +5922,29 @@
}
/**
+ * Get the specified firewall chain status.
+ *
+ * @param chain target chain.
+ * @return {@code true} if chain is enabled, {@code false} if chain is disabled.
+ * @throws UnsupportedOperationException if called on pre-T devices.
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ * @hide
+ */
+ @RequiresPermission(anyOf = {
+ android.Manifest.permission.NETWORK_SETTINGS,
+ android.Manifest.permission.NETWORK_STACK,
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK
+ })
+ public boolean getFirewallChainEnabled(@FirewallChain final int chain) {
+ try {
+ return mService.getFirewallChainEnabled(chain);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Replaces the contents of the specified UID-based firewall chain.
*
* @param chain target chain to replace.
diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl
index bc73769..29fea00 100644
--- a/framework/src/android/net/IConnectivityManager.aidl
+++ b/framework/src/android/net/IConnectivityManager.aidl
@@ -244,5 +244,7 @@
void setFirewallChainEnabled(int chain, boolean enable);
+ boolean getFirewallChainEnabled(int chain);
+
void replaceFirewallChain(int chain, in int[] uids);
}
diff --git a/framework/src/android/net/TestNetworkManager.java b/framework/src/android/net/TestNetworkManager.java
index 7b18765..9cae9e6 100644
--- a/framework/src/android/net/TestNetworkManager.java
+++ b/framework/src/android/net/TestNetworkManager.java
@@ -236,6 +236,8 @@
/**
* Create a tap interface with or without carrier for testing purposes.
*
+ * Note: setting carrierUp = false is not supported until kernel version 5.0.
+ *
* @param carrierUp whether the created interface has a carrier or not.
* @param bringUp whether to bring up the interface before returning it.
* @hide
@@ -254,7 +256,6 @@
* Enable / disable carrier on TestNetworkInterface
*
* Note: TUNSETCARRIER is not supported until kernel version 5.0.
- * TODO: add RequiresApi annotation.
*
* @param iface the interface to configure.
* @param enabled true to turn carrier on, false to turn carrier off.
diff --git a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
index 71d3e4f..f058f94 100644
--- a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
+++ b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
@@ -16,12 +16,14 @@
package com.android.server.ethernet;
+import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.pm.PackageManager;
+import android.net.EthernetNetworkSpecifier;
import android.net.EthernetNetworkUpdateRequest;
import android.net.IEthernetManager;
import android.net.IEthernetServiceListener;
@@ -29,6 +31,7 @@
import android.net.ITetheredInterfaceCallback;
import android.net.IpConfiguration;
import android.net.NetworkCapabilities;
+import android.net.NetworkSpecifier;
import android.os.Binder;
import android.os.Handler;
import android.os.RemoteException;
@@ -216,19 +219,39 @@
"EthernetServiceImpl");
}
- private void maybeValidateTestCapabilities(final String iface,
- @Nullable final NetworkCapabilities nc) {
+ private void validateOrSetNetworkSpecifier(String iface, NetworkCapabilities nc) {
+ final NetworkSpecifier spec = nc.getNetworkSpecifier();
+ if (spec == null) {
+ nc.setNetworkSpecifier(new EthernetNetworkSpecifier(iface));
+ return;
+ }
+ if (!(spec instanceof EthernetNetworkSpecifier)) {
+ throw new IllegalArgumentException("Invalid specifier type for request.");
+ }
+ if (!((EthernetNetworkSpecifier) spec).getInterfaceName().matches(iface)) {
+ throw new IllegalArgumentException("Invalid interface name set on specifier.");
+ }
+ }
+
+ private void maybeValidateTestCapabilities(String iface, NetworkCapabilities nc) {
if (!mTracker.isValidTestInterface(iface)) {
return;
}
- // For test interfaces, only null or capabilities that include TRANSPORT_TEST are
- // allowed.
- if (nc != null && !nc.hasTransport(TRANSPORT_TEST)) {
+ if (!nc.hasTransport(TRANSPORT_TEST)) {
throw new IllegalArgumentException(
"Updates to test interfaces must have NetworkCapabilities.TRANSPORT_TEST.");
}
}
+ private void maybeValidateEthernetTransport(String iface, NetworkCapabilities nc) {
+ if (mTracker.isValidTestInterface(iface)) {
+ return;
+ }
+ if (!nc.hasSingleTransport(TRANSPORT_ETHERNET)) {
+ throw new IllegalArgumentException("Invalid transport type for request.");
+ }
+ }
+
private void enforceAdminPermission(final String iface, boolean enforceAutomotive,
final String logMessage) {
if (mTracker.isValidTestInterface(iface)) {
@@ -251,12 +274,17 @@
// TODO: validate that iface is listed in overlay config_ethernet_interfaces
// only automotive devices are allowed to set the NetworkCapabilities using this API
- enforceAdminPermission(iface, request.getNetworkCapabilities() != null,
- "updateConfiguration() with non-null capabilities");
- maybeValidateTestCapabilities(iface, request.getNetworkCapabilities());
+ final NetworkCapabilities nc = request.getNetworkCapabilities();
+ enforceAdminPermission(
+ iface, nc != null, "updateConfiguration() with non-null capabilities");
+ if (nc != null) {
+ validateOrSetNetworkSpecifier(iface, nc);
+ maybeValidateTestCapabilities(iface, nc);
+ maybeValidateEthernetTransport(iface, nc);
+ }
mTracker.updateConfiguration(
- iface, request.getIpConfiguration(), request.getNetworkCapabilities(), listener);
+ iface, request.getIpConfiguration(), nc, listener);
}
@Override
diff --git a/service-t/src/com/android/server/net/NetworkStatsRecorder.java b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
index d99e164..7c801d7 100644
--- a/service-t/src/com/android/server/net/NetworkStatsRecorder.java
+++ b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
@@ -78,6 +78,7 @@
private final long mBucketDuration;
private final boolean mOnlyTags;
+ private final boolean mWipeOnError;
private long mPersistThresholdBytes = 2 * MB_IN_BYTES;
private NetworkStats mLastSnapshot;
@@ -102,6 +103,7 @@
// slack to avoid overflow
mBucketDuration = YEAR_IN_MILLIS;
mOnlyTags = false;
+ mWipeOnError = true;
mPending = null;
mSinceBoot = new NetworkStatsCollection(mBucketDuration);
@@ -113,7 +115,8 @@
* Persisted recorder.
*/
public NetworkStatsRecorder(FileRotator rotator, NonMonotonicObserver<String> observer,
- DropBoxManager dropBox, String cookie, long bucketDuration, boolean onlyTags) {
+ DropBoxManager dropBox, String cookie, long bucketDuration, boolean onlyTags,
+ boolean wipeOnError) {
mRotator = Objects.requireNonNull(rotator, "missing FileRotator");
mObserver = Objects.requireNonNull(observer, "missing NonMonotonicObserver");
mDropBox = Objects.requireNonNull(dropBox, "missing DropBoxManager");
@@ -121,6 +124,7 @@
mBucketDuration = bucketDuration;
mOnlyTags = onlyTags;
+ mWipeOnError = wipeOnError;
mPending = new NetworkStatsCollection(bucketDuration);
mSinceBoot = new NetworkStatsCollection(bucketDuration);
@@ -552,7 +556,9 @@
}
mDropBox.addData(TAG_NETSTATS_DUMP, os.toByteArray(), 0);
}
-
- mRotator.deleteAll();
+ // Delete all files if this recorder is set wipe on error.
+ if (mWipeOnError) {
+ mRotator.deleteAll();
+ }
}
}
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index ff6e45d..424dcd9 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -76,8 +76,10 @@
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import android.content.res.Resources;
import android.database.ContentObserver;
import android.net.ConnectivityManager;
+import android.net.ConnectivityResources;
import android.net.DataUsageRequest;
import android.net.INetd;
import android.net.INetworkStatsService;
@@ -140,6 +142,7 @@
import android.util.SparseIntArray;
import android.util.proto.ProtoOutputStream;
+import com.android.connectivity.resources.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.FileRotator;
@@ -255,7 +258,8 @@
"netstats_import_legacy_target_attempts";
static final int DEFAULT_NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS = 1;
static final String NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME = "import.attempts";
- static final String NETSTATS_IMPORT_SUCCESS_COUNTER_NAME = "import.successes";
+ static final String NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME = "import.successes";
+ static final String NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME = "import.fallbacks";
private final Context mContext;
private final NetworkStatsFactory mStatsFactory;
@@ -275,10 +279,11 @@
private final AlertObserver mAlertObserver = new AlertObserver();
// Persistent counters that backed by AtomicFile which stored in the data directory as a file,
- // to track attempts/successes count across reboot. Note that these counter values will be
- // rollback as the module rollbacks.
+ // to track attempts/successes/fallbacks count across reboot. Note that these counter values
+ // will be rollback as the module rollbacks.
private PersistentInt mImportLegacyAttemptsCounter = null;
private PersistentInt mImportLegacySuccessesCounter = null;
+ private PersistentInt mImportLegacyFallbacksCounter = null;
@VisibleForTesting
public static final String ACTION_NETWORK_STATS_POLL =
@@ -626,21 +631,14 @@
}
/**
- * Create the persistent counter that counts total import legacy stats attempts.
+ * Create a persistent counter for given directory and name.
*/
- public PersistentInt createImportLegacyAttemptsCounter(@NonNull Path path)
+ public PersistentInt createPersistentCounter(@NonNull Path dir, @NonNull String name)
throws IOException {
// TODO: Modify PersistentInt to call setStartTime every time a write is made.
// Create and pass a real logger here.
- return new PersistentInt(path.toString(), null /* logger */);
- }
-
- /**
- * Create the persistent counter that counts total import legacy stats successes.
- */
- public PersistentInt createImportLegacySuccessesCounter(@NonNull Path path)
- throws IOException {
- return new PersistentInt(path.toString(), null /* logger */);
+ final String path = dir.resolve(name).toString();
+ return new PersistentInt(path, null /* logger */);
}
/**
@@ -770,6 +768,11 @@
return null;
}
}
+
+ /** Gets whether the build is userdebug. */
+ public boolean isDebuggable() {
+ return Build.isDebuggable();
+ }
}
/**
@@ -797,11 +800,14 @@
mSystemReady = true;
// create data recorders along with historical rotators
- mDevRecorder = buildRecorder(PREFIX_DEV, mSettings.getDevConfig(), false, mStatsDir);
- mXtRecorder = buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, mStatsDir);
- mUidRecorder = buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, mStatsDir);
+ mDevRecorder = buildRecorder(PREFIX_DEV, mSettings.getDevConfig(), false, mStatsDir,
+ true /* wipeOnError */);
+ mXtRecorder = buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, mStatsDir,
+ true /* wipeOnError */);
+ mUidRecorder = buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, mStatsDir,
+ true /* wipeOnError */);
mUidTagRecorder = buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true,
- mStatsDir);
+ mStatsDir, true /* wipeOnError */);
updatePersistThresholdsLocked();
@@ -866,12 +872,13 @@
private NetworkStatsRecorder buildRecorder(
String prefix, NetworkStatsSettings.Config config, boolean includeTags,
- File baseDir) {
+ File baseDir, boolean wipeOnError) {
final DropBoxManager dropBox = (DropBoxManager) mContext.getSystemService(
Context.DROPBOX_SERVICE);
return new NetworkStatsRecorder(new FileRotator(
baseDir, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
- mNonMonotonicObserver, dropBox, prefix, config.bucketDuration, includeTags);
+ mNonMonotonicObserver, dropBox, prefix, config.bucketDuration, includeTags,
+ wipeOnError);
}
@GuardedBy("mStatsLock")
@@ -918,10 +925,12 @@
return;
}
try {
- mImportLegacyAttemptsCounter = mDeps.createImportLegacyAttemptsCounter(
- mStatsDir.toPath().resolve(NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME));
- mImportLegacySuccessesCounter = mDeps.createImportLegacySuccessesCounter(
- mStatsDir.toPath().resolve(NETSTATS_IMPORT_SUCCESS_COUNTER_NAME));
+ mImportLegacyAttemptsCounter = mDeps.createPersistentCounter(mStatsDir.toPath(),
+ NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME);
+ mImportLegacySuccessesCounter = mDeps.createPersistentCounter(mStatsDir.toPath(),
+ NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME);
+ mImportLegacyFallbacksCounter = mDeps.createPersistentCounter(mStatsDir.toPath(),
+ NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME);
} catch (IOException e) {
Log.wtf(TAG, "Failed to create persistent counters, skip.", e);
return;
@@ -929,15 +938,33 @@
final int targetAttempts = mDeps.getImportLegacyTargetAttempts();
final int attempts;
+ final int fallbacks;
+ final boolean runComparison;
try {
attempts = mImportLegacyAttemptsCounter.get();
+ // Fallbacks counter would be set to non-zero value to indicate the migration was
+ // not successful.
+ fallbacks = mImportLegacyFallbacksCounter.get();
+ runComparison = shouldRunComparison();
} catch (IOException e) {
- Log.wtf(TAG, "Failed to read attempts counter, skip.", e);
+ Log.wtf(TAG, "Failed to read counters, skip.", e);
return;
}
- if (attempts >= targetAttempts) return;
- Log.i(TAG, "Starting import : attempts " + attempts + "/" + targetAttempts);
+ // If the target number of attempts are reached, don't import any data.
+ // However, if comparison is requested, still read the legacy data and compare
+ // it to the importer output. This allows OEMs to debug issues with the
+ // importer code and to collect signals from the field.
+ final boolean dryRunImportOnly =
+ fallbacks != 0 && runComparison && (attempts >= targetAttempts);
+ // Return if target attempts are reached and there is no need to dry run.
+ if (attempts >= targetAttempts && !dryRunImportOnly) return;
+
+ if (dryRunImportOnly) {
+ Log.i(TAG, "Starting import : only perform read");
+ } else {
+ Log.i(TAG, "Starting import : attempts " + attempts + "/" + targetAttempts);
+ }
final MigrationInfo[] migrations = new MigrationInfo[]{
new MigrationInfo(mDevRecorder), new MigrationInfo(mXtRecorder),
@@ -945,68 +972,62 @@
};
// Legacy directories will be created by recorders if they do not exist
- final File legacyBaseDir = mDeps.getLegacyStatsDir();
- final NetworkStatsRecorder[] legacyRecorders = new NetworkStatsRecorder[]{
- buildRecorder(PREFIX_DEV, mSettings.getDevConfig(), false, legacyBaseDir),
- buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, legacyBaseDir),
- buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, legacyBaseDir),
- buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true, legacyBaseDir)
- };
+ final NetworkStatsRecorder[] legacyRecorders;
+ if (runComparison) {
+ final File legacyBaseDir = mDeps.getLegacyStatsDir();
+ // Set wipeOnError flag false so the recorder won't damage persistent data if reads
+ // failed and calling deleteAll.
+ legacyRecorders = new NetworkStatsRecorder[]{
+ buildRecorder(PREFIX_DEV, mSettings.getDevConfig(), false, legacyBaseDir,
+ false /* wipeOnError */),
+ buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, legacyBaseDir,
+ false /* wipeOnError */),
+ buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, legacyBaseDir,
+ false /* wipeOnError */),
+ buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true, legacyBaseDir,
+ false /* wipeOnError */)};
+ } else {
+ legacyRecorders = null;
+ }
long migrationEndTime = Long.MIN_VALUE;
- boolean endedWithFallback = false;
try {
// First, read all legacy collections. This is OEM code and it can throw. Don't
// commit any data to disk until all are read.
for (int i = 0; i < migrations.length; i++) {
- String errMsg = null;
- Throwable exception = null;
final MigrationInfo migration = migrations[i];
- // Read the collection from platform code, and using fallback method if throws.
+ // Read the collection from platform code, and set fallbacks counter if throws
+ // for better debugging.
try {
migration.collection = readPlatformCollectionForRecorder(migration.recorder);
} catch (Throwable e) {
- errMsg = "Failed to read stats from platform";
- exception = e;
- }
-
- // Also read the collection with legacy method
- final NetworkStatsRecorder legacyRecorder = legacyRecorders[i];
-
- final NetworkStatsCollection legacyStats;
- try {
- legacyStats = legacyRecorder.getOrLoadCompleteLocked();
- } catch (Throwable e) {
- Log.wtf(TAG, "Failed to read stats with legacy method for recorder " + i, e);
- if (exception != null) {
- throw exception;
+ if (dryRunImportOnly) {
+ Log.wtf(TAG, "Platform data read failed. ", e);
+ return;
} else {
- // Use newer stats, since that's all that is available
- continue;
+ // Data is not imported successfully, set fallbacks counter to non-zero
+ // value to trigger dry run every later boot when the runComparison is
+ // true, in order to make it easier to debug issues.
+ tryIncrementLegacyFallbacksCounter();
+ // Re-throw for error handling. This will increase attempts counter.
+ throw e;
}
}
- if (errMsg == null) {
- try {
- errMsg = compareStats(migration.collection, legacyStats);
- } catch (Throwable e) {
- errMsg = "Failed to compare migrated stats with all stats";
- exception = e;
+ if (runComparison) {
+ final boolean success =
+ compareImportedToLegacyStats(migration, legacyRecorders[i]);
+ if (!success && !dryRunImportOnly) {
+ tryIncrementLegacyFallbacksCounter();
}
}
-
- if (errMsg != null) {
- Log.wtf(TAG, "NetworkStats import for migration " + i
- + " returned invalid data: " + errMsg, exception);
- // Fall back to legacy stats for this boot. The stats for old data will be
- // re-imported again on next boot until they succeed the import. This is fine
- // since every import clears the previous stats for the imported timespan.
- migration.collection = legacyStats;
- endedWithFallback = true;
- }
}
+ // For cases where the fallbacks are not zero but target attempts counts reached,
+ // only perform reads above and return here.
+ if (dryRunImportOnly) return;
+
// Find the latest end time.
for (final MigrationInfo migration : migrations) {
final long migrationEnd = migration.collection.getEndMillis();
@@ -1029,11 +1050,7 @@
migration.recorder.importCollectionLocked(migration.collection);
}
- if (endedWithFallback) {
- Log.wtf(TAG, "Imported platform collections with legacy fallback");
- } else {
- Log.i(TAG, "Successfully imported platform collections");
- }
+ // Success normally or uses fallback method.
} catch (Throwable e) {
// The code above calls OEM code that may behave differently across devices.
// It can throw any exception including RuntimeExceptions and
@@ -1073,8 +1090,9 @@
// Success ! No need to import again next time.
try {
mImportLegacyAttemptsCounter.set(targetAttempts);
+ Log.i(TAG, "Successfully imported platform collections");
// The successes counter is only for debugging. Hence, the synchronization
- // between these two counters are not very critical.
+ // between successes counter and attempts counter are not very critical.
final int successCount = mImportLegacySuccessesCounter.get();
mImportLegacySuccessesCounter.set(successCount + 1);
} catch (IOException e) {
@@ -1082,6 +1100,68 @@
}
}
+ void tryIncrementLegacyFallbacksCounter() {
+ try {
+ final int fallbacks = mImportLegacyFallbacksCounter.get();
+ mImportLegacyFallbacksCounter.set(fallbacks + 1);
+ } catch (IOException e) {
+ Log.wtf(TAG, "Failed to update fallback counter.", e);
+ }
+ }
+
+ @VisibleForTesting
+ boolean shouldRunComparison() {
+ final ConnectivityResources resources = new ConnectivityResources(mContext);
+ // 0 if id not found.
+ Boolean overlayValue = null;
+ try {
+ switch (resources.get().getInteger(R.integer.config_netstats_validate_import)) {
+ case 1:
+ overlayValue = Boolean.TRUE;
+ break;
+ case 0:
+ overlayValue = Boolean.FALSE;
+ break;
+ }
+ } catch (Resources.NotFoundException e) {
+ // Overlay value is not defined.
+ }
+ return overlayValue != null ? overlayValue : mDeps.isDebuggable();
+ }
+
+ /**
+ * Compare imported data with the data returned by legacy recorders.
+ *
+ * @return true if the data matches, false if the data does not match or throw with exceptions.
+ */
+ private boolean compareImportedToLegacyStats(@NonNull MigrationInfo migration,
+ @NonNull NetworkStatsRecorder legacyRecorder) {
+ final NetworkStatsCollection legacyStats;
+ try {
+ legacyStats = legacyRecorder.getOrLoadCompleteLocked();
+ } catch (Throwable e) {
+ Log.wtf(TAG, "Failed to read stats with legacy method for recorder "
+ + legacyRecorder.getCookie(), e);
+ // Cannot read data from legacy method, skip comparison.
+ return false;
+ }
+
+ // The result of comparison is only for logging.
+ try {
+ final String error = compareStats(migration.collection, legacyStats);
+ if (error != null) {
+ Log.wtf(TAG, "Unexpected comparison result for recorder "
+ + legacyRecorder.getCookie() + ": " + error);
+ return false;
+ }
+ } catch (Throwable e) {
+ Log.wtf(TAG, "Failed to compare migrated stats with legacy stats for recorder "
+ + legacyRecorder.getCookie(), e);
+ return false;
+ }
+ return true;
+ }
+
private static String str(NetworkStatsCollection.Key key) {
StringBuilder sb = new StringBuilder()
.append(key.ident.toString())
@@ -2506,6 +2586,9 @@
pw.print("platform legacy stats import successes count",
mImportLegacySuccessesCounter.get());
pw.println();
+ pw.print("platform legacy stats import fallbacks count",
+ mImportLegacyFallbacksCounter.get());
+ pw.println();
} catch (IOException e) {
pw.println("(failed to dump platform legacy stats import counters)");
}
diff --git a/service/Android.bp b/service/Android.bp
index 45e43bc..c2dbce1 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -181,6 +181,25 @@
],
}
+// TODO: Remove this temporary library and put code into module when test coverage is enough.
+java_library {
+ name: "service-mdns",
+ sdk_version: "system_server_current",
+ min_sdk_version: "30",
+ srcs: [
+ "mdns/**/*.java",
+ ],
+ libs: [
+ "framework-annotations-lib",
+ "framework-connectivity-pre-jarjar",
+ "framework-wifi.stubs.module_lib",
+ "service-connectivity-pre-jarjar",
+ ],
+ visibility: [
+ "//packages/modules/Connectivity/tests:__subpackages__",
+ ],
+}
+
java_library {
name: "service-connectivity-protos",
sdk_version: "system_current",
diff --git a/service/ServiceConnectivityResources/res/values/config.xml b/service/ServiceConnectivityResources/res/values/config.xml
index 81782f9..bff6953 100644
--- a/service/ServiceConnectivityResources/res/values/config.xml
+++ b/service/ServiceConnectivityResources/res/values/config.xml
@@ -179,4 +179,13 @@
Only supported up to S. On T+, the Wi-Fi code should use unregisterAfterReplacement in order
to ensure that apps see the network disconnect and reconnect. -->
<integer translatable="false" name="config_validationFailureAfterRoamIgnoreTimeMillis">-1</integer>
+
+ <!-- Whether the network stats service should run compare on the result of
+ {@link NetworkStatsDataMigrationUtils#readPlatformCollection} and the result
+ of reading from legacy recorders. Possible values are:
+ 0 = never compare,
+ 1 = always compare,
+ 2 = compare on debuggable builds (default value)
+ -->
+ <integer translatable="false" name="config_netstats_validate_import">2</integer>
</resources>
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index b92dd08..3389d63 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -41,6 +41,7 @@
<item type="array" name="config_ethernet_interfaces"/>
<item type="string" name="config_ethernet_iface_regex"/>
<item type="integer" name="config_validationFailureAfterRoamIgnoreTimeMillis" />
+ <item type="integer" name="config_netstats_validate_import" />
</policy>
</overlayable>
</resources>
diff --git a/service/jni/com_android_server_BpfNetMaps.cpp b/service/jni/com_android_server_BpfNetMaps.cpp
index 2780044..49392e0 100644
--- a/service/jni/com_android_server_BpfNetMaps.cpp
+++ b/service/jni/com_android_server_BpfNetMaps.cpp
@@ -82,13 +82,6 @@
return (jint)status.code();
}
-static jint native_setChildChain(JNIEnv* env, jobject self, jint childChain, jboolean enable) {
- auto chain = static_cast<ChildChain>(childChain);
- int res = mTc.toggleUidOwnerMap(chain, enable);
- if (res) ALOGE("%s failed, error code = %d", __func__, res);
- return (jint)res;
-}
-
static jint native_replaceUidChain(JNIEnv* env, jobject self, jstring name, jboolean isAllowlist,
jintArray jUids) {
const ScopedUtfChars chainNameUtf8(env, name);
@@ -199,8 +192,6 @@
(void*)native_addNiceApp},
{"native_removeNiceApp", "(I)I",
(void*)native_removeNiceApp},
- {"native_setChildChain", "(IZ)I",
- (void*)native_setChildChain},
{"native_replaceUidChain", "(Ljava/lang/String;Z[I)I",
(void*)native_replaceUidChain},
{"native_setUidRule", "(III)I",
diff --git a/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitor.java b/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitor.java
new file mode 100644
index 0000000..2b99d0a
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitor.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2021 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;
+
+/** Interface for monitoring connectivity changes. */
+public interface ConnectivityMonitor {
+ /**
+ * Starts monitoring changes of connectivity of this device, which may indicate that the list of
+ * network interfaces available for multi-cast messaging has changed.
+ */
+ void startWatchingConnectivityChanges();
+
+ /** Stops monitoring changes of connectivity. */
+ void stopWatchingConnectivityChanges();
+
+ void notifyConnectivityChange();
+
+ /** Listener interface for receiving connectivity changes. */
+ interface Listener {
+ void onConnectivityChanged();
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManager.java b/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManager.java
new file mode 100644
index 0000000..3563d61
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManager.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2021 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.TargetApi;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.Build;
+
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+/** Class for monitoring connectivity changes using {@link ConnectivityManager}. */
+public class ConnectivityMonitorWithConnectivityManager implements ConnectivityMonitor {
+ private static final String TAG = "ConnMntrWConnMgr";
+ private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
+
+ private final Listener listener;
+ private final ConnectivityManager.NetworkCallback networkCallback;
+ private final ConnectivityManager connectivityManager;
+ // TODO(b/71901993): Ideally we shouldn't need this flag. However we still don't have clues why
+ // the receiver is unregistered twice yet.
+ private boolean isCallbackRegistered = false;
+
+ @SuppressWarnings({"nullness:assignment", "nullness:method.invocation"})
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public ConnectivityMonitorWithConnectivityManager(Context context, Listener listener) {
+ this.listener = listener;
+
+ connectivityManager =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ networkCallback =
+ new ConnectivityManager.NetworkCallback() {
+ @Override
+ public void onAvailable(Network network) {
+ LOGGER.log("network available.");
+ notifyConnectivityChange();
+ }
+
+ @Override
+ public void onLost(Network network) {
+ LOGGER.log("network lost.");
+ notifyConnectivityChange();
+ }
+
+ @Override
+ public void onUnavailable() {
+ LOGGER.log("network unavailable.");
+ notifyConnectivityChange();
+ }
+ };
+ }
+
+ @Override
+ public void notifyConnectivityChange() {
+ listener.onConnectivityChanged();
+ }
+
+ /**
+ * Starts monitoring changes of connectivity of this device, which may indicate that the list of
+ * network interfaces available for multi-cast messaging has changed.
+ */
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ @Override
+ public void startWatchingConnectivityChanges() {
+ LOGGER.log("Start watching connectivity changes");
+ if (isCallbackRegistered) {
+ return;
+ }
+
+ connectivityManager.registerNetworkCallback(
+ new NetworkRequest.Builder().addTransportType(
+ NetworkCapabilities.TRANSPORT_WIFI).build(),
+ networkCallback);
+ isCallbackRegistered = true;
+ }
+
+ /** Stops monitoring changes of connectivity. */
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ @Override
+ public void stopWatchingConnectivityChanges() {
+ LOGGER.log("Stop watching connectivity changes");
+ if (!isCallbackRegistered) {
+ return;
+ }
+
+ connectivityManager.unregisterNetworkCallback(networkCallback);
+ isCallbackRegistered = false;
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java b/service/mdns/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
new file mode 100644
index 0000000..3db1b22
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2021 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.text.TextUtils;
+import android.util.Pair;
+
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+/**
+ * A {@link Callable} that builds and enqueues a mDNS query to send over the multicast socket. If a
+ * query is built and enqueued successfully, then call to {@link #call()} returns the transaction ID
+ * and the list of the subtypes in the query as a {@link Pair}. If a query is failed to build, or if
+ * it can not be enqueued, then call to {@link #call()} returns {@code null}.
+ */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public class EnqueueMdnsQueryCallable implements Callable<Pair<Integer, List<String>>> {
+
+ private static final String TAG = "MdnsQueryCallable";
+ private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
+ private static final List<Integer> castShellEmulatorMdnsPorts;
+
+ static {
+ castShellEmulatorMdnsPorts = new ArrayList<>();
+ String[] stringPorts = MdnsConfigs.castShellEmulatorMdnsPorts();
+
+ for (String port : stringPorts) {
+ try {
+ castShellEmulatorMdnsPorts.add(Integer.parseInt(port));
+ } catch (NumberFormatException e) {
+ // Ignore.
+ }
+ }
+ }
+
+ private final WeakReference<MdnsSocketClient> weakRequestSender;
+ private final MdnsPacketWriter packetWriter;
+ private final String[] serviceTypeLabels;
+ private final List<String> subtypes;
+ private final boolean expectUnicastResponse;
+ private final int transactionId;
+
+ EnqueueMdnsQueryCallable(
+ @NonNull MdnsSocketClient requestSender,
+ @NonNull MdnsPacketWriter packetWriter,
+ @NonNull String serviceType,
+ @NonNull Collection<String> subtypes,
+ boolean expectUnicastResponse,
+ int transactionId) {
+ weakRequestSender = new WeakReference<>(requestSender);
+ this.packetWriter = packetWriter;
+ serviceTypeLabels = TextUtils.split(serviceType, "\\.");
+ this.subtypes = new ArrayList<>(subtypes);
+ this.expectUnicastResponse = expectUnicastResponse;
+ this.transactionId = transactionId;
+ }
+
+ @Override
+ public Pair<Integer, List<String>> call() {
+ try {
+ MdnsSocketClient requestSender = weakRequestSender.get();
+ if (requestSender == null) {
+ return null;
+ }
+
+ int numQuestions = 1;
+ if (!subtypes.isEmpty()) {
+ numQuestions += subtypes.size();
+ }
+
+ // Header.
+ packetWriter.writeUInt16(transactionId); // transaction ID
+ packetWriter.writeUInt16(MdnsConstants.FLAGS_QUERY); // flags
+ packetWriter.writeUInt16(numQuestions); // number of questions
+ packetWriter.writeUInt16(0); // number of answers (not yet known; will be written later)
+ packetWriter.writeUInt16(0); // number of authority entries
+ packetWriter.writeUInt16(0); // number of additional records
+
+ // Question(s). There will be one question for each (fqdn+subtype, recordType)
+ // combination,
+ // as well as one for each (fqdn, recordType) combination.
+
+ for (String subtype : subtypes) {
+ String[] labels = new String[serviceTypeLabels.length + 2];
+ labels[0] = MdnsConstants.SUBTYPE_PREFIX + subtype;
+ labels[1] = MdnsConstants.SUBTYPE_LABEL;
+ System.arraycopy(serviceTypeLabels, 0, labels, 2, serviceTypeLabels.length);
+
+ packetWriter.writeLabels(labels);
+ packetWriter.writeUInt16(MdnsRecord.TYPE_PTR);
+ packetWriter.writeUInt16(
+ MdnsConstants.QCLASS_INTERNET
+ | (expectUnicastResponse ? MdnsConstants.QCLASS_UNICAST : 0));
+ }
+
+ packetWriter.writeLabels(serviceTypeLabels);
+ packetWriter.writeUInt16(MdnsRecord.TYPE_PTR);
+ packetWriter.writeUInt16(
+ MdnsConstants.QCLASS_INTERNET
+ | (expectUnicastResponse ? MdnsConstants.QCLASS_UNICAST : 0));
+
+ InetAddress mdnsAddress = MdnsConstants.getMdnsIPv4Address();
+ if (requestSender.isOnIPv6OnlyNetwork()) {
+ mdnsAddress = MdnsConstants.getMdnsIPv6Address();
+ }
+
+ sendPacketTo(requestSender,
+ new InetSocketAddress(mdnsAddress, MdnsConstants.MDNS_PORT));
+ for (Integer emulatorPort : castShellEmulatorMdnsPorts) {
+ sendPacketTo(requestSender, new InetSocketAddress(mdnsAddress, emulatorPort));
+ }
+ return Pair.create(transactionId, subtypes);
+ } catch (IOException e) {
+ LOGGER.e(String.format("Failed to create mDNS packet for subtype: %s.",
+ TextUtils.join(",", subtypes)), e);
+ return null;
+ }
+ }
+
+ private void sendPacketTo(MdnsSocketClient requestSender, InetSocketAddress address)
+ throws IOException {
+ DatagramPacket packet = packetWriter.getPacket(address);
+ if (expectUnicastResponse) {
+ requestSender.sendUnicastPacket(packet);
+ } else {
+ requestSender.sendMulticastPacket(packet);
+ }
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/ExecutorProvider.java b/service/mdns/com/android/server/connectivity/mdns/ExecutorProvider.java
new file mode 100644
index 0000000..72b65e0
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/ExecutorProvider.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2021 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.util.ArraySet;
+
+import java.util.Set;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+
+/**
+ * This class provides {@link ScheduledExecutorService} instances to {@link MdnsServiceTypeClient}
+ * instances, and provides method to shutdown all the created executors.
+ */
+public class ExecutorProvider {
+
+ private final Set<ScheduledExecutorService> serviceTypeClientSchedulerExecutors =
+ new ArraySet<>();
+
+ /** Returns a new {@link ScheduledExecutorService} instance. */
+ public ScheduledExecutorService newServiceTypeClientSchedulerExecutor() {
+ // TODO: actually use a pool ?
+ ScheduledExecutorService executor = new ScheduledThreadPoolExecutor(1);
+ serviceTypeClientSchedulerExecutors.add(executor);
+ return executor;
+ }
+
+ /** Shuts down all the created {@link ScheduledExecutorService} instances. */
+ public void shutdownAll() {
+ for (ScheduledExecutorService executor : serviceTypeClientSchedulerExecutors) {
+ executor.shutdownNow();
+ }
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsConfigs.java b/service/mdns/com/android/server/connectivity/mdns/MdnsConfigs.java
new file mode 100644
index 0000000..922037b
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsConfigs.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2021 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;
+
+/**
+ * mDNS configuration values.
+ *
+ * TODO: consider making some of these adjustable via flags.
+ */
+public class MdnsConfigs {
+ public static String[] castShellEmulatorMdnsPorts() {
+ return new String[0];
+ }
+
+ public static long initialTimeBetweenBurstsMs() {
+ return 5000L;
+ }
+
+ public static long timeBetweenBurstsMs() {
+ return 20_000L;
+ }
+
+ public static int queriesPerBurst() {
+ return 3;
+ }
+
+ public static long timeBetweenQueriesInBurstMs() {
+ return 1000L;
+ }
+
+ public static int queriesPerBurstPassive() {
+ return 1;
+ }
+
+ public static boolean alwaysAskForUnicastResponseInEachBurst() {
+ return false;
+ }
+
+ public static boolean useSessionIdToScheduleMdnsTask() {
+ return false;
+ }
+
+ public static boolean shouldCancelScanTaskWhenFutureIsNull() {
+ return false;
+ }
+
+ public static long sleepTimeForSocketThreadMs() {
+ return 20_000L;
+ }
+
+ public static boolean checkMulticastResponse() {
+ return false;
+ }
+
+ public static boolean useSeparateSocketToSendUnicastQuery() {
+ return false;
+ }
+
+ public static long checkMulticastResponseIntervalMs() {
+ return 10_000L;
+ }
+
+ public static boolean clearMdnsPacketQueueAfterDiscoveryStops() {
+ return true;
+ }
+
+ public static boolean allowAddMdnsPacketAfterDiscoveryStops() {
+ return false;
+ }
+
+ public static int mdnsPacketQueueMaxSize() {
+ return Integer.MAX_VALUE;
+ }
+
+ public static boolean preferIpv6() {
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsConstants.java b/service/mdns/com/android/server/connectivity/mdns/MdnsConstants.java
new file mode 100644
index 0000000..ed28700
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsConstants.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2021 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.TargetApi;
+import android.os.Build;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+/** mDNS-related constants. */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+@VisibleForTesting
+public final class MdnsConstants {
+ public static final int MDNS_PORT = 5353;
+ // Flags word format is:
+ // 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00
+ // QR [ Opcode ] AA TC RD RA Z AD CD [ Rcode ]
+ // See http://www.networksorcery.com/enp/protocol/dns.htm
+ // For responses, QR bit should be 1, AA - CD bits should be ignored, and all other bits
+ // should be 0.
+ public static final int FLAGS_QUERY = 0x0000;
+ public static final int FLAGS_RESPONSE_MASK = 0xF80F;
+ public static final int FLAGS_RESPONSE = 0x8000;
+ public static final int QCLASS_INTERNET = 0x0001;
+ public static final int QCLASS_UNICAST = 0x8000;
+ public static final String SUBTYPE_LABEL = "_sub";
+ public static final String SUBTYPE_PREFIX = "_";
+ private static final String MDNS_IPV4_HOST_ADDRESS = "224.0.0.251";
+ private static final String MDNS_IPV6_HOST_ADDRESS = "FF02::FB";
+ private static InetAddress mdnsAddress;
+ private static Charset utf8Charset;
+ private MdnsConstants() {
+ }
+
+ public static InetAddress getMdnsIPv4Address() {
+ synchronized (MdnsConstants.class) {
+ InetAddress addr = null;
+ try {
+ addr = InetAddress.getByName(MDNS_IPV4_HOST_ADDRESS);
+ } catch (UnknownHostException e) {
+ /* won't happen */
+ }
+ mdnsAddress = addr;
+ return mdnsAddress;
+ }
+ }
+
+ public static InetAddress getMdnsIPv6Address() {
+ synchronized (MdnsConstants.class) {
+ InetAddress addr = null;
+ try {
+ addr = InetAddress.getByName(MDNS_IPV6_HOST_ADDRESS);
+ } catch (UnknownHostException e) {
+ /* won't happen */
+ }
+ mdnsAddress = addr;
+ return mdnsAddress;
+ }
+ }
+
+ public static Charset getUtf8Charset() {
+ synchronized (MdnsConstants.class) {
+ if (utf8Charset == null) {
+ utf8Charset = getUtf8CharsetOnKitKat();
+ }
+ return utf8Charset;
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.KITKAT)
+ private static Charset getUtf8CharsetOnKitKat() {
+ return StandardCharsets.UTF_8;
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java b/service/mdns/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
new file mode 100644
index 0000000..1faa6ce
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2021 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.Manifest.permission;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Map;
+
+/**
+ * This class keeps tracking the set of registered {@link MdnsServiceBrowserListener} instances, and
+ * notify them when a mDNS service instance is found, updated, or removed?
+ */
+public class MdnsDiscoveryManager implements MdnsSocketClient.Callback {
+
+ private static final MdnsLogger LOGGER = new MdnsLogger("MdnsDiscoveryManager");
+
+ private final ExecutorProvider executorProvider;
+ private final MdnsSocketClient socketClient;
+
+ private final Map<String, MdnsServiceTypeClient> serviceTypeClients = new ArrayMap<>();
+
+ public MdnsDiscoveryManager(
+ @NonNull ExecutorProvider executorProvider, @NonNull MdnsSocketClient socketClient) {
+ this.executorProvider = executorProvider;
+ this.socketClient = socketClient;
+ }
+
+ /**
+ * Starts (or continue) to discovery mDNS services with given {@code serviceType}, and registers
+ * {@code listener} for receiving mDNS service discovery responses.
+ *
+ * @param serviceType The type of the service to discover.
+ * @param listener The {@link MdnsServiceBrowserListener} listener.
+ * @param searchOptions The {@link MdnsSearchOptions} to be used for discovering {@code
+ * serviceType}.
+ */
+ @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
+ public synchronized void registerListener(
+ @NonNull String serviceType,
+ @NonNull MdnsServiceBrowserListener listener,
+ @NonNull MdnsSearchOptions searchOptions) {
+ LOGGER.log(
+ "Registering listener for subtypes: %s",
+ TextUtils.join(",", searchOptions.getSubtypes()));
+ if (serviceTypeClients.isEmpty()) {
+ // First listener. Starts the socket client.
+ try {
+ socketClient.startDiscovery();
+ } catch (IOException e) {
+ LOGGER.e("Failed to start discover.", e);
+ return;
+ }
+ }
+ // All listeners of the same service types shares the same MdnsServiceTypeClient.
+ MdnsServiceTypeClient serviceTypeClient = serviceTypeClients.get(serviceType);
+ if (serviceTypeClient == null) {
+ serviceTypeClient = createServiceTypeClient(serviceType);
+ serviceTypeClients.put(serviceType, serviceTypeClient);
+ }
+ serviceTypeClient.startSendAndReceive(listener, searchOptions);
+ }
+
+ /**
+ * Unregister {@code listener} for receiving mDNS service discovery responses. IF no listener is
+ * registered for the given service type, stops discovery for the service type.
+ *
+ * @param serviceType The type of the service to discover.
+ * @param listener The {@link MdnsServiceBrowserListener} listener.
+ */
+ @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
+ public synchronized void unregisterListener(
+ @NonNull String serviceType, @NonNull MdnsServiceBrowserListener listener) {
+ LOGGER.log("Unregistering listener for service type: %s", serviceType);
+ MdnsServiceTypeClient serviceTypeClient = serviceTypeClients.get(serviceType);
+ if (serviceTypeClient == null) {
+ return;
+ }
+ if (serviceTypeClient.stopSendAndReceive(listener)) {
+ // No listener is registered for the service type anymore, remove it from the list of
+ // the
+ // service type clients.
+ serviceTypeClients.remove(serviceType);
+ if (serviceTypeClients.isEmpty()) {
+ // No discovery request. Stops the socket client.
+ socketClient.stopDiscovery();
+ }
+ }
+ }
+
+ @Override
+ public synchronized void onResponseReceived(@NonNull MdnsResponse response) {
+ String[] name =
+ response.getPointerRecords().isEmpty()
+ ? null
+ : response.getPointerRecords().get(0).getName();
+ if (name != null) {
+ for (MdnsServiceTypeClient serviceTypeClient : serviceTypeClients.values()) {
+ String[] serviceType = serviceTypeClient.getServiceTypeLabels();
+ if ((Arrays.equals(name, serviceType)
+ || ((name.length == (serviceType.length + 2))
+ && name[1].equals(MdnsConstants.SUBTYPE_LABEL)
+ && MdnsRecord.labelsAreSuffix(serviceType, name)))) {
+ serviceTypeClient.processResponse(response);
+ return;
+ }
+ }
+ }
+ }
+
+ @Override
+ public synchronized void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode) {
+ for (MdnsServiceTypeClient serviceTypeClient : serviceTypeClients.values()) {
+ serviceTypeClient.onFailedToParseMdnsResponse(receivedPacketNumber, errorCode);
+ }
+ }
+
+ @VisibleForTesting
+ MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType) {
+ return new MdnsServiceTypeClient(
+ serviceType, socketClient,
+ executorProvider.newServiceTypeClientSchedulerExecutor());
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java b/service/mdns/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java
new file mode 100644
index 0000000..e35743c
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2021 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 com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Locale;
+import java.util.Objects;
+
+/** An mDNS "AAAA" or "A" record, which holds an IPv6 or IPv4 address. */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+@VisibleForTesting
+public class MdnsInetAddressRecord extends MdnsRecord {
+ private Inet6Address inet6Address;
+ private Inet4Address inet4Address;
+
+ /**
+ * Constructs the {@link MdnsRecord}
+ *
+ * @param name the service host name
+ * @param type the type of record (either Type 'AAAA' or Type 'A')
+ * @param reader the reader to read the record from.
+ */
+ public MdnsInetAddressRecord(String[] name, int type, MdnsPacketReader reader)
+ throws IOException {
+ super(name, type, reader);
+ }
+
+ /** Returns the IPv6 address. */
+ public Inet6Address getInet6Address() {
+ return inet6Address;
+ }
+
+ /** Returns the IPv4 address. */
+ public Inet4Address getInet4Address() {
+ return inet4Address;
+ }
+
+ @Override
+ protected void readData(MdnsPacketReader reader) throws IOException {
+ int size = 4;
+ if (super.getType() == MdnsRecord.TYPE_AAAA) {
+ size = 16;
+ }
+ byte[] buf = new byte[size];
+ reader.readBytes(buf);
+ try {
+ InetAddress address = InetAddress.getByAddress(buf);
+ if (address instanceof Inet4Address) {
+ inet4Address = (Inet4Address) address;
+ inet6Address = null;
+ } else if (address instanceof Inet6Address) {
+ inet4Address = null;
+ inet6Address = (Inet6Address) address;
+ } else {
+ inet4Address = null;
+ inet6Address = null;
+ }
+ } catch (UnknownHostException e) {
+ // Ignore exception
+ }
+ }
+
+ @Override
+ protected void writeData(MdnsPacketWriter writer) throws IOException {
+ byte[] buf = null;
+ if (inet4Address != null) {
+ buf = inet4Address.getAddress();
+ } else if (inet6Address != null) {
+ buf = inet6Address.getAddress();
+ }
+ if (buf != null) {
+ writer.writeBytes(buf);
+ }
+ }
+
+ @Override
+ public String toString() {
+ String type = "AAAA";
+ if (super.getType() == MdnsRecord.TYPE_A) {
+ type = "A";
+ }
+ return String.format(
+ Locale.ROOT, "%s: Inet4Address: %s Inet6Address: %s", type, inet4Address,
+ inet6Address);
+ }
+
+ @Override
+ public int hashCode() {
+ return (super.hashCode() * 31)
+ + Objects.hashCode(inet4Address)
+ + Objects.hashCode(inet6Address);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof MdnsInetAddressRecord)) {
+ return false;
+ }
+
+ return super.equals(other)
+ && Objects.equals(inet4Address, ((MdnsInetAddressRecord) other).inet4Address)
+ && Objects.equals(inet6Address, ((MdnsInetAddressRecord) other).inet6Address);
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsPacketReader.java b/service/mdns/com/android/server/connectivity/mdns/MdnsPacketReader.java
new file mode 100644
index 0000000..61c5f5a
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsPacketReader.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2021 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.util.SparseArray;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/** Simple decoder for mDNS packets. */
+public class MdnsPacketReader {
+ private final byte[] buf;
+ private final int count;
+ private final SparseArray<LabelEntry> labelDictionary;
+ private int pos;
+ private int limit;
+
+ /** Constructs a reader for the given packet. */
+ public MdnsPacketReader(DatagramPacket packet) {
+ buf = packet.getData();
+ count = packet.getLength();
+ pos = 0;
+ limit = -1;
+ labelDictionary = new SparseArray<>(16);
+ }
+
+ /**
+ * Sets a temporary limit (from the current read position) for subsequent reads. Any attempt to
+ * read past this limit will result in an EOFException.
+ *
+ * @param limit The new limit.
+ * @throws IOException If there is insufficient data for the new limit.
+ */
+ public void setLimit(int limit) throws IOException {
+ if (limit >= 0) {
+ if (pos + limit <= count) {
+ this.limit = pos + limit;
+ } else {
+ throw new IOException(
+ String.format(
+ Locale.ROOT,
+ "attempt to set limit beyond available data: %d exceeds %d",
+ pos + limit,
+ count));
+ }
+ }
+ }
+
+ /** Clears the limit set by {@link #setLimit}. */
+ public void clearLimit() {
+ limit = -1;
+ }
+
+ /**
+ * Returns the number of bytes left to read, between the current read position and either the
+ * limit (if set) or the end of the packet.
+ */
+ public int getRemaining() {
+ return (limit >= 0 ? limit : count) - pos;
+ }
+
+ /**
+ * Reads an unsigned 8-bit integer.
+ *
+ * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+ * read.
+ */
+ public int readUInt8() throws EOFException {
+ checkRemaining(1);
+ byte val = buf[pos++];
+ return val & 0xFF;
+ }
+
+ /**
+ * Reads an unsigned 16-bit integer.
+ *
+ * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+ * read.
+ */
+ public int readUInt16() throws EOFException {
+ checkRemaining(2);
+ int val = (buf[pos++] & 0xFF) << 8;
+ val |= (buf[pos++]) & 0xFF;
+ return val;
+ }
+
+ /**
+ * Reads an unsigned 32-bit integer.
+ *
+ * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+ * read.
+ */
+ public long readUInt32() throws EOFException {
+ checkRemaining(4);
+ long val = (long) (buf[pos++] & 0xFF) << 24;
+ val |= (long) (buf[pos++] & 0xFF) << 16;
+ val |= (long) (buf[pos++] & 0xFF) << 8;
+ val |= buf[pos++] & 0xFF;
+ return val;
+ }
+
+ /**
+ * Reads a sequence of labels and returns them as an array of strings. A sequence of labels is
+ * either a sequence of strings terminated by a NUL byte, a sequence of strings terminated by a
+ * pointer, or a pointer.
+ *
+ * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+ * read.
+ * @throws IOException If invalid data is read.
+ */
+ public String[] readLabels() throws IOException {
+ List<String> result = new ArrayList<>(5);
+ LabelEntry previousEntry = null;
+
+ while (getRemaining() > 0) {
+ byte nextByte = peekByte();
+
+ if (nextByte == 0) {
+ // A NUL byte terminates a sequence of labels.
+ skip(1);
+ break;
+ }
+
+ int currentOffset = pos;
+
+ boolean isLabelPointer = (nextByte & 0xC0) == 0xC0;
+ if (isLabelPointer) {
+ // A pointer terminates a sequence of labels. Store the pointer value in the
+ // previous label entry.
+ int labelOffset = ((readUInt8() & 0x3F) << 8) | (readUInt8() & 0xFF);
+ if (previousEntry != null) {
+ previousEntry.nextOffset = labelOffset;
+ }
+
+ // Follow the chain of labels starting at this pointer, adding all of them onto the
+ // result.
+ while (labelOffset != 0) {
+ LabelEntry entry = labelDictionary.get(labelOffset);
+ if (entry == null) {
+ throw new IOException(
+ String.format(Locale.ROOT, "Invalid label pointer: %04X",
+ labelOffset));
+ }
+ result.add(entry.label);
+ labelOffset = entry.nextOffset;
+ }
+ break;
+ } else {
+ // It's an ordinary label. Chain it onto the previous label entry (if any), and add
+ // it onto the result.
+ String val = readString();
+ LabelEntry newEntry = new LabelEntry(val);
+ labelDictionary.put(currentOffset, newEntry);
+
+ if (previousEntry != null) {
+ previousEntry.nextOffset = currentOffset;
+ }
+ previousEntry = newEntry;
+ result.add(val);
+ }
+ }
+
+ return result.toArray(new String[result.size()]);
+ }
+
+ /**
+ * Reads a length-prefixed string.
+ *
+ * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+ * read.
+ */
+ public String readString() throws EOFException {
+ int len = readUInt8();
+ checkRemaining(len);
+ String val = new String(buf, pos, len, MdnsConstants.getUtf8Charset());
+ pos += len;
+ return val;
+ }
+
+ /**
+ * Reads a specific number of bytes.
+ *
+ * @param bytes The array to fill.
+ * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+ * read.
+ */
+ public void readBytes(byte[] bytes) throws EOFException {
+ checkRemaining(bytes.length);
+ System.arraycopy(buf, pos, bytes, 0, bytes.length);
+ pos += bytes.length;
+ }
+
+ /**
+ * Skips over the given number of bytes.
+ *
+ * @param count The number of bytes to read and discard.
+ * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+ * read.
+ */
+ public void skip(int count) throws EOFException {
+ checkRemaining(count);
+ pos += count;
+ }
+
+ /**
+ * Peeks at and returns the next byte in the packet, without advancing the read position.
+ *
+ * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+ * read.
+ */
+ public byte peekByte() throws EOFException {
+ checkRemaining(1);
+ return buf[pos];
+ }
+
+ /** Returns the current byte position of the reader for the data packet. */
+ public int getPosition() {
+ return pos;
+ }
+
+ // Checks if the number of remaining bytes to be read in the packet is at least |count|.
+ private void checkRemaining(int count) throws EOFException {
+ if (getRemaining() < count) {
+ throw new EOFException();
+ }
+ }
+
+ private static class LabelEntry {
+ public final String label;
+ public int nextOffset = 0;
+
+ public LabelEntry(String label) {
+ this.label = label;
+ }
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsPacketWriter.java b/service/mdns/com/android/server/connectivity/mdns/MdnsPacketWriter.java
new file mode 100644
index 0000000..2fed36d
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsPacketWriter.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2021 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 java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.SocketAddress;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Simple encoder for mDNS packets. */
+public class MdnsPacketWriter {
+ private static final int MDNS_POINTER_MASK = 0xC000;
+ private final byte[] data;
+ private final Map<Integer, String[]> labelDictionary;
+ private int pos = 0;
+ private int savedWritePos = -1;
+
+ /**
+ * Constructs a writer for a new packet.
+ *
+ * @param maxSize The maximum size of a packet.
+ */
+ public MdnsPacketWriter(int maxSize) {
+ if (maxSize <= 0) {
+ throw new IllegalArgumentException("invalid size");
+ }
+
+ data = new byte[maxSize];
+ labelDictionary = new HashMap<>();
+ }
+
+ /** Returns the current write position. */
+ public int getWritePosition() {
+ return pos;
+ }
+
+ /**
+ * Saves the current write position and then rewinds the write position by the given number of
+ * bytes. This is useful for updating length fields earlier in the packet. Rewinds cannot be
+ * nested.
+ *
+ * @param position The position to rewind to.
+ * @throws IOException If the count would go beyond the beginning of the packet, or if there is
+ * already a rewind in effect.
+ */
+ public void rewind(int position) throws IOException {
+ if ((savedWritePos != -1) || (position > pos) || (position < 0)) {
+ throw new IOException("invalid rewind");
+ }
+
+ savedWritePos = pos;
+ pos = position;
+ }
+
+ /**
+ * Sets the current write position to what it was prior to the last rewind.
+ *
+ * @throws IOException If there was no rewind in effect.
+ */
+ public void unrewind() throws IOException {
+ if (savedWritePos == -1) {
+ throw new IOException("no rewind is in effect");
+ }
+ pos = savedWritePos;
+ savedWritePos = -1;
+ }
+
+ /** Clears any rewind state. */
+ public void clearRewind() {
+ savedWritePos = -1;
+ }
+
+ /**
+ * Writes an unsigned 8-bit integer.
+ *
+ * @param value The value to write.
+ * @throws IOException If there is not enough space remaining in the packet.
+ */
+ public void writeUInt8(int value) throws IOException {
+ checkRemaining(1);
+ data[pos++] = (byte) (value & 0xFF);
+ }
+
+ /**
+ * Writes an unsigned 16-bit integer.
+ *
+ * @param value The value to write.
+ * @throws IOException If there is not enough space remaining in the packet.
+ */
+ public void writeUInt16(int value) throws IOException {
+ checkRemaining(2);
+ data[pos++] = (byte) ((value >>> 8) & 0xFF);
+ data[pos++] = (byte) (value & 0xFF);
+ }
+
+ /**
+ * Writes an unsigned 32-bit integer.
+ *
+ * @param value The value to write.
+ * @throws IOException If there is not enough space remaining in the packet.
+ */
+ public void writeUInt32(long value) throws IOException {
+ checkRemaining(4);
+ data[pos++] = (byte) ((value >>> 24) & 0xFF);
+ data[pos++] = (byte) ((value >>> 16) & 0xFF);
+ data[pos++] = (byte) ((value >>> 8) & 0xFF);
+ data[pos++] = (byte) (value & 0xFF);
+ }
+
+ /**
+ * Writes a specific number of bytes.
+ *
+ * @param data The array to write.
+ * @throws IOException If there is not enough space remaining in the packet.
+ */
+ public void writeBytes(byte[] data) throws IOException {
+ checkRemaining(data.length);
+ System.arraycopy(data, 0, this.data, pos, data.length);
+ pos += data.length;
+ }
+
+ /**
+ * Writes a string.
+ *
+ * @param value The string to write.
+ * @throws IOException If there is not enough space remaining in the packet.
+ */
+ public void writeString(String value) throws IOException {
+ byte[] utf8 = value.getBytes(MdnsConstants.getUtf8Charset());
+ writeUInt8(utf8.length);
+ writeBytes(utf8);
+ }
+
+ /**
+ * Writes a series of labels. Uses name compression.
+ *
+ * @param labels The labels to write.
+ * @throws IOException If there is not enough space remaining in the packet.
+ */
+ public void writeLabels(String[] labels) throws IOException {
+ // See section 4.1.4 of RFC 1035 (http://tools.ietf.org/html/rfc1035) for a description
+ // of the name compression method used here.
+
+ int suffixLength = 0;
+ int suffixPointer = 0;
+
+ for (Map.Entry<Integer, String[]> entry : labelDictionary.entrySet()) {
+ int existingOffset = entry.getKey();
+ String[] existingLabels = entry.getValue();
+
+ if (Arrays.equals(existingLabels, labels)) {
+ writePointer(existingOffset);
+ return;
+ } else if (MdnsRecord.labelsAreSuffix(existingLabels, labels)) {
+ // Keep track of the longest matching suffix so far.
+ if (existingLabels.length > suffixLength) {
+ suffixLength = existingLabels.length;
+ suffixPointer = existingOffset;
+ }
+ }
+ }
+
+ if (suffixLength > 0) {
+ for (int i = 0; i < (labels.length - suffixLength); ++i) {
+ writeString(labels[i]);
+ }
+ writePointer(suffixPointer);
+ } else {
+ int[] offsets = new int[labels.length];
+ for (int i = 0; i < labels.length; ++i) {
+ offsets[i] = getWritePosition();
+ writeString(labels[i]);
+ }
+ writeUInt8(0); // NUL terminator
+
+ // Add entries to the label dictionary for each suffix of the label list, including
+ // the whole list itself.
+ for (int i = 0, len = labels.length; i < labels.length; ++i, --len) {
+ String[] value = new String[len];
+ System.arraycopy(labels, i, value, 0, len);
+ labelDictionary.put(offsets[i], value);
+ }
+ }
+ }
+
+ /** Returns the number of bytes that can still be written. */
+ public int getRemaining() {
+ return data.length - pos;
+ }
+
+ // Writes a pointer to a label.
+ private void writePointer(int offset) throws IOException {
+ writeUInt16(MDNS_POINTER_MASK | offset);
+ }
+
+ // Checks if the remaining space in the packet is at least |count|.
+ private void checkRemaining(int count) throws IOException {
+ if (getRemaining() < count) {
+ throw new IOException();
+ }
+ }
+
+ /** Builds and returns the packet. */
+ public DatagramPacket getPacket(SocketAddress destAddress) throws IOException {
+ return new DatagramPacket(data, pos, destAddress);
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsPointerRecord.java b/service/mdns/com/android/server/connectivity/mdns/MdnsPointerRecord.java
new file mode 100644
index 0000000..2b36a3c
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsPointerRecord.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2021 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 com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+/** An mDNS "PTR" record, which holds a name (the "pointer"). */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+@VisibleForTesting
+public class MdnsPointerRecord extends MdnsRecord {
+ private String[] pointer;
+
+ public MdnsPointerRecord(String[] name, MdnsPacketReader reader) throws IOException {
+ super(name, TYPE_PTR, reader);
+ }
+
+ /** Returns the pointer as an array of labels. */
+ public String[] getPointer() {
+ return pointer;
+ }
+
+ @Override
+ protected void readData(MdnsPacketReader reader) throws IOException {
+ pointer = reader.readLabels();
+ }
+
+ @Override
+ protected void writeData(MdnsPacketWriter writer) throws IOException {
+ writer.writeLabels(pointer);
+ }
+
+ public boolean hasSubtype() {
+ return (name != null) && (name.length > 2) && name[1].equals(MdnsConstants.SUBTYPE_LABEL);
+ }
+
+ public String getSubtype() {
+ return hasSubtype() ? name[0] : null;
+ }
+
+ @Override
+ public String toString() {
+ return "PTR: " + labelsToString(name) + " -> " + labelsToString(pointer);
+ }
+
+ @Override
+ public int hashCode() {
+ return (super.hashCode() * 31) + Arrays.hashCode(pointer);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof MdnsPointerRecord)) {
+ return false;
+ }
+
+ return super.equals(other) && Arrays.equals(pointer, ((MdnsPointerRecord) other).pointer);
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsRecord.java b/service/mdns/com/android/server/connectivity/mdns/MdnsRecord.java
new file mode 100644
index 0000000..4bfdb2c
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsRecord.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2021 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.SystemClock;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Abstract base class for mDNS records. Stores the header fields and provides methods for reading
+ * the record from and writing it to a packet.
+ */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public abstract class MdnsRecord {
+ public static final int TYPE_A = 0x0001;
+ public static final int TYPE_AAAA = 0x001C;
+ public static final int TYPE_PTR = 0x000C;
+ public static final int TYPE_SRV = 0x0021;
+ public static final int TYPE_TXT = 0x0010;
+
+ /** Status indicating that the record is current. */
+ public static final int STATUS_OK = 0;
+ /** Status indicating that the record has expired (TTL reached 0). */
+ public static final int STATUS_EXPIRED = 1;
+ /** Status indicating that the record should be refreshed (Less than half of TTL remains.) */
+ public static final int STATUS_NEEDS_REFRESH = 2;
+
+ protected final String[] name;
+ private final int type;
+ private final int cls;
+ private final long receiptTimeMillis;
+ private final long ttlMillis;
+ private Object key;
+
+ /**
+ * Constructs a new record with the given name and type.
+ *
+ * @param reader The reader to read the record from.
+ * @throws IOException If an error occurs while reading the packet.
+ */
+ protected MdnsRecord(String[] name, int type, MdnsPacketReader reader) throws IOException {
+ this.name = name;
+ this.type = type;
+ cls = reader.readUInt16();
+ ttlMillis = TimeUnit.SECONDS.toMillis(reader.readUInt32());
+ int dataLength = reader.readUInt16();
+
+ receiptTimeMillis = SystemClock.elapsedRealtime();
+
+ reader.setLimit(dataLength);
+ readData(reader);
+ reader.clearLimit();
+ }
+
+ /**
+ * Converts an array of labels into their dot-separated string representation. This method
+ * should
+ * be used for logging purposes only.
+ */
+ public static String labelsToString(String[] labels) {
+ if (labels == null) {
+ return null;
+ }
+ return TextUtils.join(".", labels);
+ }
+
+ /** Tests if |list1| is a suffix of |list2|. */
+ public static boolean labelsAreSuffix(String[] list1, String[] list2) {
+ int offset = list2.length - list1.length;
+
+ if (offset < 1) {
+ return false;
+ }
+
+ for (int i = 0; i < list1.length; ++i) {
+ if (!list1[i].equals(list2[i + offset])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /** Returns the record's receipt (creation) time. */
+ public final long getReceiptTime() {
+ return receiptTimeMillis;
+ }
+
+ /** Returns the record's name. */
+ public String[] getName() {
+ return name;
+ }
+
+ /** Returns the record's original TTL, in milliseconds. */
+ public final long getTtl() {
+ return ttlMillis;
+ }
+
+ /** Returns the record's type. */
+ public final int getType() {
+ return type;
+ }
+
+ /**
+ * Returns the record's remaining TTL.
+ *
+ * @param now The current system time.
+ * @return The remaning TTL, in milliseconds.
+ */
+ public long getRemainingTTL(final long now) {
+ long age = now - receiptTimeMillis;
+ if (age > ttlMillis) {
+ return 0;
+ }
+
+ return ttlMillis - age;
+ }
+
+ /**
+ * Reads the record's payload from a packet.
+ *
+ * @param reader The reader to use.
+ * @throws IOException If an I/O error occurs.
+ */
+ protected abstract void readData(MdnsPacketReader reader) throws IOException;
+
+ /**
+ * Writes the record to a packet.
+ *
+ * @param writer The writer to use.
+ * @param now The current system time. This is used when writing the updated TTL.
+ */
+ @VisibleForTesting
+ public final void write(MdnsPacketWriter writer, long now) throws IOException {
+ writer.writeLabels(name);
+ writer.writeUInt16(type);
+ writer.writeUInt16(cls);
+
+ writer.writeUInt32(TimeUnit.MILLISECONDS.toSeconds(getRemainingTTL(now)));
+
+ int dataLengthPos = writer.getWritePosition();
+ writer.writeUInt16(0); // data length
+ int dataPos = writer.getWritePosition();
+
+ writeData(writer);
+
+ // Calculate amount of data written, and overwrite the data field earlier in the packet.
+ int endPos = writer.getWritePosition();
+ int dataLength = endPos - dataPos;
+ writer.rewind(dataLengthPos);
+ writer.writeUInt16(dataLength);
+ writer.unrewind();
+ }
+
+ /**
+ * Writes the record's payload to a packet.
+ *
+ * @param writer The writer to use.
+ * @throws IOException If an I/O error occurs.
+ */
+ protected abstract void writeData(MdnsPacketWriter writer) throws IOException;
+
+ /** Gets the status of the record. */
+ public int getStatus(final long now) {
+ final long age = now - receiptTimeMillis;
+ if (age > ttlMillis) {
+ return STATUS_EXPIRED;
+ }
+ if (age > (ttlMillis / 2)) {
+ return STATUS_NEEDS_REFRESH;
+ }
+ return STATUS_OK;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof MdnsRecord)) {
+ return false;
+ }
+
+ MdnsRecord otherRecord = (MdnsRecord) other;
+
+ return Arrays.equals(name, otherRecord.name) && (type == otherRecord.type);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(Arrays.hashCode(name), type);
+ }
+
+ /**
+ * Returns an opaque object that uniquely identifies this record through a combination of its
+ * type
+ * and name. Suitable for use as a key in caches.
+ */
+ public final Object getKey() {
+ if (key == null) {
+ key = new Key(type, name);
+ }
+ return key;
+ }
+
+ private static final class Key {
+ private final int recordType;
+ private final String[] recordName;
+
+ public Key(int recordType, String[] recordName) {
+ this.recordType = recordType;
+ this.recordName = recordName;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof Key)) {
+ return false;
+ }
+
+ Key otherKey = (Key) other;
+
+ return (recordType == otherKey.recordType) && Arrays.equals(recordName,
+ otherKey.recordName);
+ }
+
+ @Override
+ public int hashCode() {
+ return (recordType * 31) + Arrays.hashCode(recordName);
+ }
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsResponse.java b/service/mdns/com/android/server/connectivity/mdns/MdnsResponse.java
new file mode 100644
index 0000000..1305e07
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsResponse.java
@@ -0,0 +1,380 @@
+/*
+ * Copyright (C) 2021 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 com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+/** An mDNS response. */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public class MdnsResponse {
+ private final List<MdnsRecord> records;
+ private final List<MdnsPointerRecord> pointerRecords;
+ private MdnsServiceRecord serviceRecord;
+ private MdnsTextRecord textRecord;
+ private MdnsInetAddressRecord inet4AddressRecord;
+ private MdnsInetAddressRecord inet6AddressRecord;
+ private long lastUpdateTime;
+
+ /** Constructs a new, empty response. */
+ public MdnsResponse(long now) {
+ lastUpdateTime = now;
+ records = new LinkedList<>();
+ pointerRecords = new LinkedList<>();
+ }
+
+ // This generic typed helper compares records for equality.
+ // Returns True if records are the same.
+ private <T> boolean recordsAreSame(T a, T b) {
+ return ((a == null) && (b == null)) || ((a != null) && (b != null) && a.equals(b));
+ }
+
+ /**
+ * Adds a pointer record.
+ *
+ * @return <code>true</code> if the record was added, or <code>false</code> if a matching
+ * pointer
+ * record is already present in the response.
+ */
+ public synchronized boolean addPointerRecord(MdnsPointerRecord pointerRecord) {
+ if (!pointerRecords.contains(pointerRecord)) {
+ pointerRecords.add(pointerRecord);
+ records.add(pointerRecord);
+ return true;
+ }
+
+ return false;
+ }
+
+ /** Gets the pointer records. */
+ public synchronized List<MdnsPointerRecord> getPointerRecords() {
+ // Returns a shallow copy.
+ return new LinkedList<>(pointerRecords);
+ }
+
+ public synchronized boolean hasPointerRecords() {
+ return !pointerRecords.isEmpty();
+ }
+
+ @VisibleForTesting
+ /* package */ synchronized void clearPointerRecords() {
+ pointerRecords.clear();
+ }
+
+ public synchronized boolean hasSubtypes() {
+ for (MdnsPointerRecord pointerRecord : pointerRecords) {
+ if (pointerRecord.hasSubtype()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public synchronized List<String> getSubtypes() {
+ List<String> subtypes = null;
+
+ for (MdnsPointerRecord pointerRecord : pointerRecords) {
+ if (pointerRecord.hasSubtype()) {
+ if (subtypes == null) {
+ subtypes = new LinkedList<>();
+ }
+ subtypes.add(pointerRecord.getSubtype());
+ }
+ }
+
+ return subtypes;
+ }
+
+ @VisibleForTesting
+ public synchronized void removeSubtypes() {
+ Iterator<MdnsPointerRecord> iter = pointerRecords.iterator();
+ while (iter.hasNext()) {
+ MdnsPointerRecord pointerRecord = iter.next();
+ if (pointerRecord.hasSubtype()) {
+ iter.remove();
+ }
+ }
+ }
+
+ /** Sets the service record. */
+ public synchronized boolean setServiceRecord(MdnsServiceRecord serviceRecord) {
+ if (recordsAreSame(this.serviceRecord, serviceRecord)) {
+ return false;
+ }
+ if (this.serviceRecord != null) {
+ records.remove(this.serviceRecord);
+ }
+ this.serviceRecord = serviceRecord;
+ if (this.serviceRecord != null) {
+ records.add(this.serviceRecord);
+ }
+ return true;
+ }
+
+ /** Gets the service record. */
+ public synchronized MdnsServiceRecord getServiceRecord() {
+ return serviceRecord;
+ }
+
+ public synchronized boolean hasServiceRecord() {
+ return serviceRecord != null;
+ }
+
+ /** Sets the text record. */
+ public synchronized boolean setTextRecord(MdnsTextRecord textRecord) {
+ if (recordsAreSame(this.textRecord, textRecord)) {
+ return false;
+ }
+ if (this.textRecord != null) {
+ records.remove(this.textRecord);
+ }
+ this.textRecord = textRecord;
+ if (this.textRecord != null) {
+ records.add(this.textRecord);
+ }
+ return true;
+ }
+
+ /** Gets the text record. */
+ public synchronized MdnsTextRecord getTextRecord() {
+ return textRecord;
+ }
+
+ public synchronized boolean hasTextRecord() {
+ return textRecord != null;
+ }
+
+ /** Sets the IPv4 address record. */
+ public synchronized boolean setInet4AddressRecord(MdnsInetAddressRecord newInet4AddressRecord) {
+ if (recordsAreSame(this.inet4AddressRecord, newInet4AddressRecord)) {
+ return false;
+ }
+ if (this.inet4AddressRecord != null) {
+ records.remove(this.inet4AddressRecord);
+ }
+ if (newInet4AddressRecord != null && newInet4AddressRecord.getInet4Address() != null) {
+ this.inet4AddressRecord = newInet4AddressRecord;
+ records.add(this.inet4AddressRecord);
+ }
+ return true;
+ }
+
+ /** Gets the IPv4 address record. */
+ public synchronized MdnsInetAddressRecord getInet4AddressRecord() {
+ return inet4AddressRecord;
+ }
+
+ public synchronized boolean hasInet4AddressRecord() {
+ return inet4AddressRecord != null;
+ }
+
+ /** Sets the IPv6 address record. */
+ public synchronized boolean setInet6AddressRecord(MdnsInetAddressRecord newInet6AddressRecord) {
+ if (recordsAreSame(this.inet6AddressRecord, newInet6AddressRecord)) {
+ return false;
+ }
+ if (this.inet6AddressRecord != null) {
+ records.remove(this.inet6AddressRecord);
+ }
+ if (newInet6AddressRecord != null && newInet6AddressRecord.getInet6Address() != null) {
+ this.inet6AddressRecord = newInet6AddressRecord;
+ records.add(this.inet6AddressRecord);
+ }
+ return true;
+ }
+
+
+ /** Gets the IPv6 address record. */
+ public synchronized MdnsInetAddressRecord getInet6AddressRecord() {
+ return inet6AddressRecord;
+ }
+
+ public synchronized boolean hasInet6AddressRecord() {
+ return inet6AddressRecord != null;
+ }
+
+ /** Gets all of the records. */
+ public synchronized List<MdnsRecord> getRecords() {
+ return new LinkedList<>(records);
+ }
+
+ /**
+ * Merges any records that are present in another response into this one.
+ *
+ * @return <code>true</code> if any records were added or updated.
+ */
+ public synchronized boolean mergeRecordsFrom(MdnsResponse other) {
+ lastUpdateTime = other.lastUpdateTime;
+
+ boolean updated = false;
+
+ List<MdnsPointerRecord> pointerRecords = other.getPointerRecords();
+ if (pointerRecords != null) {
+ for (MdnsPointerRecord pointerRecord : pointerRecords) {
+ if (addPointerRecord(pointerRecord)) {
+ updated = true;
+ }
+ }
+ }
+
+ MdnsServiceRecord serviceRecord = other.getServiceRecord();
+ if (serviceRecord != null) {
+ if (setServiceRecord(serviceRecord)) {
+ updated = true;
+ }
+ }
+
+ MdnsTextRecord textRecord = other.getTextRecord();
+ if (textRecord != null) {
+ if (setTextRecord(textRecord)) {
+ updated = true;
+ }
+ }
+
+ MdnsInetAddressRecord otherInet4AddressRecord = other.getInet4AddressRecord();
+ if (otherInet4AddressRecord != null && otherInet4AddressRecord.getInet4Address() != null) {
+ if (setInet4AddressRecord(otherInet4AddressRecord)) {
+ updated = true;
+ }
+ }
+
+ MdnsInetAddressRecord otherInet6AddressRecord = other.getInet6AddressRecord();
+ if (otherInet6AddressRecord != null && otherInet6AddressRecord.getInet6Address() != null) {
+ if (setInet6AddressRecord(otherInet6AddressRecord)) {
+ updated = true;
+ }
+ }
+
+ // If the hostname in the service record no longer matches the hostname in either of the
+ // address records, then drop the address records.
+ if (this.serviceRecord != null) {
+ boolean dropAddressRecords = false;
+
+ if (this.inet4AddressRecord != null) {
+ if (!Arrays.equals(
+ this.serviceRecord.getServiceHost(), this.inet4AddressRecord.getName())) {
+ dropAddressRecords = true;
+ }
+ }
+ if (this.inet6AddressRecord != null) {
+ if (!Arrays.equals(
+ this.serviceRecord.getServiceHost(), this.inet6AddressRecord.getName())) {
+ dropAddressRecords = true;
+ }
+ }
+
+ if (dropAddressRecords) {
+ setInet4AddressRecord(null);
+ setInet6AddressRecord(null);
+ updated = true;
+ }
+ }
+
+ return updated;
+ }
+
+ /**
+ * Tests if the response is complete. A response is considered complete if it contains PTR, SRV,
+ * TXT, and A (for IPv4) or AAAA (for IPv6) records.
+ */
+ public synchronized boolean isComplete() {
+ return !pointerRecords.isEmpty()
+ && (serviceRecord != null)
+ && (textRecord != null)
+ && (inet4AddressRecord != null || inet6AddressRecord != null);
+ }
+
+ /**
+ * Returns the key for this response. The key uniquely identifies the response by its service
+ * name.
+ */
+ public synchronized String getServiceInstanceName() {
+ if (pointerRecords.isEmpty()) {
+ return null;
+ }
+ String[] pointers = pointerRecords.get(0).getPointer();
+ return ((pointers != null) && (pointers.length > 0)) ? pointers[0] : null;
+ }
+
+ /**
+ * Tests if this response is a goodbye message. This will be true if a service record is present
+ * and any of the records have a TTL of 0.
+ */
+ public synchronized boolean isGoodbye() {
+ if (getServiceInstanceName() != null) {
+ for (MdnsRecord record : records) {
+ // Expiring PTR records with subtypes just signal a change in known supported
+ // criteria, not the device itself going offline, so ignore those.
+ if ((record instanceof MdnsPointerRecord)
+ && ((MdnsPointerRecord) record).hasSubtype()) {
+ continue;
+ }
+
+ if (record.getTtl() == 0) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Writes the response to a packet.
+ *
+ * @param writer The writer to use.
+ * @param now The current time. This is used to write updated TTLs that reflect the remaining
+ * TTL
+ * since the response was received.
+ * @return The number of records that were written.
+ * @throws IOException If an error occurred while writing (typically indicating overflow).
+ */
+ public synchronized int write(MdnsPacketWriter writer, long now) throws IOException {
+ int count = 0;
+ for (MdnsPointerRecord pointerRecord : pointerRecords) {
+ pointerRecord.write(writer, now);
+ ++count;
+ }
+
+ if (serviceRecord != null) {
+ serviceRecord.write(writer, now);
+ ++count;
+ }
+
+ if (textRecord != null) {
+ textRecord.write(writer, now);
+ ++count;
+ }
+
+ if (inet4AddressRecord != null) {
+ inet4AddressRecord.write(writer, now);
+ ++count;
+ }
+
+ if (inet6AddressRecord != null) {
+ inet6AddressRecord.write(writer, now);
+ ++count;
+ }
+
+ return count;
+ }
+}
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsResponseDecoder.java b/service/mdns/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
new file mode 100644
index 0000000..72c3156
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2021 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.SystemClock;
+
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+
+/** A class that decodes mDNS responses from UDP packets. */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public class MdnsResponseDecoder {
+
+ public static final int SUCCESS = 0;
+ private static final String TAG = "MdnsResponseDecoder";
+ private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
+ private final String[] serviceType;
+ private final Clock clock;
+
+ /** Constructs a new decoder that will extract responses for the given service type. */
+ public MdnsResponseDecoder(@NonNull Clock clock, @Nullable String[] serviceType) {
+ this.clock = clock;
+ this.serviceType = serviceType;
+ }
+
+ private static void skipMdnsRecord(MdnsPacketReader reader) throws IOException {
+ reader.skip(2 + 4); // skip the class and TTL
+ int dataLength = reader.readUInt16();
+ reader.skip(dataLength);
+ }
+
+ private static MdnsResponse findResponseWithPointer(
+ List<MdnsResponse> responses, String[] pointer) {
+ if (responses != null) {
+ for (MdnsResponse response : responses) {
+ List<MdnsPointerRecord> pointerRecords = response.getPointerRecords();
+ if (pointerRecords == null) {
+ continue;
+ }
+ for (MdnsPointerRecord pointerRecord : pointerRecords) {
+ if (Arrays.equals(pointerRecord.getPointer(), pointer)) {
+ return response;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ private static MdnsResponse findResponseWithHostName(
+ List<MdnsResponse> responses, String[] hostName) {
+ if (responses != null) {
+ for (MdnsResponse response : responses) {
+ MdnsServiceRecord serviceRecord = response.getServiceRecord();
+ if (serviceRecord == null) {
+ continue;
+ }
+ if (Arrays.equals(serviceRecord.getServiceHost(), hostName)) {
+ return response;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Decodes all mDNS responses for the desired service type from a packet. The class does not
+ * check
+ * the responses for completeness; the caller should do that.
+ *
+ * @param packet The packet to read from.
+ * @return A list of mDNS responses, or null if the packet contained no appropriate responses.
+ */
+ public int decode(@NonNull DatagramPacket packet, @NonNull List<MdnsResponse> responses) {
+ MdnsPacketReader reader = new MdnsPacketReader(packet);
+
+ List<MdnsRecord> records;
+ try {
+ reader.readUInt16(); // transaction ID (not used)
+ int flags = reader.readUInt16();
+ if ((flags & MdnsConstants.FLAGS_RESPONSE_MASK) != MdnsConstants.FLAGS_RESPONSE) {
+ return MdnsResponseErrorCode.ERROR_NOT_RESPONSE_MESSAGE;
+ }
+
+ int numQuestions = reader.readUInt16();
+ int numAnswers = reader.readUInt16();
+ int numAuthority = reader.readUInt16();
+ int numRecords = reader.readUInt16();
+
+ LOGGER.log(String.format(
+ "num questions: %d, num answers: %d, num authority: %d, num records: %d",
+ numQuestions, numAnswers, numAuthority, numRecords));
+
+ if (numAnswers < 1) {
+ return MdnsResponseErrorCode.ERROR_NO_ANSWERS;
+ }
+
+ records = new LinkedList<>();
+
+ for (int i = 0; i < (numAnswers + numAuthority + numRecords); ++i) {
+ String[] name;
+ try {
+ name = reader.readLabels();
+ } catch (IOException e) {
+ LOGGER.e("Failed to read labels from mDNS response.", e);
+ return MdnsResponseErrorCode.ERROR_READING_RECORD_NAME;
+ }
+ int type = reader.readUInt16();
+
+ switch (type) {
+ case MdnsRecord.TYPE_A: {
+ try {
+ records.add(new MdnsInetAddressRecord(name, MdnsRecord.TYPE_A, reader));
+ } catch (IOException e) {
+ LOGGER.e("Failed to read A record from mDNS response.", e);
+ return MdnsResponseErrorCode.ERROR_READING_A_RDATA;
+ }
+ break;
+ }
+
+ case MdnsRecord.TYPE_AAAA: {
+ try {
+ // AAAA should only contain the IPv6 address.
+ MdnsInetAddressRecord record =
+ new MdnsInetAddressRecord(name, MdnsRecord.TYPE_AAAA, reader);
+ if (record.getInet6Address() != null) {
+ records.add(record);
+ }
+ } catch (IOException e) {
+ LOGGER.e("Failed to read AAAA record from mDNS response.", e);
+ return MdnsResponseErrorCode.ERROR_READING_AAAA_RDATA;
+ }
+ break;
+ }
+
+ case MdnsRecord.TYPE_PTR: {
+ try {
+ records.add(new MdnsPointerRecord(name, reader));
+ } catch (IOException e) {
+ LOGGER.e("Failed to read PTR record from mDNS response.", e);
+ return MdnsResponseErrorCode.ERROR_READING_PTR_RDATA;
+ }
+ break;
+ }
+
+ case MdnsRecord.TYPE_SRV: {
+ if (name.length == 4) {
+ try {
+ records.add(new MdnsServiceRecord(name, reader));
+ } catch (IOException e) {
+ LOGGER.e("Failed to read SRV record from mDNS response.", e);
+ return MdnsResponseErrorCode.ERROR_READING_SRV_RDATA;
+ }
+ } else {
+ try {
+ skipMdnsRecord(reader);
+ } catch (IOException e) {
+ LOGGER.e("Failed to skip SVR record from mDNS response.", e);
+ return MdnsResponseErrorCode.ERROR_SKIPPING_SRV_RDATA;
+ }
+ }
+ break;
+ }
+
+ case MdnsRecord.TYPE_TXT: {
+ try {
+ records.add(new MdnsTextRecord(name, reader));
+ } catch (IOException e) {
+ LOGGER.e("Failed to read TXT record from mDNS response.", e);
+ return MdnsResponseErrorCode.ERROR_READING_TXT_RDATA;
+ }
+ break;
+ }
+
+ default: {
+ try {
+ skipMdnsRecord(reader);
+ } catch (IOException e) {
+ LOGGER.e("Failed to skip mDNS record.", e);
+ return MdnsResponseErrorCode.ERROR_SKIPPING_UNKNOWN_RECORD;
+ }
+ }
+ }
+ }
+ } catch (EOFException e) {
+ LOGGER.e("Reached the end of the mDNS response unexpectedly.", e);
+ return MdnsResponseErrorCode.ERROR_END_OF_FILE;
+ }
+
+ // The response records are structured in a hierarchy, where some records reference
+ // others, as follows:
+ //
+ // PTR
+ // / \
+ // / \
+ // TXT SRV
+ // / \
+ // / \
+ // A AAAA
+ //
+ // But the order in which these records appear in the response packet is completely
+ // arbitrary. This means that we need to rescan the record list to construct each level of
+ // this hierarchy.
+ //
+ // PTR: service type -> service instance name
+ //
+ // SRV: service instance name -> host name (priority, weight)
+ //
+ // TXT: service instance name -> machine readable txt entries.
+ //
+ // A: host name -> IP address
+
+ // Loop 1: find PTR records, which identify distinct service instances.
+ long now = SystemClock.elapsedRealtime();
+ for (MdnsRecord record : records) {
+ if (record instanceof MdnsPointerRecord) {
+ String[] name = record.getName();
+ if ((serviceType == null)
+ || Arrays.equals(name, serviceType)
+ || ((name.length == (serviceType.length + 2))
+ && name[1].equals(MdnsConstants.SUBTYPE_LABEL)
+ && MdnsRecord.labelsAreSuffix(serviceType, name))) {
+ MdnsPointerRecord pointerRecord = (MdnsPointerRecord) record;
+ // Group PTR records that refer to the same service instance name into a single
+ // response.
+ MdnsResponse response = findResponseWithPointer(responses,
+ pointerRecord.getPointer());
+ if (response == null) {
+ response = new MdnsResponse(now);
+ responses.add(response);
+ }
+ response.addPointerRecord((MdnsPointerRecord) record);
+ }
+ }
+ }
+
+ // Loop 2: find SRV and TXT records, which reference the pointer in the PTR record.
+ for (MdnsRecord record : records) {
+ if (record instanceof MdnsServiceRecord) {
+ MdnsServiceRecord serviceRecord = (MdnsServiceRecord) record;
+ MdnsResponse response = findResponseWithPointer(responses, serviceRecord.getName());
+ if (response != null) {
+ response.setServiceRecord(serviceRecord);
+ }
+ } else if (record instanceof MdnsTextRecord) {
+ MdnsTextRecord textRecord = (MdnsTextRecord) record;
+ MdnsResponse response = findResponseWithPointer(responses, textRecord.getName());
+ if (response != null) {
+ response.setTextRecord(textRecord);
+ }
+ }
+ }
+
+ // Loop 3: find A and AAAA records, which reference the host name in the SRV record.
+ for (MdnsRecord record : records) {
+ if (record instanceof MdnsInetAddressRecord) {
+ MdnsInetAddressRecord inetRecord = (MdnsInetAddressRecord) record;
+ MdnsResponse response = findResponseWithHostName(responses, inetRecord.getName());
+ if (inetRecord.getInet4Address() != null && response != null) {
+ response.setInet4AddressRecord(inetRecord);
+ } else if (inetRecord.getInet6Address() != null && response != null) {
+ response.setInet6AddressRecord(inetRecord);
+ }
+ }
+ }
+
+ return SUCCESS;
+ }
+
+ public static class Clock {
+ public long elapsedRealtime() {
+ return SystemClock.elapsedRealtime();
+ }
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsResponseErrorCode.java b/service/mdns/com/android/server/connectivity/mdns/MdnsResponseErrorCode.java
new file mode 100644
index 0000000..fcf9058
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsResponseErrorCode.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2021 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;
+
+/**
+ * The list of error code for parsing mDNS response.
+ *
+ * @hide
+ */
+public class MdnsResponseErrorCode {
+ public static final int SUCCESS = 0;
+ public static final int ERROR_NOT_RESPONSE_MESSAGE = 1;
+ public static final int ERROR_NO_ANSWERS = 2;
+ public static final int ERROR_READING_RECORD_NAME = 3;
+ public static final int ERROR_READING_A_RDATA = 4;
+ public static final int ERROR_READING_AAAA_RDATA = 5;
+ public static final int ERROR_READING_PTR_RDATA = 6;
+ public static final int ERROR_SKIPPING_PTR_RDATA = 7;
+ public static final int ERROR_READING_SRV_RDATA = 8;
+ public static final int ERROR_SKIPPING_SRV_RDATA = 9;
+ public static final int ERROR_READING_TXT_RDATA = 10;
+ public static final int ERROR_SKIPPING_UNKNOWN_RECORD = 11;
+ public static final int ERROR_END_OF_FILE = 12;
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsSearchOptions.java b/service/mdns/com/android/server/connectivity/mdns/MdnsSearchOptions.java
new file mode 100644
index 0000000..6e90d2c
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsSearchOptions.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2021 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.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.ArraySet;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * API configuration parameters for searching the mDNS service.
+ *
+ * <p>Use {@link MdnsSearchOptions.Builder} to create {@link MdnsSearchOptions}.
+ *
+ * @hide
+ */
+public class MdnsSearchOptions implements Parcelable {
+
+ /** @hide */
+ public static final Parcelable.Creator<MdnsSearchOptions> CREATOR =
+ new Parcelable.Creator<MdnsSearchOptions>() {
+ @Override
+ public MdnsSearchOptions createFromParcel(Parcel source) {
+ return new MdnsSearchOptions(source.createStringArrayList(),
+ source.readBoolean());
+ }
+
+ @Override
+ public MdnsSearchOptions[] newArray(int size) {
+ return new MdnsSearchOptions[size];
+ }
+ };
+ private static MdnsSearchOptions defaultOptions;
+ private final List<String> subtypes;
+
+ private final boolean isPassiveMode;
+
+ /** Parcelable constructs for a {@link MdnsServiceInfo}. */
+ MdnsSearchOptions(List<String> subtypes, boolean isPassiveMode) {
+ this.subtypes = new ArrayList<>();
+ if (subtypes != null) {
+ this.subtypes.addAll(subtypes);
+ }
+ this.isPassiveMode = isPassiveMode;
+ }
+
+ /** Returns a {@link Builder} for {@link MdnsSearchOptions}. */
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ /** Returns a default search options. */
+ public static synchronized MdnsSearchOptions getDefaultOptions() {
+ if (defaultOptions == null) {
+ defaultOptions = newBuilder().build();
+ }
+ return defaultOptions;
+ }
+
+ /** @return the list of subtypes to search. */
+ public List<String> getSubtypes() {
+ return subtypes;
+ }
+
+ /**
+ * @return {@code true} if the passive mode is used. The passive mode scans less frequently in
+ * order to conserve battery and produce less network traffic.
+ */
+ public boolean isPassiveMode() {
+ return isPassiveMode;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeStringList(subtypes);
+ out.writeBoolean(isPassiveMode);
+ }
+
+ /** A builder to create {@link MdnsSearchOptions}. */
+ public static final class Builder {
+ private final Set<String> subtypes;
+ private boolean isPassiveMode = true;
+
+ private Builder() {
+ subtypes = new ArraySet<>();
+ }
+
+ /**
+ * Adds a subtype to search.
+ *
+ * @param subtype the subtype to add.
+ */
+ public Builder addSubtype(@NonNull String subtype) {
+ if (TextUtils.isEmpty(subtype)) {
+ throw new IllegalArgumentException("Empty subtype");
+ }
+ subtypes.add(subtype);
+ return this;
+ }
+
+ /**
+ * Adds a set of subtypes to search.
+ *
+ * @param subtypes The list of subtypes to add.
+ */
+ public Builder addSubtypes(@NonNull Collection<String> subtypes) {
+ this.subtypes.addAll(Objects.requireNonNull(subtypes));
+ return this;
+ }
+
+ /**
+ * Sets if the passive mode scan should be used. The passive mode scans less frequently in
+ * order
+ * to conserve battery and produce less network traffic.
+ *
+ * @param isPassiveMode If set to {@code true}, passive mode will be used. If set to {@code
+ * false}, active mode will be used.
+ */
+ public Builder setIsPassiveMode(boolean isPassiveMode) {
+ this.isPassiveMode = isPassiveMode;
+ return this;
+ }
+
+ /** Builds a {@link MdnsSearchOptions} with the arguments supplied to this builder. */
+ public MdnsSearchOptions build() {
+ return new MdnsSearchOptions(new ArrayList<>(subtypes), isPassiveMode);
+ }
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceBrowserListener.java b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceBrowserListener.java
new file mode 100644
index 0000000..53e58d1
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceBrowserListener.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2021 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 java.util.List;
+
+/**
+ * Listener interface for mDNS service instance discovery events.
+ *
+ * @hide
+ */
+public interface MdnsServiceBrowserListener {
+
+ /**
+ * Called when an mDNS service instance is found.
+ *
+ * @param serviceInfo The found mDNS service instance.
+ */
+ void onServiceFound(@NonNull MdnsServiceInfo serviceInfo);
+
+ /**
+ * Called when an mDNS service instance is updated.
+ *
+ * @param serviceInfo The updated mDNS service instance.
+ */
+ void onServiceUpdated(@NonNull MdnsServiceInfo serviceInfo);
+
+ /**
+ * Called when an mDNS service instance is no longer valid and removed.
+ *
+ * @param serviceInstanceName The service instance name of the removed mDNS service.
+ */
+ void onServiceRemoved(@NonNull String serviceInstanceName);
+
+ /**
+ * Called when searching for mDNS service has stopped because of an error.
+ *
+ * TODO (changed when importing code): define error constants
+ *
+ * @param error The error code of the stop reason.
+ */
+ void onSearchStoppedWithError(int error);
+
+ /** Called when it failed to start an mDNS service discovery process. */
+ void onSearchFailedToStart();
+
+ /**
+ * Called when a mDNS service discovery query has been sent.
+ *
+ * @param subtypes The list of subtypes in the discovery query.
+ * @param transactionId The transaction ID of the query.
+ */
+ void onDiscoveryQuerySent(@NonNull List<String> subtypes, int transactionId);
+
+ /**
+ * Called when an error has happened when parsing a received mDNS response packet.
+ *
+ * @param receivedPacketNumber The packet sequence number of the received packet.
+ * @param errorCode The error code, defined in {@link MdnsResponseErrorCode}.
+ */
+ void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode);
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceInfo.java b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceInfo.java
new file mode 100644
index 0000000..2e4a4e5
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceInfo.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2021 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.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * A class representing a discovered mDNS service instance.
+ *
+ * @hide
+ */
+public class MdnsServiceInfo implements Parcelable {
+
+ /** @hide */
+ public static final Parcelable.Creator<MdnsServiceInfo> CREATOR =
+ new Parcelable.Creator<MdnsServiceInfo>() {
+
+ @Override
+ public MdnsServiceInfo createFromParcel(Parcel source) {
+ return new MdnsServiceInfo(
+ source.readString(),
+ source.createStringArray(),
+ source.createStringArrayList(),
+ source.createStringArray(),
+ source.readInt(),
+ source.readString(),
+ source.readString(),
+ source.createStringArrayList());
+ }
+
+ @Override
+ public MdnsServiceInfo[] newArray(int size) {
+ return new MdnsServiceInfo[size];
+ }
+ };
+
+ private final String serviceInstanceName;
+ private final String[] serviceType;
+ private final List<String> subtypes;
+ private final String[] hostName;
+ private final int port;
+ private final String ipv4Address;
+ private final String ipv6Address;
+ private final Map<String, String> attributes = new HashMap<>();
+ List<String> textStrings;
+
+ /**
+ * Constructs a {@link MdnsServiceInfo} object with default values.
+ *
+ * @hide
+ */
+ public MdnsServiceInfo(
+ String serviceInstanceName,
+ String[] serviceType,
+ List<String> subtypes,
+ String[] hostName,
+ int port,
+ String ipv4Address,
+ String ipv6Address,
+ List<String> textStrings) {
+ this.serviceInstanceName = serviceInstanceName;
+ this.serviceType = serviceType;
+ this.subtypes = new ArrayList<>();
+ if (subtypes != null) {
+ this.subtypes.addAll(subtypes);
+ }
+ this.hostName = hostName;
+ this.port = port;
+ this.ipv4Address = ipv4Address;
+ this.ipv6Address = ipv6Address;
+ if (textStrings != null) {
+ for (String text : textStrings) {
+ int pos = text.indexOf('=');
+ if (pos < 1) {
+ continue;
+ }
+ attributes.put(text.substring(0, pos).toLowerCase(Locale.ENGLISH),
+ text.substring(++pos));
+ }
+ }
+ }
+
+ /** @return the name of this service instance. */
+ public String getServiceInstanceName() {
+ return serviceInstanceName;
+ }
+
+ /** @return the type of this service instance. */
+ public String[] getServiceType() {
+ return serviceType;
+ }
+
+ /** @return the list of subtypes supported by this service instance. */
+ public List<String> getSubtypes() {
+ return new ArrayList<>(subtypes);
+ }
+
+ /**
+ * @return {@code true} if this service instance supports any subtypes.
+ * @return {@code false} if this service instance does not support any subtypes.
+ */
+ public boolean hasSubtypes() {
+ return !subtypes.isEmpty();
+ }
+
+ /** @return the host name of this service instance. */
+ public String[] getHostName() {
+ return hostName;
+ }
+
+ /** @return the port number of this service instance. */
+ public int getPort() {
+ return port;
+ }
+
+ /** @return the IPV4 address of this service instance. */
+ public String getIpv4Address() {
+ return ipv4Address;
+ }
+
+ /** @return the IPV6 address of this service instance. */
+ public String getIpv6Address() {
+ return ipv6Address;
+ }
+
+ /**
+ * @return the attribute value for {@code key}.
+ * @return {@code null} if no attribute value exists for {@code key}.
+ */
+ public String getAttributeByKey(@NonNull String key) {
+ return attributes.get(key.toLowerCase(Locale.ENGLISH));
+ }
+
+ /** @return an immutable map of all attributes. */
+ public Map<String, String> getAttributes() {
+ return Collections.unmodifiableMap(attributes);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ if (textStrings == null) {
+ // Lazily initialize the parcelable field mTextStrings.
+ textStrings = new ArrayList<>(attributes.size());
+ for (Map.Entry<String, String> kv : attributes.entrySet()) {
+ textStrings.add(String.format(Locale.ROOT, "%s=%s", kv.getKey(), kv.getValue()));
+ }
+ }
+
+ out.writeString(serviceInstanceName);
+ out.writeStringArray(serviceType);
+ out.writeStringList(subtypes);
+ out.writeStringArray(hostName);
+ out.writeInt(port);
+ out.writeString(ipv4Address);
+ out.writeString(ipv6Address);
+ out.writeStringList(textStrings);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.ROOT,
+ "Name: %s, subtypes: %s, ip: %s, port: %d",
+ serviceInstanceName,
+ TextUtils.join(",", subtypes),
+ ipv4Address,
+ port);
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceRecord.java b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceRecord.java
new file mode 100644
index 0000000..51de3b2
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceRecord.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2021 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 com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.Objects;
+
+/** An mDNS "SRV" record, which contains service information. */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+@VisibleForTesting
+public class MdnsServiceRecord extends MdnsRecord {
+ public static final int PROTO_NONE = 0;
+ public static final int PROTO_TCP = 1;
+ public static final int PROTO_UDP = 2;
+ private static final String PROTO_TOKEN_TCP = "_tcp";
+ private static final String PROTO_TOKEN_UDP = "_udp";
+ private int servicePriority;
+ private int serviceWeight;
+ private int servicePort;
+ private String[] serviceHost;
+
+ public MdnsServiceRecord(String[] name, MdnsPacketReader reader) throws IOException {
+ super(name, TYPE_SRV, reader);
+ }
+
+ /** Returns the service's port number. */
+ public int getServicePort() {
+ return servicePort;
+ }
+
+ /** Returns the service's host name. */
+ public String[] getServiceHost() {
+ return serviceHost;
+ }
+
+ /** Returns the service's priority. */
+ public int getServicePriority() {
+ return servicePriority;
+ }
+
+ /** Returns the service's weight. */
+ public int getServiceWeight() {
+ return serviceWeight;
+ }
+
+ // Format of name is <instance-name>.<service-name>.<protocol>.<domain>
+
+ /** Returns the service's instance name, which uniquely identifies the service instance. */
+ public String getServiceInstanceName() {
+ if (name.length < 1) {
+ return null;
+ }
+ return name[0];
+ }
+
+ /** Returns the service's name. */
+ public String getServiceName() {
+ if (name.length < 2) {
+ return null;
+ }
+ return name[1];
+ }
+
+ /** Returns the service's protocol. */
+ public int getServiceProtocol() {
+ if (name.length < 3) {
+ return PROTO_NONE;
+ }
+
+ String protocol = name[2];
+ if (protocol.equals(PROTO_TOKEN_TCP)) {
+ return PROTO_TCP;
+ }
+ if (protocol.equals(PROTO_TOKEN_UDP)) {
+ return PROTO_UDP;
+ }
+ return PROTO_NONE;
+ }
+
+ @Override
+ protected void readData(MdnsPacketReader reader) throws IOException {
+ servicePriority = reader.readUInt16();
+ serviceWeight = reader.readUInt16();
+ servicePort = reader.readUInt16();
+ serviceHost = reader.readLabels();
+ }
+
+ @Override
+ protected void writeData(MdnsPacketWriter writer) throws IOException {
+ writer.writeUInt16(servicePriority);
+ writer.writeUInt16(serviceWeight);
+ writer.writeUInt16(servicePort);
+ writer.writeLabels(serviceHost);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.ROOT,
+ "SRV: %s:%d (prio=%d, weight=%d)",
+ labelsToString(serviceHost),
+ servicePort,
+ servicePriority,
+ serviceWeight);
+ }
+
+ @Override
+ public int hashCode() {
+ return (super.hashCode() * 31)
+ + Objects.hash(servicePriority, serviceWeight, Arrays.hashCode(serviceHost),
+ servicePort);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof MdnsServiceRecord)) {
+ return false;
+ }
+ MdnsServiceRecord otherRecord = (MdnsServiceRecord) other;
+
+ return super.equals(other)
+ && (servicePriority == otherRecord.servicePriority)
+ && (serviceWeight == otherRecord.serviceWeight)
+ && Objects.equals(serviceHost, otherRecord.serviceHost)
+ && (servicePort == otherRecord.servicePort);
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
new file mode 100644
index 0000000..c3a86e3
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2021 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.text.TextUtils;
+import android.util.ArraySet;
+import android.util.Pair;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Instance of this class sends and receives mDNS packets of a given service type and invoke
+ * registered {@link MdnsServiceBrowserListener} instances.
+ */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public class MdnsServiceTypeClient {
+
+ private static final int DEFAULT_MTU = 1500;
+ private static final MdnsLogger LOGGER = new MdnsLogger("MdnsServiceTypeClient");
+
+ private final String serviceType;
+ private final String[] serviceTypeLabels;
+ private final MdnsSocketClient socketClient;
+ private final ScheduledExecutorService executor;
+ private final Object lock = new Object();
+ private final Set<MdnsServiceBrowserListener> listeners = new ArraySet<>();
+ private final Map<String, MdnsResponse> instanceNameToResponse = new HashMap<>();
+
+ // The session ID increases when startSendAndReceive() is called where we schedule a
+ // QueryTask for
+ // new subtypes. It stays the same between packets for same subtypes.
+ private long currentSessionId = 0;
+
+ @GuardedBy("lock")
+ private Future<?> requestTaskFuture;
+
+ /**
+ * Constructor of {@link MdnsServiceTypeClient}.
+ *
+ * @param socketClient Sends and receives mDNS packet.
+ * @param executor A {@link ScheduledExecutorService} used to schedule query tasks.
+ */
+ public MdnsServiceTypeClient(
+ @NonNull String serviceType,
+ @NonNull MdnsSocketClient socketClient,
+ @NonNull ScheduledExecutorService executor) {
+ this.serviceType = serviceType;
+ this.socketClient = socketClient;
+ this.executor = executor;
+ serviceTypeLabels = TextUtils.split(serviceType, "\\.");
+ }
+
+ private static MdnsServiceInfo buildMdnsServiceInfoFromResponse(
+ @NonNull MdnsResponse response, @NonNull String[] serviceTypeLabels) {
+ String[] hostName = response.getServiceRecord().getServiceHost();
+ int port = response.getServiceRecord().getServicePort();
+
+ String ipv4Address = null;
+ String ipv6Address = null;
+ if (response.hasInet4AddressRecord()) {
+ ipv4Address = response.getInet4AddressRecord().getInet4Address().getHostAddress();
+ }
+ if (response.hasInet6AddressRecord()) {
+ ipv6Address = response.getInet6AddressRecord().getInet6Address().getHostAddress();
+ }
+ // TODO: Throw an error message if response doesn't have Inet6 or Inet4 address.
+ return new MdnsServiceInfo(
+ response.getServiceInstanceName(),
+ serviceTypeLabels,
+ response.getSubtypes(),
+ hostName,
+ port,
+ ipv4Address,
+ ipv6Address,
+ response.getTextRecord().getStrings());
+ }
+
+ /**
+ * Registers {@code listener} for receiving discovery event of mDNS service instances, and
+ * starts
+ * (or continue) to send mDNS queries periodically.
+ *
+ * @param listener The {@link MdnsServiceBrowserListener} to register.
+ * @param searchOptions {@link MdnsSearchOptions} contains the list of subtypes to discover.
+ */
+ public void startSendAndReceive(
+ @NonNull MdnsServiceBrowserListener listener,
+ @NonNull MdnsSearchOptions searchOptions) {
+ synchronized (lock) {
+ if (!listeners.contains(listener)) {
+ listeners.add(listener);
+ for (MdnsResponse existingResponse : instanceNameToResponse.values()) {
+ if (existingResponse.isComplete()) {
+ listener.onServiceFound(
+ buildMdnsServiceInfoFromResponse(existingResponse,
+ serviceTypeLabels));
+ }
+ }
+ }
+ // Cancel the next scheduled periodical task.
+ if (requestTaskFuture != null) {
+ requestTaskFuture.cancel(true);
+ }
+ // Keep tracking the ScheduledFuture for the task so we can cancel it if caller is not
+ // interested anymore.
+ requestTaskFuture =
+ executor.submit(
+ new QueryTask(
+ new QueryTaskConfig(
+ searchOptions.getSubtypes(),
+ searchOptions.isPassiveMode(),
+ ++currentSessionId)));
+ }
+ }
+
+ /**
+ * Unregisters {@code listener} from receiving discovery event of mDNS service instances.
+ *
+ * @param listener The {@link MdnsServiceBrowserListener} to unregister.
+ * @return {@code true} if no listener is registered with this client after unregistering {@code
+ * listener}. Otherwise returns {@code false}.
+ */
+ public boolean stopSendAndReceive(@NonNull MdnsServiceBrowserListener listener) {
+ synchronized (lock) {
+ listeners.remove(listener);
+ if (listeners.isEmpty() && requestTaskFuture != null) {
+ requestTaskFuture.cancel(true);
+ requestTaskFuture = null;
+ }
+ return listeners.isEmpty();
+ }
+ }
+
+ public String[] getServiceTypeLabels() {
+ return serviceTypeLabels;
+ }
+
+ public synchronized void processResponse(@NonNull MdnsResponse response) {
+ if (response.isGoodbye()) {
+ onGoodbyeReceived(response.getServiceInstanceName());
+ } else {
+ onResponseReceived(response);
+ }
+ }
+
+ public synchronized void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode) {
+ for (MdnsServiceBrowserListener listener : listeners) {
+ listener.onFailedToParseMdnsResponse(receivedPacketNumber, errorCode);
+ }
+ }
+
+ private void onResponseReceived(@NonNull MdnsResponse response) {
+ MdnsResponse currentResponse;
+ currentResponse = instanceNameToResponse.get(response.getServiceInstanceName());
+
+ boolean newServiceFound = false;
+ boolean existingServiceChanged = false;
+ if (currentResponse == null) {
+ newServiceFound = true;
+ currentResponse = response;
+ instanceNameToResponse.put(response.getServiceInstanceName(), currentResponse);
+ } else if (currentResponse.mergeRecordsFrom(response)) {
+ existingServiceChanged = true;
+ }
+ if (!currentResponse.isComplete() || (!newServiceFound && !existingServiceChanged)) {
+ return;
+ }
+ MdnsServiceInfo serviceInfo =
+ buildMdnsServiceInfoFromResponse(currentResponse, serviceTypeLabels);
+
+ for (MdnsServiceBrowserListener listener : listeners) {
+ if (newServiceFound) {
+ listener.onServiceFound(serviceInfo);
+ } else {
+ listener.onServiceUpdated(serviceInfo);
+ }
+ }
+ }
+
+ private void onGoodbyeReceived(@NonNull String serviceInstanceName) {
+ instanceNameToResponse.remove(serviceInstanceName);
+ for (MdnsServiceBrowserListener listener : listeners) {
+ listener.onServiceRemoved(serviceInstanceName);
+ }
+ }
+
+ @VisibleForTesting
+ MdnsPacketWriter createMdnsPacketWriter() {
+ return new MdnsPacketWriter(DEFAULT_MTU);
+ }
+
+ // A configuration for the PeriodicalQueryTask that contains parameters to build a query packet.
+ // Call to getConfigForNextRun returns a config that can be used to build the next query task.
+ @VisibleForTesting
+ static class QueryTaskConfig {
+
+ private static final int INITIAL_TIME_BETWEEN_BURSTS_MS =
+ (int) MdnsConfigs.initialTimeBetweenBurstsMs();
+ private static final int TIME_BETWEEN_BURSTS_MS = (int) MdnsConfigs.timeBetweenBurstsMs();
+ private static final int QUERIES_PER_BURST = (int) MdnsConfigs.queriesPerBurst();
+ private static final int TIME_BETWEEN_QUERIES_IN_BURST_MS =
+ (int) MdnsConfigs.timeBetweenQueriesInBurstMs();
+ private static final int QUERIES_PER_BURST_PASSIVE_MODE =
+ (int) MdnsConfigs.queriesPerBurstPassive();
+ private static final int UNSIGNED_SHORT_MAX_VALUE = 65536;
+ // The following fields are used by QueryTask so we need to test them.
+ @VisibleForTesting
+ final List<String> subtypes;
+ private final boolean alwaysAskForUnicastResponse =
+ MdnsConfigs.alwaysAskForUnicastResponseInEachBurst();
+ private final boolean usePassiveMode;
+ private final long sessionId;
+ @VisibleForTesting
+ int transactionId;
+ @VisibleForTesting
+ boolean expectUnicastResponse;
+ private int queriesPerBurst;
+ private int timeBetweenBurstsInMs;
+ private int burstCounter;
+ private int timeToRunNextTaskInMs;
+ private boolean isFirstBurst;
+
+ QueryTaskConfig(@NonNull Collection<String> subtypes, boolean usePassiveMode,
+ long sessionId) {
+ this.usePassiveMode = usePassiveMode;
+ this.subtypes = new ArrayList<>(subtypes);
+ this.queriesPerBurst = QUERIES_PER_BURST;
+ this.burstCounter = 0;
+ this.transactionId = 1;
+ this.expectUnicastResponse = true;
+ this.isFirstBurst = true;
+ this.sessionId = sessionId;
+ // Config the scan frequency based on the scan mode.
+ if (this.usePassiveMode) {
+ // In passive scan mode, sends a single burst of QUERIES_PER_BURST queries, and then
+ // in each TIME_BETWEEN_BURSTS interval, sends QUERIES_PER_BURST_PASSIVE_MODE
+ // queries.
+ this.timeBetweenBurstsInMs = TIME_BETWEEN_BURSTS_MS;
+ } else {
+ // In active scan mode, sends a burst of QUERIES_PER_BURST queries,
+ // TIME_BETWEEN_QUERIES_IN_BURST_MS apart, then waits for the scan interval, and
+ // then repeats. The scan interval starts as INITIAL_TIME_BETWEEN_BURSTS_MS and
+ // doubles until it maxes out at TIME_BETWEEN_BURSTS_MS.
+ this.timeBetweenBurstsInMs = INITIAL_TIME_BETWEEN_BURSTS_MS;
+ }
+ }
+
+ QueryTaskConfig getConfigForNextRun() {
+ if (++transactionId > UNSIGNED_SHORT_MAX_VALUE) {
+ transactionId = 1;
+ }
+ // Only the first query expects uni-cast response.
+ expectUnicastResponse = false;
+ if (++burstCounter == queriesPerBurst) {
+ burstCounter = 0;
+
+ if (alwaysAskForUnicastResponse) {
+ expectUnicastResponse = true;
+ }
+ // In passive scan mode, sends a single burst of QUERIES_PER_BURST queries, and
+ // then in each TIME_BETWEEN_BURSTS interval, sends QUERIES_PER_BURST_PASSIVE_MODE
+ // queries.
+ if (isFirstBurst) {
+ isFirstBurst = false;
+ if (usePassiveMode) {
+ queriesPerBurst = QUERIES_PER_BURST_PASSIVE_MODE;
+ }
+ }
+ // In active scan mode, sends a burst of QUERIES_PER_BURST queries,
+ // TIME_BETWEEN_QUERIES_IN_BURST_MS apart, then waits for the scan interval, and
+ // then repeats. The scan interval starts as INITIAL_TIME_BETWEEN_BURSTS_MS and
+ // doubles until it maxes out at TIME_BETWEEN_BURSTS_MS.
+ timeToRunNextTaskInMs = timeBetweenBurstsInMs;
+ if (timeBetweenBurstsInMs < TIME_BETWEEN_BURSTS_MS) {
+ timeBetweenBurstsInMs = Math.min(timeBetweenBurstsInMs * 2,
+ TIME_BETWEEN_BURSTS_MS);
+ }
+ } else {
+ timeToRunNextTaskInMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
+ }
+ return this;
+ }
+ }
+
+ // A FutureTask that enqueues a single query, and schedule a new FutureTask for the next task.
+ private class QueryTask implements Runnable {
+
+ private final QueryTaskConfig config;
+
+ QueryTask(@NonNull QueryTaskConfig config) {
+ this.config = config;
+ }
+
+ @Override
+ public void run() {
+ Pair<Integer, List<String>> result;
+ try {
+ result =
+ new EnqueueMdnsQueryCallable(
+ socketClient,
+ createMdnsPacketWriter(),
+ serviceType,
+ config.subtypes,
+ config.expectUnicastResponse,
+ config.transactionId)
+ .call();
+ } catch (Exception e) {
+ LOGGER.e(String.format("Failed to run EnqueueMdnsQueryCallable for subtype: %s",
+ TextUtils.join(",", config.subtypes)), e);
+ result = null;
+ }
+ synchronized (lock) {
+ if (MdnsConfigs.useSessionIdToScheduleMdnsTask()) {
+ // In case that the task is not canceled successfully, use session ID to check
+ // if this task should continue to schedule more.
+ if (config.sessionId != currentSessionId) {
+ return;
+ }
+ }
+
+ if (MdnsConfigs.shouldCancelScanTaskWhenFutureIsNull()) {
+ if (requestTaskFuture == null) {
+ // If requestTaskFuture is set to null, the task is cancelled. We can't use
+ // isCancelled() here because this QueryTask is different from the future
+ // that is returned from executor.schedule(). See b/71646910.
+ return;
+ }
+ }
+ if ((result != null)) {
+ for (MdnsServiceBrowserListener listener : listeners) {
+ listener.onDiscoveryQuerySent(result.second, result.first);
+ }
+ }
+ QueryTaskConfig config = this.config.getConfigForNextRun();
+ requestTaskFuture =
+ executor.schedule(
+ new QueryTask(config), config.timeToRunNextTaskInMs,
+ TimeUnit.MILLISECONDS);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsSocket.java b/service/mdns/com/android/server/connectivity/mdns/MdnsSocket.java
new file mode 100644
index 0000000..241a52a
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsSocket.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2021 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 com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.InetSocketAddress;
+import java.net.MulticastSocket;
+import java.util.List;
+
+/**
+ * {@link MdnsSocket} provides a similar interface to {@link MulticastSocket} and binds to all
+ * available multi-cast network interfaces.
+ *
+ * @see MulticastSocket for javadoc of each public method.
+ */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public class MdnsSocket {
+ private static final InetSocketAddress MULTICAST_IPV4_ADDRESS =
+ new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT);
+ private static final InetSocketAddress MULTICAST_IPV6_ADDRESS =
+ new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT);
+ private static boolean isOnIPv6OnlyNetwork = false;
+ private final MulticastNetworkInterfaceProvider multicastNetworkInterfaceProvider;
+ private final MulticastSocket multicastSocket;
+
+ public MdnsSocket(
+ @NonNull MulticastNetworkInterfaceProvider multicastNetworkInterfaceProvider, int port)
+ throws IOException {
+ this.multicastNetworkInterfaceProvider = multicastNetworkInterfaceProvider;
+ this.multicastNetworkInterfaceProvider.startWatchingConnectivityChanges();
+ multicastSocket = createMulticastSocket(port);
+ // RFC Spec: https://tools.ietf.org/html/rfc6762
+ // Time to live is set 255, which is similar to the jMDNS implementation.
+ multicastSocket.setTimeToLive(255);
+
+ // TODO (changed when importing code): consider tagging the socket for data usage
+ isOnIPv6OnlyNetwork = false;
+ }
+
+ public void send(DatagramPacket packet) throws IOException {
+ List<NetworkInterfaceWrapper> networkInterfaces =
+ multicastNetworkInterfaceProvider.getMulticastNetworkInterfaces();
+ for (NetworkInterfaceWrapper networkInterface : networkInterfaces) {
+ multicastSocket.setNetworkInterface(networkInterface.getNetworkInterface());
+ multicastSocket.send(packet);
+ }
+ }
+
+ public void receive(DatagramPacket packet) throws IOException {
+ multicastSocket.receive(packet);
+ }
+
+ public void joinGroup() throws IOException {
+ List<NetworkInterfaceWrapper> networkInterfaces =
+ multicastNetworkInterfaceProvider.getMulticastNetworkInterfaces();
+ InetSocketAddress multicastAddress = MULTICAST_IPV4_ADDRESS;
+ if (multicastNetworkInterfaceProvider.isOnIpV6OnlyNetwork(networkInterfaces)) {
+ isOnIPv6OnlyNetwork = true;
+ multicastAddress = MULTICAST_IPV6_ADDRESS;
+ } else {
+ isOnIPv6OnlyNetwork = false;
+ }
+ for (NetworkInterfaceWrapper networkInterface : networkInterfaces) {
+ multicastSocket.joinGroup(multicastAddress, networkInterface.getNetworkInterface());
+ }
+ }
+
+ public void leaveGroup() throws IOException {
+ List<NetworkInterfaceWrapper> networkInterfaces =
+ multicastNetworkInterfaceProvider.getMulticastNetworkInterfaces();
+ InetSocketAddress multicastAddress = MULTICAST_IPV4_ADDRESS;
+ if (multicastNetworkInterfaceProvider.isOnIpV6OnlyNetwork(networkInterfaces)) {
+ multicastAddress = MULTICAST_IPV6_ADDRESS;
+ }
+ for (NetworkInterfaceWrapper networkInterface : networkInterfaces) {
+ multicastSocket.leaveGroup(multicastAddress, networkInterface.getNetworkInterface());
+ }
+ }
+
+ public void close() {
+ // This is a race with the use of the file descriptor (b/27403984).
+ multicastSocket.close();
+ multicastNetworkInterfaceProvider.stopWatchingConnectivityChanges();
+ }
+
+ @VisibleForTesting
+ MulticastSocket createMulticastSocket(int port) throws IOException {
+ return new MulticastSocket(port);
+ }
+
+ public boolean isOnIPv6OnlyNetwork() {
+ return isOnIPv6OnlyNetwork;
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsSocketClient.java b/service/mdns/com/android/server/connectivity/mdns/MdnsSocketClient.java
new file mode 100644
index 0000000..e689d6c
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsSocketClient.java
@@ -0,0 +1,504 @@
+/*
+ * Copyright (C) 2021 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.Manifest.permission;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.content.Context;
+import android.net.wifi.WifiManager.MulticastLock;
+import android.os.SystemClock;
+import android.text.format.DateUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * The {@link MdnsSocketClient} maintains separate threads to send and receive mDNS packets for all
+ * the requested service types.
+ *
+ * <p>See https://tools.ietf.org/html/rfc6763 (namely sections 4 and 5).
+ */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public class MdnsSocketClient {
+
+ private static final String TAG = "MdnsClient";
+ // TODO: The following values are copied from cast module. We need to think about the
+ // better way to share those.
+ private static final String CAST_SENDER_LOG_SOURCE = "CAST_SENDER_SDK";
+ private static final String CAST_PREFS_NAME = "google_cast";
+ private static final String PREF_CAST_SENDER_ID = "PREF_CAST_SENDER_ID";
+ private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
+ private static final String MULTICAST_TYPE = "multicast";
+ private static final String UNICAST_TYPE = "unicast";
+
+ private static final long SLEEP_TIME_FOR_SOCKET_THREAD_MS =
+ MdnsConfigs.sleepTimeForSocketThreadMs();
+ // A value of 0 leads to an infinite wait.
+ private static final long THREAD_JOIN_TIMEOUT_MS = DateUtils.SECOND_IN_MILLIS;
+ private static final int RECEIVER_BUFFER_SIZE = 2048;
+ @VisibleForTesting
+ final Queue<DatagramPacket> multicastPacketQueue = new ArrayDeque<>();
+ @VisibleForTesting
+ final Queue<DatagramPacket> unicastPacketQueue = new ArrayDeque<>();
+ private final Context context;
+ private final byte[] multicastReceiverBuffer = new byte[RECEIVER_BUFFER_SIZE];
+ private final byte[] unicastReceiverBuffer;
+ private final MdnsResponseDecoder responseDecoder;
+ private final MulticastLock multicastLock;
+ private final boolean useSeparateSocketForUnicast =
+ MdnsConfigs.useSeparateSocketToSendUnicastQuery();
+ private final boolean checkMulticastResponse = MdnsConfigs.checkMulticastResponse();
+ private final long checkMulticastResponseIntervalMs =
+ MdnsConfigs.checkMulticastResponseIntervalMs();
+ private final Object socketLock = new Object();
+ private final Object timerObject = new Object();
+ // If multicast response was received in the current session. The value is reset in the
+ // beginning of each session.
+ @VisibleForTesting
+ boolean receivedMulticastResponse;
+ // If unicast response was received in the current session. The value is reset in the beginning
+ // of each session.
+ @VisibleForTesting
+ boolean receivedUnicastResponse;
+ // If the phone is the bad state where it can't receive any multicast response.
+ @VisibleForTesting
+ AtomicBoolean cannotReceiveMulticastResponse = new AtomicBoolean(false);
+ @VisibleForTesting
+ volatile Thread sendThread;
+ @VisibleForTesting
+ Thread multicastReceiveThread;
+ @VisibleForTesting
+ Thread unicastReceiveThread;
+ private volatile boolean shouldStopSocketLoop;
+ private Callback callback;
+ private MdnsSocket multicastSocket;
+ private MdnsSocket unicastSocket;
+ private int receivedPacketNumber = 0;
+ private Timer logMdnsPacketTimer;
+ private AtomicInteger packetsCount;
+ private Timer checkMulticastResponseTimer;
+
+ public MdnsSocketClient(@NonNull Context context, @NonNull MulticastLock multicastLock) {
+ this.context = context;
+ this.multicastLock = multicastLock;
+ responseDecoder = new MdnsResponseDecoder(new MdnsResponseDecoder.Clock(), null);
+ if (useSeparateSocketForUnicast) {
+ unicastReceiverBuffer = new byte[RECEIVER_BUFFER_SIZE];
+ } else {
+ unicastReceiverBuffer = null;
+ }
+ }
+
+ public synchronized void setCallback(@Nullable Callback callback) {
+ this.callback = callback;
+ }
+
+ @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
+ public synchronized void startDiscovery() throws IOException {
+ if (multicastSocket != null) {
+ LOGGER.w("Discovery is already in progress.");
+ return;
+ }
+
+ receivedMulticastResponse = false;
+ receivedUnicastResponse = false;
+ cannotReceiveMulticastResponse.set(false);
+
+ shouldStopSocketLoop = false;
+ try {
+ // TODO (changed when importing code): consider setting thread stats tag
+ multicastSocket = createMdnsSocket(MdnsConstants.MDNS_PORT);
+ multicastSocket.joinGroup();
+ if (useSeparateSocketForUnicast) {
+ // For unicast, use port 0 and the system will assign it with any available port.
+ unicastSocket = createMdnsSocket(0);
+ }
+ multicastLock.acquire();
+ } catch (IOException e) {
+ multicastLock.release();
+ if (multicastSocket != null) {
+ multicastSocket.close();
+ multicastSocket = null;
+ }
+ if (unicastSocket != null) {
+ unicastSocket.close();
+ unicastSocket = null;
+ }
+ throw e;
+ } finally {
+ // TODO (changed when importing code): consider resetting thread stats tag
+ }
+ createAndStartSendThread();
+ createAndStartReceiverThreads();
+ }
+
+ @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
+ public void stopDiscovery() {
+ LOGGER.log("Stop discovery.");
+ if (multicastSocket == null && unicastSocket == null) {
+ return;
+ }
+
+ if (MdnsConfigs.clearMdnsPacketQueueAfterDiscoveryStops()) {
+ synchronized (multicastPacketQueue) {
+ multicastPacketQueue.clear();
+ }
+ synchronized (unicastPacketQueue) {
+ unicastPacketQueue.clear();
+ }
+ }
+
+ multicastLock.release();
+
+ shouldStopSocketLoop = true;
+ waitForSendThreadToStop();
+ waitForReceiverThreadsToStop();
+
+ synchronized (socketLock) {
+ multicastSocket = null;
+ unicastSocket = null;
+ }
+
+ synchronized (timerObject) {
+ if (checkMulticastResponseTimer != null) {
+ checkMulticastResponseTimer.cancel();
+ checkMulticastResponseTimer = null;
+ }
+ }
+ }
+
+ /** Sends a mDNS request packet that asks for multicast response. */
+ public void sendMulticastPacket(@NonNull DatagramPacket packet) {
+ sendMdnsPacket(packet, multicastPacketQueue);
+ }
+
+ /** Sends a mDNS request packet that asks for unicast response. */
+ public void sendUnicastPacket(DatagramPacket packet) {
+ if (useSeparateSocketForUnicast) {
+ sendMdnsPacket(packet, unicastPacketQueue);
+ } else {
+ sendMdnsPacket(packet, multicastPacketQueue);
+ }
+ }
+
+ private void sendMdnsPacket(DatagramPacket packet, Queue<DatagramPacket> packetQueueToUse) {
+ if (shouldStopSocketLoop && !MdnsConfigs.allowAddMdnsPacketAfterDiscoveryStops()) {
+ LOGGER.w("sendMdnsPacket() is called after discovery already stopped");
+ return;
+ }
+ synchronized (packetQueueToUse) {
+ while (packetQueueToUse.size() >= MdnsConfigs.mdnsPacketQueueMaxSize()) {
+ packetQueueToUse.remove();
+ }
+ packetQueueToUse.add(packet);
+ }
+ triggerSendThread();
+ }
+
+ private void createAndStartSendThread() {
+ if (sendThread != null) {
+ LOGGER.w("A socket thread already exists.");
+ return;
+ }
+ sendThread = new Thread(this::sendThreadMain);
+ sendThread.setName("mdns-send");
+ sendThread.start();
+ }
+
+ private void createAndStartReceiverThreads() {
+ if (multicastReceiveThread != null) {
+ LOGGER.w("A multicast receiver thread already exists.");
+ return;
+ }
+ multicastReceiveThread =
+ new Thread(() -> receiveThreadMain(multicastReceiverBuffer, multicastSocket));
+ multicastReceiveThread.setName("mdns-multicast-receive");
+ multicastReceiveThread.start();
+
+ if (useSeparateSocketForUnicast) {
+ unicastReceiveThread =
+ new Thread(() -> receiveThreadMain(unicastReceiverBuffer, unicastSocket));
+ unicastReceiveThread.setName("mdns-unicast-receive");
+ unicastReceiveThread.start();
+ }
+ }
+
+ private void triggerSendThread() {
+ LOGGER.log("Trigger send thread.");
+ Thread sendThread = this.sendThread;
+ if (sendThread != null) {
+ sendThread.interrupt();
+ } else {
+ LOGGER.w("Socket thread is null");
+ }
+ }
+
+ private void waitForReceiverThreadsToStop() {
+ if (multicastReceiveThread != null) {
+ waitForThread(multicastReceiveThread);
+ multicastReceiveThread = null;
+ }
+
+ if (unicastReceiveThread != null) {
+ waitForThread(unicastReceiveThread);
+ unicastReceiveThread = null;
+ }
+ }
+
+ private void waitForSendThreadToStop() {
+ LOGGER.log("wait For Send Thread To Stop");
+ if (sendThread == null) {
+ LOGGER.w("socket thread is already dead.");
+ return;
+ }
+ waitForThread(sendThread);
+ sendThread = null;
+ }
+
+ private void waitForThread(Thread thread) {
+ long startMs = SystemClock.elapsedRealtime();
+ long waitMs = THREAD_JOIN_TIMEOUT_MS;
+ while (thread.isAlive() && (waitMs > 0)) {
+ try {
+ thread.interrupt();
+ thread.join(waitMs);
+ if (thread.isAlive()) {
+ LOGGER.w("Failed to join thread: " + thread);
+ }
+ break;
+ } catch (InterruptedException e) {
+ // Compute remaining time after at least a single join call, in case the clock
+ // resolution is poor.
+ waitMs = THREAD_JOIN_TIMEOUT_MS - (SystemClock.elapsedRealtime() - startMs);
+ }
+ }
+ }
+
+ private void sendThreadMain() {
+ List<DatagramPacket> multicastPacketsToSend = new ArrayList<>();
+ List<DatagramPacket> unicastPacketsToSend = new ArrayList<>();
+ boolean shouldThreadSleep;
+ try {
+ while (!shouldStopSocketLoop) {
+ try {
+ // Make a local copy of all packets, and clear the queue.
+ // Send packets that ask for multicast response.
+ multicastPacketsToSend.clear();
+ synchronized (multicastPacketQueue) {
+ multicastPacketsToSend.addAll(multicastPacketQueue);
+ multicastPacketQueue.clear();
+ }
+
+ // Send packets that ask for unicast response.
+ if (useSeparateSocketForUnicast) {
+ unicastPacketsToSend.clear();
+ synchronized (unicastPacketQueue) {
+ unicastPacketsToSend.addAll(unicastPacketQueue);
+ unicastPacketQueue.clear();
+ }
+ }
+
+ // Send all the packets.
+ sendPackets(multicastPacketsToSend, multicastSocket);
+ sendPackets(unicastPacketsToSend, unicastSocket);
+
+ // Sleep ONLY if no more packets have been added to the queue, while packets
+ // were being sent.
+ synchronized (multicastPacketQueue) {
+ synchronized (unicastPacketQueue) {
+ shouldThreadSleep =
+ multicastPacketQueue.isEmpty() && unicastPacketQueue.isEmpty();
+ }
+ }
+ if (shouldThreadSleep) {
+ Thread.sleep(SLEEP_TIME_FOR_SOCKET_THREAD_MS);
+ }
+ } catch (InterruptedException e) {
+ // Don't log the interruption as it's expected.
+ }
+ }
+ } finally {
+ LOGGER.log("Send thread stopped.");
+ try {
+ multicastSocket.leaveGroup();
+ } catch (Exception t) {
+ LOGGER.e("Failed to leave the group.", t);
+ }
+
+ // Close the socket first. This is the only way to interrupt a blocking receive.
+ try {
+ // This is a race with the use of the file descriptor (b/27403984).
+ multicastSocket.close();
+ if (unicastSocket != null) {
+ unicastSocket.close();
+ }
+ } catch (Exception t) {
+ LOGGER.e("Failed to close the mdns socket.", t);
+ }
+ }
+ }
+
+ private void receiveThreadMain(byte[] receiverBuffer, MdnsSocket socket) {
+ DatagramPacket packet = new DatagramPacket(receiverBuffer, receiverBuffer.length);
+
+ while (!shouldStopSocketLoop) {
+ try {
+ // This is a race with the use of the file descriptor (b/27403984).
+ synchronized (socketLock) {
+ // This checks is to make sure the socket was not set to null.
+ if (socket != null && (socket == multicastSocket || socket == unicastSocket)) {
+ socket.receive(packet);
+ }
+ }
+
+ if (!shouldStopSocketLoop) {
+ String responseType = socket == multicastSocket ? MULTICAST_TYPE : UNICAST_TYPE;
+ processResponsePacket(packet, responseType);
+ }
+ } catch (IOException e) {
+ if (!shouldStopSocketLoop) {
+ LOGGER.e("Failed to receive mDNS packets.", e);
+ }
+ }
+ }
+ LOGGER.log("Receive thread stopped.");
+ }
+
+ private int processResponsePacket(@NonNull DatagramPacket packet, String responseType)
+ throws IOException {
+ int packetNumber = ++receivedPacketNumber;
+
+ List<MdnsResponse> responses = new LinkedList<>();
+ int errorCode = responseDecoder.decode(packet, responses);
+ if (errorCode == MdnsResponseDecoder.SUCCESS) {
+ if (responseType.equals(MULTICAST_TYPE)) {
+ receivedMulticastResponse = true;
+ if (cannotReceiveMulticastResponse.getAndSet(false)) {
+ // If we are already in the bad state, receiving a multicast response means
+ // we are recovered.
+ LOGGER.e(
+ "Recovered from the state where the phone can't receive any multicast"
+ + " response");
+ }
+ } else {
+ receivedUnicastResponse = true;
+ }
+ for (MdnsResponse response : responses) {
+ String serviceInstanceName = response.getServiceInstanceName();
+ LOGGER.log("mDNS %s response received: %s", responseType, serviceInstanceName);
+ if (callback != null) {
+ callback.onResponseReceived(response);
+ }
+ }
+ } else if (errorCode != MdnsResponseErrorCode.ERROR_NOT_RESPONSE_MESSAGE) {
+ LOGGER.w(String.format("Error while decoding %s packet (%d): %d",
+ responseType, packetNumber, errorCode));
+ if (callback != null) {
+ callback.onFailedToParseMdnsResponse(packetNumber, errorCode);
+ }
+ }
+ return errorCode;
+ }
+
+ @VisibleForTesting
+ MdnsSocket createMdnsSocket(int port) throws IOException {
+ return new MdnsSocket(new MulticastNetworkInterfaceProvider(context), port);
+ }
+
+ private void sendPackets(List<DatagramPacket> packets, MdnsSocket socket) {
+ String requestType = socket == multicastSocket ? "multicast" : "unicast";
+ for (DatagramPacket packet : packets) {
+ if (shouldStopSocketLoop) {
+ break;
+ }
+ try {
+ LOGGER.log("Sending a %s mDNS packet...", requestType);
+ socket.send(packet);
+
+ // Start the timer task to monitor the response.
+ synchronized (timerObject) {
+ if (socket == multicastSocket) {
+ if (cannotReceiveMulticastResponse.get()) {
+ // Don't schedule the timer task if we are already in the bad state.
+ return;
+ }
+ if (checkMulticastResponseTimer != null) {
+ // Don't schedule the timer task if it's already scheduled.
+ return;
+ }
+ if (checkMulticastResponse && useSeparateSocketForUnicast) {
+ // Only when useSeparateSocketForUnicast is true, we can tell if we
+ // received a multicast or unicast response.
+ checkMulticastResponseTimer = new Timer();
+ checkMulticastResponseTimer.schedule(
+ new TimerTask() {
+ @Override
+ public void run() {
+ synchronized (timerObject) {
+ if (checkMulticastResponseTimer == null) {
+ // Discovery already stopped.
+ return;
+ }
+ if ((!receivedMulticastResponse)
+ && receivedUnicastResponse) {
+ LOGGER.e(String.format(
+ "Haven't received multicast response"
+ + " in the last %d ms.",
+ checkMulticastResponseIntervalMs));
+ cannotReceiveMulticastResponse.set(true);
+ }
+ checkMulticastResponseTimer = null;
+ }
+ }
+ },
+ checkMulticastResponseIntervalMs);
+ }
+ }
+ }
+ } catch (IOException e) {
+ LOGGER.e(String.format("Failed to send a %s mDNS packet.", requestType), e);
+ }
+ }
+ packets.clear();
+ }
+
+ public boolean isOnIPv6OnlyNetwork() {
+ return multicastSocket.isOnIPv6OnlyNetwork();
+ }
+
+ /** Callback for {@link MdnsSocketClient}. */
+ public interface Callback {
+ void onResponseReceived(@NonNull MdnsResponse response);
+
+ void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode);
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsTextRecord.java b/service/mdns/com/android/server/connectivity/mdns/MdnsTextRecord.java
new file mode 100644
index 0000000..a5b5595
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsTextRecord.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2021 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 com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/** An mDNS "TXT" record, which contains a list of text strings. */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+@VisibleForTesting
+public class MdnsTextRecord extends MdnsRecord {
+ private List<String> strings;
+
+ public MdnsTextRecord(String[] name, MdnsPacketReader reader) throws IOException {
+ super(name, TYPE_TXT, reader);
+ }
+
+ /** Returns the list of strings. */
+ public List<String> getStrings() {
+ return Collections.unmodifiableList(strings);
+ }
+
+ @Override
+ protected void readData(MdnsPacketReader reader) throws IOException {
+ strings = new ArrayList<>();
+ while (reader.getRemaining() > 0) {
+ strings.add(reader.readString());
+ }
+ }
+
+ @Override
+ protected void writeData(MdnsPacketWriter writer) throws IOException {
+ if (strings != null) {
+ for (String string : strings) {
+ writer.writeString(string);
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("TXT: {");
+ if (strings != null) {
+ for (String string : strings) {
+ sb.append(' ').append(string);
+ }
+ }
+ sb.append("}");
+
+ return sb.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return (super.hashCode() * 31) + Objects.hash(strings);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof MdnsTextRecord)) {
+ return false;
+ }
+
+ return super.equals(other) && Objects.equals(strings, ((MdnsTextRecord) other).strings);
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProvider.java b/service/mdns/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProvider.java
new file mode 100644
index 0000000..e0d8fa6
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProvider.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2021 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.content.Context;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+
+/**
+ * This class is used by the {@link MdnsSocket} to monitor the list of {@link NetworkInterface}
+ * instances that are currently available for multi-cast messaging.
+ */
+public class MulticastNetworkInterfaceProvider {
+
+ private static final String TAG = "MdnsNIProvider";
+ private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
+ private static final boolean PREFER_IPV6 = MdnsConfigs.preferIpv6();
+
+ private final List<NetworkInterfaceWrapper> multicastNetworkInterfaces = new ArrayList<>();
+ // Only modifiable from tests.
+ @VisibleForTesting
+ ConnectivityMonitor connectivityMonitor;
+ private volatile boolean connectivityChanged = true;
+
+ @SuppressWarnings("nullness:methodref.receiver.bound")
+ public MulticastNetworkInterfaceProvider(@NonNull Context context) {
+ // IMPORT CHANGED
+ this.connectivityMonitor = new ConnectivityMonitorWithConnectivityManager(
+ context, this::onConnectivityChanged);
+ }
+
+ private void onConnectivityChanged() {
+ connectivityChanged = true;
+ }
+
+ /**
+ * Starts monitoring changes of connectivity of this device, which may indicate that the list of
+ * network interfaces available for multi-cast messaging has changed.
+ */
+ public void startWatchingConnectivityChanges() {
+ connectivityMonitor.startWatchingConnectivityChanges();
+ }
+
+ /** Stops monitoring changes of connectivity. */
+ public void stopWatchingConnectivityChanges() {
+ connectivityMonitor.stopWatchingConnectivityChanges();
+ }
+
+ /**
+ * Returns the list of {@link NetworkInterfaceWrapper} instances available for multi-cast
+ * messaging.
+ */
+ public synchronized List<NetworkInterfaceWrapper> getMulticastNetworkInterfaces() {
+ if (connectivityChanged) {
+ connectivityChanged = false;
+ updateMulticastNetworkInterfaces();
+ if (multicastNetworkInterfaces.isEmpty()) {
+ LOGGER.log("No network interface available for mDNS scanning.");
+ }
+ }
+ return new ArrayList<>(multicastNetworkInterfaces);
+ }
+
+ private void updateMulticastNetworkInterfaces() {
+ multicastNetworkInterfaces.clear();
+ List<NetworkInterfaceWrapper> networkInterfaceWrappers = getNetworkInterfaces();
+ for (NetworkInterfaceWrapper interfaceWrapper : networkInterfaceWrappers) {
+ if (canScanOnInterface(interfaceWrapper)) {
+ multicastNetworkInterfaces.add(interfaceWrapper);
+ }
+ }
+ }
+
+ public boolean isOnIpV6OnlyNetwork(List<NetworkInterfaceWrapper> networkInterfaces) {
+ if (networkInterfaces.isEmpty()) {
+ return false;
+ }
+
+ // TODO(b/79866499): Remove this when the bug is resolved.
+ if (PREFER_IPV6) {
+ return true;
+ }
+ boolean hasAtleastOneIPv6Address = false;
+ for (NetworkInterfaceWrapper interfaceWrapper : networkInterfaces) {
+ for (InterfaceAddress ifAddr : interfaceWrapper.getInterfaceAddresses()) {
+ if (!(ifAddr.getAddress() instanceof Inet6Address)) {
+ return false;
+ } else {
+ hasAtleastOneIPv6Address = true;
+ }
+ }
+ }
+ return hasAtleastOneIPv6Address;
+ }
+
+ @VisibleForTesting
+ List<NetworkInterfaceWrapper> getNetworkInterfaces() {
+ List<NetworkInterfaceWrapper> networkInterfaceWrappers = new ArrayList<>();
+ try {
+ Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
+ if (interfaces != null) {
+ while (interfaces.hasMoreElements()) {
+ networkInterfaceWrappers.add(
+ new NetworkInterfaceWrapper(interfaces.nextElement()));
+ }
+ }
+ } catch (SocketException e) {
+ LOGGER.e("Failed to get network interfaces.", e);
+ } catch (NullPointerException e) {
+ // Android R has a bug that could lead to a NPE. See b/159277702.
+ LOGGER.e("Failed to call getNetworkInterfaces API", e);
+ }
+
+ return networkInterfaceWrappers;
+ }
+
+ private boolean canScanOnInterface(@Nullable NetworkInterfaceWrapper networkInterface) {
+ try {
+ if ((networkInterface == null)
+ || networkInterface.isLoopback()
+ || networkInterface.isPointToPoint()
+ || networkInterface.isVirtual()
+ || !networkInterface.isUp()
+ || !networkInterface.supportsMulticast()) {
+ return false;
+ }
+ return hasInet4Address(networkInterface) || hasInet6Address(networkInterface);
+ } catch (IOException e) {
+ LOGGER.e(String.format("Failed to check interface %s.",
+ networkInterface.getNetworkInterface().getDisplayName()), e);
+ }
+
+ return false;
+ }
+
+ private boolean hasInet4Address(@NonNull NetworkInterfaceWrapper networkInterface) {
+ for (InterfaceAddress ifAddr : networkInterface.getInterfaceAddresses()) {
+ if (ifAddr.getAddress() instanceof Inet4Address) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean hasInet6Address(@NonNull NetworkInterfaceWrapper networkInterface) {
+ for (InterfaceAddress ifAddr : networkInterface.getInterfaceAddresses()) {
+ if (ifAddr.getAddress() instanceof Inet6Address) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/NetworkInterfaceWrapper.java b/service/mdns/com/android/server/connectivity/mdns/NetworkInterfaceWrapper.java
new file mode 100644
index 0000000..0ecae48
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/NetworkInterfaceWrapper.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2021 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 java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.List;
+
+/** A wrapper class of {@link NetworkInterface} to be mocked in unit tests. */
+public class NetworkInterfaceWrapper {
+ private final NetworkInterface networkInterface;
+
+ public NetworkInterfaceWrapper(NetworkInterface networkInterface) {
+ this.networkInterface = networkInterface;
+ }
+
+ public NetworkInterface getNetworkInterface() {
+ return networkInterface;
+ }
+
+ public boolean isUp() throws SocketException {
+ return networkInterface.isUp();
+ }
+
+ public boolean isLoopback() throws SocketException {
+ return networkInterface.isLoopback();
+ }
+
+ public boolean isPointToPoint() throws SocketException {
+ return networkInterface.isPointToPoint();
+ }
+
+ public boolean isVirtual() {
+ return networkInterface.isVirtual();
+ }
+
+ public boolean supportsMulticast() throws SocketException {
+ return networkInterface.supportsMulticast();
+ }
+
+ public List<InterfaceAddress> getInterfaceAddresses() {
+ return networkInterface.getInterfaceAddresses();
+ }
+
+ @Override
+ public String toString() {
+ return networkInterface.toString();
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/util/MdnsLogger.java b/service/mdns/com/android/server/connectivity/mdns/util/MdnsLogger.java
new file mode 100644
index 0000000..31c62f5
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/util/MdnsLogger.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2021 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.util;
+
+import android.net.util.SharedLog;
+import android.text.TextUtils;
+
+/**
+ * The logger used in mDNS.
+ */
+public class MdnsLogger {
+ // Make this logger public for other level logging than dogfood.
+ public final SharedLog mLog;
+
+ /**
+ * Constructs a new {@link MdnsLogger} with the given logging tag.
+ *
+ * @param tag The log tag that will be used by this logger
+ */
+ public MdnsLogger(String tag) {
+ mLog = new SharedLog(tag);
+ }
+
+ public void log(String message) {
+ mLog.log(message);
+ }
+
+ public void log(String message, Object... args) {
+ mLog.log(message + " ; " + TextUtils.join(" ; ", args));
+ }
+
+ public void d(String message) {
+ mLog.log(message);
+ }
+
+ public void e(String message) {
+ mLog.e(message);
+ }
+
+ public void e(String message, Throwable e) {
+ mLog.e(message, e);
+ }
+
+ public void w(String message) {
+ mLog.w(message);
+ }
+}
\ No newline at end of file
diff --git a/service/native/TrafficController.cpp b/service/native/TrafficController.cpp
index 4dc056d..9331548 100644
--- a/service/native/TrafficController.cpp
+++ b/service/native/TrafficController.cpp
@@ -451,53 +451,6 @@
return 0;
}
-int TrafficController::toggleUidOwnerMap(ChildChain chain, bool enable) {
- std::lock_guard guard(mMutex);
- uint32_t key = UID_RULES_CONFIGURATION_KEY;
- auto oldConfigure = mConfigurationMap.readValue(key);
- if (!oldConfigure.ok()) {
- ALOGE("Cannot read the old configuration from map: %s",
- oldConfigure.error().message().c_str());
- return -oldConfigure.error().code();
- }
- uint32_t match;
- switch (chain) {
- case DOZABLE:
- match = DOZABLE_MATCH;
- break;
- case STANDBY:
- match = STANDBY_MATCH;
- break;
- case POWERSAVE:
- match = POWERSAVE_MATCH;
- break;
- case RESTRICTED:
- match = RESTRICTED_MATCH;
- break;
- case LOW_POWER_STANDBY:
- match = LOW_POWER_STANDBY_MATCH;
- break;
- case OEM_DENY_1:
- match = OEM_DENY_1_MATCH;
- break;
- case OEM_DENY_2:
- match = OEM_DENY_2_MATCH;
- break;
- case OEM_DENY_3:
- match = OEM_DENY_3_MATCH;
- break;
- default:
- return -EINVAL;
- }
- BpfConfig newConfiguration =
- enable ? (oldConfigure.value() | match) : (oldConfigure.value() & ~match);
- Status res = mConfigurationMap.writeValue(key, newConfiguration, BPF_EXIST);
- if (!isOk(res)) {
- ALOGE("Failed to toggleUidOwnerMap(%d): %s", chain, res.msg().c_str());
- }
- return -res.code();
-}
-
Status TrafficController::swapActiveStatsMap() {
std::lock_guard guard(mMutex);
diff --git a/service/native/include/TrafficController.h b/service/native/include/TrafficController.h
index 8512929..14c5eaf 100644
--- a/service/native/include/TrafficController.h
+++ b/service/native/include/TrafficController.h
@@ -71,8 +71,6 @@
netdutils::Status updateUidOwnerMap(const uint32_t uid,
UidOwnerMatchType matchType, IptOp op) EXCLUDES(mMutex);
- int toggleUidOwnerMap(ChildChain chain, bool enable) EXCLUDES(mMutex);
-
static netdutils::StatusOr<std::unique_ptr<netdutils::NetlinkListenerInterface>>
makeSkDestroyListener();
diff --git a/service/proguard.flags b/service/proguard.flags
index 94397ab..cffa490 100644
--- a/service/proguard.flags
+++ b/service/proguard.flags
@@ -8,11 +8,10 @@
# Prevent proguard from stripping out any nearby-service and fast-pair-lite-protos fields.
-keep class com.android.server.nearby.NearbyService { *; }
--keep class com.android.server.nearby.service.proto { *; }
# The lite proto runtime uses reflection to access fields based on the names in
# the schema, keep all the fields.
# This replicates the base proguard rule used by the build by default
# (proguard_basic_keeps.flags), but needs to be specified here because the
# com.google.protobuf package is jarjared to the below package.
--keepclassmembers class * extends com.android.connectivity.com.google.protobuf.MessageLite { <fields>; }
+-keepclassmembers class * extends android.net.connectivity.com.google.protobuf.MessageLite { <fields>; }
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index 151d0e3..3ee3ea1 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -16,15 +16,30 @@
package com.android.server;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_POWERSAVE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_RESTRICTED;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_STANDBY;
+import static android.system.OsConstants.EINVAL;
+import static android.system.OsConstants.ENOENT;
import static android.system.OsConstants.EOPNOTSUPP;
import android.net.INetd;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
+import android.system.ErrnoException;
import android.system.Os;
import android.util.Log;
+import android.util.SparseLongArray;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.BpfMap;
+import com.android.net.module.util.Struct.U32;
import java.io.FileDescriptor;
import java.io.IOException;
@@ -41,6 +56,56 @@
private static final boolean USE_NETD = !SdkLevel.isAtLeastT();
private static boolean sInitialized = false;
+ // Lock for sConfigurationMap entry for UID_RULES_CONFIGURATION_KEY.
+ // This entry is not accessed by others.
+ // BpfNetMaps acquires this lock while sequence of read, modify, and write.
+ private static final Object sUidRulesConfigBpfMapLock = new Object();
+
+ private static final String CONFIGURATION_MAP_PATH =
+ "/sys/fs/bpf/netd_shared/map_netd_configuration_map";
+ private static final U32 UID_RULES_CONFIGURATION_KEY = new U32(0);
+ private static BpfMap<U32, U32> sConfigurationMap = null;
+
+ // LINT.IfChange(match_type)
+ private static final long NO_MATCH = 0;
+ private static final long HAPPY_BOX_MATCH = (1 << 0);
+ private static final long PENALTY_BOX_MATCH = (1 << 1);
+ private static final long DOZABLE_MATCH = (1 << 2);
+ private static final long STANDBY_MATCH = (1 << 3);
+ private static final long POWERSAVE_MATCH = (1 << 4);
+ private static final long RESTRICTED_MATCH = (1 << 5);
+ private static final long LOW_POWER_STANDBY_MATCH = (1 << 6);
+ private static final long IIF_MATCH = (1 << 7);
+ private static final long LOCKDOWN_VPN_MATCH = (1 << 8);
+ private static final long OEM_DENY_1_MATCH = (1 << 9);
+ private static final long OEM_DENY_2_MATCH = (1 << 10);
+ private static final long OEM_DENY_3_MATCH = (1 << 11);
+ // LINT.ThenChange(packages/modules/Connectivity/bpf_progs/bpf_shared.h)
+
+ // TODO: Use Java BpfMap instead of JNI code (TrafficController) for map update.
+ // Currently, BpfNetMaps uses TrafficController for map update and TrafficController
+ // (changeUidOwnerRule and toggleUidOwnerMap) also does conversion from "firewall chain" to
+ // "match". Migrating map update from JNI to Java BpfMap will solve this duplication.
+ private static final SparseLongArray FIREWALL_CHAIN_TO_MATCH = new SparseLongArray();
+ static {
+ FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_DOZABLE, DOZABLE_MATCH);
+ FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_STANDBY, STANDBY_MATCH);
+ FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_POWERSAVE, POWERSAVE_MATCH);
+ FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_RESTRICTED, RESTRICTED_MATCH);
+ FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_LOW_POWER_STANDBY, LOW_POWER_STANDBY_MATCH);
+ FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_OEM_DENY_1, OEM_DENY_1_MATCH);
+ FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_OEM_DENY_2, OEM_DENY_2_MATCH);
+ FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_OEM_DENY_3, OEM_DENY_3_MATCH);
+ }
+
+ /**
+ * Only tests or BpfNetMaps#ensureInitialized can call this function.
+ */
+ @VisibleForTesting
+ public static void initialize(final Dependencies deps) {
+ sConfigurationMap = deps.getConfigurationMap();
+ }
+
/**
* Initializes the class if it is not already initialized. This method will open maps but not
* cause any other effects. This method may be called multiple times on any thread.
@@ -50,10 +115,30 @@
if (!USE_NETD) {
System.loadLibrary("service-connectivity");
native_init();
+ initialize(new Dependencies());
}
sInitialized = true;
}
+ /**
+ * Dependencies of BpfNetMaps, for injection in tests.
+ */
+ @VisibleForTesting
+ public static class Dependencies {
+ /**
+ * Get configuration BPF map.
+ */
+ public BpfMap<U32, U32> getConfigurationMap() {
+ try {
+ return new BpfMap<>(
+ CONFIGURATION_MAP_PATH, BpfMap.BPF_F_RDWR, U32.class, U32.class);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Cannot open netd configuration map: " + e);
+ return null;
+ }
+ }
+ }
+
/** Constructor used after T that doesn't need to use netd anymore. */
public BpfNetMaps() {
this(null);
@@ -61,17 +146,35 @@
if (USE_NETD) throw new IllegalArgumentException("BpfNetMaps need to use netd before T");
}
- public BpfNetMaps(INetd netd) {
+ public BpfNetMaps(final INetd netd) {
ensureInitialized();
mNetd = netd;
}
+ /**
+ * Get corresponding match from firewall chain.
+ */
+ @VisibleForTesting
+ public long getMatchByFirewallChain(final int chain) {
+ final long match = FIREWALL_CHAIN_TO_MATCH.get(chain, NO_MATCH);
+ if (match == NO_MATCH) {
+ throw new ServiceSpecificException(EINVAL, "Invalid firewall chain: " + chain);
+ }
+ return match;
+ }
+
private void maybeThrow(final int err, final String msg) {
if (err != 0) {
throw new ServiceSpecificException(err, msg + ": " + Os.strerror(err));
}
}
+ private void throwIfUseNetd(final String msg) {
+ if (USE_NETD) {
+ throw new UnsupportedOperationException(msg);
+ }
+ }
+
/**
* Add naughty app bandwidth rule for specific app
*
@@ -125,12 +228,56 @@
*
* @param childChain target chain to enable
* @param enable whether to enable or disable child chain.
+ * @throws UnsupportedOperationException if called on pre-T devices.
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
*/
public void setChildChain(final int childChain, final boolean enable) {
- final int err = native_setChildChain(childChain, enable);
- maybeThrow(err, "Unable to set child chain");
+ throwIfUseNetd("setChildChain is not available on pre-T devices");
+
+ final long match = getMatchByFirewallChain(childChain);
+ try {
+ synchronized (sUidRulesConfigBpfMapLock) {
+ final U32 config = sConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY);
+ if (config == null) {
+ throw new ServiceSpecificException(ENOENT,
+ "Unable to get firewall chain status: sConfigurationMap does not have"
+ + " entry for UID_RULES_CONFIGURATION_KEY");
+ }
+ final long newConfig = enable ? (config.val | match) : (config.val & (~match));
+ sConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(newConfig));
+ }
+ } catch (ErrnoException e) {
+ throw new ServiceSpecificException(e.errno,
+ "Unable to set child chain: " + Os.strerror(e.errno));
+ }
+ }
+
+ /**
+ * Get the specified firewall chain status.
+ *
+ * @param childChain target chain
+ * @return {@code true} if chain is enabled, {@code false} if chain is not enabled.
+ * @throws UnsupportedOperationException if called on pre-T devices.
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public boolean getChainEnabled(final int childChain) {
+ throwIfUseNetd("getChainEnabled is not available on pre-T devices");
+
+ final long match = getMatchByFirewallChain(childChain);
+ try {
+ final U32 config = sConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY);
+ if (config == null) {
+ throw new ServiceSpecificException(ENOENT,
+ "Unable to get firewall chain status: sConfigurationMap does not have"
+ + " entry for UID_RULES_CONFIGURATION_KEY");
+ }
+ return (config.val & match) != 0;
+ } catch (ErrnoException e) {
+ throw new ServiceSpecificException(e.errno,
+ "Unable to get firewall chain status: " + Os.strerror(e.errno));
+ }
}
/**
@@ -279,7 +426,6 @@
private native int native_removeNaughtyApp(int uid);
private native int native_addNiceApp(int uid);
private native int native_removeNiceApp(int uid);
- private native int native_setChildChain(int childChain, boolean enable);
private native int native_replaceUidChain(String name, boolean isAllowlist, int[] uids);
private native int native_setUidRule(int childChain, int uid, int firewallRule);
private native int native_addUidInterfaceRules(String ifName, int[] uids);
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 853a1a2..6568654 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -11384,6 +11384,13 @@
}
@Override
+ public boolean getFirewallChainEnabled(final int chain) {
+ enforceNetworkStackOrSettingsPermission();
+
+ return mBpfNetMaps.getChainEnabled(chain);
+ }
+
+ @Override
public void replaceFirewallChain(final int chain, final int[] uids) {
enforceNetworkStackOrSettingsPermission();
diff --git a/service/src/com/android/server/connectivity/PermissionMonitor.java b/service/src/com/android/server/connectivity/PermissionMonitor.java
index dedeb38..34c6d2d 100755
--- a/service/src/com/android/server/connectivity/PermissionMonitor.java
+++ b/service/src/com/android/server/connectivity/PermissionMonitor.java
@@ -791,7 +791,7 @@
mAllApps.add(appId);
// Log package added.
- mPermissionUpdateLogs.log("Package add: name=" + packageName + ", uid=" + uid
+ mPermissionUpdateLogs.log("Package add: uid=" + uid
+ ", nPerm=(" + permissionToString(permission) + "/"
+ permissionToString(currentPermission) + ")"
+ ", tPerm=" + permissionToString(appIdTrafficPerm));
@@ -844,7 +844,7 @@
final int permission = highestUidNetworkPermission(uid);
// Log package removed.
- mPermissionUpdateLogs.log("Package remove: name=" + packageName + ", uid=" + uid
+ mPermissionUpdateLogs.log("Package remove: uid=" + uid
+ ", nPerm=(" + permissionToString(permission) + "/"
+ permissionToString(currentPermission) + ")"
+ ", tPerm=" + permissionToString(appIdTrafficPerm));
diff --git a/tests/cts/net/native/src/BpfCompatTest.cpp b/tests/cts/net/native/src/BpfCompatTest.cpp
index 97ecb9e..e52533b 100644
--- a/tests/cts/net/native/src/BpfCompatTest.cpp
+++ b/tests/cts/net/native/src/BpfCompatTest.cpp
@@ -31,8 +31,13 @@
std::ifstream elfFile(elfPath, std::ios::in | std::ios::binary);
ASSERT_TRUE(elfFile.is_open());
- EXPECT_EQ(48, readSectionUint("size_of_bpf_map_def", elfFile, 0));
- EXPECT_EQ(28, readSectionUint("size_of_bpf_prog_def", elfFile, 0));
+ if (android::modules::sdklevel::IsAtLeastT()) {
+ EXPECT_EQ(116, readSectionUint("size_of_bpf_map_def", elfFile, 0));
+ EXPECT_EQ(92, readSectionUint("size_of_bpf_prog_def", elfFile, 0));
+ } else {
+ EXPECT_EQ(48, readSectionUint("size_of_bpf_map_def", elfFile, 0));
+ EXPECT_EQ(28, readSectionUint("size_of_bpf_prog_def", elfFile, 0));
+ }
}
TEST(BpfTest, bpfStructSizeTestPreT) {
diff --git a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
index 0344604..1b77d5f 100644
--- a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
+++ b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
@@ -33,7 +33,6 @@
import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.net.NetworkRequest
import android.net.Uri
-import android.net.cts.NetworkValidationTestUtil.clearValidationTestUrlsDeviceConfig
import android.net.cts.NetworkValidationTestUtil.setHttpUrlDeviceConfig
import android.net.cts.NetworkValidationTestUtil.setHttpsUrlDeviceConfig
import android.net.cts.NetworkValidationTestUtil.setUrlExpirationDeviceConfig
@@ -60,6 +59,8 @@
import org.junit.Assume.assumeTrue
import org.junit.Assume.assumeFalse
import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Rule
import org.junit.runner.RunWith
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
@@ -99,34 +100,42 @@
private val server = TestHttpServer("localhost")
+ @get:Rule
+ val deviceConfigRule = DeviceConfigRule(retryCountBeforeSIfConfigChanged = 5)
+
+ companion object {
+ @JvmStatic @BeforeClass
+ fun setUpClass() {
+ runAsShell(READ_DEVICE_CONFIG) {
+ // Verify that the test URLs are not normally set on the device, but do not fail if
+ // the test URLs are set to what this test uses (URLs on localhost), in case the
+ // test was interrupted manually and rerun.
+ assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL)
+ assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTP_URL)
+ }
+ NetworkValidationTestUtil.clearValidationTestUrlsDeviceConfig()
+ }
+
+ private fun assertEmptyOrLocalhostUrl(urlKey: String) {
+ val url = DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY, urlKey)
+ assertTrue(TextUtils.isEmpty(url) || LOCALHOST_HOSTNAME == Uri.parse(url).host,
+ "$urlKey must not be set in production scenarios (current value: $url)")
+ }
+ }
+
@Before
fun setUp() {
- runAsShell(READ_DEVICE_CONFIG) {
- // Verify that the test URLs are not normally set on the device, but do not fail if the
- // test URLs are set to what this test uses (URLs on localhost), in case the test was
- // interrupted manually and rerun.
- assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL)
- assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTP_URL)
- }
- clearValidationTestUrlsDeviceConfig()
server.start()
}
@After
fun tearDown() {
- clearValidationTestUrlsDeviceConfig()
if (pm.hasSystemFeature(FEATURE_WIFI)) {
- reconnectWifi()
+ deviceConfigRule.runAfterNextCleanup { reconnectWifi() }
}
server.stop()
}
- private fun assertEmptyOrLocalhostUrl(urlKey: String) {
- val url = DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY, urlKey)
- assertTrue(TextUtils.isEmpty(url) || LOCALHOST_HOSTNAME == Uri.parse(url).host,
- "$urlKey must not be set in production scenarios (current value: $url)")
- }
-
@Test
fun testCaptivePortalIsNotDefaultNetwork() {
assumeTrue(pm.hasSystemFeature(FEATURE_TELEPHONY))
@@ -154,12 +163,13 @@
server.addResponse(Request(TEST_HTTPS_URL_PATH), Status.INTERNAL_ERROR)
val headers = mapOf("Location" to makeUrl(TEST_PORTAL_URL_PATH))
server.addResponse(Request(TEST_HTTP_URL_PATH), Status.REDIRECT, headers)
- setHttpsUrlDeviceConfig(makeUrl(TEST_HTTPS_URL_PATH))
- setHttpUrlDeviceConfig(makeUrl(TEST_HTTP_URL_PATH))
+ setHttpsUrlDeviceConfig(deviceConfigRule, makeUrl(TEST_HTTPS_URL_PATH))
+ setHttpUrlDeviceConfig(deviceConfigRule, makeUrl(TEST_HTTP_URL_PATH))
Log.d(TAG, "Set portal URLs to $TEST_HTTPS_URL_PATH and $TEST_HTTP_URL_PATH")
// URL expiration needs to be in the next 10 minutes
assertTrue(WIFI_CONNECT_TIMEOUT_MS < TimeUnit.MINUTES.toMillis(10))
- setUrlExpirationDeviceConfig(System.currentTimeMillis() + WIFI_CONNECT_TIMEOUT_MS)
+ setUrlExpirationDeviceConfig(deviceConfigRule,
+ System.currentTimeMillis() + WIFI_CONNECT_TIMEOUT_MS)
// Wait for a captive portal to be detected on the network
val wifiNetworkFuture = CompletableFuture<Network>()
@@ -215,4 +225,4 @@
utils.ensureWifiDisconnected(null /* wifiNetworkToCheck */)
utils.ensureWifiConnected()
}
-}
\ No newline at end of file
+}
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
index 68fa38d..7d1e13f 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
@@ -113,7 +113,7 @@
private static final int UNKNOWN_DETECTION_METHOD = 4;
private static final int FILTERED_UNKNOWN_DETECTION_METHOD = 0;
private static final int CARRIER_CONFIG_CHANGED_BROADCAST_TIMEOUT = 5000;
- private static final int DELAY_FOR_ADMIN_UIDS_MILLIS = 2000;
+ private static final int DELAY_FOR_ADMIN_UIDS_MILLIS = 5000;
private static final Executor INLINE_EXECUTOR = x -> x.run();
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 08cf0d7..766d62f 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -249,6 +249,10 @@
@Rule
public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+ @Rule
+ public final DeviceConfigRule mTestValidationConfigRule = new DeviceConfigRule(
+ 5 /* retryCountBeforeSIfConfigChanged */);
+
private static final String TAG = ConnectivityManagerTest.class.getSimpleName();
public static final int TYPE_MOBILE = ConnectivityManager.TYPE_MOBILE;
@@ -2765,9 +2769,8 @@
// Accept partial connectivity network should result in a validated network
expectNetworkHasCapability(network, NET_CAPABILITY_VALIDATED, WIFI_CONNECT_TIMEOUT_MS);
} finally {
- resetValidationConfig();
- // Reconnect wifi to reset the wifi status
- reconnectWifi();
+ mHttpServer.stop();
+ mTestValidationConfigRule.runAfterNextCleanup(this::reconnectWifi);
}
}
@@ -2792,11 +2795,13 @@
// Reject partial connectivity network should cause the network being torn down
assertEquals(network, cb.waitForLost());
} finally {
- resetValidationConfig();
+ mHttpServer.stop();
// Wifi will not automatically reconnect to the network. ensureWifiDisconnected cannot
// apply here. Thus, turn off wifi first and restart to restore.
- runShellCommand("svc wifi disable");
- mCtsNetUtils.ensureWifiConnected();
+ mTestValidationConfigRule.runAfterNextCleanup(() -> {
+ runShellCommand("svc wifi disable");
+ mCtsNetUtils.ensureWifiConnected();
+ });
}
}
@@ -2832,11 +2837,13 @@
});
waitForLost(wifiCb);
} finally {
- resetValidationConfig();
+ mHttpServer.stop();
/// Wifi will not automatically reconnect to the network. ensureWifiDisconnected cannot
// apply here. Thus, turn off wifi first and restart to restore.
- runShellCommand("svc wifi disable");
- mCtsNetUtils.ensureWifiConnected();
+ mTestValidationConfigRule.runAfterNextCleanup(() -> {
+ runShellCommand("svc wifi disable");
+ mCtsNetUtils.ensureWifiConnected();
+ });
}
}
@@ -2896,9 +2903,8 @@
wifiCb.assertNoCallbackThat(NO_CALLBACK_TIMEOUT_MS, c -> isValidatedCaps(c));
} finally {
resetAvoidBadWifi(previousAvoidBadWifi);
- resetValidationConfig();
- // Reconnect wifi to reset the wifi status
- reconnectWifi();
+ mHttpServer.stop();
+ mTestValidationConfigRule.runAfterNextCleanup(this::reconnectWifi);
}
}
@@ -2942,11 +2948,6 @@
return future.get(timeout, TimeUnit.MILLISECONDS);
}
- private void resetValidationConfig() {
- NetworkValidationTestUtil.clearValidationTestUrlsDeviceConfig();
- mHttpServer.stop();
- }
-
private void prepareHttpServer() throws Exception {
runAsShell(READ_DEVICE_CONFIG, () -> {
// Verify that the test URLs are not normally set on the device, but do not fail if the
@@ -3019,9 +3020,11 @@
mHttpServer.addResponse(new TestHttpServer.Request(
TEST_HTTP_URL_PATH, Method.GET, "" /* queryParameters */),
httpStatusCode, null /* locationHeader */, "" /* content */);
- NetworkValidationTestUtil.setHttpsUrlDeviceConfig(makeUrl(TEST_HTTPS_URL_PATH));
- NetworkValidationTestUtil.setHttpUrlDeviceConfig(makeUrl(TEST_HTTP_URL_PATH));
- NetworkValidationTestUtil.setUrlExpirationDeviceConfig(
+ NetworkValidationTestUtil.setHttpsUrlDeviceConfig(mTestValidationConfigRule,
+ makeUrl(TEST_HTTPS_URL_PATH));
+ NetworkValidationTestUtil.setHttpUrlDeviceConfig(mTestValidationConfigRule,
+ makeUrl(TEST_HTTP_URL_PATH));
+ NetworkValidationTestUtil.setUrlExpirationDeviceConfig(mTestValidationConfigRule,
System.currentTimeMillis() + WIFI_CONNECT_TIMEOUT_MS);
}
@@ -3374,6 +3377,7 @@
}
@Test @IgnoreUpTo(SC_V2)
+ @AppModeFull(reason = "Socket cannot bind in instant app mode")
public void testFirewallBlocking() {
// Following tests affect the actual state of networking on the device after the test.
// This might cause unexpected behaviour of the device. So, we skip them for now.
diff --git a/tests/cts/net/src/android/net/cts/DeviceConfigRule.kt b/tests/cts/net/src/android/net/cts/DeviceConfigRule.kt
new file mode 100644
index 0000000..d31a4e0
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/DeviceConfigRule.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2022 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.cts
+
+import android.Manifest.permission.READ_DEVICE_CONFIG
+import android.Manifest.permission.WRITE_DEVICE_CONFIG
+import android.provider.DeviceConfig
+import android.util.Log
+import com.android.modules.utils.build.SdkLevel
+import com.android.testutils.runAsShell
+import com.android.testutils.tryTest
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+private val TAG = DeviceConfigRule::class.simpleName
+
+/**
+ * A [TestRule] that helps set [DeviceConfig] for tests and clean up the test configuration
+ * automatically on teardown.
+ *
+ * The rule can also optionally retry tests when they fail following an external change of
+ * DeviceConfig before S; this typically happens because device config flags are synced while the
+ * test is running, and DisableConfigSyncTargetPreparer is only usable starting from S.
+ *
+ * @param retryCountBeforeSIfConfigChanged if > 0, when the test fails before S, check if
+ * the configs that were set through this rule were changed, and retry the test
+ * up to the specified number of times if yes.
+ */
+class DeviceConfigRule @JvmOverloads constructor(
+ val retryCountBeforeSIfConfigChanged: Int = 0
+) : TestRule {
+ // Maps (namespace, key) -> value
+ private val originalConfig = mutableMapOf<Pair<String, String>, String?>()
+ private val usedConfig = mutableMapOf<Pair<String, String>, String?>()
+
+ /**
+ * Actions to be run after cleanup of the config, for the current test only.
+ */
+ private val currentTestCleanupActions = mutableListOf<Runnable>()
+
+ override fun apply(base: Statement, description: Description): Statement {
+ return TestValidationUrlStatement(base, description)
+ }
+
+ private inner class TestValidationUrlStatement(
+ private val base: Statement,
+ private val description: Description
+ ) : Statement() {
+ override fun evaluate() {
+ var retryCount = if (SdkLevel.isAtLeastS()) 1 else retryCountBeforeSIfConfigChanged + 1
+ while (retryCount > 0) {
+ retryCount--
+ tryTest {
+ base.evaluate()
+ // Can't use break/return out of a loop here because this is a tryTest lambda,
+ // so set retryCount to exit instead
+ retryCount = 0
+ }.catch<Throwable> { e -> // junit AssertionFailedError does not extend Exception
+ if (retryCount == 0) throw e
+ usedConfig.forEach { (key, value) ->
+ val currentValue = runAsShell(READ_DEVICE_CONFIG) {
+ DeviceConfig.getProperty(key.first, key.second)
+ }
+ if (currentValue != value) {
+ Log.w(TAG, "Test failed with unexpected device config change, retrying")
+ return@catch
+ }
+ }
+ throw e
+ } cleanupStep {
+ runAsShell(WRITE_DEVICE_CONFIG) {
+ originalConfig.forEach { (key, value) ->
+ DeviceConfig.setProperty(
+ key.first, key.second, value, false /* makeDefault */)
+ }
+ }
+ } cleanupStep {
+ originalConfig.clear()
+ usedConfig.clear()
+ } cleanup {
+ currentTestCleanupActions.forEach { it.run() }
+ currentTestCleanupActions.clear()
+ }
+ }
+ }
+ }
+
+ /**
+ * Set a configuration key/value. After the test case ends, it will be restored to the value it
+ * had when this method was first called.
+ */
+ fun setConfig(namespace: String, key: String, value: String?) {
+ runAsShell(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG) {
+ val keyPair = Pair(namespace, key)
+ if (!originalConfig.containsKey(keyPair)) {
+ originalConfig[keyPair] = DeviceConfig.getProperty(namespace, key)
+ }
+ usedConfig[keyPair] = value
+ DeviceConfig.setProperty(namespace, key, value, false /* makeDefault */)
+ }
+ }
+
+ /**
+ * Add an action to be run after config cleanup when the current test case ends.
+ */
+ fun runAfterNextCleanup(action: Runnable) {
+ currentTestCleanupActions.add(action)
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
index d0d44dc..458d225 100644
--- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
@@ -56,6 +56,8 @@
import com.android.net.module.util.ArrayTrackRecord
import com.android.net.module.util.TrackRecord
import com.android.testutils.anyNetwork
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DeviceInfoUtils.isKernelVersionAtLeast
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.RecorderCallback.CallbackEntry.Available
@@ -66,6 +68,7 @@
import com.android.testutils.runAsShell
import com.android.testutils.waitForIdle
import org.junit.After
+import org.junit.Assume.assumeTrue
import org.junit.Assume.assumeFalse
import org.junit.Before
import org.junit.Test
@@ -97,6 +100,8 @@
@AppModeFull(reason = "Instant apps can't access EthernetManager")
// EthernetManager is not updatable before T, so tests do not need to be backwards compatible.
@RunWith(DevSdkIgnoreRunner::class)
+// This test depends on behavior introduced post-T as part of connectivity module updates
+@ConnectivityModuleTest
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
class EthernetManagerTest {
@@ -140,6 +145,8 @@
raResponder.start()
}
+ // WARNING: this function requires kernel support. Call assumeChangingCarrierSupported() at
+ // the top of your test.
fun setCarrierEnabled(enabled: Boolean) {
runAsShell(MANAGE_TEST_NETWORKS) {
tnm.setCarrierEnabled(tapInterface, enabled)
@@ -290,6 +297,9 @@
releaseTetheredInterface()
}
+ // Setting the carrier up / down relies on TUNSETCARRIER which was added in kernel version 5.0.
+ private fun assumeChangingCarrierSupported() = assumeTrue(isKernelVersionAtLeast("5.0.0"))
+
private fun addInterfaceStateListener(listener: EthernetStateListener) {
runAsShell(CONNECTIVITY_USE_RESTRICTED_NETWORKS) {
em.addInterfaceStateListener(handler::post, listener)
@@ -297,6 +307,8 @@
addedListeners.add(listener)
}
+ // WARNING: setting hasCarrier to false requires kernel support. Call
+ // assumeChangingCarrierSupported() at the top of your test.
private fun createInterface(hasCarrier: Boolean = true): EthernetTestInterface {
val iface = EthernetTestInterface(
context,
@@ -626,6 +638,8 @@
@Test
fun testNetworkRequest_forInterfaceWhileTogglingCarrier() {
+ assumeChangingCarrierSupported()
+
val iface = createInterface(false /* hasCarrier */)
val cb = requestNetwork(ETH_REQUEST)
diff --git a/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt b/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt
index 391d03a..462c8a3 100644
--- a/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt
@@ -16,16 +16,11 @@
package android.net.cts
-import android.Manifest
+import android.Manifest.permission.WRITE_DEVICE_CONFIG
import android.net.util.NetworkStackUtils
import android.provider.DeviceConfig
import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
-import android.util.Log
import com.android.testutils.runAsShell
-import com.android.testutils.tryTest
-import java.util.concurrent.CompletableFuture
-import java.util.concurrent.Executor
-import java.util.concurrent.TimeUnit
/**
* Collection of utility methods for configuring network validation.
@@ -38,9 +33,14 @@
* Clear the test network validation URLs.
*/
@JvmStatic fun clearValidationTestUrlsDeviceConfig() {
- setHttpsUrlDeviceConfig(null)
- setHttpUrlDeviceConfig(null)
- setUrlExpirationDeviceConfig(null)
+ runAsShell(WRITE_DEVICE_CONFIG) {
+ DeviceConfig.setProperty(NAMESPACE_CONNECTIVITY,
+ NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL, null, false)
+ DeviceConfig.setProperty(NAMESPACE_CONNECTIVITY,
+ NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL, null, false)
+ DeviceConfig.setProperty(NAMESPACE_CONNECTIVITY,
+ NetworkStackUtils.TEST_URL_EXPIRATION_TIME, null, false)
+ }
}
/**
@@ -48,71 +48,28 @@
*
* @see NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL
*/
- @JvmStatic fun setHttpsUrlDeviceConfig(url: String?) =
- setConfig(NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL, url)
+ @JvmStatic
+ fun setHttpsUrlDeviceConfig(rule: DeviceConfigRule, url: String?) =
+ rule.setConfig(NAMESPACE_CONNECTIVITY,
+ NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL, url)
/**
* Set the test validation HTTP URL.
*
* @see NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL
*/
- @JvmStatic fun setHttpUrlDeviceConfig(url: String?) =
- setConfig(NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL, url)
+ @JvmStatic
+ fun setHttpUrlDeviceConfig(rule: DeviceConfigRule, url: String?) =
+ rule.setConfig(NAMESPACE_CONNECTIVITY,
+ NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL, url)
/**
* Set the test validation URL expiration.
*
* @see NetworkStackUtils.TEST_URL_EXPIRATION_TIME
*/
- @JvmStatic fun setUrlExpirationDeviceConfig(timestamp: Long?) =
- setConfig(NetworkStackUtils.TEST_URL_EXPIRATION_TIME, timestamp?.toString())
-
- private fun setConfig(configKey: String, value: String?): String? {
- Log.i(TAG, "Setting config \"$configKey\" to \"$value\"")
- val readWritePermissions = arrayOf(
- Manifest.permission.READ_DEVICE_CONFIG,
- Manifest.permission.WRITE_DEVICE_CONFIG)
-
- val existingValue = runAsShell(*readWritePermissions) {
- DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY, configKey)
- }
- if (existingValue == value) {
- // Already the correct value. There may be a race if a change is already in flight,
- // but if multiple threads update the config there is no way to fix that anyway.
- Log.i(TAG, "\$configKey\" already had value \"$value\"")
- return value
- }
-
- val future = CompletableFuture<String>()
- val listener = DeviceConfig.OnPropertiesChangedListener {
- // The listener receives updates for any change to any key, so don't react to
- // changes that do not affect the relevant key
- if (!it.keyset.contains(configKey)) return@OnPropertiesChangedListener
- if (it.getString(configKey, null) == value) {
- future.complete(value)
- }
- }
-
- return tryTest {
- runAsShell(*readWritePermissions) {
- DeviceConfig.addOnPropertiesChangedListener(
- NAMESPACE_CONNECTIVITY,
- inlineExecutor,
- listener)
- DeviceConfig.setProperty(
- NAMESPACE_CONNECTIVITY,
- configKey,
- value,
- false /* makeDefault */)
- // Don't drop the permission until the config is applied, just in case
- future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
- }.also {
- Log.i(TAG, "Config \"$configKey\" successfully set to \"$value\"")
- }
- } cleanup {
- DeviceConfig.removeOnPropertiesChangedListener(listener)
- }
- }
-
- private val inlineExecutor get() = Executor { r -> r.run() }
+ @JvmStatic
+ fun setUrlExpirationDeviceConfig(rule: DeviceConfigRule, timestamp: Long?) =
+ rule.setConfig(NAMESPACE_CONNECTIVITY,
+ NetworkStackUtils.TEST_URL_EXPIRATION_TIME, timestamp?.toString())
}
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 3ea27f7..9d746b5 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -69,6 +69,7 @@
"java/com/android/server/connectivity/NetdEventListenerServiceTest.java",
"java/com/android/server/connectivity/VpnTest.java",
"java/com/android/server/net/ipmemorystore/*.java",
+ "java/com/android/server/connectivity/mdns/**/*.java",
]
}
@@ -143,6 +144,7 @@
static_libs: [
"services.core",
"services.net",
+ "service-mdns",
],
jni_libs: [
"libandroid_net_connectivity_com_android_net_module_util_jni",
diff --git a/tests/unit/java/android/net/NetworkStatsRecorderTest.java b/tests/unit/java/android/net/NetworkStatsRecorderTest.java
new file mode 100644
index 0000000..fad11a3
--- /dev/null
+++ b/tests/unit/java/android/net/NetworkStatsRecorderTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *i
+ * 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.net;
+
+import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.net.NetworkStats;
+import android.os.DropBoxManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.util.FileRotator;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public final class NetworkStatsRecorderTest {
+ private static final String TAG = NetworkStatsRecorderTest.class.getSimpleName();
+
+ private static final String TEST_PREFIX = "test";
+
+ @Mock private DropBoxManager mDropBox;
+ @Mock private NetworkStats.NonMonotonicObserver mObserver;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ private NetworkStatsRecorder buildRecorder(FileRotator rotator, boolean wipeOnError) {
+ return new NetworkStatsRecorder(rotator, mObserver, mDropBox, TEST_PREFIX,
+ HOUR_IN_MILLIS, false /* includeTags */, wipeOnError);
+ }
+
+ @Test
+ public void testWipeOnError() throws Exception {
+ final FileRotator rotator = mock(FileRotator.class);
+ final NetworkStatsRecorder wipeOnErrorRecorder = buildRecorder(rotator, true);
+
+ // Assuming that the rotator gets an exception happened when read data.
+ doThrow(new IOException()).when(rotator).readMatching(any(), anyLong(), anyLong());
+ wipeOnErrorRecorder.getOrLoadPartialLocked(Long.MIN_VALUE, Long.MAX_VALUE);
+ // Verify that the files will be deleted.
+ verify(rotator, times(1)).deleteAll();
+ reset(rotator);
+
+ final NetworkStatsRecorder noWipeOnErrorRecorder = buildRecorder(rotator, false);
+ doThrow(new IOException()).when(rotator).readMatching(any(), anyLong(), anyLong());
+ noWipeOnErrorRecorder.getOrLoadPartialLocked(Long.MIN_VALUE, Long.MAX_VALUE);
+ // Verify that the rotator won't delete files.
+ verify(rotator, never()).deleteAll();
+ }
+}
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index f07a10d..99e7ecc 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -16,43 +16,93 @@
package com.android.server;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_POWERSAVE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_RESTRICTED;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_STANDBY;
import static android.net.INetd.PERMISSION_INTERNET;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeFalse;
import static org.mockito.Mockito.verify;
import android.net.INetd;
import android.os.Build;
+import android.os.ServiceSpecificException;
import androidx.test.filters.SmallTest;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.BpfMap;
+import com.android.net.module.util.Struct.U32;
import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.TestBpfMap;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.List;
+
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
public final class BpfNetMapsTest {
private static final String TAG = "BpfNetMapsTest";
+
+ @Rule
+ public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
private static final int TEST_UID = 10086;
private static final int[] TEST_UIDS = {10002, 10003};
private static final String IFNAME = "wlan0";
private static final String CHAINNAME = "fw_dozable";
+ private static final U32 UID_RULES_CONFIGURATION_KEY = new U32(0);
+ private static final List<Integer> FIREWALL_CHAINS = List.of(
+ FIREWALL_CHAIN_DOZABLE,
+ FIREWALL_CHAIN_STANDBY,
+ FIREWALL_CHAIN_POWERSAVE,
+ FIREWALL_CHAIN_RESTRICTED,
+ FIREWALL_CHAIN_LOW_POWER_STANDBY,
+ FIREWALL_CHAIN_OEM_DENY_1,
+ FIREWALL_CHAIN_OEM_DENY_2,
+ FIREWALL_CHAIN_OEM_DENY_3
+ );
+
private BpfNetMaps mBpfNetMaps;
@Mock INetd mNetd;
+ private static final TestBpfMap<U32, U32> sConfigurationMap =
+ new TestBpfMap<>(U32.class, U32.class);
@Before
- public void setUp() {
+ public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mBpfNetMaps = new BpfNetMaps(mNetd);
+ BpfNetMaps.initialize(makeDependencies());
+ sConfigurationMap.clear();
+ }
+
+ private static BpfNetMaps.Dependencies makeDependencies() {
+ return new BpfNetMaps.Dependencies() {
+ @Override
+ public BpfMap<U32, U32> getConfigurationMap() {
+ return sConfigurationMap;
+ }
+ };
}
@Test
@@ -65,4 +115,154 @@
mBpfNetMaps.setNetPermForUids(PERMISSION_INTERNET, TEST_UIDS);
verify(mNetd).trafficSetNetPermForUids(PERMISSION_INTERNET, TEST_UIDS);
}
+
+ private void doTestGetChainEnabled(final List<Integer> enableChains) throws Exception {
+ long match = 0;
+ for (final int chain: enableChains) {
+ match |= mBpfNetMaps.getMatchByFirewallChain(chain);
+ }
+ sConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(match));
+
+ for (final int chain: FIREWALL_CHAINS) {
+ final String testCase = "EnabledChains: " + enableChains + " CheckedChain: " + chain;
+ if (enableChains.contains(chain)) {
+ assertTrue("Expected getChainEnabled returns True, " + testCase,
+ mBpfNetMaps.getChainEnabled(chain));
+ } else {
+ assertFalse("Expected getChainEnabled returns False, " + testCase,
+ mBpfNetMaps.getChainEnabled(chain));
+ }
+ }
+ }
+
+ private void doTestGetChainEnabled(final int enableChain) throws Exception {
+ doTestGetChainEnabled(List.of(enableChain));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testGetChainEnabled() throws Exception {
+ doTestGetChainEnabled(FIREWALL_CHAIN_DOZABLE);
+ doTestGetChainEnabled(FIREWALL_CHAIN_STANDBY);
+ doTestGetChainEnabled(FIREWALL_CHAIN_POWERSAVE);
+ doTestGetChainEnabled(FIREWALL_CHAIN_RESTRICTED);
+ doTestGetChainEnabled(FIREWALL_CHAIN_LOW_POWER_STANDBY);
+ doTestGetChainEnabled(FIREWALL_CHAIN_OEM_DENY_1);
+ doTestGetChainEnabled(FIREWALL_CHAIN_OEM_DENY_2);
+ doTestGetChainEnabled(FIREWALL_CHAIN_OEM_DENY_3);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testGetChainEnabledMultipleChainEnabled() throws Exception {
+ doTestGetChainEnabled(List.of(
+ FIREWALL_CHAIN_DOZABLE,
+ FIREWALL_CHAIN_STANDBY));
+ doTestGetChainEnabled(List.of(
+ FIREWALL_CHAIN_DOZABLE,
+ FIREWALL_CHAIN_STANDBY,
+ FIREWALL_CHAIN_POWERSAVE,
+ FIREWALL_CHAIN_RESTRICTED));
+ doTestGetChainEnabled(FIREWALL_CHAINS);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testGetChainEnabledInvalidChain() {
+ final Class<ServiceSpecificException> expected = ServiceSpecificException.class;
+ assertThrows(expected, () -> mBpfNetMaps.getChainEnabled(-1 /* childChain */));
+ assertThrows(expected, () -> mBpfNetMaps.getChainEnabled(1000 /* childChain */));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testGetChainEnabledMissingConfiguration() {
+ // sConfigurationMap does not have entry for UID_RULES_CONFIGURATION_KEY
+ assertThrows(ServiceSpecificException.class,
+ () -> mBpfNetMaps.getChainEnabled(FIREWALL_CHAIN_DOZABLE));
+ }
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.S_V2)
+ public void testGetChainEnabledBeforeT() {
+ assertThrows(UnsupportedOperationException.class,
+ () -> mBpfNetMaps.getChainEnabled(FIREWALL_CHAIN_DOZABLE));
+ }
+
+ private void doTestSetChildChain(final List<Integer> testChains) throws Exception {
+ long expectedMatch = 0;
+ for (final int chain: testChains) {
+ expectedMatch |= mBpfNetMaps.getMatchByFirewallChain(chain);
+ }
+
+ assertEquals(0, sConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY).val);
+
+ for (final int chain: testChains) {
+ mBpfNetMaps.setChildChain(chain, true /* enable */);
+ }
+ assertEquals(expectedMatch, sConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY).val);
+
+ for (final int chain: testChains) {
+ mBpfNetMaps.setChildChain(chain, false /* enable */);
+ }
+ assertEquals(0, sConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY).val);
+ }
+
+ private void doTestSetChildChain(final int testChain) throws Exception {
+ doTestSetChildChain(List.of(testChain));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testSetChildChain() throws Exception {
+ sConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(0));
+ doTestSetChildChain(FIREWALL_CHAIN_DOZABLE);
+ doTestSetChildChain(FIREWALL_CHAIN_STANDBY);
+ doTestSetChildChain(FIREWALL_CHAIN_POWERSAVE);
+ doTestSetChildChain(FIREWALL_CHAIN_RESTRICTED);
+ doTestSetChildChain(FIREWALL_CHAIN_LOW_POWER_STANDBY);
+ doTestSetChildChain(FIREWALL_CHAIN_OEM_DENY_1);
+ doTestSetChildChain(FIREWALL_CHAIN_OEM_DENY_2);
+ doTestSetChildChain(FIREWALL_CHAIN_OEM_DENY_3);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testSetChildChainMultipleChain() throws Exception {
+ sConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(0));
+ doTestSetChildChain(List.of(
+ FIREWALL_CHAIN_DOZABLE,
+ FIREWALL_CHAIN_STANDBY));
+ doTestSetChildChain(List.of(
+ FIREWALL_CHAIN_DOZABLE,
+ FIREWALL_CHAIN_STANDBY,
+ FIREWALL_CHAIN_POWERSAVE,
+ FIREWALL_CHAIN_RESTRICTED));
+ doTestSetChildChain(FIREWALL_CHAINS);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testSetChildChainInvalidChain() {
+ final Class<ServiceSpecificException> expected = ServiceSpecificException.class;
+ assertThrows(expected,
+ () -> mBpfNetMaps.setChildChain(-1 /* childChain */, true /* enable */));
+ assertThrows(expected,
+ () -> mBpfNetMaps.setChildChain(1000 /* childChain */, true /* enable */));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testSetChildChainMissingConfiguration() {
+ // sConfigurationMap does not have entry for UID_RULES_CONFIGURATION_KEY
+ assertThrows(ServiceSpecificException.class,
+ () -> mBpfNetMaps.setChildChain(FIREWALL_CHAIN_DOZABLE, true /* enable */));
+ }
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.S_V2)
+ public void testSetChildChainBeforeT() {
+ assertThrows(UnsupportedOperationException.class,
+ () -> mBpfNetMaps.setChildChain(FIREWALL_CHAIN_DOZABLE, true /* enable */));
+ }
}
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index 5899fd0..50df51a 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -34,6 +34,7 @@
import static android.os.UserHandle.PER_USER_RANGE;
import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
+import static com.android.testutils.Cleanup.testAndCleanup;
import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import static com.android.testutils.MiscAsserts.assertThrows;
@@ -41,6 +42,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
@@ -177,6 +179,7 @@
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
@@ -279,6 +282,8 @@
@Mock private ConnectivityManager mConnectivityManager;
@Mock private IpSecService mIpSecService;
@Mock private VpnProfileStore mVpnProfileStore;
+ @Mock private ScheduledThreadPoolExecutor mExecutor;
+ @Mock private ScheduledFuture mScheduledFuture;
@Mock DeviceIdleInternal mDeviceIdleInternal;
private final VpnProfile mVpnProfile;
@@ -342,7 +347,9 @@
// PERMISSION_DENIED.
doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(any());
+ // Set up mIkev2SessionCreator and mExecutor
resetIkev2SessionCreator(mIkeSessionWrapper);
+ resetExecutor(mScheduledFuture);
}
private void resetIkev2SessionCreator(Vpn.IkeSessionWrapper ikeSession) {
@@ -351,6 +358,18 @@
.thenReturn(ikeSession);
}
+ private void resetExecutor(ScheduledFuture scheduledFuture) {
+ doAnswer(
+ (invocation) -> {
+ ((Runnable) invocation.getArgument(0)).run();
+ return null;
+ })
+ .when(mExecutor)
+ .execute(any());
+ when(mExecutor.schedule(
+ any(Runnable.class), anyLong(), any())).thenReturn(mScheduledFuture);
+ }
+
@After
public void tearDown() throws Exception {
doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
@@ -1372,10 +1391,6 @@
final ArgumentCaptor<IkeSessionCallback> captor =
ArgumentCaptor.forClass(IkeSessionCallback.class);
- // This test depends on a real ScheduledThreadPoolExecutor
- doReturn(new ScheduledThreadPoolExecutor(1)).when(mTestDeps)
- .newScheduledThreadPoolExecutor();
-
final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
.thenReturn(mVpnProfile.encode());
@@ -1400,25 +1415,38 @@
verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS))
.unregisterNetworkCallback(eq(cb));
} else if (errorType == VpnManager.ERROR_CLASS_RECOVERABLE) {
- // To prevent spending much time to test the retry function, only retry 2 times here.
int retryIndex = 0;
- verify(mIkev2SessionCreator,
- timeout(((TestDeps) vpn.mDeps).getNextRetryDelaySeconds(retryIndex++) * 1000
- + TEST_TIMEOUT_MS))
- .createIkeSession(any(), any(), any(), any(), captor.capture(), any());
+ final IkeSessionCallback ikeCb2 = verifyRetryAndGetNewIkeCb(retryIndex++);
- // Capture a new IkeSessionCallback to get the latest token.
- reset(mIkev2SessionCreator);
- final IkeSessionCallback ikeCb2 = captor.getValue();
ikeCb2.onClosedWithException(exception);
- verify(mIkev2SessionCreator,
- timeout(((TestDeps) vpn.mDeps).getNextRetryDelaySeconds(retryIndex++) * 1000
- + TEST_TIMEOUT_MS))
- .createIkeSession(any(), any(), any(), any(), captor.capture(), any());
- reset(mIkev2SessionCreator);
+ verifyRetryAndGetNewIkeCb(retryIndex++);
}
}
+ private IkeSessionCallback verifyRetryAndGetNewIkeCb(int retryIndex) {
+ final ArgumentCaptor<Runnable> runnableCaptor =
+ ArgumentCaptor.forClass(Runnable.class);
+ final ArgumentCaptor<IkeSessionCallback> ikeCbCaptor =
+ ArgumentCaptor.forClass(IkeSessionCallback.class);
+
+ // Verify retry is scheduled
+ final long expectedDelay = mTestDeps.getNextRetryDelaySeconds(retryIndex);
+ verify(mExecutor).schedule(runnableCaptor.capture(), eq(expectedDelay), any());
+
+ // Mock the event of firing the retry task
+ runnableCaptor.getValue().run();
+
+ verify(mIkev2SessionCreator)
+ .createIkeSession(any(), any(), any(), any(), ikeCbCaptor.capture(), any());
+
+ // Forget the mIkev2SessionCreator#createIkeSession call and mExecutor#schedule call
+ // for the next retry verification
+ resetIkev2SessionCreator(mIkeSessionWrapper);
+ resetExecutor(mScheduledFuture);
+
+ return ikeCbCaptor.getValue();
+ }
+
@Test
public void testStartPlatformVpnAuthenticationFailed() throws Exception {
final IkeProtocolException exception = mock(IkeProtocolException.class);
@@ -1685,9 +1713,13 @@
final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
- // Mock network switch
+ // Mock network loss and verify a cleanup task is scheduled
vpnSnapShot.nwCb.onLost(TEST_NETWORK);
+ verify(mExecutor).schedule(any(Runnable.class), anyLong(), any());
+
+ // Mock new network comes up and the cleanup task is cancelled
vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
+ verify(mScheduledFuture).cancel(anyBoolean());
// Verify MOBIKE is triggered
verify(mIkeSessionWrapper).setNetwork(TEST_NETWORK_2);
@@ -1755,7 +1787,55 @@
vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
}
- // TODO: Add a test for network loss without mobility
+ private void verifyHandlingNetworkLoss() throws Exception {
+ final ArgumentCaptor<LinkProperties> lpCaptor =
+ ArgumentCaptor.forClass(LinkProperties.class);
+ verify(mMockNetworkAgent).sendLinkProperties(lpCaptor.capture());
+ final LinkProperties lp = lpCaptor.getValue();
+
+ assertNull(lp.getInterfaceName());
+ final List<RouteInfo> expectedRoutes = Arrays.asList(
+ new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null /*gateway*/,
+ null /*iface*/, RTN_UNREACHABLE),
+ new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null /*gateway*/,
+ null /*iface*/, RTN_UNREACHABLE));
+ assertEquals(expectedRoutes, lp.getRoutes());
+ }
+
+ @Test
+ public void testStartPlatformVpnHandlesNetworkLoss_mobikeEnabled() throws Exception {
+ final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
+ createIkeConfig(createIkeConnectInfo(), false /* isMobikeEnabled */));
+
+ // Forget the #sendLinkProperties during first setup.
+ reset(mMockNetworkAgent);
+
+ final ArgumentCaptor<Runnable> runnableCaptor =
+ ArgumentCaptor.forClass(Runnable.class);
+
+ // Mock network loss
+ vpnSnapShot.nwCb.onLost(TEST_NETWORK);
+
+ // Mock the grace period expires
+ verify(mExecutor).schedule(runnableCaptor.capture(), anyLong(), any());
+ runnableCaptor.getValue().run();
+
+ verifyHandlingNetworkLoss();
+ }
+
+ @Test
+ public void testStartPlatformVpnHandlesNetworkLoss_mobikeDisabled() throws Exception {
+ final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
+ createIkeConfig(createIkeConnectInfo(), false /* isMobikeEnabled */));
+
+ // Forget the #sendLinkProperties during first setup.
+ reset(mMockNetworkAgent);
+
+ // Mock network loss
+ vpnSnapShot.nwCb.onLost(TEST_NETWORK);
+
+ verifyHandlingNetworkLoss();
+ }
@Test
public void testStartRacoonNumericAddress() throws Exception {
@@ -1767,6 +1847,16 @@
startRacoon("hostname", "5.6.7.8"); // address returned by deps.resolve
}
+ @Test
+ public void testStartPptp() throws Exception {
+ startPptp(true /* useMppe */);
+ }
+
+ @Test
+ public void testStartPptp_NoMppe() throws Exception {
+ startPptp(false /* useMppe */);
+ }
+
private void assertTransportInfoMatches(NetworkCapabilities nc, int type) {
assertNotNull(nc);
VpnTransportInfo ti = (VpnTransportInfo) nc.getTransportInfo();
@@ -1774,6 +1864,48 @@
assertEquals(type, ti.getType());
}
+ private void startPptp(boolean useMppe) throws Exception {
+ final VpnProfile profile = new VpnProfile("testProfile" /* key */);
+ profile.type = VpnProfile.TYPE_PPTP;
+ profile.name = "testProfileName";
+ profile.username = "userName";
+ profile.password = "thePassword";
+ profile.server = "192.0.2.123";
+ profile.mppe = useMppe;
+
+ doReturn(new Network[] { new Network(101) }).when(mConnectivityManager).getAllNetworks();
+ doReturn(new Network(102)).when(mConnectivityManager).registerNetworkAgent(any(), any(),
+ any(), any(), any(), any(), anyInt());
+
+ final Vpn vpn = startLegacyVpn(createVpn(primaryUser.id), profile);
+ final TestDeps deps = (TestDeps) vpn.mDeps;
+
+ testAndCleanup(() -> {
+ final String[] mtpdArgs = deps.mtpdArgs.get(10, TimeUnit.SECONDS);
+ final String[] argsPrefix = new String[]{
+ EGRESS_IFACE, "pptp", profile.server, "1723", "name", profile.username,
+ "password", profile.password, "linkname", "vpn", "refuse-eap", "nodefaultroute",
+ "usepeerdns", "idle", "1800", "mtu", "1270", "mru", "1270"
+ };
+ assertArrayEquals(argsPrefix, Arrays.copyOf(mtpdArgs, argsPrefix.length));
+ if (useMppe) {
+ assertEquals(argsPrefix.length + 2, mtpdArgs.length);
+ assertEquals("+mppe", mtpdArgs[argsPrefix.length]);
+ assertEquals("-pap", mtpdArgs[argsPrefix.length + 1]);
+ } else {
+ assertEquals(argsPrefix.length + 1, mtpdArgs.length);
+ assertEquals("nomppe", mtpdArgs[argsPrefix.length]);
+ }
+
+ verify(mConnectivityManager, timeout(10_000)).registerNetworkAgent(any(), any(),
+ any(), any(), any(), any(), anyInt());
+ }, () -> { // Cleanup
+ vpn.mVpnRunner.exitVpnRunner();
+ deps.getStateFile().delete(); // set to delete on exit, but this deletes it earlier
+ vpn.mVpnRunner.join(10_000); // wait for up to 10s for the runner to die and cleanup
+ });
+ }
+
public void startRacoon(final String serverAddr, final String expectedAddr)
throws Exception {
final ConditionVariable legacyRunnerReady = new ConditionVariable();
@@ -1981,16 +2113,7 @@
@Override
public ScheduledThreadPoolExecutor newScheduledThreadPoolExecutor() {
- final ScheduledThreadPoolExecutor mockExecutor =
- mock(ScheduledThreadPoolExecutor.class);
- doAnswer(
- (invocation) -> {
- ((Runnable) invocation.getArgument(0)).run();
- return null;
- })
- .when(mockExecutor)
- .execute(any());
- return mockExecutor;
+ return mExecutor;
}
}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManagerTests.java b/tests/unit/java/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManagerTests.java
new file mode 100644
index 0000000..f84e2d8
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManagerTests.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2021 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 static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.Network;
+import android.net.NetworkRequest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Tests for {@link ConnectivityMonitor}. */
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class ConnectivityMonitorWithConnectivityManagerTests {
+ @Mock private Context mContext;
+ @Mock private ConnectivityMonitor.Listener mockListener;
+ @Mock private ConnectivityManager mConnectivityManager;
+
+ private ConnectivityMonitorWithConnectivityManager monitor;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ doReturn(mConnectivityManager).when(mContext)
+ .getSystemService(Context.CONNECTIVITY_SERVICE);
+ monitor = new ConnectivityMonitorWithConnectivityManager(mContext, mockListener);
+ }
+
+ @Test
+ public void testInitialState_shouldNotRegisterNetworkCallback() {
+ verifyNetworkCallbackRegistered(0 /* time */);
+ verifyNetworkCallbackUnregistered(0 /* time */);
+ }
+
+ @Test
+ public void testStartDiscovery_shouldRegisterNetworkCallback() {
+ monitor.startWatchingConnectivityChanges();
+
+ verifyNetworkCallbackRegistered(1 /* time */);
+ verifyNetworkCallbackUnregistered(0 /* time */);
+ }
+
+ @Test
+ public void testStartDiscoveryTwice_shouldRegisterOneNetworkCallback() {
+ monitor.startWatchingConnectivityChanges();
+ monitor.startWatchingConnectivityChanges();
+
+ verifyNetworkCallbackRegistered(1 /* time */);
+ verifyNetworkCallbackUnregistered(0 /* time */);
+ }
+
+ @Test
+ public void testStopDiscovery_shouldUnregisterNetworkCallback() {
+ monitor.startWatchingConnectivityChanges();
+ monitor.stopWatchingConnectivityChanges();
+
+ verifyNetworkCallbackRegistered(1 /* time */);
+ verifyNetworkCallbackUnregistered(1 /* time */);
+ }
+
+ @Test
+ public void testStopDiscoveryTwice_shouldUnregisterNetworkCallback() {
+ monitor.startWatchingConnectivityChanges();
+ monitor.stopWatchingConnectivityChanges();
+
+ verifyNetworkCallbackRegistered(1 /* time */);
+ verifyNetworkCallbackUnregistered(1 /* time */);
+ }
+
+ @Test
+ public void testIntentFired_shouldNotifyListener() {
+ InOrder inOrder = inOrder(mockListener);
+ monitor.startWatchingConnectivityChanges();
+
+ final ArgumentCaptor<NetworkCallback> callbackCaptor =
+ ArgumentCaptor.forClass(NetworkCallback.class);
+ verify(mConnectivityManager, times(1)).registerNetworkCallback(
+ any(NetworkRequest.class), callbackCaptor.capture());
+
+ final NetworkCallback callback = callbackCaptor.getValue();
+ final Network testNetwork = new Network(1 /* netId */);
+
+ // Simulate network available.
+ callback.onAvailable(testNetwork);
+ inOrder.verify(mockListener).onConnectivityChanged();
+
+ // Simulate network lost.
+ callback.onLost(testNetwork);
+ inOrder.verify(mockListener).onConnectivityChanged();
+
+ // Simulate network unavailable.
+ callback.onUnavailable();
+ inOrder.verify(mockListener).onConnectivityChanged();
+ }
+
+ private void verifyNetworkCallbackRegistered(int time) {
+ verify(mConnectivityManager, times(time)).registerNetworkCallback(
+ any(NetworkRequest.class), any(NetworkCallback.class));
+ }
+
+ private void verifyNetworkCallbackUnregistered(int time) {
+ verify(mConnectivityManager, times(time))
+ .unregisterNetworkCallback(any(NetworkCallback.class));
+ }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
new file mode 100644
index 0000000..3e3c3bf
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2021 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 static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.text.TextUtils;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+import java.util.Collections;
+
+/** Tests for {@link MdnsDiscoveryManager}. */
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsDiscoveryManagerTests {
+
+ private static final String SERVICE_TYPE_1 = "_googlecast._tcp.local";
+ private static final String SERVICE_TYPE_2 = "_test._tcp.local";
+
+ @Mock private ExecutorProvider executorProvider;
+ @Mock private MdnsSocketClient socketClient;
+ @Mock private MdnsServiceTypeClient mockServiceTypeClientOne;
+ @Mock private MdnsServiceTypeClient mockServiceTypeClientTwo;
+
+ @Mock MdnsServiceBrowserListener mockListenerOne;
+ @Mock MdnsServiceBrowserListener mockListenerTwo;
+ private MdnsDiscoveryManager discoveryManager;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ when(mockServiceTypeClientOne.getServiceTypeLabels())
+ .thenReturn(TextUtils.split(SERVICE_TYPE_1, "\\."));
+ when(mockServiceTypeClientTwo.getServiceTypeLabels())
+ .thenReturn(TextUtils.split(SERVICE_TYPE_2, "\\."));
+
+ discoveryManager = new MdnsDiscoveryManager(executorProvider, socketClient) {
+ @Override
+ MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType) {
+ if (serviceType.equals(SERVICE_TYPE_1)) {
+ return mockServiceTypeClientOne;
+ } else if (serviceType.equals(SERVICE_TYPE_2)) {
+ return mockServiceTypeClientTwo;
+ }
+ return null;
+ }
+ };
+ }
+
+ @Test
+ public void registerListener_unregisterListener() throws IOException {
+ discoveryManager.registerListener(
+ SERVICE_TYPE_1, mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+ verify(socketClient).startDiscovery();
+ verify(mockServiceTypeClientOne)
+ .startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+
+ when(mockServiceTypeClientOne.stopSendAndReceive(mockListenerOne)).thenReturn(true);
+ discoveryManager.unregisterListener(SERVICE_TYPE_1, mockListenerOne);
+ verify(mockServiceTypeClientOne).stopSendAndReceive(mockListenerOne);
+ verify(socketClient).stopDiscovery();
+ }
+
+ @Test
+ public void registerMultipleListeners() throws IOException {
+ discoveryManager.registerListener(
+ SERVICE_TYPE_1, mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+ verify(socketClient).startDiscovery();
+ verify(mockServiceTypeClientOne)
+ .startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+
+ discoveryManager.registerListener(
+ SERVICE_TYPE_2, mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+ verify(mockServiceTypeClientTwo)
+ .startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+ }
+
+ @Test
+ public void onResponseReceived() {
+ discoveryManager.registerListener(
+ SERVICE_TYPE_1, mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+ discoveryManager.registerListener(
+ SERVICE_TYPE_2, mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+
+ MdnsResponse responseForServiceTypeOne = createMockResponse(SERVICE_TYPE_1);
+ discoveryManager.onResponseReceived(responseForServiceTypeOne);
+ verify(mockServiceTypeClientOne).processResponse(responseForServiceTypeOne);
+
+ MdnsResponse responseForServiceTypeTwo = createMockResponse(SERVICE_TYPE_2);
+ discoveryManager.onResponseReceived(responseForServiceTypeTwo);
+ verify(mockServiceTypeClientTwo).processResponse(responseForServiceTypeTwo);
+
+ MdnsResponse responseForSubtype = createMockResponse("subtype._sub._googlecast._tcp.local");
+ discoveryManager.onResponseReceived(responseForSubtype);
+ verify(mockServiceTypeClientOne).processResponse(responseForSubtype);
+ }
+
+ private MdnsResponse createMockResponse(String serviceType) {
+ MdnsPointerRecord mockPointerRecord = mock(MdnsPointerRecord.class);
+ MdnsResponse mockResponse = mock(MdnsResponse.class);
+ when(mockResponse.getPointerRecords())
+ .thenReturn(Collections.singletonList(mockPointerRecord));
+ when(mockPointerRecord.getName()).thenReturn(TextUtils.split(serviceType, "\\."));
+ return mockResponse;
+ }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketReaderTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketReaderTests.java
new file mode 100644
index 0000000..19d8a00
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketReaderTests.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2021 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 static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.Locale;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsPacketReaderTests {
+
+ @Test
+ public void testLimits() throws IOException {
+ byte[] data = new byte[25];
+ DatagramPacket datagramPacket = new DatagramPacket(data, data.length);
+
+ // After creating a new reader, confirm that the remaining is equal to the packet length
+ // (or that there is no temporary limit).
+ MdnsPacketReader packetReader = new MdnsPacketReader(datagramPacket);
+ assertEquals(data.length, packetReader.getRemaining());
+
+ // Confirm that we can set the temporary limit to 0.
+ packetReader.setLimit(0);
+ assertEquals(0, packetReader.getRemaining());
+
+ // Confirm that we can clear the temporary limit, and restore to the length of the packet.
+ packetReader.clearLimit();
+ assertEquals(data.length, packetReader.getRemaining());
+
+ // Confirm that we can set the temporary limit to the actual length of the packet.
+ // While parsing packets, it is common to set the limit to the length of the packet.
+ packetReader.setLimit(data.length);
+ assertEquals(data.length, packetReader.getRemaining());
+
+ // Confirm that we ignore negative limits.
+ packetReader.setLimit(-10);
+ assertEquals(data.length, packetReader.getRemaining());
+
+ // Confirm that we can set the temporary limit to something less than the packet length.
+ packetReader.setLimit(data.length / 2);
+ assertEquals(data.length / 2, packetReader.getRemaining());
+
+ // Confirm that we throw an exception if trying to set the temporary limit beyond the
+ // packet length.
+ packetReader.clearLimit();
+ try {
+ packetReader.setLimit(data.length * 2 + 1);
+ fail("Should have thrown an IOException when trying to set the temporary limit beyond "
+ + "the packet length");
+ } catch (IOException e) {
+ // Expected
+ } catch (Exception e) {
+ fail(String.format(
+ Locale.ROOT,
+ "Should not have thrown any other exception except " + "for IOException: %s",
+ e.getMessage()));
+ }
+ assertEquals(data.length, packetReader.getRemaining());
+ }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java
new file mode 100644
index 0000000..fdb4d4a
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright (C) 2021 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 static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import android.util.Log;
+
+import com.android.net.module.util.HexDump;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetSocketAddress;
+import java.util.List;
+
+// The record test data does not use compressed names (label pointers), since that would require
+// additional data to populate the label dictionary accordingly.
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsRecordTests {
+ private static final String TAG = "MdnsRecordTests";
+ private static final int MAX_PACKET_SIZE = 4096;
+ private static final InetSocketAddress MULTICAST_IPV4_ADDRESS =
+ new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT);
+ private static final InetSocketAddress MULTICAST_IPV6_ADDRESS =
+ new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT);
+
+ @Test
+ public void testInet4AddressRecord() throws IOException {
+ final byte[] dataIn = HexDump.hexStringToByteArray(
+ "0474657374000001" + "0001000011940004" + "0A010203");
+ assertNotNull(dataIn);
+ String dataInText = HexDump.dumpHexString(dataIn, 0, dataIn.length);
+
+ // Decode
+ DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
+ MdnsPacketReader reader = new MdnsPacketReader(packet);
+
+ String[] name = reader.readLabels();
+ assertNotNull(name);
+ assertEquals(1, name.length);
+ assertEquals("test", name[0]);
+ String fqdn = MdnsRecord.labelsToString(name);
+ assertEquals("test", fqdn);
+
+ int type = reader.readUInt16();
+ assertEquals(MdnsRecord.TYPE_A, type);
+
+ MdnsInetAddressRecord record = new MdnsInetAddressRecord(name, MdnsRecord.TYPE_A, reader);
+ Inet4Address addr = record.getInet4Address();
+ assertEquals("/10.1.2.3", addr.toString());
+
+ // Encode
+ MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
+ record.write(writer, record.getReceiptTime());
+
+ packet = writer.getPacket(MULTICAST_IPV4_ADDRESS);
+ byte[] dataOut = packet.getData();
+
+ String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
+ Log.d(TAG, dataOutText);
+
+ assertEquals(dataInText, dataOutText);
+ }
+
+ @Test
+ public void testTypeAAAInet6AddressRecord() throws IOException {
+ final byte[] dataIn = HexDump.hexStringToByteArray(
+ "047465737400001C"
+ + "0001000011940010"
+ + "AABBCCDD11223344"
+ + "A0B0C0D010203040");
+ assertNotNull(dataIn);
+ String dataInText = HexDump.dumpHexString(dataIn, 0, dataIn.length);
+
+ // Decode
+ DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
+ packet.setSocketAddress(
+ new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT));
+ MdnsPacketReader reader = new MdnsPacketReader(packet);
+
+ String[] name = reader.readLabels();
+ assertNotNull(name);
+ assertEquals(1, name.length);
+ String fqdn = MdnsRecord.labelsToString(name);
+ assertEquals("test", fqdn);
+
+ int type = reader.readUInt16();
+ assertEquals(MdnsRecord.TYPE_AAAA, type);
+
+ MdnsInetAddressRecord record = new MdnsInetAddressRecord(name, MdnsRecord.TYPE_AAAA,
+ reader);
+ assertNull(record.getInet4Address());
+ Inet6Address addr = record.getInet6Address();
+ assertEquals("/aabb:ccdd:1122:3344:a0b0:c0d0:1020:3040", addr.toString());
+
+ // Encode
+ MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
+ record.write(writer, record.getReceiptTime());
+
+ packet = writer.getPacket(MULTICAST_IPV6_ADDRESS);
+ byte[] dataOut = packet.getData();
+
+ String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
+ Log.d(TAG, dataOutText);
+
+ assertEquals(dataInText, dataOutText);
+ }
+
+ @Test
+ public void testTypeAAAInet4AddressRecord() throws IOException {
+ final byte[] dataIn = HexDump.hexStringToByteArray(
+ "047465737400001C"
+ + "0001000011940010"
+ + "0000000000000000"
+ + "0000FFFF10203040");
+ assertNotNull(dataIn);
+ HexDump.dumpHexString(dataIn, 0, dataIn.length);
+
+ // Decode
+ DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
+ packet.setSocketAddress(
+ new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT));
+ MdnsPacketReader reader = new MdnsPacketReader(packet);
+
+ String[] name = reader.readLabels();
+ assertNotNull(name);
+ assertEquals(1, name.length);
+ String fqdn = MdnsRecord.labelsToString(name);
+ assertEquals("test", fqdn);
+
+ int type = reader.readUInt16();
+ assertEquals(MdnsRecord.TYPE_AAAA, type);
+
+ MdnsInetAddressRecord record = new MdnsInetAddressRecord(name, MdnsRecord.TYPE_AAAA,
+ reader);
+ assertNull(record.getInet6Address());
+ Inet4Address addr = record.getInet4Address();
+ assertEquals("/16.32.48.64", addr.toString());
+
+ // Encode
+ MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
+ record.write(writer, record.getReceiptTime());
+
+ packet = writer.getPacket(MULTICAST_IPV4_ADDRESS);
+ byte[] dataOut = packet.getData();
+
+ String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
+ Log.d(TAG, dataOutText);
+
+ final byte[] expectedDataIn =
+ HexDump.hexStringToByteArray("047465737400001C000100001194000410203040");
+ assertNotNull(expectedDataIn);
+ String expectedDataInText = HexDump.dumpHexString(expectedDataIn, 0, expectedDataIn.length);
+
+ assertEquals(expectedDataInText, dataOutText);
+ }
+
+ @Test
+ public void testPointerRecord() throws IOException {
+ final byte[] dataIn = HexDump.hexStringToByteArray(
+ "047465737400000C"
+ + "000100001194000E"
+ + "03666F6F03626172"
+ + "047175787800");
+ assertNotNull(dataIn);
+ String dataInText = HexDump.dumpHexString(dataIn, 0, dataIn.length);
+
+ // Decode
+ DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
+ MdnsPacketReader reader = new MdnsPacketReader(packet);
+
+ String[] name = reader.readLabels();
+ assertNotNull(name);
+ assertEquals(1, name.length);
+ String fqdn = MdnsRecord.labelsToString(name);
+ assertEquals("test", fqdn);
+
+ int type = reader.readUInt16();
+ assertEquals(MdnsRecord.TYPE_PTR, type);
+
+ MdnsPointerRecord record = new MdnsPointerRecord(name, reader);
+ String[] pointer = record.getPointer();
+ assertEquals("foo.bar.quxx", MdnsRecord.labelsToString(pointer));
+
+ assertFalse(record.hasSubtype());
+ assertNull(record.getSubtype());
+
+ // Encode
+ MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
+ record.write(writer, record.getReceiptTime());
+
+ packet = writer.getPacket(MULTICAST_IPV4_ADDRESS);
+ byte[] dataOut = packet.getData();
+
+ String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
+ Log.d(TAG, dataOutText);
+
+ assertEquals(dataInText, dataOutText);
+ }
+
+ @Test
+ public void testServiceRecord() throws IOException {
+ final byte[] dataIn = HexDump.hexStringToByteArray(
+ "0474657374000021"
+ + "0001000011940014"
+ + "000100FF1F480366"
+ + "6F6F036261720471"
+ + "75787800");
+ assertNotNull(dataIn);
+ String dataInText = HexDump.dumpHexString(dataIn, 0, dataIn.length);
+
+ // Decode
+ DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
+ MdnsPacketReader reader = new MdnsPacketReader(packet);
+
+ String[] name = reader.readLabels();
+ assertNotNull(name);
+ assertEquals(1, name.length);
+ String fqdn = MdnsRecord.labelsToString(name);
+ assertEquals("test", fqdn);
+
+ int type = reader.readUInt16();
+ assertEquals(MdnsRecord.TYPE_SRV, type);
+
+ MdnsServiceRecord record = new MdnsServiceRecord(name, reader);
+
+ int servicePort = record.getServicePort();
+ assertEquals(8008, servicePort);
+
+ String serviceHost = MdnsRecord.labelsToString(record.getServiceHost());
+ assertEquals("foo.bar.quxx", serviceHost);
+
+ assertEquals(1, record.getServicePriority());
+ assertEquals(255, record.getServiceWeight());
+
+ // Encode
+ MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
+ record.write(writer, record.getReceiptTime());
+
+ packet = writer.getPacket(MULTICAST_IPV4_ADDRESS);
+ byte[] dataOut = packet.getData();
+
+ String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
+ Log.d(TAG, dataOutText);
+
+ assertEquals(dataInText, dataOutText);
+ }
+
+ @Test
+ public void testTextRecord() throws IOException {
+ final byte[] dataIn = HexDump.hexStringToByteArray(
+ "0474657374000010"
+ + "0001000011940024"
+ + "0D613D68656C6C6F"
+ + "2074686572650C62"
+ + "3D31323334353637"
+ + "3839300878797A3D"
+ + "21402324");
+ assertNotNull(dataIn);
+ String dataInText = HexDump.dumpHexString(dataIn, 0, dataIn.length);
+
+ // Decode
+ DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
+ MdnsPacketReader reader = new MdnsPacketReader(packet);
+
+ String[] name = reader.readLabels();
+ assertNotNull(name);
+ assertEquals(1, name.length);
+ String fqdn = MdnsRecord.labelsToString(name);
+ assertEquals("test", fqdn);
+
+ int type = reader.readUInt16();
+ assertEquals(MdnsRecord.TYPE_TXT, type);
+
+ MdnsTextRecord record = new MdnsTextRecord(name, reader);
+
+ List<String> strings = record.getStrings();
+ assertNotNull(strings);
+ assertEquals(3, strings.size());
+
+ assertEquals("a=hello there", strings.get(0));
+ assertEquals("b=1234567890", strings.get(1));
+ assertEquals("xyz=!@#$", strings.get(2));
+
+ // Encode
+ MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
+ record.write(writer, record.getReceiptTime());
+
+ packet = writer.getPacket(MULTICAST_IPV4_ADDRESS);
+ byte[] dataOut = packet.getData();
+
+ String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
+ Log.d(TAG, dataOutText);
+
+ assertEquals(dataInText, dataOutText);
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java
new file mode 100644
index 0000000..ea9156c
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2021 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 static com.android.server.connectivity.mdns.MdnsResponseDecoder.Clock;
+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.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import com.android.net.module.util.HexDump;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetSocketAddress;
+import java.util.LinkedList;
+import java.util.List;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsResponseDecoderTests {
+ private static final byte[] data = HexDump.hexStringToByteArray(
+ "0000840000000004"
+ + "00000003134A6F68"
+ + "6E6E792773204368"
+ + "726F6D6563617374"
+ + "0B5F676F6F676C65"
+ + "63617374045F7463"
+ + "70056C6F63616C00"
+ + "0010800100001194"
+ + "006C2369643D3937"
+ + "3062663534376237"
+ + "3533666336336332"
+ + "6432613336626238"
+ + "3936616261380576"
+ + "653D30320D6D643D"
+ + "4368726F6D656361"
+ + "73741269633D2F73"
+ + "657475702F69636F"
+ + "6E2E706E6716666E"
+ + "3D4A6F686E6E7927"
+ + "73204368726F6D65"
+ + "636173740463613D"
+ + "350473743D30095F"
+ + "7365727669636573"
+ + "075F646E732D7364"
+ + "045F756470C03100"
+ + "0C00010000119400"
+ + "02C020C020000C00"
+ + "01000011940002C0"
+ + "0CC00C0021800100"
+ + "000078001C000000"
+ + "001F49134A6F686E"
+ + "6E79277320436872"
+ + "6F6D6563617374C0"
+ + "31C0F30001800100"
+ + "0000780004C0A864"
+ + "68C0F3002F800100"
+ + "0000780005C0F300"
+ + "0140C00C002F8001"
+ + "000011940009C00C"
+ + "00050000800040");
+
+ private static final byte[] data6 = HexDump.hexStringToByteArray(
+ "0000840000000001000000030B5F676F6F676C656361737404"
+ + "5F746370056C6F63616C00000C000100000078003330476F6F676C"
+ + "652D486F6D652D4D61782D61363836666331323961366638636265"
+ + "31643636353139343065336164353766C00CC02E00108001000011"
+ + "9400C02369643D6136383666633132396136663863626531643636"
+ + "3531393430653361643537662363643D4133304233303032363546"
+ + "36384341313233353532434639344141353742314613726D3D4335"
+ + "35393134383530383841313638330576653D3035126D643D476F6F"
+ + "676C6520486F6D65204D61781269633D2F73657475702F69636F6E"
+ + "2E706E6710666E3D417474696320737065616B65720863613D3130"
+ + "3234340473743D320F62733D464138464341363734453537046E66"
+ + "3D320372733DC02E0021800100000078002D000000001F49246136"
+ + "3836666331322D396136662D386362652D316436362D3531393430"
+ + "65336164353766C01DC13F001C8001000000780010200033330000"
+ + "0000DA6C63FFFE7C74830109018001000000780004C0A801026C6F"
+ + "63616C0000018001000000780004C0A8010A000001800100000078"
+ + "0004C0A8010A00000000000000");
+
+ private static final String DUMMY_CAST_SERVICE_NAME = "_googlecast";
+ private static final String[] DUMMY_CAST_SERVICE_TYPE =
+ new String[] {DUMMY_CAST_SERVICE_NAME, "_tcp", "local"};
+
+ private final List<MdnsResponse> responses = new LinkedList<>();
+
+ private final Clock mClock = mock(Clock.class);
+
+ @Before
+ public void setUp() {
+ MdnsResponseDecoder decoder = new MdnsResponseDecoder(mClock, DUMMY_CAST_SERVICE_TYPE);
+ assertNotNull(data);
+ DatagramPacket packet = new DatagramPacket(data, data.length);
+ packet.setSocketAddress(
+ new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT));
+ responses.clear();
+ int errorCode = decoder.decode(packet, responses);
+ assertEquals(MdnsResponseDecoder.SUCCESS, errorCode);
+ assertEquals(1, responses.size());
+ }
+
+ @Test
+ public void testDecodeWithNullServiceType() {
+ MdnsResponseDecoder decoder = new MdnsResponseDecoder(mClock, null);
+ assertNotNull(data);
+ DatagramPacket packet = new DatagramPacket(data, data.length);
+ packet.setSocketAddress(
+ new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT));
+ responses.clear();
+ int errorCode = decoder.decode(packet, responses);
+ assertEquals(MdnsResponseDecoder.SUCCESS, errorCode);
+ assertEquals(2, responses.size());
+ }
+
+ @Test
+ public void testDecodeMultipleAnswerPacket() throws IOException {
+ MdnsResponse response = responses.get(0);
+ assertTrue(response.isComplete());
+
+ MdnsInetAddressRecord inet4AddressRecord = response.getInet4AddressRecord();
+ Inet4Address inet4Addr = inet4AddressRecord.getInet4Address();
+
+ assertNotNull(inet4Addr);
+ assertEquals("/192.168.100.104", inet4Addr.toString());
+
+ MdnsServiceRecord serviceRecord = response.getServiceRecord();
+ String serviceName = serviceRecord.getServiceName();
+ assertEquals(DUMMY_CAST_SERVICE_NAME, serviceName);
+
+ String serviceInstanceName = serviceRecord.getServiceInstanceName();
+ assertEquals("Johnny's Chromecast", serviceInstanceName);
+
+ String serviceHost = MdnsRecord.labelsToString(serviceRecord.getServiceHost());
+ assertEquals("Johnny's Chromecast.local", serviceHost);
+
+ int serviceProto = serviceRecord.getServiceProtocol();
+ assertEquals(MdnsServiceRecord.PROTO_TCP, serviceProto);
+
+ int servicePort = serviceRecord.getServicePort();
+ assertEquals(8009, servicePort);
+
+ int servicePriority = serviceRecord.getServicePriority();
+ assertEquals(0, servicePriority);
+
+ int serviceWeight = serviceRecord.getServiceWeight();
+ assertEquals(0, serviceWeight);
+
+ MdnsTextRecord textRecord = response.getTextRecord();
+ List<String> textStrings = textRecord.getStrings();
+ assertEquals(7, textStrings.size());
+ assertEquals("id=970bf547b753fc63c2d2a36bb896aba8", textStrings.get(0));
+ assertEquals("ve=02", textStrings.get(1));
+ assertEquals("md=Chromecast", textStrings.get(2));
+ assertEquals("ic=/setup/icon.png", textStrings.get(3));
+ assertEquals("fn=Johnny's Chromecast", textStrings.get(4));
+ assertEquals("ca=5", textStrings.get(5));
+ assertEquals("st=0", textStrings.get(6));
+ }
+
+ @Test
+ public void testDecodeIPv6AnswerPacket() throws IOException {
+ MdnsResponseDecoder decoder = new MdnsResponseDecoder(mClock, DUMMY_CAST_SERVICE_TYPE);
+ assertNotNull(data6);
+ DatagramPacket packet = new DatagramPacket(data6, data6.length);
+ packet.setSocketAddress(
+ new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT));
+
+ responses.clear();
+ int errorCode = decoder.decode(packet, responses);
+ assertEquals(MdnsResponseDecoder.SUCCESS, errorCode);
+
+ MdnsResponse response = responses.get(0);
+ assertTrue(response.isComplete());
+
+ MdnsInetAddressRecord inet6AddressRecord = response.getInet6AddressRecord();
+ assertNotNull(inet6AddressRecord);
+ Inet4Address inet4Addr = inet6AddressRecord.getInet4Address();
+ assertNull(inet4Addr);
+
+ Inet6Address inet6Addr = inet6AddressRecord.getInet6Address();
+ assertNotNull(inet6Addr);
+ assertEquals(inet6Addr.getHostAddress(), "2000:3333::da6c:63ff:fe7c:7483");
+ }
+
+ @Test
+ public void testIsComplete() {
+ MdnsResponse response = responses.get(0);
+ assertTrue(response.isComplete());
+
+ response.clearPointerRecords();
+ assertFalse(response.isComplete());
+
+ response = responses.get(0);
+ response.setInet4AddressRecord(null);
+ assertFalse(response.isComplete());
+
+ response = responses.get(0);
+ response.setInet6AddressRecord(null);
+ assertFalse(response.isComplete());
+
+ response = responses.get(0);
+ response.setServiceRecord(null);
+ assertFalse(response.isComplete());
+
+ response = responses.get(0);
+ response.setTextRecord(null);
+ assertFalse(response.isComplete());
+ }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseTests.java
new file mode 100644
index 0000000..ae16f2b
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseTests.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2021 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 static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.net.module.util.HexDump;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.Arrays;
+import java.util.List;
+
+// The record test data does not use compressed names (label pointers), since that would require
+// additional data to populate the label dictionary accordingly.
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsResponseTests {
+ private static final String TAG = "MdnsResponseTests";
+ // MDNS response packet for name "test" with an IPv4 address of 10.1.2.3
+ private static final byte[] dataIn_ipv4_1 = HexDump.hexStringToByteArray(
+ "0474657374000001" + "0001000011940004" + "0A010203");
+ // MDNS response packet for name "tess" with an IPv4 address of 10.1.2.4
+ private static final byte[] dataIn_ipv4_2 = HexDump.hexStringToByteArray(
+ "0474657373000001" + "0001000011940004" + "0A010204");
+ // MDNS response w/name "test" & IPv6 address of aabb:ccdd:1122:3344:a0b0:c0d0:1020:3040
+ private static final byte[] dataIn_ipv6_1 = HexDump.hexStringToByteArray(
+ "047465737400001C" + "0001000011940010" + "AABBCCDD11223344" + "A0B0C0D010203040");
+ // MDNS response w/name "test" & IPv6 address of aabb:ccdd:1122:3344:a0b0:c0d0:1020:3030
+ private static final byte[] dataIn_ipv6_2 = HexDump.hexStringToByteArray(
+ "047465737400001C" + "0001000011940010" + "AABBCCDD11223344" + "A0B0C0D010203030");
+ // MDNS response w/name "test" & PTR to foo.bar.quxx
+ private static final byte[] dataIn_ptr_1 = HexDump.hexStringToByteArray(
+ "047465737400000C" + "000100001194000E" + "03666F6F03626172" + "047175787800");
+ // MDNS response w/name "test" & PTR to foo.bar.quxy
+ private static final byte[] dataIn_ptr_2 = HexDump.hexStringToByteArray(
+ "047465737400000C" + "000100001194000E" + "03666F6F03626172" + "047175787900");
+ // MDNS response w/name "test" & Service for host foo.bar.quxx
+ private static final byte[] dataIn_service_1 = HexDump.hexStringToByteArray(
+ "0474657374000021"
+ + "0001000011940014"
+ + "000100FF1F480366"
+ + "6F6F036261720471"
+ + "75787800");
+ // MDNS response w/name "test" & Service for host test
+ private static final byte[] dataIn_service_2 = HexDump.hexStringToByteArray(
+ "0474657374000021" + "000100001194000B" + "000100FF1F480474" + "657374");
+ // MDNS response w/name "test" & the following text strings:
+ // "a=hello there", "b=1234567890", and "xyz=!$$$"
+ private static final byte[] dataIn_text_1 = HexDump.hexStringToByteArray(
+ "0474657374000010"
+ + "0001000011940024"
+ + "0D613D68656C6C6F"
+ + "2074686572650C62"
+ + "3D31323334353637"
+ + "3839300878797A3D"
+ + "21242424");
+ // MDNS response w/name "test" & the following text strings:
+ // "a=hello there", "b=1234567890", and "xyz=!@#$"
+ private static final byte[] dataIn_text_2 = HexDump.hexStringToByteArray(
+ "0474657374000010"
+ + "0001000011940024"
+ + "0D613D68656C6C6F"
+ + "2074686572650C62"
+ + "3D31323334353637"
+ + "3839300878797A3D"
+ + "21402324");
+
+ // The following helper classes act as wrappers so that IPv4 and IPv6 address records can
+ // be explicitly created by type using same constructor signature as all other records.
+ static class MdnsInet4AddressRecord extends MdnsInetAddressRecord {
+ public MdnsInet4AddressRecord(String[] name, MdnsPacketReader reader) throws IOException {
+ super(name, MdnsRecord.TYPE_A, reader);
+ }
+ }
+
+ static class MdnsInet6AddressRecord extends MdnsInetAddressRecord {
+ public MdnsInet6AddressRecord(String[] name, MdnsPacketReader reader) throws IOException {
+ super(name, MdnsRecord.TYPE_AAAA, reader);
+ }
+ }
+
+ // This helper class just wraps the data bytes of a response packet with the contained record
+ // type.
+ // Its only purpose is to make the test code a bit more readable.
+ static class PacketAndRecordClass {
+ public final byte[] packetData;
+ public final Class<?> recordClass;
+
+ public PacketAndRecordClass() {
+ packetData = null;
+ recordClass = null;
+ }
+
+ public PacketAndRecordClass(byte[] data, Class<?> c) {
+ packetData = data;
+ recordClass = c;
+ }
+ }
+
+ // Construct an MdnsResponse with the specified data packets applied.
+ private MdnsResponse makeMdnsResponse(long time, List<PacketAndRecordClass> responseList)
+ throws IOException {
+ MdnsResponse response = new MdnsResponse(time);
+ for (PacketAndRecordClass responseData : responseList) {
+ DatagramPacket packet =
+ new DatagramPacket(responseData.packetData, responseData.packetData.length);
+ MdnsPacketReader reader = new MdnsPacketReader(packet);
+ String[] name = reader.readLabels();
+ reader.skip(2); // skip record type indication.
+ // Apply the right kind of record to the response.
+ if (responseData.recordClass == MdnsInet4AddressRecord.class) {
+ response.setInet4AddressRecord(new MdnsInet4AddressRecord(name, reader));
+ } else if (responseData.recordClass == MdnsInet6AddressRecord.class) {
+ response.setInet6AddressRecord(new MdnsInet6AddressRecord(name, reader));
+ } else if (responseData.recordClass == MdnsPointerRecord.class) {
+ response.addPointerRecord(new MdnsPointerRecord(name, reader));
+ } else if (responseData.recordClass == MdnsServiceRecord.class) {
+ response.setServiceRecord(new MdnsServiceRecord(name, reader));
+ } else if (responseData.recordClass == MdnsTextRecord.class) {
+ response.setTextRecord(new MdnsTextRecord(name, reader));
+ } else {
+ fail("Unsupported/unexpected MdnsRecord subtype used in test - invalid test!");
+ }
+ }
+ return response;
+ }
+
+ @Test
+ public void getInet4AddressRecord_returnsAddedRecord() throws IOException {
+ DatagramPacket packet = new DatagramPacket(dataIn_ipv4_1, dataIn_ipv4_1.length);
+ MdnsPacketReader reader = new MdnsPacketReader(packet);
+ String[] name = reader.readLabels();
+ reader.skip(2); // skip record type indication.
+ MdnsInetAddressRecord record = new MdnsInetAddressRecord(name, MdnsRecord.TYPE_A, reader);
+ MdnsResponse response = new MdnsResponse(0);
+ assertFalse(response.hasInet4AddressRecord());
+ assertTrue(response.setInet4AddressRecord(record));
+ assertEquals(response.getInet4AddressRecord(), record);
+ }
+
+ @Test
+ public void getInet6AddressRecord_returnsAddedRecord() throws IOException {
+ DatagramPacket packet = new DatagramPacket(dataIn_ipv6_1, dataIn_ipv6_1.length);
+ MdnsPacketReader reader = new MdnsPacketReader(packet);
+ String[] name = reader.readLabels();
+ reader.skip(2); // skip record type indication.
+ MdnsInetAddressRecord record =
+ new MdnsInetAddressRecord(name, MdnsRecord.TYPE_AAAA, reader);
+ MdnsResponse response = new MdnsResponse(0);
+ assertFalse(response.hasInet6AddressRecord());
+ assertTrue(response.setInet6AddressRecord(record));
+ assertEquals(response.getInet6AddressRecord(), record);
+ }
+
+ @Test
+ public void getPointerRecords_returnsAddedRecord() throws IOException {
+ DatagramPacket packet = new DatagramPacket(dataIn_ptr_1, dataIn_ptr_1.length);
+ MdnsPacketReader reader = new MdnsPacketReader(packet);
+ String[] name = reader.readLabels();
+ reader.skip(2); // skip record type indication.
+ MdnsPointerRecord record = new MdnsPointerRecord(name, reader);
+ MdnsResponse response = new MdnsResponse(0);
+ assertFalse(response.hasPointerRecords());
+ assertTrue(response.addPointerRecord(record));
+ List<MdnsPointerRecord> recordList = response.getPointerRecords();
+ assertNotNull(recordList);
+ assertEquals(1, recordList.size());
+ assertEquals(record, recordList.get(0));
+ }
+
+ @Test
+ public void getServiceRecord_returnsAddedRecord() throws IOException {
+ DatagramPacket packet = new DatagramPacket(dataIn_service_1, dataIn_service_1.length);
+ MdnsPacketReader reader = new MdnsPacketReader(packet);
+ String[] name = reader.readLabels();
+ reader.skip(2); // skip record type indication.
+ MdnsServiceRecord record = new MdnsServiceRecord(name, reader);
+ MdnsResponse response = new MdnsResponse(0);
+ assertFalse(response.hasServiceRecord());
+ assertTrue(response.setServiceRecord(record));
+ assertEquals(response.getServiceRecord(), record);
+ }
+
+ @Test
+ public void getTextRecord_returnsAddedRecord() throws IOException {
+ DatagramPacket packet = new DatagramPacket(dataIn_text_1, dataIn_text_1.length);
+ MdnsPacketReader reader = new MdnsPacketReader(packet);
+ String[] name = reader.readLabels();
+ reader.skip(2); // skip record type indication.
+ MdnsTextRecord record = new MdnsTextRecord(name, reader);
+ MdnsResponse response = new MdnsResponse(0);
+ assertFalse(response.hasTextRecord());
+ assertTrue(response.setTextRecord(record));
+ assertEquals(response.getTextRecord(), record);
+ }
+
+ @Test
+ public void mergeRecordsFrom_indicates_change_on_ipv4_address() throws IOException {
+ MdnsResponse response = makeMdnsResponse(
+ 0,
+ Arrays.asList(
+ new PacketAndRecordClass(dataIn_ipv4_1, MdnsInet4AddressRecord.class)));
+ // Now create a new response that updates the address.
+ MdnsResponse response2 = makeMdnsResponse(
+ 100,
+ Arrays.asList(
+ new PacketAndRecordClass(dataIn_ipv4_2, MdnsInet4AddressRecord.class)));
+ assertTrue(response.mergeRecordsFrom(response2));
+ }
+
+ @Test
+ public void mergeRecordsFrom_indicates_change_on_ipv6_address() throws IOException {
+ MdnsResponse response = makeMdnsResponse(
+ 0,
+ Arrays.asList(
+ new PacketAndRecordClass(dataIn_ipv6_1, MdnsInet6AddressRecord.class)));
+ // Now create a new response that updates the address.
+ MdnsResponse response2 = makeMdnsResponse(
+ 100,
+ Arrays.asList(
+ new PacketAndRecordClass(dataIn_ipv6_2, MdnsInet6AddressRecord.class)));
+ assertTrue(response.mergeRecordsFrom(response2));
+ }
+
+ @Test
+ public void mergeRecordsFrom_indicates_change_on_text() throws IOException {
+ MdnsResponse response = makeMdnsResponse(
+ 0,
+ Arrays.asList(new PacketAndRecordClass(dataIn_text_1, MdnsTextRecord.class)));
+ // Now create a new response that updates the address.
+ MdnsResponse response2 = makeMdnsResponse(
+ 100,
+ Arrays.asList(new PacketAndRecordClass(dataIn_text_2, MdnsTextRecord.class)));
+ assertTrue(response.mergeRecordsFrom(response2));
+ }
+
+ @Test
+ public void mergeRecordsFrom_indicates_change_on_service() throws IOException {
+ MdnsResponse response = makeMdnsResponse(
+ 0,
+ Arrays.asList(new PacketAndRecordClass(dataIn_service_1, MdnsServiceRecord.class)));
+ // Now create a new response that updates the address.
+ MdnsResponse response2 = makeMdnsResponse(
+ 100,
+ Arrays.asList(new PacketAndRecordClass(dataIn_service_2, MdnsServiceRecord.class)));
+ assertTrue(response.mergeRecordsFrom(response2));
+ }
+
+ @Test
+ public void mergeRecordsFrom_indicates_change_on_pointer() throws IOException {
+ MdnsResponse response = makeMdnsResponse(
+ 0,
+ Arrays.asList(new PacketAndRecordClass(dataIn_ptr_1, MdnsPointerRecord.class)));
+ // Now create a new response that updates the address.
+ MdnsResponse response2 = makeMdnsResponse(
+ 100,
+ Arrays.asList(new PacketAndRecordClass(dataIn_ptr_2, MdnsPointerRecord.class)));
+ assertTrue(response.mergeRecordsFrom(response2));
+ }
+
+ @Test
+ @Ignore("MdnsConfigs is not configurable currently.")
+ public void mergeRecordsFrom_indicates_noChange() throws IOException {
+ //MdnsConfigsFlagsImpl.useReducedMergeRecordUpdateEvents.override(true);
+ List<PacketAndRecordClass> recordList =
+ Arrays.asList(
+ new PacketAndRecordClass(dataIn_ipv4_1, MdnsInet4AddressRecord.class),
+ new PacketAndRecordClass(dataIn_ipv6_1, MdnsInet6AddressRecord.class),
+ new PacketAndRecordClass(dataIn_ptr_1, MdnsPointerRecord.class),
+ new PacketAndRecordClass(dataIn_service_2, MdnsServiceRecord.class),
+ new PacketAndRecordClass(dataIn_text_1, MdnsTextRecord.class));
+ // Create a two identical responses.
+ MdnsResponse response = makeMdnsResponse(0, recordList);
+ MdnsResponse response2 = makeMdnsResponse(100, recordList);
+ // Merging should not indicate any change.
+ assertFalse(response.mergeRecordsFrom(response2));
+ }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
new file mode 100644
index 0000000..5843fd0
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -0,0 +1,770 @@
+/*
+ * Copyright (C) 2021 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 static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+
+import com.android.server.connectivity.mdns.MdnsServiceTypeClient.QueryTaskConfig;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/** Tests for {@link MdnsServiceTypeClient}. */
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsServiceTypeClientTests {
+
+ private static final String SERVICE_TYPE = "_googlecast._tcp.local";
+
+ @Mock
+ private MdnsServiceBrowserListener mockListenerOne;
+ @Mock
+ private MdnsServiceBrowserListener mockListenerTwo;
+ @Mock
+ private MdnsPacketWriter mockPacketWriter;
+ @Mock
+ private MdnsSocketClient mockSocketClient;
+ @Captor
+ private ArgumentCaptor<MdnsServiceInfo> serviceInfoCaptor;
+
+ private final byte[] buf = new byte[10];
+
+ private DatagramPacket[] expectedPackets;
+ private ScheduledFuture<?>[] expectedSendFutures;
+ private FakeExecutor currentThreadExecutor = new FakeExecutor();
+
+ private MdnsServiceTypeClient client;
+
+ @Before
+ @SuppressWarnings("DoNotMock")
+ public void setUp() throws IOException {
+ MockitoAnnotations.initMocks(this);
+
+ expectedPackets = new DatagramPacket[16];
+ expectedSendFutures = new ScheduledFuture<?>[16];
+
+ for (int i = 0; i < expectedSendFutures.length; ++i) {
+ expectedPackets[i] = new DatagramPacket(buf, 0, 5);
+ expectedSendFutures[i] = Mockito.mock(ScheduledFuture.class);
+ }
+ when(mockPacketWriter.getPacket(any(SocketAddress.class)))
+ .thenReturn(expectedPackets[0])
+ .thenReturn(expectedPackets[1])
+ .thenReturn(expectedPackets[2])
+ .thenReturn(expectedPackets[3])
+ .thenReturn(expectedPackets[4])
+ .thenReturn(expectedPackets[5])
+ .thenReturn(expectedPackets[6])
+ .thenReturn(expectedPackets[7])
+ .thenReturn(expectedPackets[8])
+ .thenReturn(expectedPackets[9])
+ .thenReturn(expectedPackets[10])
+ .thenReturn(expectedPackets[11])
+ .thenReturn(expectedPackets[12])
+ .thenReturn(expectedPackets[13])
+ .thenReturn(expectedPackets[14])
+ .thenReturn(expectedPackets[15]);
+
+ client =
+ new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor) {
+ @Override
+ MdnsPacketWriter createMdnsPacketWriter() {
+ return mockPacketWriter;
+ }
+ };
+ }
+
+ @Test
+ public void sendQueries_activeScanMode() {
+ MdnsSearchOptions searchOptions =
+ MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build();
+ client.startSendAndReceive(mockListenerOne, searchOptions);
+
+ // First burst, 3 queries.
+ verifyAndSendQuery(0, 0, /* expectsUnicastResponse= */ true);
+ verifyAndSendQuery(
+ 1, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+ verifyAndSendQuery(
+ 2, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+ // Second burst will be sent after initialTimeBetweenBurstsMs, 3 queries.
+ verifyAndSendQuery(
+ 3, MdnsConfigs.initialTimeBetweenBurstsMs(), /* expectsUnicastResponse= */ false);
+ verifyAndSendQuery(
+ 4, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+ verifyAndSendQuery(
+ 5, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+ // Third burst will be sent after initialTimeBetweenBurstsMs * 2, 3 queries.
+ verifyAndSendQuery(
+ 6, MdnsConfigs.initialTimeBetweenBurstsMs() * 2, /* expectsUnicastResponse= */
+ false);
+ verifyAndSendQuery(
+ 7, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+ verifyAndSendQuery(
+ 8, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+ // Forth burst will be sent after initialTimeBetweenBurstsMs * 4, 3 queries.
+ verifyAndSendQuery(
+ 9, MdnsConfigs.initialTimeBetweenBurstsMs() * 4, /* expectsUnicastResponse= */
+ false);
+ verifyAndSendQuery(
+ 10, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+ verifyAndSendQuery(
+ 11, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+ // Fifth burst will be sent after timeBetweenBurstsMs, 3 queries.
+ verifyAndSendQuery(12, MdnsConfigs.timeBetweenBurstsMs(), /* expectsUnicastResponse= */
+ false);
+ verifyAndSendQuery(
+ 13, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+ verifyAndSendQuery(
+ 14, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+
+ // Stop sending packets.
+ client.stopSendAndReceive(mockListenerOne);
+ verify(expectedSendFutures[15]).cancel(true);
+ }
+
+ @Test
+ public void sendQueries_reentry_activeScanMode() {
+ MdnsSearchOptions searchOptions =
+ MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build();
+ client.startSendAndReceive(mockListenerOne, searchOptions);
+
+ // First burst, first query is sent.
+ verifyAndSendQuery(0, 0, /* expectsUnicastResponse= */ true);
+
+ // After the first query is sent, change the subtypes, and restart.
+ searchOptions =
+ MdnsSearchOptions.newBuilder()
+ .addSubtype("12345")
+ .addSubtype("abcde")
+ .setIsPassiveMode(false)
+ .build();
+ client.startSendAndReceive(mockListenerOne, searchOptions);
+ // The previous scheduled task should be canceled.
+ verify(expectedSendFutures[1]).cancel(true);
+
+ // Queries should continue to be sent.
+ verifyAndSendQuery(1, 0, /* expectsUnicastResponse= */ true);
+ verifyAndSendQuery(
+ 2, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+ verifyAndSendQuery(
+ 3, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+
+ // Stop sending packets.
+ client.stopSendAndReceive(mockListenerOne);
+ verify(expectedSendFutures[5]).cancel(true);
+ }
+
+ @Test
+ public void sendQueries_passiveScanMode() {
+ MdnsSearchOptions searchOptions =
+ MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(true).build();
+ client.startSendAndReceive(mockListenerOne, searchOptions);
+
+ // First burst, 3 query.
+ verifyAndSendQuery(0, 0, /* expectsUnicastResponse= */ true);
+ verifyAndSendQuery(
+ 1, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+ verifyAndSendQuery(
+ 2, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+ // Second burst will be sent after timeBetweenBurstsMs, 1 query.
+ verifyAndSendQuery(3, MdnsConfigs.timeBetweenBurstsMs(), /* expectsUnicastResponse= */
+ false);
+ // Third burst will be sent after timeBetweenBurstsMs, 1 query.
+ verifyAndSendQuery(4, MdnsConfigs.timeBetweenBurstsMs(), /* expectsUnicastResponse= */
+ false);
+
+ // Stop sending packets.
+ client.stopSendAndReceive(mockListenerOne);
+ verify(expectedSendFutures[5]).cancel(true);
+ }
+
+ @Test
+ public void sendQueries_reentry_passiveScanMode() {
+ MdnsSearchOptions searchOptions =
+ MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(true).build();
+ client.startSendAndReceive(mockListenerOne, searchOptions);
+
+ // First burst, first query is sent.
+ verifyAndSendQuery(0, 0, /* expectsUnicastResponse= */ true);
+
+ // After the first query is sent, change the subtypes, and restart.
+ searchOptions =
+ MdnsSearchOptions.newBuilder()
+ .addSubtype("12345")
+ .addSubtype("abcde")
+ .setIsPassiveMode(true)
+ .build();
+ client.startSendAndReceive(mockListenerOne, searchOptions);
+ // The previous scheduled task should be canceled.
+ verify(expectedSendFutures[1]).cancel(true);
+
+ // Queries should continue to be sent.
+ verifyAndSendQuery(1, 0, /* expectsUnicastResponse= */ true);
+ verifyAndSendQuery(
+ 2, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+ verifyAndSendQuery(
+ 3, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+
+ // Stop sending packets.
+ client.stopSendAndReceive(mockListenerOne);
+ verify(expectedSendFutures[5]).cancel(true);
+ }
+
+ @Test
+ @Ignore("MdnsConfigs is not configurable currently.")
+ public void testQueryTaskConfig_alwaysAskForUnicastResponse() {
+ //MdnsConfigsFlagsImpl.alwaysAskForUnicastResponseInEachBurst.override(true);
+ MdnsSearchOptions searchOptions =
+ MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build();
+ QueryTaskConfig config =
+ new QueryTaskConfig(searchOptions.getSubtypes(), searchOptions.isPassiveMode(), 1);
+
+ // This is the first query. We will ask for unicast response.
+ assertTrue(config.expectUnicastResponse);
+ assertEquals(config.subtypes, searchOptions.getSubtypes());
+ assertEquals(config.transactionId, 1);
+
+ // For the rest of queries in this burst, we will NOT ask for unicast response.
+ for (int i = 1; i < MdnsConfigs.queriesPerBurst(); i++) {
+ int oldTransactionId = config.transactionId;
+ config = config.getConfigForNextRun();
+ assertFalse(config.expectUnicastResponse);
+ assertEquals(config.subtypes, searchOptions.getSubtypes());
+ assertEquals(config.transactionId, oldTransactionId + 1);
+ }
+
+ // This is the first query of a new burst. We will ask for unicast response.
+ int oldTransactionId = config.transactionId;
+ config = config.getConfigForNextRun();
+ assertTrue(config.expectUnicastResponse);
+ assertEquals(config.subtypes, searchOptions.getSubtypes());
+ assertEquals(config.transactionId, oldTransactionId + 1);
+ }
+
+ @Test
+ public void testQueryTaskConfig_askForUnicastInFirstQuery() {
+ MdnsSearchOptions searchOptions =
+ MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build();
+ QueryTaskConfig config =
+ new QueryTaskConfig(searchOptions.getSubtypes(), searchOptions.isPassiveMode(), 1);
+
+ // This is the first query. We will ask for unicast response.
+ assertTrue(config.expectUnicastResponse);
+ assertEquals(config.subtypes, searchOptions.getSubtypes());
+ assertEquals(config.transactionId, 1);
+
+ // For the rest of queries in this burst, we will NOT ask for unicast response.
+ for (int i = 1; i < MdnsConfigs.queriesPerBurst(); i++) {
+ int oldTransactionId = config.transactionId;
+ config = config.getConfigForNextRun();
+ assertFalse(config.expectUnicastResponse);
+ assertEquals(config.subtypes, searchOptions.getSubtypes());
+ assertEquals(config.transactionId, oldTransactionId + 1);
+ }
+
+ // This is the first query of a new burst. We will NOT ask for unicast response.
+ int oldTransactionId = config.transactionId;
+ config = config.getConfigForNextRun();
+ assertFalse(config.expectUnicastResponse);
+ assertEquals(config.subtypes, searchOptions.getSubtypes());
+ assertEquals(config.transactionId, oldTransactionId + 1);
+ }
+
+ @Test
+ @Ignore("MdnsConfigs is not configurable currently.")
+ public void testIfPreviousTaskIsCanceledWhenNewSessionStarts() {
+ //MdnsConfigsFlagsImpl.useSessionIdToScheduleMdnsTask.override(true);
+ MdnsSearchOptions searchOptions =
+ MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(true).build();
+ client.startSendAndReceive(mockListenerOne, searchOptions);
+ Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
+
+ // Change the sutypes and start a new session.
+ searchOptions =
+ MdnsSearchOptions.newBuilder()
+ .addSubtype("12345")
+ .addSubtype("abcde")
+ .setIsPassiveMode(true)
+ .build();
+ client.startSendAndReceive(mockListenerOne, searchOptions);
+
+ // Clear the scheduled runnable.
+ currentThreadExecutor.getAndClearLastScheduledRunnable();
+
+ // Simulate the case where the first mdns task is not successful canceled and it gets
+ // executed anyway.
+ firstMdnsTask.run();
+
+ // Although it gets executes, no more task gets scheduled.
+ assertNull(currentThreadExecutor.getAndClearLastScheduledRunnable());
+ }
+
+ @Test
+ @Ignore("MdnsConfigs is not configurable currently.")
+ public void testIfPreviousTaskIsCanceledWhenSessionStops() {
+ //MdnsConfigsFlagsImpl.shouldCancelScanTaskWhenFutureIsNull.override(true);
+ MdnsSearchOptions searchOptions =
+ MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(true).build();
+ client.startSendAndReceive(mockListenerOne, searchOptions);
+ // Change the sutypes and start a new session.
+ client.stopSendAndReceive(mockListenerOne);
+ // Clear the scheduled runnable.
+ currentThreadExecutor.getAndClearLastScheduledRunnable();
+
+ // Simulate the case where the first mdns task is not successful canceled and it gets
+ // executed anyway.
+ currentThreadExecutor.getAndClearSubmittedRunnable().run();
+
+ // Although it gets executes, no more task gets scheduled.
+ assertNull(currentThreadExecutor.getAndClearLastScheduledRunnable());
+ }
+
+ @Test
+ public void processResponse_incompleteResponse() {
+ client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+
+ MdnsResponse response = mock(MdnsResponse.class);
+ when(response.getServiceInstanceName()).thenReturn("service-instance-1");
+ when(response.isComplete()).thenReturn(false);
+
+ client.processResponse(response);
+
+ verify(mockListenerOne, never()).onServiceFound(any(MdnsServiceInfo.class));
+ verify(mockListenerOne, never()).onServiceUpdated(any(MdnsServiceInfo.class));
+ }
+
+ @Test
+ public void processIPv4Response_completeResponseForNewServiceInstance() throws Exception {
+ final String ipV4Address = "192.168.1.1";
+ client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+
+ // Process the initial response.
+ MdnsResponse initialResponse =
+ createResponse(
+ "service-instance-1",
+ ipV4Address,
+ 5353,
+ Collections.singletonList("ABCDE"),
+ Collections.emptyMap());
+ client.processResponse(initialResponse);
+
+ // Process a second response with a different port and updated text attributes.
+ MdnsResponse secondResponse =
+ createResponse(
+ "service-instance-1",
+ ipV4Address,
+ 5354,
+ Collections.singletonList("ABCDE"),
+ Collections.singletonMap("key", "value"));
+ client.processResponse(secondResponse);
+
+ // Verify onServiceFound was called once for the initial response.
+ verify(mockListenerOne).onServiceFound(serviceInfoCaptor.capture());
+ MdnsServiceInfo initialServiceInfo = serviceInfoCaptor.getAllValues().get(0);
+ assertEquals(initialServiceInfo.getServiceInstanceName(), "service-instance-1");
+ assertEquals(initialServiceInfo.getIpv4Address(), ipV4Address);
+ assertEquals(initialServiceInfo.getPort(), 5353);
+ assertEquals(initialServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
+ assertNull(initialServiceInfo.getAttributeByKey("key"));
+
+ // Verify onServiceUpdated was called once for the second response.
+ verify(mockListenerOne).onServiceUpdated(serviceInfoCaptor.capture());
+ MdnsServiceInfo updatedServiceInfo = serviceInfoCaptor.getAllValues().get(1);
+ assertEquals(updatedServiceInfo.getServiceInstanceName(), "service-instance-1");
+ assertEquals(updatedServiceInfo.getIpv4Address(), ipV4Address);
+ assertEquals(updatedServiceInfo.getPort(), 5354);
+ assertTrue(updatedServiceInfo.hasSubtypes());
+ assertEquals(updatedServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
+ assertEquals(updatedServiceInfo.getAttributeByKey("key"), "value");
+ }
+
+ @Test
+ public void processIPv6Response_getCorrectServiceInfo() throws Exception {
+ final String ipV6Address = "2000:3333::da6c:63ff:fe7c:7483";
+ client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+
+ // Process the initial response.
+ MdnsResponse initialResponse =
+ createResponse(
+ "service-instance-1",
+ ipV6Address,
+ 5353,
+ Collections.singletonList("ABCDE"),
+ Collections.emptyMap());
+ client.processResponse(initialResponse);
+
+ // Process a second response with a different port and updated text attributes.
+ MdnsResponse secondResponse =
+ createResponse(
+ "service-instance-1",
+ ipV6Address,
+ 5354,
+ Collections.singletonList("ABCDE"),
+ Collections.singletonMap("key", "value"));
+ client.processResponse(secondResponse);
+
+ System.out.println("secondResponses ip"
+ + secondResponse.getInet6AddressRecord().getInet6Address().getHostAddress());
+
+ // Verify onServiceFound was called once for the initial response.
+ verify(mockListenerOne).onServiceFound(serviceInfoCaptor.capture());
+ MdnsServiceInfo initialServiceInfo = serviceInfoCaptor.getAllValues().get(0);
+ assertEquals(initialServiceInfo.getServiceInstanceName(), "service-instance-1");
+ assertEquals(initialServiceInfo.getIpv6Address(), ipV6Address);
+ assertEquals(initialServiceInfo.getPort(), 5353);
+ assertEquals(initialServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
+ assertNull(initialServiceInfo.getAttributeByKey("key"));
+
+ // Verify onServiceUpdated was called once for the second response.
+ verify(mockListenerOne).onServiceUpdated(serviceInfoCaptor.capture());
+ MdnsServiceInfo updatedServiceInfo = serviceInfoCaptor.getAllValues().get(1);
+ assertEquals(updatedServiceInfo.getServiceInstanceName(), "service-instance-1");
+ assertEquals(updatedServiceInfo.getIpv6Address(), ipV6Address);
+ assertEquals(updatedServiceInfo.getPort(), 5354);
+ assertTrue(updatedServiceInfo.hasSubtypes());
+ assertEquals(updatedServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
+ assertEquals(updatedServiceInfo.getAttributeByKey("key"), "value");
+ }
+
+ @Test
+ public void processResponse_goodBye() {
+ client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+ client.startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+
+ MdnsResponse response = mock(MdnsResponse.class);
+ when(response.getServiceInstanceName()).thenReturn("goodbye-service-instance-name");
+ when(response.isGoodbye()).thenReturn(true);
+ client.processResponse(response);
+
+ verify(mockListenerOne).onServiceRemoved("goodbye-service-instance-name");
+ verify(mockListenerTwo).onServiceRemoved("goodbye-service-instance-name");
+ }
+
+ @Test
+ public void reportExistingServiceToNewlyRegisteredListeners() throws UnknownHostException {
+ // Process the initial response.
+ MdnsResponse initialResponse =
+ createResponse(
+ "service-instance-1",
+ "192.168.1.1",
+ 5353,
+ Collections.singletonList("ABCDE"),
+ Collections.emptyMap());
+ client.processResponse(initialResponse);
+
+ client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+
+ // Verify onServiceFound was called once for the existing response.
+ verify(mockListenerOne).onServiceFound(serviceInfoCaptor.capture());
+ MdnsServiceInfo existingServiceInfo = serviceInfoCaptor.getAllValues().get(0);
+ assertEquals(existingServiceInfo.getServiceInstanceName(), "service-instance-1");
+ assertEquals(existingServiceInfo.getIpv4Address(), "192.168.1.1");
+ assertEquals(existingServiceInfo.getPort(), 5353);
+ assertEquals(existingServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
+ assertNull(existingServiceInfo.getAttributeByKey("key"));
+
+ // Process a goodbye message for the existing response.
+ MdnsResponse goodByeResponse = mock(MdnsResponse.class);
+ when(goodByeResponse.getServiceInstanceName()).thenReturn("service-instance-1");
+ when(goodByeResponse.isGoodbye()).thenReturn(true);
+ client.processResponse(goodByeResponse);
+
+ client.startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+
+ // Verify onServiceFound was not called on the newly registered listener after the existing
+ // response is gone.
+ verify(mockListenerTwo, never()).onServiceFound(any(MdnsServiceInfo.class));
+ }
+
+ @Test
+ public void processResponse_notAllowRemoveSearch_shouldNotRemove() throws Exception {
+ final String serviceInstanceName = "service-instance-1";
+ client.startSendAndReceive(
+ mockListenerOne,
+ MdnsSearchOptions.newBuilder().build());
+ Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
+
+ // Process the initial response.
+ MdnsResponse initialResponse =
+ createResponse(
+ serviceInstanceName, "192.168.1.1", 5353, List.of("ABCDE"),
+ Map.of());
+ client.processResponse(initialResponse);
+
+ // Clear the scheduled runnable.
+ currentThreadExecutor.getAndClearLastScheduledRunnable();
+
+ // Simulate the case where the response is after TTL.
+ when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 0);
+ firstMdnsTask.run();
+
+ // Verify onServiceRemoved was not called.
+ verify(mockListenerOne, never()).onServiceRemoved(serviceInstanceName);
+ }
+
+ @Test
+ @Ignore("MdnsConfigs is not configurable currently.")
+ public void processResponse_allowSearchOptionsToRemoveExpiredService_shouldRemove()
+ throws Exception {
+ //MdnsConfigsFlagsImpl.allowSearchOptionsToRemoveExpiredService.override(true);
+ final String serviceInstanceName = "service-instance-1";
+ client =
+ new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor) {
+ @Override
+ MdnsPacketWriter createMdnsPacketWriter() {
+ return mockPacketWriter;
+ }
+ };
+ client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+ Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
+
+ // Process the initial response.
+ MdnsResponse initialResponse =
+ createResponse(
+ serviceInstanceName, "192.168.1.1", 5353, List.of("ABCDE"),
+ Map.of());
+ client.processResponse(initialResponse);
+
+ // Clear the scheduled runnable.
+ currentThreadExecutor.getAndClearLastScheduledRunnable();
+
+ // Simulate the case where the response is under TTL.
+ when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 1000);
+ firstMdnsTask.run();
+
+ // Verify onServiceRemoved was not called.
+ verify(mockListenerOne, never()).onServiceRemoved(serviceInstanceName);
+
+ // Simulate the case where the response is after TTL.
+ when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 0);
+ firstMdnsTask.run();
+
+ // Verify onServiceRemoved was called.
+ verify(mockListenerOne, times(1)).onServiceRemoved(serviceInstanceName);
+ }
+
+ @Test
+ public void processResponse_searchOptionsNotEnableServiceRemoval_shouldNotRemove()
+ throws Exception {
+ final String serviceInstanceName = "service-instance-1";
+ client =
+ new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor) {
+ @Override
+ MdnsPacketWriter createMdnsPacketWriter() {
+ return mockPacketWriter;
+ }
+ };
+ client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+ Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
+
+ // Process the initial response.
+ MdnsResponse initialResponse =
+ createResponse(
+ serviceInstanceName, "192.168.1.1", 5353, List.of("ABCDE"),
+ Map.of());
+ client.processResponse(initialResponse);
+
+ // Clear the scheduled runnable.
+ currentThreadExecutor.getAndClearLastScheduledRunnable();
+
+ // Simulate the case where the response is after TTL.
+ when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 0);
+ firstMdnsTask.run();
+
+ // Verify onServiceRemoved was not called.
+ verify(mockListenerOne, never()).onServiceRemoved(serviceInstanceName);
+ }
+
+ @Test
+ @Ignore("MdnsConfigs is not configurable currently.")
+ public void processResponse_removeServiceAfterTtlExpiresEnabled_shouldRemove()
+ throws Exception {
+ //MdnsConfigsFlagsImpl.removeServiceAfterTtlExpires.override(true);
+ final String serviceInstanceName = "service-instance-1";
+ client =
+ new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor) {
+ @Override
+ MdnsPacketWriter createMdnsPacketWriter() {
+ return mockPacketWriter;
+ }
+ };
+ client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+ Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
+
+ // Process the initial response.
+ MdnsResponse initialResponse =
+ createResponse(
+ serviceInstanceName, "192.168.1.1", 5353, List.of("ABCDE"),
+ Map.of());
+ client.processResponse(initialResponse);
+
+ // Clear the scheduled runnable.
+ currentThreadExecutor.getAndClearLastScheduledRunnable();
+
+ // Simulate the case where the response is after TTL.
+ when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 0);
+ firstMdnsTask.run();
+
+ // Verify onServiceRemoved was not called.
+ verify(mockListenerOne, times(1)).onServiceRemoved(serviceInstanceName);
+ }
+
+ // verifies that the right query was enqueued with the right delay, and send query by executing
+ // the runnable.
+ private void verifyAndSendQuery(int index, long timeInMs, boolean expectsUnicastResponse) {
+ assertEquals(currentThreadExecutor.getAndClearLastScheduledDelayInMs(), timeInMs);
+ currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+ if (expectsUnicastResponse) {
+ verify(mockSocketClient).sendUnicastPacket(expectedPackets[index]);
+ } else {
+ verify(mockSocketClient).sendMulticastPacket(expectedPackets[index]);
+ }
+ }
+
+ // A fake ScheduledExecutorService that keeps tracking the last scheduled Runnable and its delay
+ // time.
+ private class FakeExecutor extends ScheduledThreadPoolExecutor {
+ private long lastScheduledDelayInMs;
+ private Runnable lastScheduledRunnable;
+ private Runnable lastSubmittedRunnable;
+ private int futureIndex;
+
+ FakeExecutor() {
+ super(1);
+ lastScheduledDelayInMs = -1;
+ }
+
+ @Override
+ public Future<?> submit(Runnable command) {
+ Future<?> future = super.submit(command);
+ lastSubmittedRunnable = command;
+ return future;
+ }
+
+ // Don't call through the real implementation, just track the scheduled Runnable, and
+ // returns a ScheduledFuture.
+ @Override
+ public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
+ lastScheduledDelayInMs = delay;
+ lastScheduledRunnable = command;
+ return expectedSendFutures[futureIndex++];
+ }
+
+ // Returns the delay of the last scheduled task, and clear it.
+ long getAndClearLastScheduledDelayInMs() {
+ long val = lastScheduledDelayInMs;
+ lastScheduledDelayInMs = -1;
+ return val;
+ }
+
+ // Returns the last scheduled task, and clear it.
+ Runnable getAndClearLastScheduledRunnable() {
+ Runnable val = lastScheduledRunnable;
+ lastScheduledRunnable = null;
+ return val;
+ }
+
+ Runnable getAndClearSubmittedRunnable() {
+ Runnable val = lastSubmittedRunnable;
+ lastSubmittedRunnable = null;
+ return val;
+ }
+ }
+
+ // Creates a complete mDNS response.
+ private MdnsResponse createResponse(
+ @NonNull String serviceInstanceName,
+ @NonNull String host,
+ int port,
+ @NonNull List<String> subtypes,
+ @NonNull Map<String, String> textAttributes)
+ throws UnknownHostException {
+ String[] hostName = new String[]{"hostname"};
+ MdnsServiceRecord serviceRecord = mock(MdnsServiceRecord.class);
+ when(serviceRecord.getServiceHost()).thenReturn(hostName);
+ when(serviceRecord.getServicePort()).thenReturn(port);
+
+ MdnsResponse response = spy(new MdnsResponse(0));
+
+ MdnsInetAddressRecord inetAddressRecord = mock(MdnsInetAddressRecord.class);
+ if (host.contains(":")) {
+ when(inetAddressRecord.getInet6Address())
+ .thenReturn((Inet6Address) Inet6Address.getByName(host));
+ response.setInet6AddressRecord(inetAddressRecord);
+ } else {
+ when(inetAddressRecord.getInet4Address())
+ .thenReturn((Inet4Address) Inet4Address.getByName(host));
+ response.setInet4AddressRecord(inetAddressRecord);
+ }
+
+ MdnsTextRecord textRecord = mock(MdnsTextRecord.class);
+ List<String> textStrings = new ArrayList<>();
+ for (Map.Entry<String, String> kv : textAttributes.entrySet()) {
+ textStrings.add(kv.getKey() + "=" + kv.getValue());
+ }
+ when(textRecord.getStrings()).thenReturn(textStrings);
+
+ response.setServiceRecord(serviceRecord);
+ response.setTextRecord(textRecord);
+
+ doReturn(false).when(response).isGoodbye();
+ doReturn(true).when(response).isComplete();
+ doReturn(serviceInstanceName).when(response).getServiceInstanceName();
+ doReturn(new ArrayList<>(subtypes)).when(response).getSubtypes();
+ return response;
+ }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
new file mode 100644
index 0000000..21ed7eb
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
@@ -0,0 +1,493 @@
+/*
+ * Copyright (C) 2021 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 static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.Manifest.permission;
+import android.annotation.RequiresPermission;
+import android.content.Context;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.MulticastLock;
+import android.text.format.DateUtils;
+
+import com.android.net.module.util.HexDump;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/** Tests for {@link MdnsSocketClient} */
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsSocketClientTests {
+ private static final long TIMEOUT = 500;
+ private final byte[] buf = new byte[10];
+ final AtomicBoolean enableMulticastResponse = new AtomicBoolean(true);
+ final AtomicBoolean enableUnicastResponse = new AtomicBoolean(true);
+
+ @Mock private Context mContext;
+ @Mock private WifiManager mockWifiManager;
+ @Mock private MdnsSocket mockMulticastSocket;
+ @Mock private MdnsSocket mockUnicastSocket;
+ @Mock private MulticastLock mockMulticastLock;
+ @Mock private MdnsSocketClient.Callback mockCallback;
+
+ private MdnsSocketClient mdnsClient;
+
+ @Before
+ public void setup() throws RuntimeException, IOException {
+ MockitoAnnotations.initMocks(this);
+
+ when(mockWifiManager.createMulticastLock(ArgumentMatchers.anyString()))
+ .thenReturn(mockMulticastLock);
+
+ mdnsClient = new MdnsSocketClient(mContext, mockMulticastLock) {
+ @Override
+ MdnsSocket createMdnsSocket(int port) throws IOException {
+ if (port == MdnsConstants.MDNS_PORT) {
+ return mockMulticastSocket;
+ }
+ return mockUnicastSocket;
+ }
+ };
+ mdnsClient.setCallback(mockCallback);
+
+ doAnswer(
+ (InvocationOnMock invocationOnMock) -> {
+ final byte[] dataIn = HexDump.hexStringToByteArray(
+ "0000840000000004"
+ + "00000003134A6F68"
+ + "6E6E792773204368"
+ + "726F6D6563617374"
+ + "0B5F676F6F676C65"
+ + "63617374045F7463"
+ + "70056C6F63616C00"
+ + "0010800100001194"
+ + "006C2369643D3937"
+ + "3062663534376237"
+ + "3533666336336332"
+ + "6432613336626238"
+ + "3936616261380576"
+ + "653D30320D6D643D"
+ + "4368726F6D656361"
+ + "73741269633D2F73"
+ + "657475702F69636F"
+ + "6E2E706E6716666E"
+ + "3D4A6F686E6E7927"
+ + "73204368726F6D65"
+ + "636173740463613D"
+ + "350473743D30095F"
+ + "7365727669636573"
+ + "075F646E732D7364"
+ + "045F756470C03100"
+ + "0C00010000119400"
+ + "02C020C020000C00"
+ + "01000011940002C0"
+ + "0CC00C0021800100"
+ + "000078001C000000"
+ + "001F49134A6F686E"
+ + "6E79277320436872"
+ + "6F6D6563617374C0"
+ + "31C0F30001800100"
+ + "0000780004C0A864"
+ + "68C0F3002F800100"
+ + "0000780005C0F300"
+ + "0140C00C002F8001"
+ + "000011940009C00C"
+ + "00050000800040");
+ if (enableMulticastResponse.get()) {
+ DatagramPacket packet = invocationOnMock.getArgument(0);
+ packet.setData(dataIn);
+ }
+ return null;
+ })
+ .when(mockMulticastSocket)
+ .receive(any(DatagramPacket.class));
+ doAnswer(
+ (InvocationOnMock invocationOnMock) -> {
+ final byte[] dataIn = HexDump.hexStringToByteArray(
+ "0000840000000004"
+ + "00000003134A6F68"
+ + "6E6E792773204368"
+ + "726F6D6563617374"
+ + "0B5F676F6F676C65"
+ + "63617374045F7463"
+ + "70056C6F63616C00"
+ + "0010800100001194"
+ + "006C2369643D3937"
+ + "3062663534376237"
+ + "3533666336336332"
+ + "6432613336626238"
+ + "3936616261380576"
+ + "653D30320D6D643D"
+ + "4368726F6D656361"
+ + "73741269633D2F73"
+ + "657475702F69636F"
+ + "6E2E706E6716666E"
+ + "3D4A6F686E6E7927"
+ + "73204368726F6D65"
+ + "636173740463613D"
+ + "350473743D30095F"
+ + "7365727669636573"
+ + "075F646E732D7364"
+ + "045F756470C03100"
+ + "0C00010000119400"
+ + "02C020C020000C00"
+ + "01000011940002C0"
+ + "0CC00C0021800100"
+ + "000078001C000000"
+ + "001F49134A6F686E"
+ + "6E79277320436872"
+ + "6F6D6563617374C0"
+ + "31C0F30001800100"
+ + "0000780004C0A864"
+ + "68C0F3002F800100"
+ + "0000780005C0F300"
+ + "0140C00C002F8001"
+ + "000011940009C00C"
+ + "00050000800040");
+ if (enableUnicastResponse.get()) {
+ DatagramPacket packet = invocationOnMock.getArgument(0);
+ packet.setData(dataIn);
+ }
+ return null;
+ })
+ .when(mockUnicastSocket)
+ .receive(any(DatagramPacket.class));
+ }
+
+ @After
+ public void tearDown() {
+ mdnsClient.stopDiscovery();
+ }
+
+ @Test
+ @Ignore("MdnsConfigs is not configurable currently.")
+ public void testSendPackets_useSeparateSocketForUnicast()
+ throws InterruptedException, IOException {
+ //MdnsConfigsFlagsImpl.useSeparateSocketToSendUnicastQuery.override(true);
+ //MdnsConfigsFlagsImpl.checkMulticastResponse.override(true);
+ //MdnsConfigsFlagsImpl.checkMulticastResponseIntervalMs
+ // .override(DateUtils.SECOND_IN_MILLIS);
+ mdnsClient.startDiscovery();
+ Thread multicastReceiverThread = mdnsClient.multicastReceiveThread;
+ Thread unicastReceiverThread = mdnsClient.unicastReceiveThread;
+ Thread sendThread = mdnsClient.sendThread;
+
+ assertTrue(multicastReceiverThread.isAlive());
+ assertTrue(sendThread.isAlive());
+ assertTrue(unicastReceiverThread.isAlive());
+
+ // Sends a packet.
+ DatagramPacket packet = new DatagramPacket(buf, 0, 5);
+ mdnsClient.sendMulticastPacket(packet);
+ // mockMulticastSocket.send() will be called on another thread. If we verify it immediately,
+ // it may not be called yet. So timeout is added.
+ verify(mockMulticastSocket, timeout(TIMEOUT).times(1)).send(packet);
+ verify(mockUnicastSocket, timeout(TIMEOUT).times(0)).send(packet);
+
+ // Verify the packet is sent by the unicast socket.
+ mdnsClient.sendUnicastPacket(packet);
+ verify(mockMulticastSocket, timeout(TIMEOUT).times(1)).send(packet);
+ verify(mockUnicastSocket, timeout(TIMEOUT).times(1)).send(packet);
+
+ // Stop the MdnsClient, and ensure that it stops in a reasonable amount of time.
+ // Run part of the test logic in a background thread, in case stopDiscovery() blocks
+ // for a long time (the foreground thread can fail the test early).
+ final CountDownLatch stopDiscoveryLatch = new CountDownLatch(1);
+ Thread testThread =
+ new Thread(
+ new Runnable() {
+ @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
+ @Override
+ public void run() {
+ mdnsClient.stopDiscovery();
+ stopDiscoveryLatch.countDown();
+ }
+ });
+ testThread.start();
+ assertTrue(stopDiscoveryLatch.await(DateUtils.SECOND_IN_MILLIS, TimeUnit.MILLISECONDS));
+
+ // We should be able to join in a reasonable amount of time, to prove that the
+ // the MdnsClient exited without sending the large queue of packets.
+ testThread.join(DateUtils.SECOND_IN_MILLIS);
+
+ assertFalse(multicastReceiverThread.isAlive());
+ assertFalse(sendThread.isAlive());
+ assertFalse(unicastReceiverThread.isAlive());
+ }
+
+ @Test
+ public void testSendPackets_useSameSocketForMulticastAndUnicast()
+ throws InterruptedException, IOException {
+ mdnsClient.startDiscovery();
+ Thread multicastReceiverThread = mdnsClient.multicastReceiveThread;
+ Thread unicastReceiverThread = mdnsClient.unicastReceiveThread;
+ Thread sendThread = mdnsClient.sendThread;
+
+ assertTrue(multicastReceiverThread.isAlive());
+ assertTrue(sendThread.isAlive());
+ assertNull(unicastReceiverThread);
+
+ // Sends a packet.
+ DatagramPacket packet = new DatagramPacket(buf, 0, 5);
+ mdnsClient.sendMulticastPacket(packet);
+ // mockMulticastSocket.send() will be called on another thread. If we verify it immediately,
+ // it may not be called yet. So timeout is added.
+ verify(mockMulticastSocket, timeout(TIMEOUT).times(1)).send(packet);
+ verify(mockUnicastSocket, timeout(TIMEOUT).times(0)).send(packet);
+
+ // Verify the packet is sent by the multicast socket as well.
+ mdnsClient.sendUnicastPacket(packet);
+ verify(mockMulticastSocket, timeout(TIMEOUT).times(2)).send(packet);
+ verify(mockUnicastSocket, timeout(TIMEOUT).times(0)).send(packet);
+
+ // Stop the MdnsClient, and ensure that it stops in a reasonable amount of time.
+ // Run part of the test logic in a background thread, in case stopDiscovery() blocks
+ // for a long time (the foreground thread can fail the test early).
+ final CountDownLatch stopDiscoveryLatch = new CountDownLatch(1);
+ Thread testThread =
+ new Thread(
+ new Runnable() {
+ @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
+ @Override
+ public void run() {
+ mdnsClient.stopDiscovery();
+ stopDiscoveryLatch.countDown();
+ }
+ });
+ testThread.start();
+ assertTrue(stopDiscoveryLatch.await(DateUtils.SECOND_IN_MILLIS, TimeUnit.MILLISECONDS));
+
+ // We should be able to join in a reasonable amount of time, to prove that the
+ // the MdnsClient exited without sending the large queue of packets.
+ testThread.join(DateUtils.SECOND_IN_MILLIS);
+
+ assertFalse(multicastReceiverThread.isAlive());
+ assertFalse(sendThread.isAlive());
+ assertNull(unicastReceiverThread);
+ }
+
+ @Test
+ public void testStartStop() throws IOException {
+ for (int i = 0; i < 5; i++) {
+ mdnsClient.startDiscovery();
+
+ Thread multicastReceiverThread = mdnsClient.multicastReceiveThread;
+ Thread socketThread = mdnsClient.sendThread;
+
+ assertTrue(multicastReceiverThread.isAlive());
+ assertTrue(socketThread.isAlive());
+
+ mdnsClient.stopDiscovery();
+
+ assertFalse(multicastReceiverThread.isAlive());
+ assertFalse(socketThread.isAlive());
+ }
+ }
+
+ @Test
+ public void testStopDiscovery_queueIsCleared() throws IOException {
+ mdnsClient.startDiscovery();
+ mdnsClient.stopDiscovery();
+ mdnsClient.sendMulticastPacket(new DatagramPacket(buf, 0, 5));
+
+ synchronized (mdnsClient.multicastPacketQueue) {
+ assertTrue(mdnsClient.multicastPacketQueue.isEmpty());
+ }
+ }
+
+ @Test
+ public void testSendPacket_afterDiscoveryStops() throws IOException {
+ mdnsClient.startDiscovery();
+ mdnsClient.stopDiscovery();
+ mdnsClient.sendMulticastPacket(new DatagramPacket(buf, 0, 5));
+
+ synchronized (mdnsClient.multicastPacketQueue) {
+ assertTrue(mdnsClient.multicastPacketQueue.isEmpty());
+ }
+ }
+
+ @Test
+ @Ignore("MdnsConfigs is not configurable currently.")
+ public void testSendPacket_queueReachesSizeLimit() throws IOException {
+ //MdnsConfigsFlagsImpl.mdnsPacketQueueMaxSize.override(2L);
+ mdnsClient.startDiscovery();
+ for (int i = 0; i < 100; i++) {
+ mdnsClient.sendMulticastPacket(new DatagramPacket(buf, 0, 5));
+ }
+
+ synchronized (mdnsClient.multicastPacketQueue) {
+ assertTrue(mdnsClient.multicastPacketQueue.size() <= 2);
+ }
+ }
+
+ @Test
+ public void testMulticastResponseReceived_useSeparateSocketForUnicast() throws IOException {
+ mdnsClient.setCallback(mockCallback);
+
+ mdnsClient.startDiscovery();
+
+ verify(mockCallback, timeout(TIMEOUT).atLeast(1))
+ .onResponseReceived(any(MdnsResponse.class));
+ }
+
+ @Test
+ public void testMulticastResponseReceived_useSameSocketForMulticastAndUnicast()
+ throws Exception {
+ mdnsClient.startDiscovery();
+
+ verify(mockCallback, timeout(TIMEOUT).atLeastOnce())
+ .onResponseReceived(any(MdnsResponse.class));
+
+ mdnsClient.stopDiscovery();
+ }
+
+ @Test
+ public void testFailedToParseMdnsResponse_useSeparateSocketForUnicast() throws IOException {
+ mdnsClient.setCallback(mockCallback);
+
+ // Both multicast socket and unicast socket receive malformed responses.
+ byte[] dataIn = HexDump.hexStringToByteArray("0000840000000004");
+ doAnswer(
+ (InvocationOnMock invocationOnMock) -> {
+ // Malformed data.
+ DatagramPacket packet = invocationOnMock.getArgument(0);
+ packet.setData(dataIn);
+ return null;
+ })
+ .when(mockMulticastSocket)
+ .receive(any(DatagramPacket.class));
+ doAnswer(
+ (InvocationOnMock invocationOnMock) -> {
+ // Malformed data.
+ DatagramPacket packet = invocationOnMock.getArgument(0);
+ packet.setData(dataIn);
+ return null;
+ })
+ .when(mockUnicastSocket)
+ .receive(any(DatagramPacket.class));
+
+ mdnsClient.startDiscovery();
+
+ verify(mockCallback, timeout(TIMEOUT).atLeast(1))
+ .onFailedToParseMdnsResponse(anyInt(), eq(MdnsResponseErrorCode.ERROR_END_OF_FILE));
+
+ mdnsClient.stopDiscovery();
+ }
+
+ @Test
+ public void testFailedToParseMdnsResponse_useSameSocketForMulticastAndUnicast()
+ throws IOException {
+ doAnswer(
+ (InvocationOnMock invocationOnMock) -> {
+ final byte[] dataIn = HexDump.hexStringToByteArray("0000840000000004");
+ DatagramPacket packet = invocationOnMock.getArgument(0);
+ packet.setData(dataIn);
+ return null;
+ })
+ .when(mockMulticastSocket)
+ .receive(any(DatagramPacket.class));
+
+ mdnsClient.startDiscovery();
+
+ verify(mockCallback, timeout(TIMEOUT).atLeast(1))
+ .onFailedToParseMdnsResponse(1, MdnsResponseErrorCode.ERROR_END_OF_FILE);
+
+ mdnsClient.stopDiscovery();
+ }
+
+ @Test
+ @Ignore("MdnsConfigs is not configurable currently.")
+ public void testMulticastResponseIsNotReceived() throws IOException, InterruptedException {
+ //MdnsConfigsFlagsImpl.checkMulticastResponse.override(true);
+ //MdnsConfigsFlagsImpl.checkMulticastResponseIntervalMs
+ // .override(DateUtils.SECOND_IN_MILLIS);
+ //MdnsConfigsFlagsImpl.useSeparateSocketToSendUnicastQuery.override(true);
+ enableMulticastResponse.set(false);
+ enableUnicastResponse.set(true);
+
+ mdnsClient.startDiscovery();
+ DatagramPacket packet = new DatagramPacket(buf, 0, 5);
+ mdnsClient.sendUnicastPacket(packet);
+ mdnsClient.sendMulticastPacket(packet);
+
+ // Wait for the timer to be triggered.
+ Thread.sleep(MdnsConfigs.checkMulticastResponseIntervalMs() * 2);
+
+ assertFalse(mdnsClient.receivedMulticastResponse);
+ assertTrue(mdnsClient.receivedUnicastResponse);
+ assertTrue(mdnsClient.cannotReceiveMulticastResponse.get());
+
+ // Allow multicast response and verify the states again.
+ enableMulticastResponse.set(true);
+ Thread.sleep(DateUtils.SECOND_IN_MILLIS);
+
+ // Verify cannotReceiveMulticastResponse is reset to false.
+ assertTrue(mdnsClient.receivedMulticastResponse);
+ assertTrue(mdnsClient.receivedUnicastResponse);
+ assertFalse(mdnsClient.cannotReceiveMulticastResponse.get());
+
+ // Stop the discovery and start a new session. Don't respond the unicsat query either in
+ // this session.
+ enableMulticastResponse.set(false);
+ enableUnicastResponse.set(false);
+ mdnsClient.stopDiscovery();
+ mdnsClient.startDiscovery();
+
+ // Verify the states are reset.
+ assertFalse(mdnsClient.receivedMulticastResponse);
+ assertFalse(mdnsClient.receivedUnicastResponse);
+ assertFalse(mdnsClient.cannotReceiveMulticastResponse.get());
+
+ mdnsClient.sendUnicastPacket(packet);
+ mdnsClient.sendMulticastPacket(packet);
+ Thread.sleep(MdnsConfigs.checkMulticastResponseIntervalMs() * 2);
+
+ // Verify cannotReceiveMulticastResponse is not set the true because we didn't receive the
+ // unicast response either. This is expected for users who don't have any cast device.
+ assertFalse(mdnsClient.receivedMulticastResponse);
+ assertFalse(mdnsClient.receivedUnicastResponse);
+ assertFalse(mdnsClient.cannotReceiveMulticastResponse.get());
+ }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketTests.java
new file mode 100644
index 0000000..9f11a4b
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketTests.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2021 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 static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.MulticastSocket;
+import java.net.NetworkInterface;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.Collections;
+
+/** Tests for {@link MdnsSocket}. */
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsSocketTests {
+
+ @Mock private NetworkInterfaceWrapper mockNetworkInterfaceWrapper;
+ @Mock private MulticastSocket mockMulticastSocket;
+ @Mock private MulticastNetworkInterfaceProvider mockMulticastNetworkInterfaceProvider;
+ private SocketAddress socketIPv4Address;
+ private SocketAddress socketIPv6Address;
+
+ private byte[] data = new byte[25];
+ private final DatagramPacket datagramPacket = new DatagramPacket(data, data.length);
+ private NetworkInterface networkInterface;
+
+ private MdnsSocket mdnsSocket;
+
+ @Before
+ public void setUp() throws SocketException, UnknownHostException {
+ MockitoAnnotations.initMocks(this);
+
+ networkInterface = createEmptyNetworkInterface();
+ when(mockNetworkInterfaceWrapper.getNetworkInterface()).thenReturn(networkInterface);
+ when(mockMulticastNetworkInterfaceProvider.getMulticastNetworkInterfaces())
+ .thenReturn(Collections.singletonList(mockNetworkInterfaceWrapper));
+ socketIPv4Address = new InetSocketAddress(
+ InetAddress.getByName("224.0.0.251"), MdnsConstants.MDNS_PORT);
+ socketIPv6Address = new InetSocketAddress(
+ InetAddress.getByName("FF02::FB"), MdnsConstants.MDNS_PORT);
+ }
+
+ @Test
+ public void testMdnsSocket() throws IOException {
+ mdnsSocket =
+ new MdnsSocket(mockMulticastNetworkInterfaceProvider, MdnsConstants.MDNS_PORT) {
+ @Override
+ MulticastSocket createMulticastSocket(int port) throws IOException {
+ return mockMulticastSocket;
+ }
+ };
+ mdnsSocket.send(datagramPacket);
+ verify(mockMulticastSocket).setNetworkInterface(networkInterface);
+ verify(mockMulticastSocket).send(datagramPacket);
+
+ mdnsSocket.receive(datagramPacket);
+ verify(mockMulticastSocket).receive(datagramPacket);
+
+ mdnsSocket.joinGroup();
+ verify(mockMulticastSocket).joinGroup(socketIPv4Address, networkInterface);
+
+ mdnsSocket.leaveGroup();
+ verify(mockMulticastSocket).leaveGroup(socketIPv4Address, networkInterface);
+
+ mdnsSocket.close();
+ verify(mockMulticastSocket).close();
+ }
+
+ @Test
+ public void testIPv6OnlyNetwork_IPv6Enabled() throws IOException {
+ // Have mockMulticastNetworkInterfaceProvider send back an IPv6Only networkInterfaceWrapper
+ networkInterface = createEmptyNetworkInterface();
+ when(mockNetworkInterfaceWrapper.getNetworkInterface()).thenReturn(networkInterface);
+ when(mockMulticastNetworkInterfaceProvider.getMulticastNetworkInterfaces())
+ .thenReturn(Collections.singletonList(mockNetworkInterfaceWrapper));
+
+ mdnsSocket =
+ new MdnsSocket(mockMulticastNetworkInterfaceProvider, MdnsConstants.MDNS_PORT) {
+ @Override
+ MulticastSocket createMulticastSocket(int port) throws IOException {
+ return mockMulticastSocket;
+ }
+ };
+
+ when(mockMulticastNetworkInterfaceProvider.isOnIpV6OnlyNetwork(
+ Collections.singletonList(mockNetworkInterfaceWrapper)))
+ .thenReturn(true);
+
+ mdnsSocket.joinGroup();
+ verify(mockMulticastSocket).joinGroup(socketIPv6Address, networkInterface);
+
+ mdnsSocket.leaveGroup();
+ verify(mockMulticastSocket).leaveGroup(socketIPv6Address, networkInterface);
+
+ mdnsSocket.close();
+ verify(mockMulticastSocket).close();
+ }
+
+ @Test
+ public void testIPv6OnlyNetwork_IPv6Toggle() throws IOException {
+ // Have mockMulticastNetworkInterfaceProvider send back a networkInterfaceWrapper
+ networkInterface = createEmptyNetworkInterface();
+ when(mockNetworkInterfaceWrapper.getNetworkInterface()).thenReturn(networkInterface);
+ when(mockMulticastNetworkInterfaceProvider.getMulticastNetworkInterfaces())
+ .thenReturn(Collections.singletonList(mockNetworkInterfaceWrapper));
+
+ mdnsSocket =
+ new MdnsSocket(mockMulticastNetworkInterfaceProvider, MdnsConstants.MDNS_PORT) {
+ @Override
+ MulticastSocket createMulticastSocket(int port) throws IOException {
+ return mockMulticastSocket;
+ }
+ };
+
+ when(mockMulticastNetworkInterfaceProvider.isOnIpV6OnlyNetwork(
+ Collections.singletonList(mockNetworkInterfaceWrapper)))
+ .thenReturn(true);
+
+ mdnsSocket.joinGroup();
+ verify(mockMulticastSocket).joinGroup(socketIPv6Address, networkInterface);
+
+ mdnsSocket.leaveGroup();
+ verify(mockMulticastSocket).leaveGroup(socketIPv6Address, networkInterface);
+
+ mdnsSocket.close();
+ verify(mockMulticastSocket).close();
+ }
+
+ private NetworkInterface createEmptyNetworkInterface() {
+ try {
+ Constructor<NetworkInterface> constructor =
+ NetworkInterface.class.getDeclaredConstructor();
+ constructor.setAccessible(true);
+ return constructor.newInstance();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProviderTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProviderTests.java
new file mode 100644
index 0000000..2268dfe
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProviderTests.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2021 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 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.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.content.Context;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InterfaceAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Tests for {@link MulticastNetworkInterfaceProvider}. */
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MulticastNetworkInterfaceProviderTests {
+
+ @Mock private NetworkInterfaceWrapper loopbackInterface;
+ @Mock private NetworkInterfaceWrapper pointToPointInterface;
+ @Mock private NetworkInterfaceWrapper virtualInterface;
+ @Mock private NetworkInterfaceWrapper inactiveMulticastInterface;
+ @Mock private NetworkInterfaceWrapper activeIpv6MulticastInterface;
+ @Mock private NetworkInterfaceWrapper activeIpv6MulticastInterfaceTwo;
+ @Mock private NetworkInterfaceWrapper nonMulticastInterface;
+ @Mock private NetworkInterfaceWrapper multicastInterfaceOne;
+ @Mock private NetworkInterfaceWrapper multicastInterfaceTwo;
+
+ private final List<NetworkInterfaceWrapper> networkInterfaces = new ArrayList<>();
+ private MulticastNetworkInterfaceProvider provider;
+ private Context context;
+
+ @Before
+ public void setUp() throws SocketException, UnknownHostException {
+ MockitoAnnotations.initMocks(this);
+ context = InstrumentationRegistry.getContext();
+
+ setupNetworkInterface(
+ loopbackInterface,
+ true /* isUp */,
+ true /* isLoopBack */,
+ false /* isPointToPoint */,
+ false /* isVirtual */,
+ true /* supportsMulticast */,
+ false /* isIpv6 */);
+
+ setupNetworkInterface(
+ pointToPointInterface,
+ true /* isUp */,
+ false /* isLoopBack */,
+ true /* isPointToPoint */,
+ false /* isVirtual */,
+ true /* supportsMulticast */,
+ false /* isIpv6 */);
+
+ setupNetworkInterface(
+ virtualInterface,
+ true /* isUp */,
+ false /* isLoopBack */,
+ false /* isPointToPoint */,
+ true /* isVirtual */,
+ true /* supportsMulticast */,
+ false /* isIpv6 */);
+
+ setupNetworkInterface(
+ inactiveMulticastInterface,
+ false /* isUp */,
+ false /* isLoopBack */,
+ false /* isPointToPoint */,
+ false /* isVirtual */,
+ true /* supportsMulticast */,
+ false /* isIpv6 */);
+
+ setupNetworkInterface(
+ nonMulticastInterface,
+ true /* isUp */,
+ false /* isLoopBack */,
+ false /* isPointToPoint */,
+ false /* isVirtual */,
+ false /* supportsMulticast */,
+ false /* isIpv6 */);
+
+ setupNetworkInterface(
+ activeIpv6MulticastInterface,
+ true /* isUp */,
+ false /* isLoopBack */,
+ false /* isPointToPoint */,
+ false /* isVirtual */,
+ true /* supportsMulticast */,
+ true /* isIpv6 */);
+
+ setupNetworkInterface(
+ activeIpv6MulticastInterfaceTwo,
+ true /* isUp */,
+ false /* isLoopBack */,
+ false /* isPointToPoint */,
+ false /* isVirtual */,
+ true /* supportsMulticast */,
+ true /* isIpv6 */);
+
+ setupNetworkInterface(
+ multicastInterfaceOne,
+ true /* isUp */,
+ false /* isLoopBack */,
+ false /* isPointToPoint */,
+ false /* isVirtual */,
+ true /* supportsMulticast */,
+ false /* isIpv6 */);
+
+ setupNetworkInterface(
+ multicastInterfaceTwo,
+ true /* isUp */,
+ false /* isLoopBack */,
+ false /* isPointToPoint */,
+ false /* isVirtual */,
+ true /* supportsMulticast */,
+ false /* isIpv6 */);
+
+ provider =
+ new MulticastNetworkInterfaceProvider(context) {
+ @Override
+ List<NetworkInterfaceWrapper> getNetworkInterfaces() {
+ return networkInterfaces;
+ }
+ };
+ }
+
+ @Test
+ public void testGetMulticastNetworkInterfaces() {
+ // getNetworkInterfaces returns 1 multicast interface and 5 interfaces that can not be used
+ // to send and receive multicast packets.
+ networkInterfaces.add(loopbackInterface);
+ networkInterfaces.add(pointToPointInterface);
+ networkInterfaces.add(virtualInterface);
+ networkInterfaces.add(inactiveMulticastInterface);
+ networkInterfaces.add(nonMulticastInterface);
+ networkInterfaces.add(multicastInterfaceOne);
+
+ assertEquals(Collections.singletonList(multicastInterfaceOne),
+ provider.getMulticastNetworkInterfaces());
+
+ // getNetworkInterfaces returns 2 multicast interfaces after a connectivity change.
+ networkInterfaces.clear();
+ networkInterfaces.add(multicastInterfaceOne);
+ networkInterfaces.add(multicastInterfaceTwo);
+
+ provider.connectivityMonitor.notifyConnectivityChange();
+
+ assertEquals(networkInterfaces, provider.getMulticastNetworkInterfaces());
+ }
+
+ @Test
+ public void testStartWatchingConnectivityChanges() {
+ ConnectivityMonitor mockMonitor = mock(ConnectivityMonitor.class);
+ provider.connectivityMonitor = mockMonitor;
+
+ InOrder inOrder = inOrder(mockMonitor);
+
+ provider.startWatchingConnectivityChanges();
+ inOrder.verify(mockMonitor).startWatchingConnectivityChanges();
+
+ provider.stopWatchingConnectivityChanges();
+ inOrder.verify(mockMonitor).stopWatchingConnectivityChanges();
+ }
+
+ @Test
+ public void testIpV6OnlyNetwork_EmptyNetwork() {
+ // getNetworkInterfaces returns no network interfaces.
+ assertFalse(provider.isOnIpV6OnlyNetwork(networkInterfaces));
+ }
+
+ @Test
+ public void testIpV6OnlyNetwork_IPv4Only() {
+ // getNetworkInterfaces returns two IPv4 network interface.
+ networkInterfaces.add(multicastInterfaceOne);
+ networkInterfaces.add(multicastInterfaceTwo);
+ assertFalse(provider.isOnIpV6OnlyNetwork(networkInterfaces));
+ }
+
+ @Test
+ public void testIpV6OnlyNetwork_MixedNetwork() {
+ // getNetworkInterfaces returns one IPv6 network interface.
+ networkInterfaces.add(activeIpv6MulticastInterface);
+ networkInterfaces.add(multicastInterfaceOne);
+ networkInterfaces.add(activeIpv6MulticastInterfaceTwo);
+ networkInterfaces.add(multicastInterfaceTwo);
+ assertFalse(provider.isOnIpV6OnlyNetwork(networkInterfaces));
+ }
+
+ @Test
+ public void testIpV6OnlyNetwork_IPv6Only() {
+ // getNetworkInterfaces returns one IPv6 network interface.
+ networkInterfaces.add(activeIpv6MulticastInterface);
+ networkInterfaces.add(activeIpv6MulticastInterfaceTwo);
+ assertTrue(provider.isOnIpV6OnlyNetwork(networkInterfaces));
+ }
+
+ @Test
+ public void testIpV6OnlyNetwork_IPv6Enabled() {
+ // getNetworkInterfaces returns one IPv6 network interface.
+ networkInterfaces.add(activeIpv6MulticastInterface);
+ assertTrue(provider.isOnIpV6OnlyNetwork(networkInterfaces));
+
+ final List<NetworkInterfaceWrapper> interfaces = provider.getMulticastNetworkInterfaces();
+ assertEquals(Collections.singletonList(activeIpv6MulticastInterface), interfaces);
+ }
+
+ private void setupNetworkInterface(
+ @NonNull NetworkInterfaceWrapper networkInterfaceWrapper,
+ boolean isUp,
+ boolean isLoopback,
+ boolean isPointToPoint,
+ boolean isVirtual,
+ boolean supportsMulticast,
+ boolean isIpv6)
+ throws SocketException, UnknownHostException {
+ when(networkInterfaceWrapper.isUp()).thenReturn(isUp);
+ when(networkInterfaceWrapper.isLoopback()).thenReturn(isLoopback);
+ when(networkInterfaceWrapper.isPointToPoint()).thenReturn(isPointToPoint);
+ when(networkInterfaceWrapper.isVirtual()).thenReturn(isVirtual);
+ when(networkInterfaceWrapper.supportsMulticast()).thenReturn(supportsMulticast);
+ if (isIpv6) {
+ InterfaceAddress interfaceAddress = mock(InterfaceAddress.class);
+ InetAddress ip6Address = Inet6Address.getByName("2001:4860:0:1001::68");
+ when(interfaceAddress.getAddress()).thenReturn(ip6Address);
+ when(networkInterfaceWrapper.getInterfaceAddresses())
+ .thenReturn(Collections.singletonList(interfaceAddress));
+ } else {
+ Inet4Address ip = (Inet4Address) Inet4Address.getByName("192.168.0.1");
+ InterfaceAddress interfaceAddress = mock(InterfaceAddress.class);
+ when(interfaceAddress.getAddress()).thenReturn(ip);
+ when(networkInterfaceWrapper.getInterfaceAddresses())
+ .thenReturn(Collections.singletonList(interfaceAddress));
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java b/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java
index b2b9f2c..a1d93a0 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java
@@ -16,6 +16,7 @@
package com.android.server.ethernet;
+import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import static org.junit.Assert.assertThrows;
@@ -35,10 +36,12 @@
import android.annotation.NonNull;
import android.content.Context;
import android.content.pm.PackageManager;
+import android.net.EthernetNetworkSpecifier;
import android.net.EthernetNetworkUpdateRequest;
import android.net.INetworkInterfaceOutcomeReceiver;
import android.net.IpConfiguration;
import android.net.NetworkCapabilities;
+import android.net.StringNetworkSpecifier;
import android.os.Build;
import android.os.Handler;
@@ -56,10 +59,14 @@
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
public class EthernetServiceImplTest {
private static final String TEST_IFACE = "test123";
+ private static final NetworkCapabilities DEFAULT_CAPS = new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_ETHERNET)
+ .setNetworkSpecifier(new EthernetNetworkSpecifier(TEST_IFACE))
+ .build();
private static final EthernetNetworkUpdateRequest UPDATE_REQUEST =
new EthernetNetworkUpdateRequest.Builder()
.setIpConfiguration(new IpConfiguration())
- .setNetworkCapabilities(new NetworkCapabilities.Builder().build())
+ .setNetworkCapabilities(DEFAULT_CAPS)
.build();
private static final EthernetNetworkUpdateRequest UPDATE_REQUEST_WITHOUT_CAPABILITIES =
new EthernetNetworkUpdateRequest.Builder()
@@ -67,7 +74,7 @@
.build();
private static final EthernetNetworkUpdateRequest UPDATE_REQUEST_WITHOUT_IP_CONFIG =
new EthernetNetworkUpdateRequest.Builder()
- .setNetworkCapabilities(new NetworkCapabilities.Builder().build())
+ .setNetworkCapabilities(DEFAULT_CAPS)
.build();
private static final INetworkInterfaceOutcomeReceiver NULL_LISTENER = null;
private EthernetServiceImpl mEthernetServiceImpl;
@@ -161,6 +168,41 @@
}
@Test
+ public void testUpdateConfigurationRejectsWithInvalidSpecifierType() {
+ final StringNetworkSpecifier invalidSpecifierType = new StringNetworkSpecifier("123");
+ final EthernetNetworkUpdateRequest request =
+ new EthernetNetworkUpdateRequest.Builder()
+ .setNetworkCapabilities(
+ new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_ETHERNET)
+ .setNetworkSpecifier(invalidSpecifierType)
+ .build()
+ ).build();
+ assertThrows(IllegalArgumentException.class, () -> {
+ mEthernetServiceImpl.updateConfiguration(
+ "" /* iface */, request, null /* listener */);
+ });
+ }
+
+ @Test
+ public void testUpdateConfigurationRejectsWithInvalidSpecifierName() {
+ final String ifaceToUpdate = "eth0";
+ final String ifaceOnSpecifier = "wlan0";
+ EthernetNetworkUpdateRequest request =
+ new EthernetNetworkUpdateRequest.Builder()
+ .setNetworkCapabilities(
+ new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_ETHERNET)
+ .setNetworkSpecifier(
+ new EthernetNetworkSpecifier(ifaceOnSpecifier))
+ .build()
+ ).build();
+ assertThrows(IllegalArgumentException.class, () -> {
+ mEthernetServiceImpl.updateConfiguration(ifaceToUpdate, request, null /* listener */);
+ });
+ }
+
+ @Test
public void testUpdateConfigurationWithCapabilitiesWithAutomotiveFeature() {
toggleAutomotiveFeature(false);
mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST_WITHOUT_CAPABILITIES,
@@ -247,6 +289,24 @@
}
@Test
+ public void testUpdateConfigurationAddsSpecifierWhenNotSet() {
+ final NetworkCapabilities nc = new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_ETHERNET).build();
+ final EthernetNetworkUpdateRequest requestSansSpecifier =
+ new EthernetNetworkUpdateRequest.Builder()
+ .setNetworkCapabilities(nc)
+ .build();
+ final NetworkCapabilities ncWithSpecifier = new NetworkCapabilities(nc)
+ .setNetworkSpecifier(new EthernetNetworkSpecifier(TEST_IFACE));
+
+ mEthernetServiceImpl.updateConfiguration(TEST_IFACE, requestSansSpecifier, NULL_LISTENER);
+ verify(mEthernetTracker).updateConfiguration(
+ eq(TEST_IFACE),
+ isNull(),
+ eq(ncWithSpecifier), eq(NULL_LISTENER));
+ }
+
+ @Test
public void testEnableInterface() {
mEthernetServiceImpl.enableInterface(TEST_IFACE, NULL_LISTENER);
verify(mEthernetTracker).enableInterface(eq(TEST_IFACE), eq(NULL_LISTENER));
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 4fbbc75..f9cbb10 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -67,6 +67,9 @@
import static com.android.net.module.util.NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT;
import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_POLL;
+import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME;
+import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME;
+import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.junit.Assert.assertEquals;
@@ -92,13 +95,16 @@
import android.app.AlarmManager;
import android.content.Context;
import android.content.Intent;
+import android.content.res.Resources;
import android.database.ContentObserver;
+import android.net.ConnectivityResources;
import android.net.DataUsageRequest;
import android.net.INetd;
import android.net.INetworkStatsSession;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
+import android.net.NetworkIdentity;
import android.net.NetworkStateSnapshot;
import android.net.NetworkStats;
import android.net.NetworkStatsCollection;
@@ -125,6 +131,7 @@
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
+import com.android.connectivity.resources.R;
import com.android.internal.util.FileRotator;
import com.android.internal.util.test.BroadcastInterceptingContext;
import com.android.net.module.util.IBpfMap;
@@ -152,6 +159,7 @@
import org.mockito.MockitoAnnotations;
import java.io.File;
+import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Clock;
@@ -161,6 +169,7 @@
import java.time.temporal.ChronoUnit;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -242,6 +251,9 @@
private int mImportLegacyTargetAttempts = 0;
private @Mock PersistentInt mImportLegacyAttemptsCounter;
private @Mock PersistentInt mImportLegacySuccessesCounter;
+ private @Mock PersistentInt mImportLegacyFallbacksCounter;
+ private @Mock Resources mResources;
+ private Boolean mIsDebuggable;
private class MockContext extends BroadcastInterceptingContext {
private final Context mBaseContext;
@@ -302,6 +314,12 @@
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
+
+ // Setup mock resources.
+ final Context mockResContext = mock(Context.class);
+ doReturn(mResources).when(mockResContext).getResources();
+ ConnectivityResources.setResourcesContextForTest(mockResContext);
+
final Context context = InstrumentationRegistry.getContext();
mServiceContext = new MockContext(context);
when(mLocationPermissionChecker.checkCallersLocationPermission(
@@ -382,15 +400,18 @@
}
@Override
- public PersistentInt createImportLegacyAttemptsCounter(
- @androidx.annotation.NonNull Path path) {
- return mImportLegacyAttemptsCounter;
- }
-
- @Override
- public PersistentInt createImportLegacySuccessesCounter(
- @androidx.annotation.NonNull Path path) {
- return mImportLegacySuccessesCounter;
+ public PersistentInt createPersistentCounter(@androidx.annotation.NonNull Path dir,
+ @androidx.annotation.NonNull String name) throws IOException {
+ switch (name) {
+ case NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME:
+ return mImportLegacyAttemptsCounter;
+ case NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME:
+ return mImportLegacySuccessesCounter;
+ case NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME:
+ return mImportLegacyFallbacksCounter;
+ default:
+ throw new IllegalArgumentException("Unknown counter name: " + name);
+ }
}
@Override
@@ -454,6 +475,11 @@
public IBpfMap<UidStatsMapKey, StatsMapValue> getAppUidStatsMap() {
return mAppUidStatsMap;
}
+
+ @Override
+ public boolean isDebuggable() {
+ return mIsDebuggable == Boolean.TRUE;
+ }
};
}
@@ -1890,19 +1916,127 @@
// will decrease the retry counter by 1.
}
+ @Test
+ public void testDataMigration_differentFromFallback() throws Exception {
+ assertStatsFilesExist(false);
+ expectDefaultSettings();
+
+ NetworkStateSnapshot[] states = new NetworkStateSnapshot[]{buildWifiState()};
+
+ mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
+ new UnderlyingNetworkInfo[0]);
+
+ // modify some number on wifi, and trigger poll event
+ incrementCurrentTime(HOUR_IN_MILLIS);
+ expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+ .insertEntry(TEST_IFACE, 1024L, 8L, 2048L, 16L));
+ expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+ .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 128L, 1L, 128L, 1L, 0L));
+ forcePollAndWaitForIdle();
+ // Simulate shutdown to force persisting data
+ mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN));
+ assertStatsFilesExist(true);
+
+ // Move the files to the legacy directory to simulate an import from old data
+ for (File f : mStatsDir.listFiles()) {
+ Files.move(f.toPath(), mLegacyStatsDir.toPath().resolve(f.getName()));
+ }
+ assertStatsFilesExist(false);
+
+ // Prepare some unexpected data.
+ final NetworkIdentity testWifiIdent = new NetworkIdentity.Builder().setType(TYPE_WIFI)
+ .setWifiNetworkKey(TEST_WIFI_NETWORK_KEY).build();
+ final NetworkStatsCollection.Key unexpectedUidAllkey = new NetworkStatsCollection.Key(
+ Set.of(testWifiIdent), UID_ALL, SET_DEFAULT, 0);
+ final NetworkStatsCollection.Key unexpectedUidBluekey = new NetworkStatsCollection.Key(
+ Set.of(testWifiIdent), UID_BLUE, SET_DEFAULT, 0);
+ final NetworkStatsHistory unexpectedHistory = new NetworkStatsHistory
+ .Builder(965L /* bucketDuration */, 1)
+ .addEntry(new NetworkStatsHistory.Entry(TEST_START, 3L, 55L, 4L, 31L, 10L, 5L))
+ .build();
+
+ // Simulate the platform stats collection somehow is different from what is read from
+ // the fallback method. The service should read them as is. This usually happens when an
+ // OEM has changed the implementation of NetworkStatsDataMigrationUtils inside the platform.
+ final NetworkStatsCollection summaryCollection =
+ getLegacyCollection(PREFIX_XT, false /* includeTags */);
+ summaryCollection.recordHistory(unexpectedUidAllkey, unexpectedHistory);
+ final NetworkStatsCollection uidCollection =
+ getLegacyCollection(PREFIX_UID, false /* includeTags */);
+ uidCollection.recordHistory(unexpectedUidBluekey, unexpectedHistory);
+ mPlatformNetworkStatsCollection.put(PREFIX_DEV, summaryCollection);
+ mPlatformNetworkStatsCollection.put(PREFIX_XT, summaryCollection);
+ mPlatformNetworkStatsCollection.put(PREFIX_UID, uidCollection);
+ mPlatformNetworkStatsCollection.put(PREFIX_UID_TAG,
+ getLegacyCollection(PREFIX_UID_TAG, true /* includeTags */));
+
+ // Mock zero usage and boot through serviceReady(), verify there is no imported data.
+ expectDefaultSettings();
+ expectNetworkStatsUidDetail(buildEmptyStats());
+ expectSystemReady();
+ mService.systemReady();
+ assertStatsFilesExist(false);
+
+ // Set the flag and reboot, verify the imported data is not there until next boot.
+ mStoreFilesInApexData = true;
+ mImportLegacyTargetAttempts = 3;
+ mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN));
+ assertStatsFilesExist(false);
+
+ // Boot through systemReady() again.
+ expectDefaultSettings();
+ expectNetworkStatsUidDetail(buildEmptyStats());
+ expectSystemReady();
+ mService.systemReady();
+
+ // Verify the result read from public API matches the result returned from the importer.
+ assertNetworkTotal(sTemplateWifi, 1024L + 55L, 8L + 4L, 2048L + 31L, 16L + 10L, 0 + 5);
+ assertUidTotal(sTemplateWifi, UID_BLUE,
+ 128L + 55L, 1L + 4L, 128L + 31L, 1L + 10L, 0 + 5);
+ assertStatsFilesExist(true);
+ verify(mImportLegacyAttemptsCounter).set(3);
+ verify(mImportLegacySuccessesCounter).set(1);
+ }
+
+ @Test
+ public void testShouldRunComparison() {
+ for (Boolean isDebuggable : Set.of(Boolean.TRUE, Boolean.FALSE)) {
+ mIsDebuggable = isDebuggable;
+ // Verify return false regardless of the device is debuggable.
+ doReturn(0).when(mResources)
+ .getInteger(R.integer.config_netstats_validate_import);
+ assertShouldRunComparison(false, isDebuggable);
+ // Verify return true regardless of the device is debuggable.
+ doReturn(1).when(mResources)
+ .getInteger(R.integer.config_netstats_validate_import);
+ assertShouldRunComparison(true, isDebuggable);
+ // Verify return true iff the device is debuggable.
+ for (int testValue : Set.of(-1, 2)) {
+ doReturn(testValue).when(mResources)
+ .getInteger(R.integer.config_netstats_validate_import);
+ assertShouldRunComparison(isDebuggable, isDebuggable);
+ }
+ }
+ }
+
+ private void assertShouldRunComparison(boolean expected, boolean isDebuggable) {
+ assertEquals("shouldRunComparison (debuggable=" + isDebuggable + "): ",
+ expected, mService.shouldRunComparison());
+ }
+
private NetworkStatsRecorder makeTestRecorder(File directory, String prefix, Config config,
- boolean includeTags) {
+ boolean includeTags, boolean wipeOnError) {
final NetworkStats.NonMonotonicObserver observer =
mock(NetworkStats.NonMonotonicObserver.class);
final DropBoxManager dropBox = mock(DropBoxManager.class);
return new NetworkStatsRecorder(new FileRotator(
directory, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
- observer, dropBox, prefix, config.bucketDuration, includeTags);
+ observer, dropBox, prefix, config.bucketDuration, includeTags, wipeOnError);
}
private NetworkStatsCollection getLegacyCollection(String prefix, boolean includeTags) {
- final NetworkStatsRecorder recorder = makeTestRecorder(mLegacyStatsDir, PREFIX_DEV,
- mSettings.getDevConfig(), includeTags);
+ final NetworkStatsRecorder recorder = makeTestRecorder(mLegacyStatsDir, prefix,
+ mSettings.getDevConfig(), includeTags, false);
return recorder.getOrLoadCompleteLocked();
}