Merge tag 'android-14.0.0_r50' into leaf-3.2
Android 14.0.0 Release 50 (AP2A.240605.024)
* tag 'android-14.0.0_r50': (327 commits)
Import translations. DO NOT MERGE ANYWHERE
Add testAllowedUids for devices without FEATURE_TELEPHONY_SUBSCRIPTION
Fix flakes due to networks obtained via sync APIs
Import translations. DO NOT MERGE ANYWHERE
Revert "24Q2: use mainline netbpfload from apex"
24Q2: use mainline netbpfload from apex
Fix the ignore_on_* flags for egress tracing.
Remove unused library visibility
Connect to IBluetoothFinder and use it
Update the imports to androidx.test.filters.*
Move VpnManagerServiceTest and VpnTest to frameworks
Use a TAP test network for MeshCoP service test cases
Fix flaky multicast tests in Thread E2E test.
NetBpfLoader: create /sys/fs/bpf/loader dir
Revert "Use a TAP test network for MeshCoP service test cases"
Use a TAP test network for MeshCoP service test cases
[mdns] add service-side impl for NSD service TTL support
Adding additional transport satellite support at getSubIdForMobile()
netbpfload: fail if platform exec of apex fails
[Thread] make Thread state customisable via resource overlay
...
Change-Id: Ie343e88e5ec7307265c6166e8a41c7be455c08a7
diff --git a/.gitignore b/.gitignore
index c9b6393..b517674 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,6 @@
# VS Code project
**/.vscode
**/*.code-workspace
+
+# Vim temporary files
+**/*.swp
diff --git a/Cronet/tests/common/Android.bp b/Cronet/tests/common/Android.bp
index e17081a..edeb0b3 100644
--- a/Cronet/tests/common/Android.bp
+++ b/Cronet/tests/common/Android.bp
@@ -17,6 +17,7 @@
// They must be fast and stable, and exercise public or test APIs.
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -28,7 +29,10 @@
name: "NetHttpCoverageTests",
enforce_default_target_sdk_version: true,
min_sdk_version: "30",
- test_suites: ["general-tests", "mts-tethering"],
+ test_suites: [
+ "general-tests",
+ "mts-tethering",
+ ],
static_libs: [
"modules-utils-native-coverage-listener",
"CtsNetHttpTestsLib",
@@ -37,6 +41,8 @@
jarjar_rules: ":net-http-test-jarjar-rules",
compile_multilib: "both", // Include both the 32 and 64 bit versions
jni_libs: [
- "cronet_aml_components_cronet_android_cronet_tests__testing"
+ "cronet_aml_components_cronet_android_cronet_tests__testing",
+ "cronet_aml_third_party_netty_tcnative_netty_tcnative_so__testing",
],
+ data: [":cronet_javatests_resources"],
}
diff --git a/Cronet/tests/common/AndroidTest.xml b/Cronet/tests/common/AndroidTest.xml
index 2ac418f..bded8fb 100644
--- a/Cronet/tests/common/AndroidTest.xml
+++ b/Cronet/tests/common/AndroidTest.xml
@@ -19,6 +19,11 @@
<option name="install-arg" value="-t" />
</target_preparer>
<option name="test-tag" value="NetHttpCoverageTests" />
+
+ <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+ <option name="push-file" key="net" value="/storage/emulated/0/chromium_tests_root/net" />
+ <option name="push-file" key="test_server" value="/storage/emulated/0/chromium_tests_root/components/cronet/testing/test_server" />
+ </target_preparer>
<!-- Tethering/Connectivity is a SDK 30+ module -->
<!-- TODO Switch back to Sdk30 when b/270049141 is fixed -->
<object type="module_controller"
@@ -28,7 +33,28 @@
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="com.android.net.http.tests.coverage" />
<option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <!-- b/298380508 -->
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsIgnoredInNativeCronetEngineBuilderImpl" />
+ <!-- b/316571753 -->
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testBaseFeatureFlagsOverridesEnabled" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAppIdMatches" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAreLoaded" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAtMinVersion" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAboveMinVersion" />
+ <!-- b/316567693 -->
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestTest#testSSLCertificateError" />
+ <!-- b/316559294 -->
+ <option name="exclude-filter" value="org.chromium.net.NQETest#testQuicDisabled" />
+ <!-- b/316559294 -->
+ <option name="exclude-filter" value="org.chromium.net.NQETest#testPrefsWriteRead" />
+ <!-- b/316554711-->
+ <option name="exclude-filter" value="org.chromium.net.NetworkChangesTest" />
+ <!-- b/316550794 -->
+ <option name="exclude-filter" value="org.chromium.net.impl.CronetLoggerTest#testEngineCreation" />
<option name="hidden-api-checks" value="false"/>
+ <option name="isolated-storage" value="false"/>
<option
name="device-listeners"
value="com.android.modules.utils.testing.NativeCoverageHackInstrumentationListener" />
diff --git a/Cronet/tests/cts/Android.bp b/Cronet/tests/cts/Android.bp
index a0b2434..92b73d9 100644
--- a/Cronet/tests/cts/Android.bp
+++ b/Cronet/tests/cts/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -46,7 +47,9 @@
"framework-connectivity",
"org.apache.http.legacy",
],
- lint: { test: true }
+ lint: {
+ test: true,
+ },
}
android_test {
@@ -62,6 +65,7 @@
test_suites: [
"cts",
"general-tests",
- "mts-tethering"
+ "mts-tethering",
+ "mcts-tethering",
],
}
diff --git a/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java b/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java
index 9fc4389..f86ac29 100644
--- a/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java
+++ b/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java
@@ -247,10 +247,8 @@
@Test
public void testHttpEngine_requestUsesDefaultUserAgent() throws Exception {
mEngine = mEngineBuilder.build();
- HttpCtsTestServer server =
- new HttpCtsTestServer(ApplicationProvider.getApplicationContext());
- String url = server.getUserAgentUrl();
+ String url = mTestServer.getUserAgentUrl();
UrlRequest request =
mEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback).build();
request.start();
@@ -266,14 +264,12 @@
@Test
public void testHttpEngine_requestUsesCustomUserAgent() throws Exception {
String userAgent = "CtsTests User Agent";
- HttpCtsTestServer server =
- new HttpCtsTestServer(ApplicationProvider.getApplicationContext());
mEngine =
new HttpEngine.Builder(ApplicationProvider.getApplicationContext())
.setUserAgent(userAgent)
.build();
- String url = server.getUserAgentUrl();
+ String url = mTestServer.getUserAgentUrl();
UrlRequest request =
mEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback).build();
request.start();
diff --git a/Cronet/tests/mts/Android.bp b/Cronet/tests/mts/Android.bp
index 63905c8..9486e1f 100644
--- a/Cronet/tests/mts/Android.bp
+++ b/Cronet/tests/mts/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -48,19 +49,20 @@
}
android_test {
- name: "NetHttpTests",
- defaults: [
+ name: "NetHttpTests",
+ defaults: [
"mts-target-sdk-version-current",
- ],
- static_libs: ["NetHttpTestsLibPreJarJar"],
- jarjar_rules: ":net-http-test-jarjar-rules",
- jni_libs: [
+ ],
+ static_libs: ["NetHttpTestsLibPreJarJar"],
+ jarjar_rules: ":net-http-test-jarjar-rules",
+ jni_libs: [
"cronet_aml_components_cronet_android_cronet__testing",
"cronet_aml_components_cronet_android_cronet_tests__testing",
- ],
- test_suites: [
- "general-tests",
- "mts-tethering",
- ],
+ "cronet_aml_third_party_netty_tcnative_netty_tcnative_so__testing",
+ ],
+ test_suites: [
+ "general-tests",
+ "mts-tethering",
+ ],
+ data: [":cronet_javatests_resources"],
}
-
diff --git a/Cronet/tests/mts/AndroidManifest.xml b/Cronet/tests/mts/AndroidManifest.xml
index f597134..2c56e3a 100644
--- a/Cronet/tests/mts/AndroidManifest.xml
+++ b/Cronet/tests/mts/AndroidManifest.xml
@@ -19,6 +19,7 @@
package="android.net.http.mts">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET"/>
<application android:networkSecurityConfig="@xml/network_security_config">
diff --git a/Cronet/tests/mts/AndroidTest.xml b/Cronet/tests/mts/AndroidTest.xml
index 0d780a1..bccbe29 100644
--- a/Cronet/tests/mts/AndroidTest.xml
+++ b/Cronet/tests/mts/AndroidTest.xml
@@ -24,11 +24,37 @@
<option name="test-file-name" value="NetHttpTests.apk" />
</target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+ <option name="push-file" key="net" value="/storage/emulated/0/chromium_tests_root/net" />
+ <option name="push-file" key="test_server" value="/storage/emulated/0/chromium_tests_root/components/cronet/testing/test_server" />
+ </target_preparer>
+
<option name="test-tag" value="NetHttpTests" />
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="android.net.http.mts" />
<option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <!-- b/298380508 -->
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsIgnoredInNativeCronetEngineBuilderImpl" />
+ <!-- b/316571753 -->
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testBaseFeatureFlagsOverridesEnabled" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAppIdMatches" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAreLoaded" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAtMinVersion" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAboveMinVersion" />
+ <!-- b/316567693 -->
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestTest#testSSLCertificateError" />
+ <!-- b/316559294 -->
+ <option name="exclude-filter" value="org.chromium.net.NQETest#testQuicDisabled" />
+ <!-- b/316559294 -->
+ <option name="exclude-filter" value="org.chromium.net.NQETest#testPrefsWriteRead" />
+ <!-- b/316554711-->
+ <option name="exclude-filter" value="org.chromium.net.NetworkChangesTest" />
+ <!-- b/316550794 -->
+ <option name="exclude-filter" value="org.chromium.net.impl.CronetLoggerTest#testEngineCreation" />
<option name="hidden-api-checks" value="false"/>
+ <option name="isolated-storage" value="false"/>
</test>
<!-- Only run NetHttpTests in MTS if the Tethering Mainline module is installed. -->
diff --git a/Cronet/tests/mts/jarjar_excludes.txt b/Cronet/tests/mts/jarjar_excludes.txt
index a0ce5c2..b5cdf6e 100644
--- a/Cronet/tests/mts/jarjar_excludes.txt
+++ b/Cronet/tests/mts/jarjar_excludes.txt
@@ -2,6 +2,8 @@
com\.android\.testutils\..+
# jarjar-gen can't handle some kotlin object expression, exclude packages that include them
androidx\..+
+# don't jarjar netty as it does JNI
+io\.netty\..+
kotlin\.test\..+
kotlin\.reflect\..+
org\.mockito\..+
@@ -12,9 +14,16 @@
org\.chromium\.base\..+
J\.cronet_tests_N(\$.+)?
+# don't jarjar automatically generated FooJni files.
+org\.chromium\.net\..+Jni(\$.+)?
+
# Do not jarjar the tests and its utils as they also do JNI with cronet_tests.so
org\.chromium\.net\..*Test.*(\$.+)?
org\.chromium\.net\.NativeTestServer(\$.+)?
org\.chromium\.net\.MockUrlRequestJobFactory(\$.+)?
org\.chromium\.net\.QuicTestServer(\$.+)?
-org\.chromium\.net\.MockCertVerifier(\$.+)?
\ No newline at end of file
+org\.chromium\.net\.MockCertVerifier(\$.+)?
+org\.chromium\.net\.LogcatCapture(\$.+)?
+org\.chromium\.net\.ReportingCollector(\$.+)?
+org\.chromium\.net\.Http2TestServer(\$.+)?
+org\.chromium\.net\.Http2TestHandler(\$.+)?
\ No newline at end of file
diff --git a/Cronet/tests/mts/res/raw/quicroot.pem b/Cronet/tests/mts/res/raw/quicroot.pem
new file mode 100644
index 0000000..af21b3e
--- /dev/null
+++ b/Cronet/tests/mts/res/raw/quicroot.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIC/jCCAeagAwIBAgIUXOi6XoxnMUjJg4jeOwRhsdqEqEQwDQYJKoZIhvcNAQEL
+BQAwFzEVMBMGA1UEAwwMVGVzdCBSb290IENBMB4XDTIzMDYwMTExMjcwMFoXDTMz
+MDUyOTExMjcwMFowFzEVMBMGA1UEAwwMVGVzdCBSb290IENBMIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl9xCMPMIvfmJWz25AG/VtgWbqNs67HXQbXWf
+pDF2wjQpHVOYbfl7Zgly5O+5es1aUbJaGyZ9G6xuYSXKFnnYLoP7M86O05fQQBAj
+K+IE5nO6136ksCAfxCFTFfn4vhPvK8Vba5rqox4WeIXYKvHYSoiHz0ELrnFOHcyN
+Innyze7bLtkMCA1ShHpmvDCR+U3Uj6JwOfoirn29jjU/48/ORha7dcJYtYXk2eGo
+RJfrtIx20tXAaKaGnXOCGYbEVXTeQkQPqKFVzqP7+KYS/Y8eNFV35ugpLNES+44T
+bQ2QruTZdrNRjJkEoyiB/E53a0OUltB/R7Z0L0xstnKfsAf3OwIDAQABo0IwQDAP
+BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUVdXNh2lk
+51/6hMmz0Z+OpIe8+f0wDQYJKoZIhvcNAQELBQADggEBADNg7G8n6DUrQ5doXzm9
+kOp5siX6iPs0zFReXKhIT1Gef63l3tb7AdPedF03aj9XkUt0shhNOGG5SK2k5KBQ
+MJc9muYRCAyo2xMr3rFUQdI5B51SCy5HeAMralgTHXN0Hv+TH04YfRrACVmr+5ke
+pH3bF1gYaT+Zy5/pHJnV5lcwS6/H44g9XXWIopjWCwbfzKxIuWofqL4fiToPSIYu
+MCUI4bKZipcJT5O6rdz/S9lbgYVjOJ4HAoT2icNQqNMMfULKevmF8SdJzfNd35yn
+tAKTROhIE2aQRVCclrjo/T3eyjWGGoJlGmxKbeCf/rXzcn1BRtk/UzLnbUFFlg5l
+axw=
+-----END CERTIFICATE-----
\ No newline at end of file
diff --git a/Cronet/tests/mts/res/values/cronet-test-rule-configuration.xml b/Cronet/tests/mts/res/values/cronet-test-rule-configuration.xml
new file mode 100644
index 0000000..48ce420
--- /dev/null
+++ b/Cronet/tests/mts/res/values/cronet-test-rule-configuration.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources>
+ <bool name="is_running_in_aosp">true</bool>
+</resources>
\ No newline at end of file
diff --git a/Cronet/tests/mts/res/xml/network_security_config.xml b/Cronet/tests/mts/res/xml/network_security_config.xml
index d44c36f..32b7171 100644
--- a/Cronet/tests/mts/res/xml/network_security_config.xml
+++ b/Cronet/tests/mts/res/xml/network_security_config.xml
@@ -17,18 +17,31 @@
-->
<network-security-config>
- <domain-config cleartextTrafficPermitted="true">
- <!-- Used as the base URL by native test server (net::EmbeddedTestServer) -->
- <domain includeSubdomains="true">127.0.0.1</domain>
- <!-- Used by CronetHttpURLConnectionTest#testIOExceptionInterruptRethrown -->
- <domain includeSubdomains="true">localhost</domain>
- <!-- Used by CronetHttpURLConnectionTest#testBadIP -->
- <domain includeSubdomains="true">0.0.0.0</domain>
- <!-- Used by CronetHttpURLConnectionTest#testSetUseCachesFalse -->
- <domain includeSubdomains="true">host-cache-test-host</domain>
- <!-- Used by CronetHttpURLConnectionTest#testBadHostname -->
- <domain includeSubdomains="true">this-weird-host-name-does-not-exist</domain>
- <!-- Used by CronetUrlRequestContextTest#testHostResolverRules -->
- <domain includeSubdomains="true">some-weird-hostname</domain>
- </domain-config>
+ <base-config>
+ <trust-anchors>
+ <certificates src="@raw/quicroot"/>
+ <certificates src="system"/>
+ </trust-anchors>
+ </base-config>
+ <!-- Since Android 9 (API 28) cleartext support is disabled by default, this
+ causes some of our tests to fail (see crbug/1220357).
+ The following configs allow http requests for the domains used in these
+ tests.
+
+ TODO(stefanoduo): Figure out if we really need to use http for these tests
+ -->
+ <domain-config cleartextTrafficPermitted="true">
+ <!-- Used as the base URL by native test server (net::EmbeddedTestServer) -->
+ <domain includeSubdomains="true">127.0.0.1</domain>
+ <!-- Used by CronetHttpURLConnectionTest#testIOExceptionInterruptRethrown -->
+ <domain includeSubdomains="true">localhost</domain>
+ <!-- Used by CronetHttpURLConnectionTest#testBadIP -->
+ <domain includeSubdomains="true">0.0.0.0</domain>
+ <!-- Used by CronetHttpURLConnectionTest#testSetUseCachesFalse -->
+ <domain includeSubdomains="true">host-cache-test-host</domain>
+ <!-- Used by CronetHttpURLConnectionTest#testBadHostname -->
+ <domain includeSubdomains="true">this-weird-host-name-does-not-exist</domain>
+ <!-- Used by CronetUrlRequestContextTest#testHostResolverRules -->
+ <domain includeSubdomains="true">some-weird-hostname</domain>
+ </domain-config>
</network-security-config>
\ No newline at end of file
diff --git a/DnsResolver/Android.bp b/DnsResolver/Android.bp
index d133034..716eb10 100644
--- a/DnsResolver/Android.bp
+++ b/DnsResolver/Android.bp
@@ -14,6 +14,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -56,7 +57,10 @@
cc_test {
name: "dns_helper_unit_test",
defaults: ["netd_defaults"],
- test_suites: ["general-tests", "mts-tethering"],
+ test_suites: [
+ "general-tests",
+ "mts-tethering",
+ ],
test_config_template: ":net_native_test_config_template",
header_libs: [
"bpf_connectivity_headers",
@@ -68,8 +72,8 @@
"libcom.android.tethering.dns_helper",
],
shared_libs: [
- "libbase",
- "libcutils",
+ "libbase",
+ "libcutils",
],
compile_multilib: "both",
multilib: {
diff --git a/DnsResolver/include/DnsHelperPublic.h b/DnsResolver/include/DnsHelperPublic.h
index 7c9fc9e..44b0012 100644
--- a/DnsResolver/include/DnsHelperPublic.h
+++ b/DnsResolver/include/DnsHelperPublic.h
@@ -25,7 +25,8 @@
* Perform any required initialization - including opening any required BPF maps. This function
* needs to be called before using other functions of this library.
*
- * Returns 0 on success, a negative POSIX error code (see errno.h) on other failures.
+ * Returns 0 on success, -EOPNOTSUPP when the function is called on the Android version before
+ * T. Returns a negative POSIX error code (see errno.h) on other failures.
*/
int ADnsHelper_init();
@@ -36,7 +37,9 @@
* |uid| is a Linux/Android UID to be queried. It is a combination of UserID and AppID.
* |metered| indicates whether the uid is currently using a billing network.
*
- * Returns 0(false)/1(true) on success, a negative POSIX error code (see errno.h) on other failures.
+ * Returns 0(false)/1(true) on success, -EUNATCH when the ADnsHelper_init is not called before
+ * calling this function. Returns a negative POSIX error code (see errno.h) on other failures
+ * that return from bpf syscall.
*/
int ADnsHelper_isUidNetworkingBlocked(uid_t uid, bool metered);
diff --git a/OWNERS_core_networking b/OWNERS_core_networking
index 6d17476..83f798a 100644
--- a/OWNERS_core_networking
+++ b/OWNERS_core_networking
@@ -1,16 +1,13 @@
chiachangwang@google.com
cken@google.com
-huangaaron@google.com
jchalard@google.com
junyulai@google.com
lifr@google.com
lorenzo@google.com
-lucaslin@google.com
markchien@google.com
martinwu@google.com
maze@google.com
motomuman@google.com
-nuccachen@google.com
paulhu@google.com
prohr@google.com
reminv@google.com
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 414e50a..e4e6c70 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -82,7 +83,6 @@
],
manifest: "AndroidManifestBase.xml",
lint: {
- strict_updatability_linting: true,
error_checks: ["NewApi"],
},
}
@@ -94,14 +94,16 @@
"ConnectivityNextEnableDefaults",
"TetheringAndroidLibraryDefaults",
"TetheringApiLevel",
- "TetheringReleaseTargetSdk"
+ "TetheringReleaseTargetSdk",
],
static_libs: [
"NetworkStackApiCurrentShims",
"net-utils-device-common-struct",
],
apex_available: ["com.android.tethering"],
- lint: { strict_updatability_linting: true },
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
android_library {
@@ -109,14 +111,16 @@
defaults: [
"TetheringAndroidLibraryDefaults",
"TetheringApiLevel",
- "TetheringReleaseTargetSdk"
+ "TetheringReleaseTargetSdk",
],
static_libs: [
"NetworkStackApiStableShims",
"net-utils-device-common-struct",
],
apex_available: ["com.android.tethering"],
- lint: { strict_updatability_linting: true },
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
// Due to b/143733063, APK can't access a jni lib that is in APEX (but not in the APK).
@@ -189,20 +193,21 @@
optimize: {
proguard_flags_files: ["proguard.flags"],
},
- lint: { strict_updatability_linting: true },
}
// Updatable tethering packaged for finalized API
android_app {
name: "Tethering",
- defaults: ["TetheringAppDefaults", "TetheringApiLevel"],
+ defaults: [
+ "TetheringAppDefaults",
+ "TetheringApiLevel",
+ ],
static_libs: ["TetheringApiStableLib"],
certificate: "networkstack",
manifest: "AndroidManifest.xml",
use_embedded_native_libs: true,
privapp_allowlist: ":privapp_allowlist_com.android.tethering",
apex_available: ["com.android.tethering"],
- lint: { strict_updatability_linting: true },
}
android_app {
@@ -219,7 +224,6 @@
privapp_allowlist: ":privapp_allowlist_com.android.tethering",
apex_available: ["com.android.tethering"],
lint: {
- strict_updatability_linting: true,
error_checks: ["NewApi"],
},
}
@@ -239,7 +243,9 @@
java_library_static {
name: "tetheringstatsprotos",
- proto: {type: "lite"},
+ proto: {
+ type: "lite",
+ },
srcs: [
"src/com/android/networkstack/tethering/metrics/stats.proto",
],
@@ -252,6 +258,6 @@
name: "statslog-tethering-java-gen",
tools: ["stats-log-api-gen"],
cmd: "$(location stats-log-api-gen) --java $(out) --module network_tethering" +
- " --javaPackage com.android.networkstack.tethering.metrics --javaClass TetheringStatsLog",
+ " --javaPackage com.android.networkstack.tethering.metrics --javaClass TetheringStatsLog",
out: ["com/android/networkstack/tethering/metrics/TetheringStatsLog.java"],
}
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index de9017a..30bdf37 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -24,6 +25,7 @@
name: "ConnectivityNextEnableDefaults",
enabled: true,
}
+
java_defaults {
name: "NetworkStackApiShimSettingsForCurrentBranch",
// API shims to include in the networking modules built from the branch. Branches that disable
@@ -31,6 +33,7 @@
// (X_current API level).
static_libs: ["NetworkStackApiCurrentShims"],
}
+
apex_defaults {
name: "ConnectivityApexDefaults",
// Tethering app to include in the AOSP apex. Branches that disable the "next" targets may use
@@ -38,6 +41,7 @@
// package names and keys, so that apex will be unused anyway.
apps: ["TetheringNext"], // Replace to "Tethering" if ConnectivityNextEnableDefaults is false.
}
+
enable_tethering_next_apex = true
// This is a placeholder comment to avoid merge conflicts
// as the above target may have different "enabled" values
@@ -50,16 +54,6 @@
"//external/cronet/third_party/boringssl:libcrypto",
"//external/cronet/third_party/boringssl:libssl",
],
- arch: {
- riscv64: {
- // TODO: remove this when there is a riscv64 libcronet
- exclude_jni_libs: [
- "cronet_aml_components_cronet_android_cronet",
- "//external/cronet/third_party/boringssl:libcrypto",
- "//external/cronet/third_party/boringssl:libssl",
- ],
- },
- },
}
apex {
@@ -92,7 +86,7 @@
both: {
jni_libs: [
"libframework-connectivity-jni",
- "libframework-connectivity-tiramisu-jni"
+ "libframework-connectivity-tiramisu-jni",
],
},
},
@@ -117,8 +111,9 @@
"ServiceConnectivityResources",
],
prebuilts: [
- "ot-daemon.init.34rc",
"current_sdkinfo",
+ "netbpfload.mainline.rc",
+ "ot-daemon.init.34rc",
],
manifest: "manifest.json",
key: "com.android.tethering.key",
diff --git a/Tethering/apex/permissions/Android.bp b/Tethering/apex/permissions/Android.bp
index 69c1aa2..20772a8 100644
--- a/Tethering/apex/permissions/Android.bp
+++ b/Tethering/apex/permissions/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
default_visibility: ["//packages/modules/Connectivity/Tethering:__subpackages__"],
}
@@ -22,4 +23,4 @@
filegroup {
name: "privapp_allowlist_com.android.tethering",
srcs: ["permissions.xml"],
-}
\ No newline at end of file
+}
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index 6e8d0c9..9fa073b 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -14,6 +14,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -35,7 +36,7 @@
"//frameworks/base/core/tests/bandwidthtests",
"//frameworks/base/core/tests/benchmarks",
"//frameworks/base/core/tests/utillib",
- "//frameworks/base/packages/Connectivity/tests:__subpackages__",
+ "//frameworks/base/services/tests/VpnTests",
"//frameworks/base/tests/vcn",
"//frameworks/opt/telephony/tests/telephonytests",
"//packages/modules/CaptivePortalLogin/tests",
@@ -43,6 +44,7 @@
"//packages/modules/Connectivity/staticlibs/tests:__subpackages__",
"//packages/modules/Connectivity/Tethering/tests:__subpackages__",
"//packages/modules/Connectivity/tests:__subpackages__",
+ "//packages/modules/Connectivity/thread/tests:__subpackages__",
"//packages/modules/IPsec/tests/iketests",
"//packages/modules/NetworkStack/tests:__subpackages__",
"//packages/modules/Wifi/service/tests/wifitests",
@@ -54,14 +56,19 @@
hostdex: true, // for hiddenapi check
permitted_packages: ["android.net"],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ },
+ aconfig_declarations: [
+ "com.android.net.flags-aconfig",
+ ],
}
java_library {
- name: "framework-tethering-pre-jarjar",
- defaults: [
- "framework-tethering-defaults",
- ],
+ name: "framework-tethering-pre-jarjar",
+ defaults: [
+ "framework-tethering-defaults",
+ ],
}
java_genrule {
@@ -87,7 +94,7 @@
name: "framework-tethering-defaults",
defaults: ["framework-module-defaults"],
srcs: [
- ":framework-tethering-srcs"
+ ":framework-tethering-srcs",
],
libs: ["framework-connectivity.stubs.module_lib"],
aidl: {
@@ -106,5 +113,5 @@
"src/**/*.aidl",
"src/**/*.java",
],
- path: "src"
+ path: "src",
}
diff --git a/Tethering/common/TetheringLib/api/system-current.txt b/Tethering/common/TetheringLib/api/system-current.txt
index 844ff64..a287b42 100644
--- a/Tethering/common/TetheringLib/api/system-current.txt
+++ b/Tethering/common/TetheringLib/api/system-current.txt
@@ -95,13 +95,16 @@
method public default void onUpstreamChanged(@Nullable android.net.Network);
}
- public static class TetheringManager.TetheringRequest {
+ public static final class TetheringManager.TetheringRequest implements android.os.Parcelable {
+ method @FlaggedApi("com.android.net.flags.tethering_request_with_soft_ap_config") public int describeContents();
method @Nullable public android.net.LinkAddress getClientStaticIpv4Address();
method public int getConnectivityScope();
method @Nullable public android.net.LinkAddress getLocalIpv4Address();
method public boolean getShouldShowEntitlementUi();
method public int getTetheringType();
method public boolean isExemptFromEntitlementCheck();
+ method @FlaggedApi("com.android.net.flags.tethering_request_with_soft_ap_config") public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @FlaggedApi("com.android.net.flags.tethering_request_with_soft_ap_config") @NonNull public static final android.os.Parcelable.Creator<android.net.TetheringManager.TetheringRequest> CREATOR;
}
public static class TetheringManager.TetheringRequest.Builder {
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index cd914d3..7b769d4 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -18,6 +18,7 @@
import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
import android.Manifest;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -28,6 +29,8 @@
import android.os.Bundle;
import android.os.ConditionVariable;
import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.util.ArrayMap;
@@ -59,6 +62,14 @@
*/
@SystemApi
public class TetheringManager {
+ // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
+ // available here
+ /** @hide */
+ public static class Flags {
+ static final String TETHERING_REQUEST_WITH_SOFT_AP_CONFIG =
+ "com.android.net.flags.tethering_request_with_soft_ap_config";
+ }
+
private static final String TAG = TetheringManager.class.getSimpleName();
private static final int DEFAULT_TIMEOUT_MS = 60_000;
private static final long CONNECTOR_POLL_INTERVAL_MILLIS = 200L;
@@ -673,14 +684,44 @@
/**
* Use with {@link #startTethering} to specify additional parameters when starting tethering.
*/
- public static class TetheringRequest {
+ public static final class TetheringRequest implements Parcelable {
/** A configuration set for TetheringRequest. */
private final TetheringRequestParcel mRequestParcel;
- private TetheringRequest(final TetheringRequestParcel request) {
+ private TetheringRequest(@NonNull final TetheringRequestParcel request) {
mRequestParcel = request;
}
+ private TetheringRequest(@NonNull Parcel in) {
+ mRequestParcel = in.readParcelable(TetheringRequestParcel.class.getClassLoader());
+ }
+
+ @FlaggedApi(Flags.TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+ @NonNull
+ public static final Creator<TetheringRequest> CREATOR = new Creator<>() {
+ @Override
+ public TetheringRequest createFromParcel(@NonNull Parcel in) {
+ return new TetheringRequest(in);
+ }
+
+ @Override
+ public TetheringRequest[] newArray(int size) {
+ return new TetheringRequest[size];
+ }
+ };
+
+ @FlaggedApi(Flags.TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @FlaggedApi(Flags.TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeParcelable(mRequestParcel, flags);
+ }
+
/** Builder used to create TetheringRequest. */
public static class Builder {
private final TetheringRequestParcel mBuilderParcel;
diff --git a/Tethering/lint-baseline.xml b/Tethering/lint-baseline.xml
index 37511c6..4f92c9c 100644
--- a/Tethering/lint-baseline.xml
+++ b/Tethering/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
<issue
id="NewApi"
@@ -8,7 +8,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location
file="packages/modules/Connectivity/Tethering/src/com/android/networkstack/tethering/OffloadController.java"
- line="293"
+ line="283"
column="44"/>
</issue>
diff --git a/Tethering/res/values-mcc310-mnc004-eu/strings.xml b/Tethering/res/values-mcc310-mnc004-eu/strings.xml
index c970dd7..ff2a505 100644
--- a/Tethering/res/values-mcc310-mnc004-eu/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-eu/strings.xml
@@ -18,7 +18,7 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="no_upstream_notification_title" msgid="3584617491053416666">"Konexioa partekatzeko aukerak ez du Interneteko konexiorik"</string>
<string name="no_upstream_notification_message" msgid="5626323795587558017">"Ezin dira konektatu gailuak"</string>
- <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Desaktibatu konexioa partekatzeko aukera"</string>
+ <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Desaktibatu konexioa partekatzea"</string>
<string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Wifi-gunea edo konexioa partekatzeko aukera aktibatuta dago"</string>
<string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Baliteke tarifa gehigarriak ordaindu behar izatea ibiltaritza erabili bitartean"</string>
</resources>
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index a8c8408..544ba01 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -33,6 +33,7 @@
import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH;
+import static com.android.networkstack.tethering.TetheringConfiguration.USE_SYNC_SM;
import static com.android.networkstack.tethering.UpstreamNetworkState.isVcnInterface;
import static com.android.networkstack.tethering.util.PrefixUtils.asIpPrefix;
import static com.android.networkstack.tethering.util.TetheringMessageBase.BASE_IPSERVER;
@@ -59,6 +60,7 @@
import android.os.Message;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
+import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
@@ -315,7 +317,6 @@
private final TetheringMetrics mTetheringMetrics;
private final Handler mHandler;
- private final boolean mIsSyncSM;
// TODO: Add a dependency object to pass the data members or variables from the tethering
// object. It helps to reduce the arguments of the constructor.
@@ -325,7 +326,7 @@
@Nullable LateSdk<RoutingCoordinatorManager> routingCoordinator, Callback callback,
TetheringConfiguration config, PrivateAddressCoordinator addressCoordinator,
TetheringMetrics tetheringMetrics, Dependencies deps) {
- super(ifaceName, config.isSyncSM() ? null : handler.getLooper());
+ super(ifaceName, USE_SYNC_SM ? null : handler.getLooper());
mHandler = handler;
mLog = log.forSubComponent(ifaceName);
mNetd = netd;
@@ -338,7 +339,6 @@
mLinkProperties = new LinkProperties();
mUsingLegacyDhcp = config.useLegacyDhcpServer();
mP2pLeasesSubnetPrefixLength = config.getP2pLeasesSubnetPrefixLength();
- mIsSyncSM = config.isSyncSM();
mPrivateAddressCoordinator = addressCoordinator;
mDeps = deps;
mTetheringMetrics = tetheringMetrics;
@@ -516,7 +516,7 @@
private void handleError() {
mLastError = TETHER_ERROR_DHCPSERVER_ERROR;
- if (mIsSyncSM) {
+ if (USE_SYNC_SM) {
sendMessage(CMD_SERVICE_FAILED_TO_START, TETHER_ERROR_DHCPSERVER_ERROR);
} else {
sendMessageAtFrontOfQueueToAsyncSM(CMD_SERVICE_FAILED_TO_START,
@@ -900,7 +900,7 @@
}
private void configureLocalIPv6Routes(
- HashSet<IpPrefix> deprecatedPrefixes, HashSet<IpPrefix> newPrefixes) {
+ ArraySet<IpPrefix> deprecatedPrefixes, ArraySet<IpPrefix> newPrefixes) {
// [1] Remove the routes that are deprecated.
if (!deprecatedPrefixes.isEmpty()) {
removeRoutesFromLocalNetwork(getLocalRoutesFor(mIfaceName, deprecatedPrefixes));
@@ -908,7 +908,7 @@
// [2] Add only the routes that have not previously been added.
if (newPrefixes != null && !newPrefixes.isEmpty()) {
- HashSet<IpPrefix> addedPrefixes = (HashSet) newPrefixes.clone();
+ ArraySet<IpPrefix> addedPrefixes = new ArraySet<IpPrefix>(newPrefixes);
if (mLastRaParams != null) {
addedPrefixes.removeAll(mLastRaParams.prefixes);
}
@@ -920,7 +920,7 @@
}
private void configureLocalIPv6Dns(
- HashSet<Inet6Address> deprecatedDnses, HashSet<Inet6Address> newDnses) {
+ ArraySet<Inet6Address> deprecatedDnses, ArraySet<Inet6Address> newDnses) {
// TODO: Is this really necessary? Can we not fail earlier if INetd cannot be located?
if (mNetd == null) {
if (newDnses != null) newDnses.clear();
@@ -941,7 +941,7 @@
// [2] Add only the local DNS IP addresses that have not previously been added.
if (newDnses != null && !newDnses.isEmpty()) {
- final HashSet<Inet6Address> addedDnses = (HashSet) newDnses.clone();
+ final ArraySet<Inet6Address> addedDnses = new ArraySet<Inet6Address>(newDnses);
if (mLastRaParams != null) {
addedDnses.removeAll(mLastRaParams.dnses);
}
@@ -1171,7 +1171,7 @@
// in previous versions of the mainline module.
// TODO : remove sendMessageAtFrontOfQueueToAsyncSM after migrating to the Sync
// StateMachine.
- if (mIsSyncSM) {
+ if (USE_SYNC_SM) {
sendSelfMessageToSyncSM(CMD_SERVICE_FAILED_TO_START, mLastError);
} else {
sendMessageAtFrontOfQueueToAsyncSM(CMD_SERVICE_FAILED_TO_START, mLastError);
@@ -1548,7 +1548,7 @@
// Accumulate routes representing "prefixes to be assigned to the local
// interface", for subsequent modification of local_network routing.
private static ArrayList<RouteInfo> getLocalRoutesFor(
- String ifname, HashSet<IpPrefix> prefixes) {
+ String ifname, ArraySet<IpPrefix> prefixes) {
final ArrayList<RouteInfo> localRoutes = new ArrayList<RouteInfo>();
for (IpPrefix ipp : prefixes) {
localRoutes.add(new RouteInfo(ipp, null, ifname, RTN_UNICAST));
@@ -1579,8 +1579,8 @@
/** Get IPv6 prefixes from LinkProperties */
@NonNull
@VisibleForTesting
- static HashSet<IpPrefix> getTetherableIpv6Prefixes(@NonNull Collection<LinkAddress> addrs) {
- final HashSet<IpPrefix> prefixes = new HashSet<>();
+ static ArraySet<IpPrefix> getTetherableIpv6Prefixes(@NonNull Collection<LinkAddress> addrs) {
+ final ArraySet<IpPrefix> prefixes = new ArraySet<>();
for (LinkAddress linkAddr : addrs) {
if (linkAddr.getPrefixLength() != RFC7421_PREFIX_LENGTH) continue;
prefixes.add(new IpPrefix(linkAddr.getAddress(), RFC7421_PREFIX_LENGTH));
@@ -1589,7 +1589,7 @@
}
@NonNull
- private HashSet<IpPrefix> getTetherableIpv6Prefixes(@NonNull LinkProperties lp) {
+ private ArraySet<IpPrefix> getTetherableIpv6Prefixes(@NonNull LinkProperties lp) {
return getTetherableIpv6Prefixes(lp.getLinkAddresses());
}
}
diff --git a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
index 5e9bbcb..d848ea8 100644
--- a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
+++ b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
@@ -18,7 +18,6 @@
import static android.system.OsConstants.AF_INET6;
import static android.system.OsConstants.IPPROTO_ICMPV6;
-import static android.system.OsConstants.SOCK_NONBLOCK;
import static android.system.OsConstants.SOCK_RAW;
import static android.system.OsConstants.SOL_SOCKET;
import static android.system.OsConstants.SO_SNDTIMEO;
@@ -39,21 +38,13 @@
import android.net.MacAddress;
import android.net.TrafficStats;
import android.net.util.SocketUtils;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.Message;
import android.system.ErrnoException;
import android.system.Os;
import android.system.StructTimeval;
+import android.util.ArraySet;
import android.util.Log;
-import android.util.Pair;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import com.android.internal.annotations.GuardedBy;
-import com.android.net.module.util.FdEventsReader;
import com.android.net.module.util.InterfaceParams;
import com.android.net.module.util.structs.Icmpv6Header;
import com.android.net.module.util.structs.LlaOption;
@@ -113,11 +104,6 @@
private static final int DAY_IN_SECONDS = 86_400;
- // Commands for IpServer to control RouterAdvertisementDaemon
- private static final int CMD_START = 1;
- private static final int CMD_STOP = 2;
- private static final int CMD_BUILD_NEW_RA = 3;
-
private final InterfaceParams mInterface;
private final InetSocketAddress mAllNodes;
@@ -135,13 +121,9 @@
@GuardedBy("mLock")
private RaParams mRaParams;
- // To be accessed only from RaMessageHandler
- private RsPacketListener mRsPacketListener;
-
private volatile FileDescriptor mSocket;
private volatile MulticastTransmitter mMulticastTransmitter;
- private volatile RaMessageHandler mRaMessageHandler;
- private volatile HandlerThread mRaHandlerThread;
+ private volatile UnicastResponder mUnicastResponder;
/** Encapsulate the RA parameters for RouterAdvertisementDaemon.*/
public static class RaParams {
@@ -153,23 +135,23 @@
public boolean hasDefaultRoute;
public byte hopLimit;
public int mtu;
- public HashSet<IpPrefix> prefixes;
- public HashSet<Inet6Address> dnses;
+ public ArraySet<IpPrefix> prefixes;
+ public ArraySet<Inet6Address> dnses;
public RaParams() {
hasDefaultRoute = false;
hopLimit = DEFAULT_HOPLIMIT;
mtu = IPV6_MIN_MTU;
- prefixes = new HashSet<IpPrefix>();
- dnses = new HashSet<Inet6Address>();
+ prefixes = new ArraySet<IpPrefix>();
+ dnses = new ArraySet<Inet6Address>();
}
public RaParams(RaParams other) {
hasDefaultRoute = other.hasDefaultRoute;
hopLimit = other.hopLimit;
mtu = other.mtu;
- prefixes = (HashSet) other.prefixes.clone();
- dnses = (HashSet) other.dnses.clone();
+ prefixes = new ArraySet<IpPrefix>(other.prefixes);
+ dnses = new ArraySet<Inet6Address>(other.dnses);
}
/**
@@ -263,94 +245,6 @@
}
}
- private class RaMessageHandler extends Handler {
- RaMessageHandler(Looper looper) {
- super(looper);
- }
-
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case CMD_START:
- mRsPacketListener = new RsPacketListener(this);
- mRsPacketListener.start();
- break;
- case CMD_STOP:
- if (mRsPacketListener != null) {
- mRsPacketListener.stop();
- mRsPacketListener = null;
- }
- break;
- case CMD_BUILD_NEW_RA:
- synchronized (mLock) {
- // raInfo.first is deprecatedParams and raInfo.second is newParams.
- final Pair<RaParams, RaParams> raInfo = (Pair<RaParams, RaParams>) msg.obj;
- if (raInfo.first != null) {
- mDeprecatedInfoTracker.putPrefixes(raInfo.first.prefixes);
- mDeprecatedInfoTracker.putDnses(raInfo.first.dnses);
- }
-
- if (raInfo.second != null) {
- // Process information that is no longer deprecated.
- mDeprecatedInfoTracker.removePrefixes(raInfo.second.prefixes);
- mDeprecatedInfoTracker.removeDnses(raInfo.second.dnses);
- }
- mRaParams = raInfo.second;
- assembleRaLocked();
- }
-
- maybeNotifyMulticastTransmitter();
- break;
- default:
- Log.e(TAG, "Unknown message, cmd = " + String.valueOf(msg.what));
- break;
- }
- }
- }
-
- private class RsPacketListener extends FdEventsReader<RsPacketListener.RecvBuffer> {
- private static final class RecvBuffer {
- // The recycled buffer for receiving Router Solicitations from clients.
- // If the RS is larger than IPV6_MIN_MTU the packets are truncated.
- // This is fine since currently only byte 0 is examined anyway.
- final byte[] mBytes = new byte[IPV6_MIN_MTU];
- final InetSocketAddress mSrcAddr = new InetSocketAddress(0);
- }
-
- RsPacketListener(@NonNull Handler handler) {
- super(handler, new RecvBuffer());
- }
-
- @Override
- protected int recvBufSize(@NonNull RecvBuffer buffer) {
- return buffer.mBytes.length;
- }
-
- @Override
- protected FileDescriptor createFd() {
- return mSocket;
- }
-
- @Override
- protected int readPacket(@NonNull FileDescriptor fd, @NonNull RecvBuffer buffer)
- throws Exception {
- return Os.recvfrom(
- fd, buffer.mBytes, 0, buffer.mBytes.length, 0 /* flags */, buffer.mSrcAddr);
- }
-
- @Override
- protected final void handlePacket(@NonNull RecvBuffer buffer, int length) {
- // Do the least possible amount of validations.
- if (buffer.mSrcAddr == null
- || length <= 0
- || buffer.mBytes[0] != asByte(ICMPV6_ROUTER_SOLICITATION)) {
- return;
- }
-
- maybeSendRA(buffer.mSrcAddr);
- }
- }
-
public RouterAdvertisementDaemon(InterfaceParams ifParams) {
mInterface = ifParams;
mAllNodes = new InetSocketAddress(getAllNodesForScopeId(mInterface.index), 0);
@@ -359,43 +253,48 @@
/** Build new RA.*/
public void buildNewRa(RaParams deprecatedParams, RaParams newParams) {
- final Pair<RaParams, RaParams> raInfo = new Pair<>(deprecatedParams, newParams);
- sendMessage(CMD_BUILD_NEW_RA, raInfo);
+ synchronized (mLock) {
+ if (deprecatedParams != null) {
+ mDeprecatedInfoTracker.putPrefixes(deprecatedParams.prefixes);
+ mDeprecatedInfoTracker.putDnses(deprecatedParams.dnses);
+ }
+
+ if (newParams != null) {
+ // Process information that is no longer deprecated.
+ mDeprecatedInfoTracker.removePrefixes(newParams.prefixes);
+ mDeprecatedInfoTracker.removeDnses(newParams.dnses);
+ }
+
+ mRaParams = newParams;
+ assembleRaLocked();
+ }
+
+ maybeNotifyMulticastTransmitter();
}
/** Start router advertisement daemon. */
public boolean start() {
if (!createSocket()) {
- Log.e(TAG, "Failed to start RouterAdvertisementDaemon.");
return false;
}
mMulticastTransmitter = new MulticastTransmitter();
mMulticastTransmitter.start();
- mRaHandlerThread = new HandlerThread(TAG);
- mRaHandlerThread.start();
- mRaMessageHandler = new RaMessageHandler(mRaHandlerThread.getLooper());
+ mUnicastResponder = new UnicastResponder();
+ mUnicastResponder.start();
- return sendMessage(CMD_START);
+ return true;
}
/** Stop router advertisement daemon. */
public void stop() {
- if (!sendMessage(CMD_STOP)) {
- Log.e(TAG, "RouterAdvertisementDaemon has been stopped or was never started.");
- return;
- }
-
- mRaHandlerThread.quitSafely();
- mRaHandlerThread = null;
- mRaMessageHandler = null;
-
closeSocket();
// Wake up mMulticastTransmitter thread to interrupt a potential 1 day sleep before
// the thread's termination.
maybeNotifyMulticastTransmitter();
mMulticastTransmitter = null;
+ mUnicastResponder = null;
}
@GuardedBy("mLock")
@@ -605,7 +504,7 @@
final int oldTag = TrafficStats.getAndSetThreadStatsTag(TAG_SYSTEM_NEIGHBOR);
try {
- mSocket = Os.socket(AF_INET6, SOCK_RAW | SOCK_NONBLOCK, IPPROTO_ICMPV6);
+ mSocket = Os.socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
// Setting SNDTIMEO is purely for defensive purposes.
Os.setsockoptTimeval(
mSocket, SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(send_timout_ms));
@@ -667,17 +566,34 @@
}
}
- private boolean sendMessage(int cmd) {
- return sendMessage(cmd, null);
- }
+ private final class UnicastResponder extends Thread {
+ private final InetSocketAddress mSolicitor = new InetSocketAddress(0);
+ // The recycled buffer for receiving Router Solicitations from clients.
+ // If the RS is larger than IPV6_MIN_MTU the packets are truncated.
+ // This is fine since currently only byte 0 is examined anyway.
+ private final byte[] mSolicitation = new byte[IPV6_MIN_MTU];
- private boolean sendMessage(int cmd, @Nullable Object obj) {
- if (mRaMessageHandler == null) {
- return false;
+ @Override
+ public void run() {
+ while (isSocketValid()) {
+ try {
+ // Blocking receive.
+ final int rval = Os.recvfrom(
+ mSocket, mSolicitation, 0, mSolicitation.length, 0, mSolicitor);
+ // Do the least possible amount of validation.
+ if (rval < 1 || mSolicitation[0] != asByte(ICMPV6_ROUTER_SOLICITATION)) {
+ continue;
+ }
+ } catch (ErrnoException | SocketException e) {
+ if (isSocketValid()) {
+ Log.e(TAG, "recvfrom error: " + e);
+ }
+ continue;
+ }
+
+ maybeSendRA(mSolicitor);
+ }
}
-
- return mRaMessageHandler.sendMessage(
- Message.obtain(mRaMessageHandler, cmd, obj));
}
// TODO: Consider moving this to run on a provided Looper as a Handler,
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 9f542f4..81e18ab 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -379,7 +379,7 @@
if (!isAtLeastS()) return null;
try {
return new BpfMap<>(TETHER_DOWNSTREAM4_MAP_PATH,
- BpfMap.BPF_F_RDWR, Tether4Key.class, Tether4Value.class);
+ Tether4Key.class, Tether4Value.class);
} catch (ErrnoException e) {
Log.e(TAG, "Cannot create downstream4 map: " + e);
return null;
@@ -391,7 +391,7 @@
if (!isAtLeastS()) return null;
try {
return new BpfMap<>(TETHER_UPSTREAM4_MAP_PATH,
- BpfMap.BPF_F_RDWR, Tether4Key.class, Tether4Value.class);
+ Tether4Key.class, Tether4Value.class);
} catch (ErrnoException e) {
Log.e(TAG, "Cannot create upstream4 map: " + e);
return null;
@@ -403,7 +403,7 @@
if (!isAtLeastS()) return null;
try {
return new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH,
- BpfMap.BPF_F_RDWR, TetherDownstream6Key.class, Tether6Value.class);
+ TetherDownstream6Key.class, Tether6Value.class);
} catch (ErrnoException e) {
Log.e(TAG, "Cannot create downstream6 map: " + e);
return null;
@@ -414,7 +414,7 @@
@Nullable public IBpfMap<TetherUpstream6Key, Tether6Value> getBpfUpstream6Map() {
if (!isAtLeastS()) return null;
try {
- return new BpfMap<>(TETHER_UPSTREAM6_FS_PATH, BpfMap.BPF_F_RDWR,
+ return new BpfMap<>(TETHER_UPSTREAM6_FS_PATH,
TetherUpstream6Key.class, Tether6Value.class);
} catch (ErrnoException e) {
Log.e(TAG, "Cannot create upstream6 map: " + e);
@@ -427,7 +427,7 @@
if (!isAtLeastS()) return null;
try {
return new BpfMap<>(TETHER_STATS_MAP_PATH,
- BpfMap.BPF_F_RDWR, TetherStatsKey.class, TetherStatsValue.class);
+ TetherStatsKey.class, TetherStatsValue.class);
} catch (ErrnoException e) {
Log.e(TAG, "Cannot create stats map: " + e);
return null;
@@ -439,7 +439,7 @@
if (!isAtLeastS()) return null;
try {
return new BpfMap<>(TETHER_LIMIT_MAP_PATH,
- BpfMap.BPF_F_RDWR, TetherLimitKey.class, TetherLimitValue.class);
+ TetherLimitKey.class, TetherLimitValue.class);
} catch (ErrnoException e) {
Log.e(TAG, "Cannot create limit map: " + e);
return null;
@@ -451,7 +451,7 @@
if (!isAtLeastS()) return null;
try {
return new BpfMap<>(TETHER_DEV_MAP_PATH,
- BpfMap.BPF_F_RDWR, TetherDevKey.class, TetherDevValue.class);
+ TetherDevKey.class, TetherDevValue.class);
} catch (ErrnoException e) {
Log.e(TAG, "Cannot create dev map: " + e);
return null;
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java b/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
index 53c80ae..13a7a22 100644
--- a/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
+++ b/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
@@ -16,6 +16,7 @@
package com.android.networkstack.tethering;
+import static com.android.net.module.util.netlink.NetlinkUtils.SOCKET_RECV_BUFSIZE;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
@@ -198,7 +199,8 @@
public NativeHandle createConntrackSocket(final int groups) {
final FileDescriptor fd;
try {
- fd = NetlinkUtils.netlinkSocketForProto(OsConstants.NETLINK_NETFILTER);
+ fd = NetlinkUtils.netlinkSocketForProto(OsConstants.NETLINK_NETFILTER,
+ SOCKET_RECV_BUFSIZE);
} catch (ErrnoException e) {
mLog.e("Unable to create conntrack socket " + e);
return null;
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 8ee28bd..6085a00 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -137,6 +137,7 @@
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.HandlerUtils;
import com.android.net.module.util.NetdUtils;
import com.android.net.module.util.SdkUtil.LateSdk;
import com.android.net.module.util.SharedLog;
@@ -162,11 +163,8 @@
import java.util.List;
import java.util.Objects;
import java.util.Set;
-import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
/**
*
@@ -371,6 +369,7 @@
// Load tethering configuration.
updateConfiguration();
+ mConfig.readEnableSyncSM(mContext);
// It is OK for the configuration to be passed to the PrivateAddressCoordinator at
// construction time because the only part of the configuration it uses is
// shouldEnableWifiP2pDedicatedIp(), and currently do not support changing that.
@@ -2712,31 +2711,10 @@
return;
}
- final CountDownLatch latch = new CountDownLatch(1);
-
- // Don't crash the system if something in doDump throws an exception, but try to propagate
- // the exception to the caller.
- AtomicReference<RuntimeException> exceptionRef = new AtomicReference<>();
- mHandler.post(() -> {
- try {
- doDump(fd, writer, args);
- } catch (RuntimeException e) {
- exceptionRef.set(e);
- }
- latch.countDown();
- });
-
- try {
- if (!latch.await(DUMP_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
- writer.println("Dump timeout after " + DUMP_TIMEOUT_MS + "ms");
- return;
- }
- } catch (InterruptedException e) {
- exceptionRef.compareAndSet(null, new IllegalStateException("Dump interrupted", e));
+ if (!HandlerUtils.runWithScissorsForDump(mHandler, () -> doDump(fd, writer, args),
+ DUMP_TIMEOUT_MS)) {
+ writer.println("Dump timeout after " + DUMP_TIMEOUT_MS + "ms");
}
-
- final RuntimeException e = exceptionRef.get();
- if (e != null) throw e;
}
private void maybeDhcpLeasesChanged() {
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
index d09183a..298940e 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -141,6 +141,9 @@
*/
public static final int DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS = 5000;
+ /** A flag for using synchronous or asynchronous state machine. */
+ public static boolean USE_SYNC_SM = false;
+
public final String[] tetherableUsbRegexs;
public final String[] tetherableWifiRegexs;
public final String[] tetherableWigigRegexs;
@@ -174,7 +177,6 @@
private final boolean mEnableWearTethering;
private final boolean mRandomPrefixBase;
- private final boolean mEnableSyncSm;
private final int mUsbTetheringFunction;
protected final ContentResolver mContentResolver;
@@ -293,7 +295,6 @@
mEnableWearTethering = shouldEnableWearTethering(ctx);
mRandomPrefixBase = mDeps.isFeatureEnabled(ctx, TETHER_FORCE_RANDOM_PREFIX_BASE_SELECTION);
- mEnableSyncSm = mDeps.isFeatureEnabled(ctx, TETHER_ENABLE_SYNC_SM);
configLog.log(toString());
}
@@ -387,8 +388,14 @@
return mRandomPrefixBase;
}
- public boolean isSyncSM() {
- return mEnableSyncSm;
+ /**
+ * Check whether sync SM is enabled then set it to USE_SYNC_SM. This should be called once
+ * when tethering is created. Otherwise if the flag is pushed while tethering is enabled,
+ * then it's possible for some IpServer(s) running the new sync state machine while others
+ * use the async state machine.
+ */
+ public void readEnableSyncSM(final Context ctx) {
+ USE_SYNC_SM = mDeps.isFeatureEnabled(ctx, TETHER_ENABLE_SYNC_SM);
}
/** Does the dumping.*/
@@ -445,8 +452,8 @@
pw.print("mRandomPrefixBase: ");
pw.println(mRandomPrefixBase);
- pw.print("mEnableSyncSm: ");
- pw.println(mEnableSyncSm);
+ pw.print("USE_SYNC_SM: ");
+ pw.println(USE_SYNC_SM);
}
/** Returns the string representation of this object.*/
diff --git a/Tethering/tests/Android.bp b/Tethering/tests/Android.bp
index 72ca666..22cf3c5 100644
--- a/Tethering/tests/Android.bp
+++ b/Tethering/tests/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -24,5 +25,5 @@
visibility: [
"//packages/modules/Connectivity/tests:__subpackages__",
"//packages/modules/Connectivity/Tethering/tests:__subpackages__",
- ]
+ ],
}
diff --git a/Tethering/tests/integration/Android.bp b/Tethering/tests/integration/Android.bp
index 2594a5e..07fa733 100644
--- a/Tethering/tests/integration/Android.bp
+++ b/Tethering/tests/integration/Android.bp
@@ -14,6 +14,7 @@
// limitations under the License.
//
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -45,12 +46,11 @@
android_library {
name: "TetheringIntegrationTestsBaseLib",
target_sdk_version: "current",
- platform_apis: true,
defaults: ["TetheringIntegrationTestsDefaults"],
visibility: [
"//packages/modules/Connectivity/Tethering/tests/mts",
"//packages/modules/Connectivity/tests/cts/net",
- ]
+ ],
}
// Library including tethering integration tests targeting the latest stable SDK.
@@ -58,7 +58,6 @@
android_library {
name: "TetheringIntegrationTestsLatestSdkLib",
target_sdk_version: "33",
- platform_apis: true,
defaults: ["TetheringIntegrationTestsDefaults"],
srcs: [
"src/**/*.java",
@@ -67,7 +66,7 @@
"//packages/modules/Connectivity/tests/cts/tethering",
"//packages/modules/Connectivity/tests:__subpackages__",
"//packages/modules/Connectivity/Tethering/tests:__subpackages__",
- ]
+ ],
}
// Library including tethering integration tests targeting current development SDK.
@@ -75,7 +74,6 @@
android_library {
name: "TetheringIntegrationTestsLib",
target_sdk_version: "current",
- platform_apis: true,
defaults: ["TetheringIntegrationTestsDefaults"],
srcs: [
"src/**/*.java",
@@ -83,7 +81,7 @@
visibility: [
"//packages/modules/Connectivity/tests/cts/tethering",
"//packages/modules/Connectivity/Tethering/tests/mts",
- ]
+ ],
}
// TODO: remove because TetheringIntegrationTests has been covered by ConnectivityCoverageTests.
diff --git a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
index 377da91..2933a44 100644
--- a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
+++ b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
@@ -31,12 +31,14 @@
import static android.net.TetheringTester.isExpectedIcmpPacket;
import static android.net.TetheringTester.isExpectedTcpPacket;
import static android.net.TetheringTester.isExpectedUdpPacket;
+
import static com.android.net.module.util.HexDump.dumpHexString;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
import static com.android.net.module.util.NetworkStackConstants.TCPHDR_ACK;
import static com.android.net.module.util.NetworkStackConstants.TCPHDR_SYN;
import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
import static com.android.testutils.TestPermissionUtil.runAsShell;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -46,7 +48,6 @@
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
-import android.app.UiAutomation;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.EthernetManager.TetheredInterfaceCallback;
@@ -56,8 +57,6 @@
import android.net.TetheringManager.TetheringRequest;
import android.net.TetheringTester.TetheredDevice;
import android.net.cts.util.CtsNetUtils;
-import android.net.cts.util.CtsTetheringUtils;
-import android.net.cts.util.CtsTetheringUtils.TestTetheringEventCallback;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.SystemClock;
@@ -141,11 +140,12 @@
protected static final ByteBuffer TX_PAYLOAD =
ByteBuffer.wrap(new byte[] { (byte) 0x56, (byte) 0x78 });
- private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
- private final EthernetManager mEm = mContext.getSystemService(EthernetManager.class);
- private final TetheringManager mTm = mContext.getSystemService(TetheringManager.class);
- private final PackageManager mPackageManager = mContext.getPackageManager();
- private final CtsNetUtils mCtsNetUtils = new CtsNetUtils(mContext);
+ private static final Context sContext =
+ InstrumentationRegistry.getInstrumentation().getContext();
+ private static final EthernetManager sEm = sContext.getSystemService(EthernetManager.class);
+ private static final TetheringManager sTm = sContext.getSystemService(TetheringManager.class);
+ private static final PackageManager sPackageManager = sContext.getPackageManager();
+ private static final CtsNetUtils sCtsNetUtils = new CtsNetUtils(sContext);
// Late initialization in setUp()
private boolean mRunTests;
@@ -161,7 +161,7 @@
private MyTetheringEventCallback mTetheringEventCallback;
public Context getContext() {
- return mContext;
+ return sContext;
}
@BeforeClass
@@ -170,19 +170,24 @@
// Tethering would cache the last upstreams so that the next enabled tethering avoids
// picking up the address that is in conflict with the upstreams. To protect subsequent
// tests, turn tethering on and off before running them.
- final Context ctx = InstrumentationRegistry.getInstrumentation().getContext();
- final CtsTetheringUtils utils = new CtsTetheringUtils(ctx);
- final TestTetheringEventCallback callback = utils.registerTetheringEventCallback();
+ MyTetheringEventCallback callback = null;
+ TestNetworkInterface testIface = null;
try {
- if (!callback.isWifiTetheringSupported(ctx)) return;
+ // If the physical ethernet interface is available, do nothing.
+ if (isInterfaceForTetheringAvailable()) return;
- callback.expectNoTetheringActive();
+ testIface = createTestInterface();
+ setIncludeTestInterfaces(true);
- utils.startWifiTethering(callback);
- callback.getCurrentValidUpstream();
- utils.stopWifiTethering(callback);
+ callback = enableEthernetTethering(testIface.getInterfaceName(), null);
+ callback.awaitUpstreamChanged(true /* throwTimeoutException */);
+ } catch (TimeoutException e) {
+ Log.d(TAG, "WARNNING " + e);
} finally {
- utils.unregisterTetheringEventCallback(callback);
+ maybeCloseTestInterface(testIface);
+ maybeUnregisterTetheringEventCallback(callback);
+
+ setIncludeTestInterfaces(false);
}
}
@@ -195,13 +200,13 @@
mRunTests = isEthernetTetheringSupported();
assumeTrue(mRunTests);
- mTetheredInterfaceRequester = new TetheredInterfaceRequester(mHandler, mEm);
+ mTetheredInterfaceRequester = new TetheredInterfaceRequester();
}
private boolean isEthernetTetheringSupported() throws Exception {
- if (mEm == null) return false;
+ if (sEm == null) return false;
- return runAsShell(NETWORK_SETTINGS, TETHER_PRIVILEGED, () -> mTm.isTetheringSupported());
+ return runAsShell(NETWORK_SETTINGS, TETHER_PRIVILEGED, () -> sTm.isTetheringSupported());
}
protected void maybeStopTapPacketReader(final TapPacketReader tapPacketReader)
@@ -212,7 +217,7 @@
}
}
- protected void maybeCloseTestInterface(final TestNetworkInterface testInterface)
+ protected static void maybeCloseTestInterface(final TestNetworkInterface testInterface)
throws Exception {
if (testInterface != null) {
testInterface.getFileDescriptor().close();
@@ -220,8 +225,8 @@
}
}
- protected void maybeUnregisterTetheringEventCallback(final MyTetheringEventCallback callback)
- throws Exception {
+ protected static void maybeUnregisterTetheringEventCallback(
+ final MyTetheringEventCallback callback) throws Exception {
if (callback != null) {
callback.awaitInterfaceUntethered();
callback.unregister();
@@ -230,7 +235,7 @@
protected void stopEthernetTethering(final MyTetheringEventCallback callback) {
runAsShell(TETHER_PRIVILEGED, () -> {
- mTm.stopTethering(TETHERING_ETHERNET);
+ sTm.stopTethering(TETHERING_ETHERNET);
maybeUnregisterTetheringEventCallback(callback);
});
}
@@ -277,18 +282,18 @@
}
}
- protected boolean isInterfaceForTetheringAvailable() throws Exception {
+ protected static boolean isInterfaceForTetheringAvailable() throws Exception {
// Before T, all ethernet interfaces could be used for server mode. Instead of
// waiting timeout, just checking whether the system currently has any
// ethernet interface is more reliable.
if (!SdkLevel.isAtLeastT()) {
- return runAsShell(CONNECTIVITY_USE_RESTRICTED_NETWORKS, () -> mEm.isAvailable());
+ return runAsShell(CONNECTIVITY_USE_RESTRICTED_NETWORKS, () -> sEm.isAvailable());
}
// If previous test case doesn't release tethering interface successfully, the other tests
// after that test may be skipped as unexcepted.
// TODO: figure out a better way to check default tethering interface existenion.
- final TetheredInterfaceRequester requester = new TetheredInterfaceRequester(mHandler, mEm);
+ final TetheredInterfaceRequester requester = new TetheredInterfaceRequester();
try {
// Use short timeout (200ms) for requesting an existing interface, if any, because
// it should reurn faster than requesting a new tethering interface. Using default
@@ -306,15 +311,15 @@
}
}
- protected void setIncludeTestInterfaces(boolean include) {
+ protected static void setIncludeTestInterfaces(boolean include) {
runAsShell(NETWORK_SETTINGS, () -> {
- mEm.setIncludeTestInterfaces(include);
+ sEm.setIncludeTestInterfaces(include);
});
}
- protected void setPreferTestNetworks(boolean prefer) {
+ protected static void setPreferTestNetworks(boolean prefer) {
runAsShell(NETWORK_SETTINGS, () -> {
- mTm.setPreferTestNetworks(prefer);
+ sTm.setPreferTestNetworks(prefer);
});
}
@@ -344,7 +349,6 @@
protected static final class MyTetheringEventCallback implements TetheringEventCallback {
- private final TetheringManager mTm;
private final CountDownLatch mTetheringStartedLatch = new CountDownLatch(1);
private final CountDownLatch mTetheringStoppedLatch = new CountDownLatch(1);
private final CountDownLatch mLocalOnlyStartedLatch = new CountDownLatch(1);
@@ -355,7 +359,7 @@
private final TetheringInterface mIface;
private final Network mExpectedUpstream;
- private boolean mAcceptAnyUpstream = false;
+ private final boolean mAcceptAnyUpstream;
private volatile boolean mInterfaceWasTethered = false;
private volatile boolean mInterfaceWasLocalOnly = false;
@@ -368,19 +372,21 @@
// seconds. See b/289881008.
private static final int EXPANDED_TIMEOUT_MS = 30000;
- MyTetheringEventCallback(TetheringManager tm, String iface) {
- this(tm, iface, null);
+ MyTetheringEventCallback(String iface) {
+ mIface = new TetheringInterface(TETHERING_ETHERNET, iface);
+ mExpectedUpstream = null;
mAcceptAnyUpstream = true;
}
- MyTetheringEventCallback(TetheringManager tm, String iface, Network expectedUpstream) {
- mTm = tm;
+ MyTetheringEventCallback(String iface, @NonNull Network expectedUpstream) {
+ Objects.requireNonNull(expectedUpstream);
mIface = new TetheringInterface(TETHERING_ETHERNET, iface);
mExpectedUpstream = expectedUpstream;
+ mAcceptAnyUpstream = false;
}
public void unregister() {
- mTm.unregisterTetheringEventCallback(this);
+ sTm.unregisterTetheringEventCallback(this);
mUnregistered = true;
}
@Override
@@ -504,6 +510,11 @@
Log.d(TAG, "Got upstream changed: " + network);
mUpstream = network;
+ // The callback always updates the current tethering status when it's first registered.
+ // If the caller registers the callback before tethering starts, the null upstream
+ // would be updated. Filtering out the null case because it's not a valid upstream that
+ // we care about.
+ if (mUpstream == null) return;
if (mAcceptAnyUpstream || Objects.equals(mUpstream, mExpectedUpstream)) {
mUpstreamLatch.countDown();
}
@@ -525,18 +536,18 @@
}
}
- protected MyTetheringEventCallback enableEthernetTethering(String iface,
+ protected static MyTetheringEventCallback enableEthernetTethering(String iface,
TetheringRequest request, Network expectedUpstream) throws Exception {
// Enable ethernet tethering with null expectedUpstream means the test accept any upstream
// after etherent tethering started.
final MyTetheringEventCallback callback;
if (expectedUpstream != null) {
- callback = new MyTetheringEventCallback(mTm, iface, expectedUpstream);
+ callback = new MyTetheringEventCallback(iface, expectedUpstream);
} else {
- callback = new MyTetheringEventCallback(mTm, iface);
+ callback = new MyTetheringEventCallback(iface);
}
runAsShell(NETWORK_SETTINGS, () -> {
- mTm.registerTetheringEventCallback(mHandler::post, callback);
+ sTm.registerTetheringEventCallback(c -> c.run() /* executor */, callback);
// Need to hold the shell permission until callback is registered. This helps to avoid
// the test become flaky.
callback.awaitCallbackRegistered();
@@ -556,7 +567,7 @@
};
Log.d(TAG, "Starting Ethernet tethering");
runAsShell(TETHER_PRIVILEGED, () -> {
- mTm.startTethering(request, mHandler::post /* executor */, startTetheringCallback);
+ sTm.startTethering(request, c -> c.run() /* executor */, startTetheringCallback);
// Binder call is an async call. Need to hold the shell permission until tethering
// started. This helps to avoid the test become flaky.
if (!tetheringStartedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
@@ -579,7 +590,7 @@
return callback;
}
- protected MyTetheringEventCallback enableEthernetTethering(String iface,
+ protected static MyTetheringEventCallback enableEthernetTethering(String iface,
Network expectedUpstream) throws Exception {
return enableEthernetTethering(iface,
new TetheringRequest.Builder(TETHERING_ETHERNET)
@@ -605,17 +616,9 @@
}
protected static final class TetheredInterfaceRequester implements TetheredInterfaceCallback {
- private final Handler mHandler;
- private final EthernetManager mEm;
-
private TetheredInterfaceRequest mRequest;
private final CompletableFuture<String> mFuture = new CompletableFuture<>();
- TetheredInterfaceRequester(Handler handler, EthernetManager em) {
- mHandler = handler;
- mEm = em;
- }
-
@Override
public void onAvailable(String iface) {
Log.d(TAG, "Ethernet interface available: " + iface);
@@ -631,7 +634,7 @@
assertNull("BUG: more than one tethered interface request", mRequest);
Log.d(TAG, "Requesting tethered interface");
mRequest = runAsShell(NETWORK_SETTINGS, () ->
- mEm.requestTetheredInterface(mHandler::post, this));
+ sEm.requestTetheredInterface(c -> c.run() /* executor */, this));
return mFuture;
}
@@ -652,9 +655,9 @@
}
}
- protected TestNetworkInterface createTestInterface() throws Exception {
+ protected static TestNetworkInterface createTestInterface() throws Exception {
TestNetworkManager tnm = runAsShell(MANAGE_TEST_NETWORKS, () ->
- mContext.getSystemService(TestNetworkManager.class));
+ sContext.getSystemService(TestNetworkManager.class));
TestNetworkInterface iface = runAsShell(MANAGE_TEST_NETWORKS, () ->
tnm.createTapInterface());
Log.d(TAG, "Created test interface " + iface.getInterfaceName());
@@ -669,7 +672,7 @@
lp.setLinkAddresses(addresses);
lp.setDnsServers(dnses);
- return runAsShell(MANAGE_TEST_NETWORKS, () -> initTestNetwork(mContext, lp, TIMEOUT_MS));
+ return runAsShell(MANAGE_TEST_NETWORKS, () -> initTestNetwork(sContext, lp, TIMEOUT_MS));
}
protected void sendDownloadPacketUdp(@NonNull final InetAddress srcIp,
@@ -851,7 +854,7 @@
private void maybeRetryTestedUpstreamChanged(final Network expectedUpstream,
final TimeoutException fallbackException) throws Exception {
// Fall back original exception because no way to reselect if there is no WIFI feature.
- assertTrue(fallbackException.toString(), mPackageManager.hasSystemFeature(FEATURE_WIFI));
+ assertTrue(fallbackException.toString(), sPackageManager.hasSystemFeature(FEATURE_WIFI));
// Try to toggle wifi network, if any, to reselect upstream network via default network
// switching. Because test network has higher priority than internet network, this can
@@ -862,12 +865,12 @@
// trigger the reselection, the total test time may over test suite 1 minmute timeout.
// Probably need to disable/restore all internet networks in a common place of test
// process. Currently, EthernetTetheringTest is part of CTS test which needs wifi network
- // connection if device has wifi feature. CtsNetUtils#toggleWifi() checks wifi connection
- // during the toggling process.
- // See Tethering#chooseUpstreamType, CtsNetUtils#toggleWifi.
+ // connection if device has wifi feature.
+ // See Tethering#chooseUpstreamType
// TODO: toggle cellular network if the device has no WIFI feature.
Log.d(TAG, "Toggle WIFI to retry upstream selection");
- mCtsNetUtils.toggleWifi();
+ sCtsNetUtils.disableWifi();
+ sCtsNetUtils.ensureWifiConnected();
// Wait for expected upstream.
final CompletableFuture<Network> future = new CompletableFuture<>();
@@ -881,14 +884,14 @@
}
};
try {
- mTm.registerTetheringEventCallback(mHandler::post, callback);
+ sTm.registerTetheringEventCallback(mHandler::post, callback);
assertEquals("onUpstreamChanged for unexpected network", expectedUpstream,
future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS));
} catch (TimeoutException e) {
throw new AssertionError("Did not receive upstream " + expectedUpstream
+ " callback after " + TIMEOUT_MS + "ms");
} finally {
- mTm.unregisterTetheringEventCallback(callback);
+ sTm.unregisterTetheringEventCallback(callback);
}
}
@@ -925,7 +928,7 @@
mDownstreamReader = makePacketReader(mDownstreamIface);
mUpstreamReader = makePacketReader(mUpstreamTracker.getTestIface());
- final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
+ final ConnectivityManager cm = sContext.getSystemService(ConnectivityManager.class);
// Currently tethering don't have API to tell when ipv6 tethering is available. Thus, make
// sure tethering already have ipv6 connectivity before testing.
if (cm.getLinkProperties(mUpstreamTracker.getNetwork()).hasGlobalIpv6Address()) {
diff --git a/Tethering/tests/mts/Android.bp b/Tethering/tests/mts/Android.bp
index 4f4b03c..a80e49e 100644
--- a/Tethering/tests/mts/Android.bp
+++ b/Tethering/tests/mts/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/Tethering/tests/privileged/Android.bp b/Tethering/tests/privileged/Android.bp
index c890197..ba6be66 100644
--- a/Tethering/tests/privileged/Android.bp
+++ b/Tethering/tests/privileged/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
index dac5b63..90ceaa1 100644
--- a/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
+++ b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
@@ -47,6 +47,7 @@
import android.os.Looper;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
+import android.util.ArraySet;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
@@ -77,7 +78,6 @@
import java.net.Inet6Address;
import java.net.InetAddress;
import java.nio.ByteBuffer;
-import java.util.HashSet;
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -236,7 +236,7 @@
final RdnssOption rdnss = Struct.parse(RdnssOption.class, RdnssBuf);
final String msg =
rdnss.lifetime > 0 ? "Unknown dns" : "Unknown deprecated dns";
- final HashSet<Inet6Address> dnses =
+ final ArraySet<Inet6Address> dnses =
rdnss.lifetime > 0 ? mNewParams.dnses : mOldParams.dnses;
assertNotNull(msg, dnses);
diff --git a/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
index 0e8b044..d5d71bc 100644
--- a/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
+++ b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
@@ -84,7 +84,7 @@
private void initTestMap() throws Exception {
mTestMap = new BpfMap<>(
- TETHER_DOWNSTREAM6_FS_PATH, BpfMap.BPF_F_RDWR,
+ TETHER_DOWNSTREAM6_FS_PATH,
TetherDownstream6Key.class, Tether6Value.class);
mTestMap.forEach((key, value) -> {
@@ -135,7 +135,7 @@
assertEquals(OsConstants.EPERM, expected.errno);
}
}
- try (BpfMap readWriteMap = new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH, BpfMap.BPF_F_RDWR,
+ try (BpfMap readWriteMap = new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH,
TetherDownstream6Key.class, Tether6Value.class)) {
assertNotNull(readWriteMap);
}
@@ -389,7 +389,7 @@
public void testOpenNonexistentMap() throws Exception {
try {
final BpfMap<TetherDownstream6Key, Tether6Value> nonexistentMap = new BpfMap<>(
- "/sys/fs/bpf/tethering/nonexistent", BpfMap.BPF_F_RDWR,
+ "/sys/fs/bpf/tethering/nonexistent",
TetherDownstream6Key.class, Tether6Value.class);
} catch (ErrnoException expected) {
assertEquals(OsConstants.ENOENT, expected.errno);
@@ -409,8 +409,8 @@
final int before = getNumOpenFds();
for (int i = 0; i < iterations; i++) {
try (BpfMap<TetherDownstream6Key, Tether6Value> map = new BpfMap<>(
- TETHER_DOWNSTREAM6_FS_PATH, BpfMap.BPF_F_RDWR,
- TetherDownstream6Key.class, Tether6Value.class)) {
+ TETHER_DOWNSTREAM6_FS_PATH,
+ TetherDownstream6Key.class, Tether6Value.class)) {
// do nothing
}
}
diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp
index 36d9a63..24407ca 100644
--- a/Tethering/tests/unit/Android.bp
+++ b/Tethering/tests/unit/Android.bp
@@ -16,6 +16,7 @@
// Tests in this folder are included both in unit tests and CTS.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -23,7 +24,7 @@
name: "TetheringCommonTests",
srcs: [
"common/**/*.java",
- "common/**/*.kt"
+ "common/**/*.kt",
],
static_libs: [
"androidx.test.rules",
@@ -95,7 +96,7 @@
visibility: [
"//packages/modules/Connectivity/tests:__subpackages__",
"//packages/modules/Connectivity/Tethering/tests:__subpackages__",
- ]
+ ],
}
android_test {
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
index 19c6e5a..dd51c7a 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
@@ -757,23 +757,24 @@
private void setTetherEnableSyncSMFlagEnabled(Boolean enabled) {
mDeps.setFeatureEnabled(TetheringConfiguration.TETHER_ENABLE_SYNC_SM, enabled);
+ new TetheringConfiguration(
+ mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps).readEnableSyncSM(mMockContext);
}
- private void assertEnableSyncSMIs(boolean value) {
- assertEquals(value, new TetheringConfiguration(
- mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps).isSyncSM());
+ private void assertEnableSyncSM(boolean value) {
+ assertEquals(value, TetheringConfiguration.USE_SYNC_SM);
}
@Test
public void testEnableSyncSMFlag() throws Exception {
// Test default disabled
setTetherEnableSyncSMFlagEnabled(null);
- assertEnableSyncSMIs(false);
+ assertEnableSyncSM(false);
setTetherEnableSyncSMFlagEnabled(true);
- assertEnableSyncSMIs(true);
+ assertEnableSyncSM(true);
setTetherEnableSyncSMFlagEnabled(false);
- assertEnableSyncSMIs(false);
+ assertEnableSyncSM(false);
}
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index 82b8845..750bfce 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -2810,12 +2810,10 @@
final FileDescriptor mockFd = mock(FileDescriptor.class);
final PrintWriter mockPw = mock(PrintWriter.class);
runUsbTethering(null);
- mLooper.startAutoDispatch();
mTethering.dump(mockFd, mockPw, new String[0]);
verify(mConfig).dump(any());
verify(mEntitleMgr).dump(any());
verify(mOffloadCtrl).dump(any());
- mLooper.stopAutoDispatch();
}
@Test
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index cdf47e7..674cd98 100644
--- a/bpf_progs/Android.bp
+++ b/bpf_progs/Android.bp
@@ -18,6 +18,7 @@
// struct definitions shared with JNI
//
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index f223dd1..5e401aa 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -446,8 +446,18 @@
const struct egress_bool egress,
const bool enable_tracing,
const struct kver_uint kver) {
+ // sock_uid will be 'overflowuid' if !sk_fullsock(sk_to_full_sk(skb->sk))
uint32_t sock_uid = bpf_get_socket_uid(skb);
- uint64_t cookie = bpf_get_socket_cookie(skb);
+
+ // kernel's DEFAULT_OVERFLOWUID is 65534, this is the overflow 'nobody' uid,
+ // usually this being returned means that skb->sk is NULL during RX
+ // (early decap socket lookup failure), which commonly happens for incoming
+ // packets to an unconnected udp socket.
+ // But it can also happen for egress from a timewait socket.
+ // Let's treat such cases as 'root' which is_system_uid()
+ if (sock_uid == 65534) sock_uid = 0;
+
+ uint64_t cookie = bpf_get_socket_cookie(skb); // 0 iff !skb->sk
UidTagValue* utag = bpf_cookie_tag_map_lookup_elem(&cookie);
uint32_t uid, tag;
if (utag) {
@@ -540,7 +550,7 @@
bpf_cgroup_egress_trace_user, KVER_5_8, KVER_INF,
BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, OPTIONAL,
"fs_bpf_netd_readonly", "",
- LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
+ IGNORE_ON_ENG, LOAD_ON_USER, IGNORE_ON_USERDEBUG)
(struct __sk_buff* skb) {
return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER_5_8);
}
@@ -616,12 +626,13 @@
uint32_t sock_uid = bpf_get_socket_uid(skb);
if (is_system_uid(sock_uid)) return BPF_MATCH;
- // 65534 is the overflow 'nobody' uid, usually this being returned means
- // that skb->sk is NULL during RX (early decap socket lookup failure),
- // which commonly happens for incoming packets to an unconnected udp socket.
- // Additionally bpf_get_socket_cookie() returns 0 if skb->sk is NULL
- if ((sock_uid == 65534) && !bpf_get_socket_cookie(skb) && is_received_skb(skb))
- return BPF_MATCH;
+ // kernel's DEFAULT_OVERFLOWUID is 65534, this is the overflow 'nobody' uid,
+ // usually this being returned means that skb->sk is NULL during RX
+ // (early decap socket lookup failure), which commonly happens for incoming
+ // packets to an unconnected udp socket.
+ // But it can also happen for egress from a timewait socket.
+ // Let's treat such cases as 'root' which is_system_uid()
+ if (sock_uid == 65534) return BPF_MATCH;
UidOwnerValue* allowlistMatch = bpf_uid_owner_map_lookup_elem(&sock_uid);
if (allowlistMatch) return allowlistMatch->rule & HAPPY_BOX_MATCH ? BPF_MATCH : BPF_NOMATCH;
diff --git a/bpf_progs/netd.h b/bpf_progs/netd.h
index 64ed633..098147f 100644
--- a/bpf_progs/netd.h
+++ b/bpf_progs/netd.h
@@ -178,7 +178,7 @@
#endif // __cplusplus
// LINT.IfChange(match_type)
-enum UidOwnerMatchType {
+enum UidOwnerMatchType : uint32_t {
NO_MATCH = 0,
HAPPY_BOX_MATCH = (1 << 0),
PENALTY_BOX_MATCH = (1 << 1),
@@ -196,14 +196,14 @@
};
// LINT.ThenChange(../framework/src/android/net/BpfNetMapsConstants.java)
-enum BpfPermissionMatch {
+enum BpfPermissionMatch : uint8_t {
BPF_PERMISSION_INTERNET = 1 << 2,
BPF_PERMISSION_UPDATE_DEVICE_STATS = 1 << 3,
};
// In production we use two identical stats maps to record per uid stats and
// do swap and clean based on the configuration specified here. The statsMapType
// value in configuration map specified which map is currently in use.
-enum StatsMapType {
+enum StatsMapType : uint32_t {
SELECT_MAP_A,
SELECT_MAP_B,
};
diff --git a/clatd/Android.bp b/clatd/Android.bp
index 595c6b9..43eb2d8 100644
--- a/clatd/Android.bp
+++ b/clatd/Android.bp
@@ -1,4 +1,5 @@
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["external_android-clat_license"],
}
@@ -54,7 +55,7 @@
defaults: ["clatd_defaults"],
srcs: [
":clatd_common",
- "main.c"
+ "main.c",
],
static_libs: [
"libip_checksum",
@@ -101,7 +102,7 @@
defaults: ["clatd_defaults"],
srcs: [
":clatd_common",
- "clatd_test.cpp"
+ "clatd_test.cpp",
],
static_libs: [
"libbase",
diff --git a/common/Android.bp b/common/Android.bp
index 1d73a46..0048a0a 100644
--- a/common/Android.bp
+++ b/common/Android.bp
@@ -15,11 +15,12 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
-build = ["TrunkStable.bp"]
+build = ["FlaggedApi.bp"]
// This is a placeholder comment to avoid merge conflicts
// as the above target may not exist
@@ -49,5 +50,7 @@
apex_available: [
"com.android.tethering",
],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ },
}
diff --git a/common/FlaggedApi.bp b/common/FlaggedApi.bp
new file mode 100644
index 0000000..56625c5
--- /dev/null
+++ b/common/FlaggedApi.bp
@@ -0,0 +1,39 @@
+//
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+aconfig_declarations {
+ name: "com.android.net.flags-aconfig",
+ package: "com.android.net.flags",
+ container: "system",
+ srcs: ["flags.aconfig"],
+ visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
+
+aconfig_declarations {
+ name: "com.android.net.thread.flags-aconfig",
+ package: "com.android.net.thread.flags",
+ container: "system",
+ srcs: ["thread_flags.aconfig"],
+ visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
+
+aconfig_declarations {
+ name: "nearby_flags",
+ package: "com.android.nearby.flags",
+ container: "system",
+ srcs: ["nearby_flags.aconfig"],
+ visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
diff --git a/common/OWNERS b/common/OWNERS
new file mode 100644
index 0000000..e7f5d11
--- /dev/null
+++ b/common/OWNERS
@@ -0,0 +1 @@
+per-file thread_flags.aconfig = file:platform/packages/modules/Connectivity:main:/thread/OWNERS
diff --git a/common/flags.aconfig b/common/flags.aconfig
new file mode 100644
index 0000000..19b522c
--- /dev/null
+++ b/common/flags.aconfig
@@ -0,0 +1,75 @@
+package: "com.android.net.flags"
+container: "system"
+
+# This file contains aconfig flags for FlaggedAPI annotations
+# Flags used from platform code must be in under frameworks
+
+flag {
+ name: "set_data_saver_via_cm"
+ namespace: "android_core_networking"
+ description: "Set data saver through ConnectivityManager API"
+ bug: "297836825"
+}
+
+flag {
+ name: "support_is_uid_networking_blocked"
+ namespace: "android_core_networking"
+ description: "This flag controls whether isUidNetworkingBlocked is supported"
+ bug: "297836825"
+}
+
+flag {
+ name: "basic_background_restrictions_enabled"
+ namespace: "android_core_networking"
+ description: "Block network access for apps in a low importance background state"
+ bug: "304347838"
+}
+
+flag {
+ name: "ipsec_transform_state"
+ namespace: "android_core_networking_ipsec"
+ description: "The flag controls the access for getIpSecTransformState and IpSecTransformState"
+ bug: "308011229"
+}
+
+flag {
+ name: "tethering_request_with_soft_ap_config"
+ namespace: "android_core_networking"
+ description: "The flag controls the access for the parcelable TetheringRequest with getSoftApConfiguration/setSoftApConfiguration API"
+ bug: "216524590"
+}
+
+flag {
+ name: "request_restricted_wifi"
+ namespace: "android_core_networking"
+ description: "Flag for API to support requesting restricted wifi"
+ bug: "315835605"
+}
+
+flag {
+ name: "net_capability_local_network"
+ namespace: "android_core_networking"
+ description: "Flag for local network capability API"
+ bug: "313000440"
+}
+
+flag {
+ name: "support_transport_satellite"
+ namespace: "android_core_networking"
+ description: "Flag for satellite transport API"
+ bug: "320514105"
+}
+
+flag {
+ name: "nsd_subtypes_support_enabled"
+ namespace: "android_core_networking"
+ description: "Flag for API to support nsd subtypes"
+ bug: "265095929"
+}
+
+flag {
+ name: "register_nsd_offload_engine_api"
+ namespace: "android_core_networking"
+ description: "Flag for API to register nsd offload engine"
+ bug: "301713539"
+}
diff --git a/common/nearby_flags.aconfig b/common/nearby_flags.aconfig
new file mode 100644
index 0000000..b957d33
--- /dev/null
+++ b/common/nearby_flags.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.nearby.flags"
+container: "system"
+
+flag {
+ name: "powered_off_finding"
+ namespace: "nearby"
+ description: "Controls whether the Powered Off Finding feature is enabled"
+ bug: "307898240"
+}
diff --git a/thread/flags/thread_base.aconfig b/common/thread_flags.aconfig
similarity index 90%
rename from thread/flags/thread_base.aconfig
rename to common/thread_flags.aconfig
index bf1f288..09595a6 100644
--- a/thread/flags/thread_base.aconfig
+++ b/common/thread_flags.aconfig
@@ -1,4 +1,5 @@
package: "com.android.net.thread.flags"
+container: "system"
flag {
name: "thread_enabled"
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index deda74e..bc919ac 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -34,6 +35,7 @@
name: "enable-framework-connectivity-t-targets",
enabled: true,
}
+
// The above defaults can be used to disable framework-connectivity t
// targets while minimizing merge conflicts in the build rules.
@@ -57,6 +59,10 @@
"app-compat-annotations",
"androidx.annotation_annotation",
],
+ static_libs: [
+ // Cannot go to framework-connectivity because mid_sdk checks require 31.
+ "modules-utils-binary-xml",
+ ],
impl_only_libs: [
// The build system will use framework-bluetooth module_current stubs, because
// of sdk_version: "module_current" above.
@@ -112,6 +118,7 @@
"framework-bluetooth",
"framework-wifi",
"framework-connectivity-pre-jarjar",
+ "framework-location.stubs.module_lib",
],
visibility: ["//packages/modules/Connectivity:__subpackages__"],
}
@@ -134,6 +141,7 @@
"sdk_module-lib_current_framework-connectivity",
],
libs: [
+ "framework-location.stubs.module_lib",
"sdk_module-lib_current_framework-connectivity",
],
permitted_packages: [
@@ -174,6 +182,7 @@
"//frameworks/base/core/tests/bandwidthtests",
"//frameworks/base/core/tests/benchmarks",
"//frameworks/base/core/tests/utillib",
+ "//frameworks/base/services/tests/VpnTests",
"//frameworks/base/tests/vcn",
"//frameworks/opt/net/ethernet/tests:__subpackages__",
"//frameworks/opt/telephony/tests/telephonytests",
@@ -182,10 +191,16 @@
"//packages/modules/Connectivity/staticlibs/tests:__subpackages__",
"//packages/modules/Connectivity/Tethering/tests:__subpackages__",
"//packages/modules/Connectivity/tests:__subpackages__",
+ "//packages/modules/Connectivity/thread/tests:__subpackages__",
"//packages/modules/IPsec/tests/iketests",
"//packages/modules/NetworkStack/tests:__subpackages__",
"//packages/modules/Wifi/service/tests/wifitests",
],
+ aconfig_declarations: [
+ "com.android.net.flags-aconfig",
+ "com.android.net.thread.flags-aconfig",
+ "nearby_flags",
+ ],
}
// This rule is not used anymore(b/268440216).
diff --git a/framework-t/api/current.txt b/framework-t/api/current.txt
index fb46ee7..9ae0cf7 100644
--- a/framework-t/api/current.txt
+++ b/framework-t/api/current.txt
@@ -127,7 +127,7 @@
public final class IpSecTransform implements java.lang.AutoCloseable {
method public void close();
- method @FlaggedApi("com.android.net.flags.ipsec_transform_state") public void getIpSecTransformState(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.net.IpSecTransformState,java.lang.RuntimeException>);
+ method @FlaggedApi("com.android.net.flags.ipsec_transform_state") public void requestIpSecTransformState(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.net.IpSecTransformState,java.lang.RuntimeException>);
}
public static class IpSecTransform.Builder {
@@ -145,7 +145,7 @@
method public long getPacketCount();
method @NonNull public byte[] getReplayBitmap();
method public long getRxHighestSequenceNumber();
- method public long getTimestamp();
+ method public long getTimestampMillis();
method public long getTxHighestSequenceNumber();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.net.IpSecTransformState> CREATOR;
@@ -158,7 +158,7 @@
method @NonNull public android.net.IpSecTransformState.Builder setPacketCount(long);
method @NonNull public android.net.IpSecTransformState.Builder setReplayBitmap(@NonNull byte[]);
method @NonNull public android.net.IpSecTransformState.Builder setRxHighestSequenceNumber(long);
- method @NonNull public android.net.IpSecTransformState.Builder setTimestamp(long);
+ method @NonNull public android.net.IpSecTransformState.Builder setTimestampMillis(long);
method @NonNull public android.net.IpSecTransformState.Builder setTxHighestSequenceNumber(long);
}
@@ -210,9 +210,26 @@
package android.net.nsd {
+ @FlaggedApi("com.android.net.flags.nsd_subtypes_support_enabled") public final class DiscoveryRequest implements android.os.Parcelable {
+ method public int describeContents();
+ method @Nullable public android.net.Network getNetwork();
+ method @NonNull public String getServiceType();
+ method @Nullable public String getSubtype();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.nsd.DiscoveryRequest> CREATOR;
+ }
+
+ public static final class DiscoveryRequest.Builder {
+ ctor public DiscoveryRequest.Builder(@NonNull String);
+ method @NonNull public android.net.nsd.DiscoveryRequest build();
+ method @NonNull public android.net.nsd.DiscoveryRequest.Builder setNetwork(@Nullable android.net.Network);
+ method @NonNull public android.net.nsd.DiscoveryRequest.Builder setSubtype(@Nullable String);
+ }
+
public final class NsdManager {
method public void discoverServices(String, int, android.net.nsd.NsdManager.DiscoveryListener);
method public void discoverServices(@NonNull String, int, @Nullable android.net.Network, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.DiscoveryListener);
+ method @FlaggedApi("com.android.net.flags.nsd_subtypes_support_enabled") public void discoverServices(@NonNull android.net.nsd.DiscoveryRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.DiscoveryListener);
method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void discoverServices(@NonNull String, int, @NonNull android.net.NetworkRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.DiscoveryListener);
method public void registerService(android.net.nsd.NsdServiceInfo, int, android.net.nsd.NsdManager.RegistrationListener);
method public void registerService(@NonNull android.net.nsd.NsdServiceInfo, int, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.RegistrationListener);
@@ -275,6 +292,7 @@
method public int getPort();
method public String getServiceName();
method public String getServiceType();
+ method @FlaggedApi("com.android.net.flags.nsd_subtypes_support_enabled") @NonNull public java.util.Set<java.lang.String> getSubtypes();
method public void removeAttribute(String);
method public void setAttribute(String, String);
method @Deprecated public void setHost(java.net.InetAddress);
@@ -283,6 +301,7 @@
method public void setPort(int);
method public void setServiceName(String);
method public void setServiceType(String);
+ method @FlaggedApi("com.android.net.flags.nsd_subtypes_support_enabled") public void setSubtypes(@NonNull java.util.Set<java.lang.String>);
method public void writeToParcel(android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.net.nsd.NsdServiceInfo> CREATOR;
}
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index 3513573..1f1953c 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -59,11 +59,17 @@
}
public class NearbyManager {
+ method @FlaggedApi("com.android.nearby.flags.powered_off_finding") @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public int getPoweredOffFindingMode();
method public void queryOffloadCapability(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.nearby.OffloadCapability>);
+ method @FlaggedApi("com.android.nearby.flags.powered_off_finding") @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public void setPoweredOffFindingEphemeralIds(@NonNull java.util.List<byte[]>);
+ method @FlaggedApi("com.android.nearby.flags.powered_off_finding") @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public void setPoweredOffFindingMode(int);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_ADVERTISE, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void startBroadcast(@NonNull android.nearby.BroadcastRequest, @NonNull java.util.concurrent.Executor, @NonNull android.nearby.BroadcastCallback);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_SCAN, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public int startScan(@NonNull android.nearby.ScanRequest, @NonNull java.util.concurrent.Executor, @NonNull android.nearby.ScanCallback);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_ADVERTISE, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void stopBroadcast(@NonNull android.nearby.BroadcastCallback);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_SCAN, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void stopScan(@NonNull android.nearby.ScanCallback);
+ field @FlaggedApi("com.android.nearby.flags.powered_off_finding") public static final int POWERED_OFF_FINDING_MODE_DISABLED = 1; // 0x1
+ field @FlaggedApi("com.android.nearby.flags.powered_off_finding") public static final int POWERED_OFF_FINDING_MODE_ENABLED = 2; // 0x2
+ field @FlaggedApi("com.android.nearby.flags.powered_off_finding") public static final int POWERED_OFF_FINDING_MODE_UNSUPPORTED = 0; // 0x0
}
public final class OffloadCapability implements android.os.Parcelable {
@@ -500,6 +506,7 @@
method @RequiresPermission(allOf={android.Manifest.permission.ACCESS_NETWORK_STATE, "android.permission.THREAD_NETWORK_PRIVILEGED"}) public void registerOperationalDatasetCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.thread.ThreadNetworkController.OperationalDatasetCallback);
method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.thread.ThreadNetworkController.StateCallback);
method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void scheduleMigration(@NonNull android.net.thread.PendingOperationalDataset, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
+ method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void setEnabled(boolean, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
method @RequiresPermission(allOf={android.Manifest.permission.ACCESS_NETWORK_STATE, "android.permission.THREAD_NETWORK_PRIVILEGED"}) public void unregisterOperationalDatasetCallback(@NonNull android.net.thread.ThreadNetworkController.OperationalDatasetCallback);
method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void unregisterStateCallback(@NonNull android.net.thread.ThreadNetworkController.StateCallback);
field public static final int DEVICE_ROLE_CHILD = 2; // 0x2
@@ -507,6 +514,9 @@
field public static final int DEVICE_ROLE_LEADER = 4; // 0x4
field public static final int DEVICE_ROLE_ROUTER = 3; // 0x3
field public static final int DEVICE_ROLE_STOPPED = 0; // 0x0
+ field public static final int STATE_DISABLED = 0; // 0x0
+ field public static final int STATE_DISABLING = 2; // 0x2
+ field public static final int STATE_ENABLED = 1; // 0x1
field public static final int THREAD_VERSION_1_3 = 4; // 0x4
}
@@ -518,6 +528,7 @@
public static interface ThreadNetworkController.StateCallback {
method public void onDeviceRoleChanged(int);
method public default void onPartitionIdChanged(long);
+ method public default void onThreadEnableStateChanged(int);
}
@FlaggedApi("com.android.net.thread.flags.thread_enabled") public class ThreadNetworkException extends java.lang.Exception {
@@ -530,8 +541,10 @@
field public static final int ERROR_REJECTED_BY_PEER = 8; // 0x8
field public static final int ERROR_RESOURCE_EXHAUSTED = 10; // 0xa
field public static final int ERROR_RESPONSE_BAD_FORMAT = 9; // 0x9
+ field public static final int ERROR_THREAD_DISABLED = 12; // 0xc
field public static final int ERROR_TIMEOUT = 3; // 0x3
field public static final int ERROR_UNAVAILABLE = 4; // 0x4
+ field public static final int ERROR_UNKNOWN = 11; // 0xb
field public static final int ERROR_UNSUPPORTED_CHANNEL = 7; // 0x7
}
diff --git a/framework-t/src/android/app/usage/NetworkStatsManager.java b/framework-t/src/android/app/usage/NetworkStatsManager.java
index d139544..7fa0661 100644
--- a/framework-t/src/android/app/usage/NetworkStatsManager.java
+++ b/framework-t/src/android/app/usage/NetworkStatsManager.java
@@ -510,6 +510,27 @@
* Query network usage statistics details for a given uid.
* This may take a long time, and apps should avoid calling this on their main thread.
*
+ * @param networkType As defined in {@link ConnectivityManager}, e.g.
+ * {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
+ * etc.
+ * @param subscriberId If applicable, the subscriber id of the network interface.
+ * <p>Starting with API level 29, the {@code subscriberId} is guarded by
+ * additional restrictions. Calling apps that do not meet the new
+ * requirements to access the {@code subscriberId} can provide a {@code
+ * null} value when querying for the mobile network type to receive usage
+ * for all mobile networks. For additional details see {@link
+ * TelephonyManager#getSubscriberId()}.
+ * <p>Starting with API level 31, calling apps can provide a
+ * {@code subscriberId} with wifi network type to receive usage for
+ * wifi networks which is under the given subscription if applicable.
+ * Otherwise, pass {@code null} when querying all wifi networks.
+ * @param startTime Start of period. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @param endTime End of period. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @param uid UID of app
+ * @return Statistics which is described above.
+ * @throws SecurityException if permissions are insufficient to read network statistics.
* @see #queryDetailsForUidTagState(int, String, long, long, int, int, int)
*/
@NonNull
diff --git a/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java b/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
index d89964d..d7cff2c 100644
--- a/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
+++ b/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
@@ -27,6 +27,8 @@
import android.net.thread.IThreadNetworkManager;
import android.net.thread.ThreadNetworkManager;
+import com.android.modules.utils.build.SdkLevel;
+
/**
* Class for performing registration for Connectivity services which are exposed via updatable APIs
* since Android T.
@@ -83,14 +85,17 @@
}
);
- SystemServiceRegistry.registerStaticService(
- MDnsManager.MDNS_SERVICE,
- MDnsManager.class,
- (serviceBinder) -> {
- IMDns service = IMDns.Stub.asInterface(serviceBinder);
- return new MDnsManager(service);
- }
- );
+ // mdns service is removed from Netd from Android V.
+ if (!SdkLevel.isAtLeastV()) {
+ SystemServiceRegistry.registerStaticService(
+ MDnsManager.MDNS_SERVICE,
+ MDnsManager.class,
+ (serviceBinder) -> {
+ IMDns service = IMDns.Stub.asInterface(serviceBinder);
+ return new MDnsManager(service);
+ }
+ );
+ }
SystemServiceRegistry.registerContextAwareService(
ThreadNetworkManager.SERVICE_NAME,
diff --git a/framework-t/src/android/net/IpSecTransform.java b/framework-t/src/android/net/IpSecTransform.java
index 246a2dd..4e10a96 100644
--- a/framework-t/src/android/net/IpSecTransform.java
+++ b/framework-t/src/android/net/IpSecTransform.java
@@ -215,7 +215,7 @@
* @see IpSecTransformState
*/
@FlaggedApi(IPSEC_TRANSFORM_STATE)
- public void getIpSecTransformState(
+ public void requestIpSecTransformState(
@CallbackExecutor @NonNull Executor executor,
@NonNull OutcomeReceiver<IpSecTransformState, RuntimeException> callback) {
Objects.requireNonNull(executor);
diff --git a/framework-t/src/android/net/IpSecTransformState.java b/framework-t/src/android/net/IpSecTransformState.java
index b575dd5..5b80ae2 100644
--- a/framework-t/src/android/net/IpSecTransformState.java
+++ b/framework-t/src/android/net/IpSecTransformState.java
@@ -23,6 +23,7 @@
import android.annotation.NonNull;
import android.os.Parcel;
import android.os.Parcelable;
+import android.os.SystemClock;
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.HexDump;
@@ -40,7 +41,7 @@
*/
@FlaggedApi(IPSEC_TRANSFORM_STATE)
public final class IpSecTransformState implements Parcelable {
- private final long mTimeStamp;
+ private final long mTimestamp;
private final long mTxHighestSequenceNumber;
private final long mRxHighestSequenceNumber;
private final long mPacketCount;
@@ -54,7 +55,7 @@
long packetCount,
long byteCount,
byte[] replayBitmap) {
- mTimeStamp = timestamp;
+ mTimestamp = timestamp;
mTxHighestSequenceNumber = txHighestSequenceNumber;
mRxHighestSequenceNumber = rxHighestSequenceNumber;
mPacketCount = packetCount;
@@ -78,7 +79,7 @@
@VisibleForTesting(visibility = Visibility.PRIVATE)
public IpSecTransformState(@NonNull Parcel in) {
Objects.requireNonNull(in, "The input PersistableBundle is null");
- mTimeStamp = in.readLong();
+ mTimestamp = in.readLong();
mTxHighestSequenceNumber = in.readLong();
mRxHighestSequenceNumber = in.readLong();
mPacketCount = in.readLong();
@@ -97,7 +98,7 @@
@Override
public void writeToParcel(@NonNull Parcel out, int flags) {
- out.writeLong(mTimeStamp);
+ out.writeLong(mTimestamp);
out.writeLong(mTxHighestSequenceNumber);
out.writeLong(mRxHighestSequenceNumber);
out.writeLong(mPacketCount);
@@ -120,16 +121,17 @@
};
/**
- * Retrieve the epoch timestamp (milliseconds) for when this state was created
+ * Retrieve the timestamp (milliseconds) when this state was created, as per {@link
+ * SystemClock#elapsedRealtime}
*
- * @see Builder#setTimestamp(long)
+ * @see Builder#setTimestampMillis(long)
*/
- public long getTimestamp() {
- return mTimeStamp;
+ public long getTimestampMillis() {
+ return mTimestamp;
}
/**
- * Retrieve the highest sequence number sent so far
+ * Retrieve the highest sequence number sent so far as an unsigned long
*
* @see Builder#setTxHighestSequenceNumber(long)
*/
@@ -138,7 +140,7 @@
}
/**
- * Retrieve the highest sequence number received so far
+ * Retrieve the highest sequence number received so far as an unsigned long
*
* @see Builder#setRxHighestSequenceNumber(long)
*/
@@ -147,7 +149,10 @@
}
/**
- * Retrieve the number of packets received AND sent so far
+ * Retrieve the number of packets processed so far as an unsigned long.
+ *
+ * <p>The packet count direction (inbound or outbound) aligns with the direction in which the
+ * IpSecTransform is applied to.
*
* @see Builder#setPacketCount(long)
*/
@@ -156,7 +161,10 @@
}
/**
- * Retrieve the number of bytes received AND sent so far
+ * Retrieve the number of bytes processed so far as an unsigned long
+ *
+ * <p>The byte count direction (inbound or outbound) aligns with the direction in which the
+ * IpSecTransform is applied to.
*
* @see Builder#setByteCount(long)
*/
@@ -183,10 +191,15 @@
return mReplayBitmap.clone();
}
- /** Builder class for testing purposes */
+ /**
+ * Builder class for testing purposes
+ *
+ * <p>Except for testing, IPsec callers normally do not instantiate {@link IpSecTransformState}
+ * themselves but instead get a reference via {@link IpSecTransformState}
+ */
@FlaggedApi(IPSEC_TRANSFORM_STATE)
public static final class Builder {
- private long mTimeStamp;
+ private long mTimestamp;
private long mTxHighestSequenceNumber;
private long mRxHighestSequenceNumber;
private long mPacketCount;
@@ -194,22 +207,22 @@
private byte[] mReplayBitmap;
public Builder() {
- mTimeStamp = System.currentTimeMillis();
+ mTimestamp = SystemClock.elapsedRealtime();
}
/**
- * Set the epoch timestamp (milliseconds) for when this state was created
+ * Set the timestamp (milliseconds) when this state was created
*
- * @see IpSecTransformState#getTimestamp()
+ * @see IpSecTransformState#getTimestampMillis()
*/
@NonNull
- public Builder setTimestamp(long timeStamp) {
- mTimeStamp = timeStamp;
+ public Builder setTimestampMillis(long timestamp) {
+ mTimestamp = timestamp;
return this;
}
/**
- * Set the highest sequence number sent so far
+ * Set the highest sequence number sent so far as an unsigned long
*
* @see IpSecTransformState#getTxHighestSequenceNumber()
*/
@@ -220,7 +233,7 @@
}
/**
- * Set the highest sequence number received so far
+ * Set the highest sequence number received so far as an unsigned long
*
* @see IpSecTransformState#getRxHighestSequenceNumber()
*/
@@ -231,7 +244,7 @@
}
/**
- * Set the number of packets received AND sent so far
+ * Set the number of packets processed so far as an unsigned long
*
* @see IpSecTransformState#getPacketCount()
*/
@@ -242,7 +255,7 @@
}
/**
- * Set the number of bytes received AND sent so far
+ * Set the number of bytes processed so far as an unsigned long
*
* @see IpSecTransformState#getByteCount()
*/
@@ -271,7 +284,7 @@
@NonNull
public IpSecTransformState build() {
return new IpSecTransformState(
- mTimeStamp,
+ mTimestamp,
mTxHighestSequenceNumber,
mRxHighestSequenceNumber,
mPacketCount,
diff --git a/framework-t/src/android/net/NetworkStatsAccess.java b/framework-t/src/android/net/NetworkStatsAccess.java
index 23902dc..7c9b3ec 100644
--- a/framework-t/src/android/net/NetworkStatsAccess.java
+++ b/framework-t/src/android/net/NetworkStatsAccess.java
@@ -23,6 +23,7 @@
import android.Manifest;
import android.annotation.IntDef;
+import android.annotation.Nullable;
import android.app.AppOpsManager;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
@@ -109,7 +110,7 @@
/** Returns the {@link NetworkStatsAccess.Level} for the given caller. */
public static @NetworkStatsAccess.Level int checkAccessLevel(
- Context context, int callingPid, int callingUid, String callingPackage) {
+ Context context, int callingPid, int callingUid, @Nullable String callingPackage) {
final DevicePolicyManager mDpm = context.getSystemService(DevicePolicyManager.class);
final TelephonyManager tm = (TelephonyManager)
context.getSystemService(Context.TELEPHONY_SERVICE);
@@ -127,7 +128,7 @@
final int appId = UserHandle.getAppId(callingUid);
- final boolean isNetworkStack = PermissionUtils.checkAnyPermissionOf(
+ final boolean isNetworkStack = PermissionUtils.hasAnyPermissionOf(
context, callingPid, callingUid, android.Manifest.permission.NETWORK_STACK,
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
diff --git a/framework-t/src/android/net/NetworkStatsCollection.java b/framework-t/src/android/net/NetworkStatsCollection.java
index b6f6dbb..934b4c6 100644
--- a/framework-t/src/android/net/NetworkStatsCollection.java
+++ b/framework-t/src/android/net/NetworkStatsCollection.java
@@ -60,6 +60,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.FileRotator;
+import com.android.modules.utils.FastDataInput;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.NetworkStatsUtils;
@@ -116,15 +117,28 @@
private long mEndMillis;
private long mTotalBytes;
private boolean mDirty;
+ private final boolean mUseFastDataInput;
/**
* Construct a {@link NetworkStatsCollection} object.
*
- * @param bucketDuration duration of the buckets in this object, in milliseconds.
+ * @param bucketDurationMillis duration of the buckets in this object, in milliseconds.
* @hide
*/
public NetworkStatsCollection(long bucketDurationMillis) {
+ this(bucketDurationMillis, false /* useFastDataInput */);
+ }
+
+ /**
+ * Construct a {@link NetworkStatsCollection} object.
+ *
+ * @param bucketDurationMillis duration of the buckets in this object, in milliseconds.
+ * @param useFastDataInput true if using {@link FastDataInput} is preferred. Otherwise, false.
+ * @hide
+ */
+ public NetworkStatsCollection(long bucketDurationMillis, boolean useFastDataInput) {
mBucketDurationMillis = bucketDurationMillis;
+ mUseFastDataInput = useFastDataInput;
reset();
}
@@ -483,7 +497,11 @@
/** @hide */
@Override
public void read(InputStream in) throws IOException {
- read((DataInput) new DataInputStream(in));
+ if (mUseFastDataInput) {
+ read(FastDataInput.obtain(in));
+ } else {
+ read((DataInput) new DataInputStream(in));
+ }
}
private void read(DataInput in) throws IOException {
@@ -967,8 +985,8 @@
* @hide
*/
@Nullable
- public static String compareStats(
- NetworkStatsCollection migrated, NetworkStatsCollection legacy) {
+ public static String compareStats(NetworkStatsCollection migrated,
+ NetworkStatsCollection legacy, boolean allowKeyChange) {
final Map<NetworkStatsCollection.Key, NetworkStatsHistory> migEntries =
migrated.getEntries();
final Map<NetworkStatsCollection.Key, NetworkStatsHistory> legEntries = legacy.getEntries();
@@ -980,7 +998,7 @@
final NetworkStatsHistory legHistory = legEntries.get(legKey);
final NetworkStatsHistory migHistory = migEntries.get(legKey);
- if (migHistory == null && couldKeyChangeOnImport(legKey)) {
+ if (migHistory == null && allowKeyChange && couldKeyChangeOnImport(legKey)) {
unmatchedLegKeys.remove(legKey);
continue;
}
diff --git a/framework-t/src/android/net/nsd/AdvertisingRequest.java b/framework-t/src/android/net/nsd/AdvertisingRequest.java
new file mode 100644
index 0000000..2895b0c
--- /dev/null
+++ b/framework-t/src/android/net/nsd/AdvertisingRequest.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.nsd;
+
+import android.annotation.LongDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.time.Duration;
+import java.util.Objects;
+
+/**
+ * Encapsulates parameters for {@link NsdManager#registerService}.
+ * @hide
+ */
+//@FlaggedApi(NsdManager.Flags.ADVERTISE_REQUEST_API)
+public final class AdvertisingRequest implements Parcelable {
+
+ /**
+ * Only update the registration without sending exit and re-announcement.
+ */
+ public static final long NSD_ADVERTISING_UPDATE_ONLY = 1;
+
+
+ @NonNull
+ public static final Creator<AdvertisingRequest> CREATOR =
+ new Creator<>() {
+ @Override
+ public AdvertisingRequest createFromParcel(Parcel in) {
+ final NsdServiceInfo serviceInfo = in.readParcelable(
+ NsdServiceInfo.class.getClassLoader(), NsdServiceInfo.class);
+ final int protocolType = in.readInt();
+ final long advertiseConfig = in.readLong();
+ final long ttlSeconds = in.readLong();
+ final Duration ttl = ttlSeconds < 0 ? null : Duration.ofSeconds(ttlSeconds);
+ return new AdvertisingRequest(serviceInfo, protocolType, advertiseConfig, ttl);
+ }
+
+ @Override
+ public AdvertisingRequest[] newArray(int size) {
+ return new AdvertisingRequest[size];
+ }
+ };
+ @NonNull
+ private final NsdServiceInfo mServiceInfo;
+ private final int mProtocolType;
+ // Bitmask of @AdvertisingConfig flags. Uses a long to allow 64 possible flags in the future.
+ private final long mAdvertisingConfig;
+
+ @Nullable
+ private final Duration mTtl;
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @LongDef(flag = true, prefix = {"NSD_ADVERTISING"}, value = {
+ NSD_ADVERTISING_UPDATE_ONLY,
+ })
+ @interface AdvertisingConfig {}
+
+ /**
+ * The constructor for the advertiseRequest
+ */
+ private AdvertisingRequest(@NonNull NsdServiceInfo serviceInfo, int protocolType,
+ long advertisingConfig, @NonNull Duration ttl) {
+ mServiceInfo = serviceInfo;
+ mProtocolType = protocolType;
+ mAdvertisingConfig = advertisingConfig;
+ mTtl = ttl;
+ }
+
+ /**
+ * Returns the {@link NsdServiceInfo}
+ */
+ @NonNull
+ public NsdServiceInfo getServiceInfo() {
+ return mServiceInfo;
+ }
+
+ /**
+ * Returns the service advertise protocol
+ */
+ public int getProtocolType() {
+ return mProtocolType;
+ }
+
+ /**
+ * Returns the advertising config.
+ */
+ public long getAdvertisingConfig() {
+ return mAdvertisingConfig;
+ }
+
+ /**
+ * Returns the time interval that the resource records may be cached on a DNS resolver or
+ * {@code null} if not specified.
+ *
+ * @hide
+ */
+ // @FlaggedApi(NsdManager.Flags.NSD_CUSTOM_TTL_ENABLED)
+ @Nullable
+ public Duration getTtl() {
+ return mTtl;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("serviceInfo: ").append(mServiceInfo)
+ .append(", protocolType: ").append(mProtocolType)
+ .append(", advertisingConfig: ").append(mAdvertisingConfig)
+ .append(", ttl: ").append(mTtl);
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ } else if (!(other instanceof AdvertisingRequest)) {
+ return false;
+ } else {
+ final AdvertisingRequest otherRequest = (AdvertisingRequest) other;
+ return mServiceInfo.equals(otherRequest.mServiceInfo)
+ && mProtocolType == otherRequest.mProtocolType
+ && mAdvertisingConfig == otherRequest.mAdvertisingConfig
+ && Objects.equals(mTtl, otherRequest.mTtl);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mServiceInfo, mProtocolType, mAdvertisingConfig, mTtl);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeParcelable(mServiceInfo, flags);
+ dest.writeInt(mProtocolType);
+ dest.writeLong(mAdvertisingConfig);
+ dest.writeLong(mTtl == null ? -1 : mTtl.getSeconds());
+ }
+
+// @FlaggedApi(NsdManager.Flags.ADVERTISE_REQUEST_API)
+ /**
+ * The builder for creating new {@link AdvertisingRequest} objects.
+ * @hide
+ */
+ public static final class Builder {
+ @NonNull
+ private final NsdServiceInfo mServiceInfo;
+ private final int mProtocolType;
+ private long mAdvertisingConfig;
+ @Nullable
+ private Duration mTtl;
+ /**
+ * Creates a new {@link Builder} object.
+ */
+ public Builder(@NonNull NsdServiceInfo serviceInfo, int protocolType) {
+ mServiceInfo = serviceInfo;
+ mProtocolType = protocolType;
+ }
+
+ /**
+ * Sets advertising configuration flags.
+ *
+ * @param advertisingConfigFlags Bitmask of {@code AdvertisingConfig} flags.
+ */
+ @NonNull
+ public Builder setAdvertisingConfig(long advertisingConfigFlags) {
+ mAdvertisingConfig = advertisingConfigFlags;
+ return this;
+ }
+
+ /**
+ * Sets the time interval that the resource records may be cached on a DNS resolver.
+ *
+ * If this method is not called or {@code ttl} is {@code null}, default TTL values
+ * will be used for the service when it's registered. Otherwise, the {@code ttl}
+ * will be used for all resource records of this service.
+ *
+ * When registering a service, {@link NsdManager#FAILURE_BAD_PARAMETERS} will be returned
+ * if {@code ttl} is smaller than 30 seconds.
+ *
+ * Note: only number of seconds of {@code ttl} is used.
+ *
+ * @param ttl the maximum duration that the DNS resource records will be cached
+ *
+ * @see AdvertisingRequest#getTtl
+ * @hide
+ */
+ // @FlaggedApi(NsdManager.Flags.NSD_CUSTOM_TTL_ENABLED)
+ @NonNull
+ public Builder setTtl(@Nullable Duration ttl) {
+ if (ttl == null) {
+ mTtl = null;
+ return this;
+ }
+ final long ttlSeconds = ttl.getSeconds();
+ if (ttlSeconds < 0 || ttlSeconds > 0xffffffffL) {
+ throw new IllegalArgumentException(
+ "ttlSeconds exceeds the allowed range (value = " + ttlSeconds
+ + ", allowedRanged = [0, 0xffffffffL])");
+ }
+ mTtl = Duration.ofSeconds(ttlSeconds);
+ return this;
+ }
+
+ /** Creates a new {@link AdvertisingRequest} object. */
+ @NonNull
+ public AdvertisingRequest build() {
+ return new AdvertisingRequest(mServiceInfo, mProtocolType, mAdvertisingConfig, mTtl);
+ }
+ }
+}
diff --git a/framework-t/src/android/net/nsd/DiscoveryRequest.java b/framework-t/src/android/net/nsd/DiscoveryRequest.java
new file mode 100644
index 0000000..b0b71ea
--- /dev/null
+++ b/framework-t/src/android/net/nsd/DiscoveryRequest.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.nsd;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.Network;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.util.Objects;
+
+/**
+ * Encapsulates parameters for {@link NsdManager#discoverServices}.
+ */
+@FlaggedApi(NsdManager.Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+public final class DiscoveryRequest implements Parcelable {
+ private final int mProtocolType;
+
+ @NonNull
+ private final String mServiceType;
+
+ @Nullable
+ private final String mSubtype;
+
+ @Nullable
+ private final Network mNetwork;
+
+ // TODO: add mDiscoveryConfig for more fine-grained discovery behavior control
+
+ @NonNull
+ public static final Creator<DiscoveryRequest> CREATOR =
+ new Creator<>() {
+ @Override
+ public DiscoveryRequest createFromParcel(Parcel in) {
+ int protocolType = in.readInt();
+ String serviceType = in.readString();
+ String subtype = in.readString();
+ Network network =
+ in.readParcelable(Network.class.getClassLoader(), Network.class);
+ return new DiscoveryRequest(protocolType, serviceType, subtype, network);
+ }
+
+ @Override
+ public DiscoveryRequest[] newArray(int size) {
+ return new DiscoveryRequest[size];
+ }
+ };
+
+ private DiscoveryRequest(int protocolType, @NonNull String serviceType,
+ @Nullable String subtype, @Nullable Network network) {
+ mProtocolType = protocolType;
+ mServiceType = serviceType;
+ mSubtype = subtype;
+ mNetwork = network;
+ }
+
+ /**
+ * Returns the service type in format of dot-joint string of two labels.
+ *
+ * For example, "_ipp._tcp" for internet printer and "_matter._tcp" for <a
+ * href="https://csa-iot.org/all-solutions/matter">Matter</a> operational device.
+ */
+ @NonNull
+ public String getServiceType() {
+ return mServiceType;
+ }
+
+ /**
+ * Returns the subtype without the trailing "._sub" label or {@code null} if no subtype is
+ * specified.
+ *
+ * For example, the return value will be "_printer" for subtype "_printer._sub".
+ */
+ @Nullable
+ public String getSubtype() {
+ return mSubtype;
+ }
+
+ /**
+ * Returns the service discovery protocol.
+ *
+ * @hide
+ */
+ public int getProtocolType() {
+ return mProtocolType;
+ }
+
+ /**
+ * Returns the {@link Network} on which the query should be sent or {@code null} if no
+ * network is specified.
+ */
+ @Nullable
+ public Network getNetwork() {
+ return mNetwork;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(", protocolType: ").append(mProtocolType)
+ .append(", serviceType: ").append(mServiceType)
+ .append(", subtype: ").append(mSubtype)
+ .append(", network: ").append(mNetwork);
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ } else if (!(other instanceof DiscoveryRequest)) {
+ return false;
+ } else {
+ DiscoveryRequest otherRequest = (DiscoveryRequest) other;
+ return mProtocolType == otherRequest.mProtocolType
+ && Objects.equals(mServiceType, otherRequest.mServiceType)
+ && Objects.equals(mSubtype, otherRequest.mSubtype)
+ && Objects.equals(mNetwork, otherRequest.mNetwork);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mProtocolType, mServiceType, mSubtype, mNetwork);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mProtocolType);
+ dest.writeString(mServiceType);
+ dest.writeString(mSubtype);
+ dest.writeParcelable(mNetwork, flags);
+ }
+
+ /** The builder for creating new {@link DiscoveryRequest} objects. */
+ public static final class Builder {
+ private final int mProtocolType;
+
+ @NonNull
+ private String mServiceType;
+
+ @Nullable
+ private String mSubtype;
+
+ @Nullable
+ private Network mNetwork;
+
+ /**
+ * Creates a new default {@link Builder} object with given service type.
+ *
+ * @throws IllegalArgumentException if {@code serviceType} is {@code null} or an empty
+ * string
+ */
+ public Builder(@NonNull String serviceType) {
+ this(NsdManager.PROTOCOL_DNS_SD, serviceType);
+ }
+
+ /** @hide */
+ public Builder(int protocolType, @NonNull String serviceType) {
+ NsdManager.checkProtocol(protocolType);
+ mProtocolType = protocolType;
+ setServiceType(serviceType);
+ }
+
+ /**
+ * Sets the service type to be discovered or {@code null} if no services should be queried.
+ *
+ * The {@code serviceType} must be a dot-joint string of two labels. For example,
+ * "_ipp._tcp" for internet printer. Additionally, the first label must start with
+ * underscore ('_') and the second label must be either "_udp" or "_tcp". Otherwise, {@link
+ * NsdManager#discoverServices} will fail with {@link NsdManager#FAILURE_BAD_PARAMETER}.
+ *
+ * @throws IllegalArgumentException if {@code serviceType} is {@code null} or an empty
+ * string
+ *
+ * @hide
+ */
+ @NonNull
+ public Builder setServiceType(@NonNull String serviceType) {
+ if (TextUtils.isEmpty(serviceType)) {
+ throw new IllegalArgumentException("Service type cannot be empty");
+ }
+ mServiceType = serviceType;
+ return this;
+ }
+
+ /**
+ * Sets the optional subtype of the services to be discovered.
+ *
+ * If a non-empty {@code subtype} is specified, it must start with underscore ('_') and
+ * have the trailing "._sub" removed. Otherwise, {@link NsdManager#discoverServices} will
+ * fail with {@link NsdManager#FAILURE_BAD_PARAMETER}. For example, {@code subtype} should
+ * be "_printer" for DNS name "_printer._sub._http._tcp". In this case, only services with
+ * this {@code subtype} will be queried, rather than all services of the base service type.
+ *
+ * Note that a non-empty service type must be specified with {@link #setServiceType} if a
+ * non-empty subtype is specified by this method.
+ */
+ @NonNull
+ public Builder setSubtype(@Nullable String subtype) {
+ mSubtype = subtype;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Network} on which the discovery queries should be sent.
+ *
+ * @param network the discovery network or {@code null} if the query should be sent on
+ * all supported networks
+ */
+ @NonNull
+ public Builder setNetwork(@Nullable Network network) {
+ mNetwork = network;
+ return this;
+ }
+
+ /**
+ * Creates a new {@link DiscoveryRequest} object.
+ */
+ @NonNull
+ public DiscoveryRequest build() {
+ return new DiscoveryRequest(mProtocolType, mServiceType, mSubtype, mNetwork);
+ }
+ }
+}
diff --git a/framework-t/src/android/net/nsd/INsdManagerCallback.aidl b/framework-t/src/android/net/nsd/INsdManagerCallback.aidl
index d89bfa9..55820ec 100644
--- a/framework-t/src/android/net/nsd/INsdManagerCallback.aidl
+++ b/framework-t/src/android/net/nsd/INsdManagerCallback.aidl
@@ -17,6 +17,7 @@
package android.net.nsd;
import android.os.Messenger;
+import android.net.nsd.DiscoveryRequest;
import android.net.nsd.NsdServiceInfo;
/**
@@ -24,7 +25,7 @@
* @hide
*/
oneway interface INsdManagerCallback {
- void onDiscoverServicesStarted(int listenerKey, in NsdServiceInfo info);
+ void onDiscoverServicesStarted(int listenerKey, in DiscoveryRequest discoveryRequest);
void onDiscoverServicesFailed(int listenerKey, int error);
void onServiceFound(int listenerKey, in NsdServiceInfo info);
void onServiceLost(int listenerKey, in NsdServiceInfo info);
diff --git a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
index e671db1..9a31278 100644
--- a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
+++ b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
@@ -16,6 +16,8 @@
package android.net.nsd;
+import android.net.nsd.AdvertisingRequest;
+import android.net.nsd.DiscoveryRequest;
import android.net.nsd.INsdManagerCallback;
import android.net.nsd.IOffloadEngine;
import android.net.nsd.NsdServiceInfo;
@@ -27,9 +29,9 @@
* {@hide}
*/
interface INsdServiceConnector {
- void registerService(int listenerKey, in NsdServiceInfo serviceInfo);
+ void registerService(int listenerKey, in AdvertisingRequest advertisingRequest);
void unregisterService(int listenerKey);
- void discoverServices(int listenerKey, in NsdServiceInfo serviceInfo);
+ void discoverServices(int listenerKey, in DiscoveryRequest discoveryRequest);
void stopDiscovery(int listenerKey);
void resolveService(int listenerKey, in NsdServiceInfo serviceInfo);
void startDaemon();
@@ -38,4 +40,4 @@
void unregisterServiceInfoCallback(int listenerKey);
void registerOffloadEngine(String ifaceName, in IOffloadEngine cb, long offloadCapabilities, long offloadType);
void unregisterOffloadEngine(in IOffloadEngine cb);
-}
\ No newline at end of file
+}
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index bf01a9d..1001423 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -46,10 +46,12 @@
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
+import android.util.Pair;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.CollectionUtils;
import java.lang.annotation.Retention;
@@ -57,6 +59,8 @@
import java.util.ArrayList;
import java.util.Objects;
import java.util.concurrent.Executor;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
* The Network Service Discovery Manager class provides the API to discover services
@@ -150,9 +154,65 @@
public static class Flags {
static final String REGISTER_NSD_OFFLOAD_ENGINE_API =
"com.android.net.flags.register_nsd_offload_engine_api";
+ static final String NSD_SUBTYPES_SUPPORT_ENABLED =
+ "com.android.net.flags.nsd_subtypes_support_enabled";
+ static final String ADVERTISE_REQUEST_API =
+ "com.android.net.flags.advertise_request_api";
+ static final String NSD_CUSTOM_HOSTNAME_ENABLED =
+ "com.android.net.flags.nsd_custom_hostname_enabled";
+ static final String NSD_CUSTOM_TTL_ENABLED =
+ "com.android.net.flags.nsd_custom_ttl_enabled";
}
/**
+ * A regex for the acceptable format of a type or subtype label.
+ * @hide
+ */
+ public static final String TYPE_LABEL_REGEX = "_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]";
+
+ /**
+ * A regex for the acceptable format of a subtype label.
+ *
+ * As per RFC 6763 7.1, "Subtype strings are not required to begin with an underscore, though
+ * they often do.", and "Subtype strings [...] may be constructed using arbitrary 8-bit data
+ * values. In many cases these data values may be UTF-8 [RFC3629] representations of text, or
+ * even (as in the example above) plain ASCII [RFC20], but they do not have to be.".
+ *
+ * This regex is overly conservative as it mandates the underscore and only allows printable
+ * ASCII characters (codes 0x20 to 0x7e, space to tilde), except for comma (0x2c) and dot
+ * (0x2e); so the NsdManager API does not allow everything the RFC allows. This may be revisited
+ * in the future, but using arbitrary bytes makes logging and testing harder, and using other
+ * characters would probably be a bad idea for interoperability for apps.
+ * @hide
+ */
+ public static final String SUBTYPE_LABEL_REGEX = "_["
+ + "\\x20-\\x2b"
+ + "\\x2d"
+ + "\\x2f-\\x7e"
+ + "]{1,62}";
+
+ /**
+ * A regex for the acceptable format of a service type specification.
+ *
+ * When it matches, matcher group 1 is an optional leading subtype when using legacy dot syntax
+ * (_subtype._type._tcp). Matcher group 2 is the actual type, and matcher group 3 contains
+ * optional comma-separated subtypes.
+ * @hide
+ */
+ public static final String TYPE_REGEX =
+ // Optional leading subtype (_subtype._type._tcp)
+ // (?: xxx) is a non-capturing parenthesis, don't capture the dot
+ "^(?:(" + SUBTYPE_LABEL_REGEX + ")\\.)?"
+ // Actual type (_type._tcp.local)
+ + "(" + TYPE_LABEL_REGEX + "\\._(?:tcp|udp))"
+ // Drop '.' at the end of service type that is compatible with old backend.
+ // e.g. allow "_type._tcp.local."
+ + "\\.?"
+ // Optional subtype after comma, for "_type._tcp,_subtype1,_subtype2" format
+ + "((?:," + SUBTYPE_LABEL_REGEX + ")*)"
+ + "$";
+
+ /**
* Broadcast intent action to indicate whether network service discovery is
* enabled or disabled. An extra {@link #EXTRA_NSD_STATE} provides the state
* information as int.
@@ -269,6 +329,20 @@
/** Dns based service discovery protocol */
public static final int PROTOCOL_DNS_SD = 0x0001;
+ /**
+ * The minimum TTL seconds which is allowed for a service registration.
+ *
+ * @hide
+ */
+ public static final long TTL_SECONDS_MIN = 30L;
+
+ /**
+ * The maximum TTL seconds which is allowed for a service registration.
+ *
+ * @hide
+ */
+ public static final long TTL_SECONDS_MAX = 10 * 3600L;
+
private static final SparseArray<String> EVENT_NAMES = new SparseArray<>();
static {
EVENT_NAMES.put(DISCOVER_SERVICES, "DISCOVER_SERVICES");
@@ -325,6 +399,8 @@
@GuardedBy("mMapLock")
private final SparseArray<NsdServiceInfo> mServiceMap = new SparseArray<>();
@GuardedBy("mMapLock")
+ private final SparseArray<DiscoveryRequest> mDiscoveryMap = new SparseArray<>();
+ @GuardedBy("mMapLock")
private final SparseArray<Executor> mExecutorMap = new SparseArray<>();
private final Object mMapLock = new Object();
// Map of listener key sent by client -> per-network discovery tracker
@@ -654,9 +730,12 @@
throw new RuntimeException("Failed to connect to NsdService");
}
- // Only proactively start the daemon if the target SDK < S, otherwise the internal service
- // would automatically start/stop the native daemon as needed.
- if (!CompatChanges.isChangeEnabled(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)) {
+ // Only proactively start the daemon if the target SDK < S AND platform < V, For target
+ // SDK >= S AND platform < V, the internal service would automatically start/stop the native
+ // daemon as needed. For platform >= V, no action is required because the native daemon is
+ // completely removed.
+ if (!CompatChanges.isChangeEnabled(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
+ && !SdkLevel.isAtLeastV()) {
try {
mService.startDaemon();
} catch (RemoteException e) {
@@ -677,6 +756,12 @@
mServHandler.sendMessage(mServHandler.obtainMessage(message, 0, listenerKey, info));
}
+ private void sendDiscoveryRequest(
+ int message, int listenerKey, DiscoveryRequest discoveryRequest) {
+ mServHandler.sendMessage(
+ mServHandler.obtainMessage(message, 0, listenerKey, discoveryRequest));
+ }
+
private void sendError(int message, int listenerKey, int error) {
mServHandler.sendMessage(mServHandler.obtainMessage(message, error, listenerKey));
}
@@ -686,8 +771,8 @@
}
@Override
- public void onDiscoverServicesStarted(int listenerKey, NsdServiceInfo info) {
- sendInfo(DISCOVER_SERVICES_STARTED, listenerKey, info);
+ public void onDiscoverServicesStarted(int listenerKey, DiscoveryRequest discoveryRequest) {
+ sendDiscoveryRequest(DISCOVER_SERVICES_STARTED, listenerKey, discoveryRequest);
}
@Override
@@ -965,10 +1050,12 @@
final Object obj = message.obj;
final Object listener;
final NsdServiceInfo ns;
+ final DiscoveryRequest discoveryRequest;
final Executor executor;
synchronized (mMapLock) {
listener = mListenerMap.get(key);
ns = mServiceMap.get(key);
+ discoveryRequest = mDiscoveryMap.get(key);
executor = mExecutorMap.get(key);
}
if (listener == null) {
@@ -976,17 +1063,22 @@
return;
}
if (DBG) {
- Log.d(TAG, "received " + nameOf(what) + " for key " + key + ", service " + ns);
+ if (discoveryRequest != null) {
+ Log.d(TAG, "received " + nameOf(what) + " for key " + key + ", discovery "
+ + discoveryRequest);
+ } else {
+ Log.d(TAG, "received " + nameOf(what) + " for key " + key + ", service " + ns);
+ }
}
switch (what) {
case DISCOVER_SERVICES_STARTED:
- final String s = getNsdServiceInfoType((NsdServiceInfo) obj);
+ final String s = getNsdServiceInfoType((DiscoveryRequest) obj);
executor.execute(() -> ((DiscoveryListener) listener).onDiscoveryStarted(s));
break;
case DISCOVER_SERVICES_FAILED:
removeListener(key);
executor.execute(() -> ((DiscoveryListener) listener).onStartDiscoveryFailed(
- getNsdServiceInfoType(ns), errorCode));
+ getNsdServiceInfoType(discoveryRequest), errorCode));
break;
case SERVICE_FOUND:
executor.execute(() -> ((DiscoveryListener) listener).onServiceFound(
@@ -1001,12 +1093,12 @@
// the effect for the client is indistinguishable from STOP_DISCOVERY_SUCCEEDED
removeListener(key);
executor.execute(() -> ((DiscoveryListener) listener).onStopDiscoveryFailed(
- getNsdServiceInfoType(ns), errorCode));
+ getNsdServiceInfoType(discoveryRequest), errorCode));
break;
case STOP_DISCOVERY_SUCCEEDED:
removeListener(key);
executor.execute(() -> ((DiscoveryListener) listener).onDiscoveryStopped(
- getNsdServiceInfoType(ns)));
+ getNsdServiceInfoType(discoveryRequest)));
break;
case REGISTER_SERVICE_FAILED:
removeListener(key);
@@ -1079,17 +1171,39 @@
return mListenerKey;
}
- // Assert that the listener is not in the map, then add it and returns its key
- private int putListener(Object listener, Executor e, NsdServiceInfo s) {
- checkListener(listener);
- final int key;
+ private int putListener(Object listener, Executor e, NsdServiceInfo serviceInfo) {
synchronized (mMapLock) {
- int valueIndex = mListenerMap.indexOfValue(listener);
+ return putListener(listener, e, mServiceMap, serviceInfo);
+ }
+ }
+
+ private int putListener(Object listener, Executor e, DiscoveryRequest discoveryRequest) {
+ synchronized (mMapLock) {
+ return putListener(listener, e, mDiscoveryMap, discoveryRequest);
+ }
+ }
+
+ // Assert that the listener is not in the map, then add it and returns its key
+ private <T> int putListener(Object listener, Executor e, SparseArray<T> map, T value) {
+ synchronized (mMapLock) {
+ checkListener(listener);
+ final int key;
+ final int valueIndex = mListenerMap.indexOfValue(listener);
if (valueIndex != -1) {
throw new IllegalArgumentException("listener already in use");
}
key = nextListenerKey();
mListenerMap.put(key, listener);
+ map.put(key, value);
+ mExecutorMap.put(key, e);
+ return key;
+ }
+ }
+
+ private int updateRegisteredListener(Object listener, Executor e, NsdServiceInfo s) {
+ final int key;
+ synchronized (mMapLock) {
+ key = getListenerKey(listener);
mServiceMap.put(key, s);
mExecutorMap.put(key, e);
}
@@ -1100,6 +1214,7 @@
synchronized (mMapLock) {
mListenerMap.remove(key);
mServiceMap.remove(key);
+ mDiscoveryMap.remove(key);
mExecutorMap.remove(key);
}
}
@@ -1115,9 +1230,9 @@
}
}
- private static String getNsdServiceInfoType(NsdServiceInfo s) {
- if (s == null) return "?";
- return s.getServiceType();
+ private static String getNsdServiceInfoType(DiscoveryRequest r) {
+ if (r == null) return "?";
+ return r.getServiceType();
}
/**
@@ -1160,14 +1275,111 @@
*/
public void registerService(@NonNull NsdServiceInfo serviceInfo, int protocolType,
@NonNull Executor executor, @NonNull RegistrationListener listener) {
- if (serviceInfo.getPort() <= 0) {
- throw new IllegalArgumentException("Invalid port number");
- }
- checkServiceInfo(serviceInfo);
+ checkServiceInfoForRegistration(serviceInfo);
checkProtocol(protocolType);
- int key = putListener(listener, executor, serviceInfo);
+ final AdvertisingRequest.Builder builder = new AdvertisingRequest.Builder(serviceInfo,
+ protocolType);
+ // Optionally assume that the request is an update request if it uses subtypes and the same
+ // listener. This is not documented behavior as support for advertising subtypes via
+ // "_servicename,_sub1,_sub2" has never been documented in the first place, and using
+ // multiple subtypes was broken in T until a later module update. Subtype registration is
+ // documented in the NsdServiceInfo.setSubtypes API instead, but this provides a limited
+ // option for users of the older undocumented behavior, only for subtype changes.
+ if (isSubtypeUpdateRequest(serviceInfo, listener)) {
+ builder.setAdvertisingConfig(AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY);
+ }
+ registerService(builder.build(), executor, listener);
+ }
+
+ private boolean isSubtypeUpdateRequest(@NonNull NsdServiceInfo serviceInfo, @NonNull
+ RegistrationListener listener) {
+ // If the listener is the same object, serviceInfo is for the same service name and
+ // type (outside of subtypes), and either of them use subtypes, treat the request as a
+ // subtype update request.
+ synchronized (mMapLock) {
+ int valueIndex = mListenerMap.indexOfValue(listener);
+ if (valueIndex == -1) {
+ return false;
+ }
+ final int key = mListenerMap.keyAt(valueIndex);
+ NsdServiceInfo existingService = mServiceMap.get(key);
+ if (existingService == null) {
+ return false;
+ }
+ final Pair<String, String> existingTypeSubtype = getTypeAndSubtypes(
+ existingService.getServiceType());
+ final Pair<String, String> newTypeSubtype = getTypeAndSubtypes(
+ serviceInfo.getServiceType());
+ if (existingTypeSubtype == null || newTypeSubtype == null) {
+ return false;
+ }
+ final boolean existingHasNoSubtype = TextUtils.isEmpty(existingTypeSubtype.second);
+ final boolean updatedHasNoSubtype = TextUtils.isEmpty(newTypeSubtype.second);
+ if (existingHasNoSubtype && updatedHasNoSubtype) {
+ // Only allow subtype changes when subtypes are used. This ensures that this
+ // behavior does not affect most requests.
+ return false;
+ }
+
+ return Objects.equals(existingService.getServiceName(), serviceInfo.getServiceName())
+ && Objects.equals(existingTypeSubtype.first, newTypeSubtype.first);
+ }
+ }
+
+ /**
+ * Get the base type from a type specification with "_type._tcp,sub1,sub2" syntax.
+ *
+ * <p>This rejects specifications using dot syntax to specify subtypes ("_sub1._type._tcp").
+ *
+ * @return Type and comma-separated list of subtypes, or null if invalid format.
+ */
+ @Nullable
+ private static Pair<String, String> getTypeAndSubtypes(@Nullable String typeWithSubtype) {
+ if (typeWithSubtype == null) {
+ return null;
+ }
+ final Matcher matcher = Pattern.compile(TYPE_REGEX).matcher(typeWithSubtype);
+ if (!matcher.matches()) return null;
+ // Reject specifications using leading subtypes with a dot
+ if (!TextUtils.isEmpty(matcher.group(1))) return null;
+ return new Pair<>(matcher.group(2), matcher.group(3));
+ }
+
+ /**
+ * Register a service to be discovered by other services.
+ *
+ * <p> The function call immediately returns after sending a request to register service
+ * to the framework. The application is notified of a successful registration
+ * through the callback {@link RegistrationListener#onServiceRegistered} or a failure
+ * through {@link RegistrationListener#onRegistrationFailed}.
+ *
+ * <p> The application should call {@link #unregisterService} when the service
+ * registration is no longer required, and/or whenever the application is stopped.
+ * @param advertisingRequest service being registered
+ * @param executor Executor to run listener callbacks with
+ * @param listener The listener notifies of a successful registration and is used to
+ * unregister this service through a call on {@link #unregisterService}. Cannot be null.
+ *
+ * @hide
+ */
+// @FlaggedApi(Flags.ADVERTISE_REQUEST_API)
+ public void registerService(@NonNull AdvertisingRequest advertisingRequest,
+ @NonNull Executor executor,
+ @NonNull RegistrationListener listener) {
+ final NsdServiceInfo serviceInfo = advertisingRequest.getServiceInfo();
+ final int protocolType = advertisingRequest.getProtocolType();
+ checkServiceInfoForRegistration(serviceInfo);
+ checkProtocol(protocolType);
+ final int key;
+ // For update only request, the old listener has to be reused
+ if ((advertisingRequest.getAdvertisingConfig()
+ & AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY) > 0) {
+ key = updateRegisteredListener(listener, executor, serviceInfo);
+ } else {
+ key = putListener(listener, executor, serviceInfo);
+ }
try {
- mService.registerService(key, serviceInfo);
+ mService.registerService(key, advertisingRequest);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
@@ -1261,15 +1473,44 @@
if (TextUtils.isEmpty(serviceType)) {
throw new IllegalArgumentException("Service type cannot be empty");
}
- checkProtocol(protocolType);
+ DiscoveryRequest request = new DiscoveryRequest.Builder(protocolType, serviceType)
+ .setNetwork(network).build();
+ discoverServices(request, executor, listener);
+ }
- NsdServiceInfo s = new NsdServiceInfo();
- s.setServiceType(serviceType);
- s.setNetwork(network);
-
- int key = putListener(listener, executor, s);
+ /**
+ * Initiates service discovery to browse for instances of a service type. Service discovery
+ * consumes network bandwidth and will continue until the application calls
+ * {@link #stopServiceDiscovery}.
+ *
+ * <p> The function call immediately returns after sending a request to start service
+ * discovery to the framework. The application is notified of a success to initiate
+ * discovery through the callback {@link DiscoveryListener#onDiscoveryStarted} or a failure
+ * through {@link DiscoveryListener#onStartDiscoveryFailed}.
+ *
+ * <p> Upon successful start, application is notified when a service is found with
+ * {@link DiscoveryListener#onServiceFound} or when a service is lost with
+ * {@link DiscoveryListener#onServiceLost}.
+ *
+ * <p> Upon failure to start, service discovery is not active and application does
+ * not need to invoke {@link #stopServiceDiscovery}
+ *
+ * <p> The application should call {@link #stopServiceDiscovery} when discovery of this
+ * service type is no longer required, and/or whenever the application is paused or
+ * stopped.
+ *
+ * @param discoveryRequest the {@link DiscoveryRequest} object which specifies the discovery
+ * parameters such as service type, subtype and network
+ * @param executor Executor to run listener callbacks with
+ * @param listener The listener notifies of a successful discovery and is used
+ * to stop discovery on this serviceType through a call on {@link #stopServiceDiscovery}.
+ */
+ @FlaggedApi(Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+ public void discoverServices(@NonNull DiscoveryRequest discoveryRequest,
+ @NonNull Executor executor, @NonNull DiscoveryListener listener) {
+ int key = putListener(listener, executor, discoveryRequest);
try {
- mService.discoverServices(key, s);
+ mService.discoverServices(key, discoveryRequest);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
@@ -1320,12 +1561,10 @@
throw new IllegalArgumentException("Service type cannot be empty");
}
Objects.requireNonNull(networkRequest, "NetworkRequest cannot be null");
- checkProtocol(protocolType);
+ DiscoveryRequest discoveryRequest =
+ new DiscoveryRequest.Builder(protocolType, serviceType).build();
- NsdServiceInfo s = new NsdServiceInfo();
- s.setServiceType(serviceType);
-
- final int baseListenerKey = putListener(listener, executor, s);
+ final int baseListenerKey = putListener(listener, executor, discoveryRequest);
final PerNetworkDiscoveryTracker discoveryInfo = new PerNetworkDiscoveryTracker(
serviceType, protocolType, executor, listener);
@@ -1406,7 +1645,7 @@
@Deprecated
public void resolveService(@NonNull NsdServiceInfo serviceInfo,
@NonNull Executor executor, @NonNull ResolveListener listener) {
- checkServiceInfo(serviceInfo);
+ checkServiceInfoForResolution(serviceInfo);
int key = putListener(listener, executor, serviceInfo);
try {
mService.resolveService(key, serviceInfo);
@@ -1457,9 +1696,10 @@
* @param executor Executor to run callbacks with
* @param listener to receive callback upon service update
*/
+ // TODO: use {@link DiscoveryRequest} to specify the service to be subscribed
public void registerServiceInfoCallback(@NonNull NsdServiceInfo serviceInfo,
@NonNull Executor executor, @NonNull ServiceInfoCallback listener) {
- checkServiceInfo(serviceInfo);
+ checkServiceInfoForResolution(serviceInfo);
int key = putListener(listener, executor, serviceInfo);
try {
mService.registerServiceInfoCallback(key, serviceInfo);
@@ -1498,13 +1738,13 @@
Objects.requireNonNull(listener, "listener cannot be null");
}
- private static void checkProtocol(int protocolType) {
+ static void checkProtocol(int protocolType) {
if (protocolType != PROTOCOL_DNS_SD) {
throw new IllegalArgumentException("Unsupported protocol");
}
}
- private static void checkServiceInfo(NsdServiceInfo serviceInfo) {
+ private static void checkServiceInfoForResolution(NsdServiceInfo serviceInfo) {
Objects.requireNonNull(serviceInfo, "NsdServiceInfo cannot be null");
if (TextUtils.isEmpty(serviceInfo.getServiceName())) {
throw new IllegalArgumentException("Service name cannot be empty");
@@ -1513,4 +1753,46 @@
throw new IllegalArgumentException("Service type cannot be empty");
}
}
+
+ /**
+ * Check if the {@link NsdServiceInfo} is valid for registration.
+ *
+ * The following can be registered:
+ * - A service with an optional host.
+ * - A hostname with addresses.
+ *
+ * Note that:
+ * - When registering a service, the service name, service type and port must be specified. If
+ * hostname is specified, the host addresses can optionally be specified.
+ * - When registering a host without a service, the addresses must be specified.
+ *
+ * @hide
+ */
+ public static void checkServiceInfoForRegistration(NsdServiceInfo serviceInfo) {
+ Objects.requireNonNull(serviceInfo, "NsdServiceInfo cannot be null");
+ boolean hasServiceName = !TextUtils.isEmpty(serviceInfo.getServiceName());
+ boolean hasServiceType = !TextUtils.isEmpty(serviceInfo.getServiceType());
+ boolean hasHostname = !TextUtils.isEmpty(serviceInfo.getHostname());
+ boolean hasHostAddresses = !CollectionUtils.isEmpty(serviceInfo.getHostAddresses());
+
+ if (serviceInfo.getPort() < 0) {
+ throw new IllegalArgumentException("Invalid port");
+ }
+
+ if (hasServiceType || hasServiceName || (serviceInfo.getPort() > 0)) {
+ if (!(hasServiceType && hasServiceName && (serviceInfo.getPort() > 0))) {
+ throw new IllegalArgumentException(
+ "The service type, service name or port is missing");
+ }
+ }
+
+ if (!hasServiceType && !hasHostname) {
+ throw new IllegalArgumentException("No service or host specified in NsdServiceInfo");
+ }
+
+ if (!hasServiceType && hasHostname && !hasHostAddresses) {
+ // TODO: b/317946010 - This may be allowed when it supports registering KEY RR.
+ throw new IllegalArgumentException("No host addresses specified in NsdServiceInfo");
+ }
+ }
}
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index caeecdd..f4cc2ac 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -16,6 +16,7 @@
package android.net.nsd;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
@@ -24,6 +25,7 @@
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.ArrayMap;
+import android.util.ArraySet;
import android.util.Log;
import com.android.net.module.util.InetAddressUtils;
@@ -31,10 +33,12 @@
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.Set;
/**
* A class representing service information for network service discovery
@@ -44,13 +48,20 @@
private static final String TAG = "NsdServiceInfo";
+ @Nullable
private String mServiceName;
+ @Nullable
private String mServiceType;
- private final ArrayMap<String, byte[]> mTxtRecord = new ArrayMap<>();
+ private final Set<String> mSubtypes;
- private final List<InetAddress> mHostAddresses = new ArrayList<>();
+ private final ArrayMap<String, byte[]> mTxtRecord;
+
+ private final List<InetAddress> mHostAddresses;
+
+ @Nullable
+ private String mHostname;
private int mPort;
@@ -59,15 +70,41 @@
private int mInterfaceIndex;
+ // The timestamp that all resource records associated with this service are considered invalid.
+ @Nullable
+ private Instant mExpirationTime;
+
public NsdServiceInfo() {
+ mSubtypes = new ArraySet<>();
+ mTxtRecord = new ArrayMap<>();
+ mHostAddresses = new ArrayList<>();
}
/** @hide */
public NsdServiceInfo(String sn, String rt) {
+ this();
mServiceName = sn;
mServiceType = rt;
}
+ /**
+ * Creates a copy of {@code other}.
+ *
+ * @hide
+ */
+ public NsdServiceInfo(@NonNull NsdServiceInfo other) {
+ mServiceName = other.getServiceName();
+ mServiceType = other.getServiceType();
+ mSubtypes = new ArraySet<>(other.getSubtypes());
+ mTxtRecord = new ArrayMap<>(other.mTxtRecord);
+ mHostAddresses = new ArrayList<>(other.getHostAddresses());
+ mHostname = other.getHostname();
+ mPort = other.getPort();
+ mNetwork = other.getNetwork();
+ mInterfaceIndex = other.getInterfaceIndex();
+ mExpirationTime = other.getExpirationTime();
+ }
+
/** Get the service name */
public String getServiceName() {
return mServiceName;
@@ -142,6 +179,43 @@
}
/**
+ * Get the hostname.
+ *
+ * <p>When a service is resolved, it returns the hostname of the resolved service . The top
+ * level domain ".local." is omitted.
+ *
+ * <p>For example, it returns "MyHost" when the service's hostname is "MyHost.local.".
+ *
+ * @hide
+ */
+// @FlaggedApi(NsdManager.Flags.NSD_CUSTOM_HOSTNAME_ENABLED)
+ @Nullable
+ public String getHostname() {
+ return mHostname;
+ }
+
+ /**
+ * Set a custom hostname for this service instance for registration.
+ *
+ * <p>A hostname must be in ".local." domain. The ".local." must be omitted when calling this
+ * method.
+ *
+ * <p>For example, you should call setHostname("MyHost") to use the hostname "MyHost.local.".
+ *
+ * <p>If a hostname is set with this method, the addresses set with {@link #setHostAddresses}
+ * will be registered with the hostname.
+ *
+ * <p>If the hostname is null (which is the default for a new {@link NsdServiceInfo}), a random
+ * hostname is used and the addresses of this device will be registered.
+ *
+ * @hide
+ */
+// @FlaggedApi(NsdManager.Flags.NSD_CUSTOM_HOSTNAME_ENABLED)
+ public void setHostname(@Nullable String hostname) {
+ mHostname = hostname;
+ }
+
+ /**
* Unpack txt information from a base-64 encoded byte array.
*
* @param txtRecordsRawBytes The raw base64 encoded byte array.
@@ -391,14 +465,78 @@
mInterfaceIndex = interfaceIndex;
}
+ /**
+ * Sets the subtypes to be advertised for this service instance.
+ *
+ * The elements in {@code subtypes} should be the subtype identifiers which have the trailing
+ * "._sub" removed. For example, the subtype should be "_printer" for
+ * "_printer._sub._http._tcp.local".
+ *
+ * Only one subtype will be registered if multiple elements of {@code subtypes} have the same
+ * case-insensitive value.
+ */
+ @FlaggedApi(NsdManager.Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+ public void setSubtypes(@NonNull Set<String> subtypes) {
+ mSubtypes.clear();
+ mSubtypes.addAll(subtypes);
+ }
+
+ /**
+ * Returns subtypes of this service instance.
+ *
+ * When this object is returned by the service discovery/browse APIs (etc. {@link
+ * NsdManager.DiscoveryListener}), the return value may or may not include the subtypes of this
+ * service.
+ */
+ @FlaggedApi(NsdManager.Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+ @NonNull
+ public Set<String> getSubtypes() {
+ return Collections.unmodifiableSet(mSubtypes);
+ }
+
+ /**
+ * Sets the timestamp after when this service is expired.
+ *
+ * Note: only number of seconds of {@code expirationTime} is used.
+ *
+ * @hide
+ */
+ public void setExpirationTime(@Nullable Instant expirationTime) {
+ if (expirationTime == null) {
+ mExpirationTime = null;
+ } else {
+ mExpirationTime = Instant.ofEpochSecond(expirationTime.getEpochSecond());
+ }
+ }
+
+ /**
+ * Returns the timestamp after when this service is expired or {@code null} if it's unknown.
+ *
+ * A service is considered expired if any of its DNS record is expired.
+ *
+ * Clients that are depending on the refreshness of the service information should not continue
+ * use this service after the returned timestamp. Instead, clients may re-send queries for the
+ * service to get updated the service information.
+ *
+ * @hide
+ */
+ // @FlaggedApi(NsdManager.Flags.NSD_CUSTOM_TTL_ENABLED)
+ @Nullable
+ public Instant getExpirationTime() {
+ return mExpirationTime;
+ }
+
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("name: ").append(mServiceName)
.append(", type: ").append(mServiceType)
+ .append(", subtypes: ").append(TextUtils.join(", ", mSubtypes))
.append(", hostAddresses: ").append(TextUtils.join(", ", mHostAddresses))
+ .append(", hostname: ").append(mHostname)
.append(", port: ").append(mPort)
- .append(", network: ").append(mNetwork);
+ .append(", network: ").append(mNetwork)
+ .append(", expirationTime: ").append(mExpirationTime);
byte[] txtRecord = getTxtRecord();
sb.append(", txtRecord: ").append(new String(txtRecord, StandardCharsets.UTF_8));
@@ -414,6 +552,7 @@
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mServiceName);
dest.writeString(mServiceType);
+ dest.writeStringList(new ArrayList<>(mSubtypes));
dest.writeInt(mPort);
// TXT record key/value pairs.
@@ -436,6 +575,8 @@
for (InetAddress address : mHostAddresses) {
InetAddressUtils.parcelInetAddress(dest, address, flags);
}
+ dest.writeString(mHostname);
+ dest.writeLong(mExpirationTime != null ? mExpirationTime.getEpochSecond() : -1);
}
/** Implement the Parcelable interface */
@@ -445,6 +586,7 @@
NsdServiceInfo info = new NsdServiceInfo();
info.mServiceName = in.readString();
info.mServiceType = in.readString();
+ info.setSubtypes(new ArraySet<>(in.createStringArrayList()));
info.mPort = in.readInt();
// TXT record key/value pairs.
@@ -464,6 +606,9 @@
for (int i = 0; i < size; i++) {
info.mHostAddresses.add(InetAddressUtils.unparcelInetAddress(in));
}
+ info.mHostname = in.readString();
+ final long seconds = in.readLong();
+ info.setExpirationTime(seconds < 0 ? null : Instant.ofEpochSecond(seconds));
return info;
}
diff --git a/framework/Android.bp b/framework/Android.bp
index 1e6262d..52f2c7c 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -105,7 +106,6 @@
apex_available: [
"com.android.tethering",
],
- lint: { strict_updatability_linting: true },
}
java_library {
@@ -134,7 +134,7 @@
"framework-tethering.impl",
"framework-wifi.stubs.module_lib",
],
- visibility: ["//packages/modules/Connectivity:__subpackages__"]
+ visibility: ["//packages/modules/Connectivity:__subpackages__"],
}
java_defaults {
@@ -165,17 +165,16 @@
"//packages/modules/Connectivity/framework-t",
"//packages/modules/Connectivity/service",
"//packages/modules/Connectivity/service-t",
- "//frameworks/base/packages/Connectivity/service",
"//frameworks/base",
// Tests using hidden APIs
"//cts/tests/netlegacy22.api",
"//cts/tests/tests/app.usage", // NetworkUsageStatsTest
"//external/sl4a:__subpackages__",
- "//frameworks/base/packages/Connectivity/tests:__subpackages__",
"//frameworks/base/core/tests/bandwidthtests",
"//frameworks/base/core/tests/benchmarks",
"//frameworks/base/core/tests/utillib",
+ "//frameworks/base/services/tests/VpnTests",
"//frameworks/base/tests/vcn",
"//frameworks/opt/net/ethernet/tests:__subpackages__",
"//frameworks/opt/telephony/tests/telephonytests",
@@ -185,10 +184,17 @@
"//packages/modules/Connectivity/Cronet/tests:__subpackages__",
"//packages/modules/Connectivity/Tethering/tests:__subpackages__",
"//packages/modules/Connectivity/tests:__subpackages__",
+ "//packages/modules/Connectivity/thread/tests:__subpackages__",
"//packages/modules/IPsec/tests/iketests",
"//packages/modules/NetworkStack/tests:__subpackages__",
"//packages/modules/Wifi/service/tests/wifitests",
],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
+ aconfig_declarations: [
+ "com.android.net.flags-aconfig",
+ ],
}
platform_compat_config {
@@ -293,9 +299,9 @@
],
flags: [
"--show-for-stub-purposes-annotation android.annotation.SystemApi" +
- "\\(client=android.annotation.SystemApi.Client.PRIVILEGED_APPS\\)",
+ "\\(client=android.annotation.SystemApi.Client.PRIVILEGED_APPS\\)",
"--show-for-stub-purposes-annotation android.annotation.SystemApi" +
- "\\(client=android.annotation.SystemApi.Client.MODULE_LIBRARIES\\)",
+ "\\(client=android.annotation.SystemApi.Client.MODULE_LIBRARIES\\)",
],
aidl: {
include_dirs: [
diff --git a/framework/aidl-export/android/net/TetheringManager.aidl b/framework/aidl-export/android/net/TetheringManager.aidl
new file mode 100644
index 0000000..1235722
--- /dev/null
+++ b/framework/aidl-export/android/net/TetheringManager.aidl
@@ -0,0 +1,20 @@
+/**
+ *
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+parcelable TetheringManager.TetheringRequest;
diff --git a/framework/aidl-export/android/net/nsd/AdvertisingRequest.aidl b/framework/aidl-export/android/net/nsd/AdvertisingRequest.aidl
new file mode 100644
index 0000000..2848074
--- /dev/null
+++ b/framework/aidl-export/android/net/nsd/AdvertisingRequest.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.nsd;
+
+@JavaOnlyStableParcelable parcelable AdvertisingRequest;
\ No newline at end of file
diff --git a/framework/aidl-export/android/net/nsd/DiscoveryRequest.aidl b/framework/aidl-export/android/net/nsd/DiscoveryRequest.aidl
new file mode 100644
index 0000000..481a066
--- /dev/null
+++ b/framework/aidl-export/android/net/nsd/DiscoveryRequest.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.nsd;
+
+@JavaOnlyStableParcelable parcelable DiscoveryRequest;
diff --git a/framework/api/current.txt b/framework/api/current.txt
index 6860c3c..ef8415c 100644
--- a/framework/api/current.txt
+++ b/framework/api/current.txt
@@ -315,6 +315,7 @@
method @Nullable public android.net.NetworkSpecifier getNetworkSpecifier();
method public int getOwnerUid();
method public int getSignalStrength();
+ method @FlaggedApi("com.android.net.flags.request_restricted_wifi") @NonNull public java.util.Set<java.lang.Integer> getSubscriptionIds();
method @Nullable public android.net.TransportInfo getTransportInfo();
method public boolean hasCapability(int);
method public boolean hasEnterpriseId(int);
@@ -332,6 +333,7 @@
field public static final int NET_CAPABILITY_IA = 7; // 0x7
field public static final int NET_CAPABILITY_IMS = 4; // 0x4
field public static final int NET_CAPABILITY_INTERNET = 12; // 0xc
+ field @FlaggedApi("com.android.net.flags.net_capability_local_network") public static final int NET_CAPABILITY_LOCAL_NETWORK = 36; // 0x24
field public static final int NET_CAPABILITY_MCX = 23; // 0x17
field public static final int NET_CAPABILITY_MMS = 0; // 0x0
field public static final int NET_CAPABILITY_MMTEL = 33; // 0x21
@@ -360,6 +362,7 @@
field public static final int TRANSPORT_CELLULAR = 0; // 0x0
field public static final int TRANSPORT_ETHERNET = 3; // 0x3
field public static final int TRANSPORT_LOWPAN = 6; // 0x6
+ field @FlaggedApi("com.android.net.flags.support_transport_satellite") public static final int TRANSPORT_SATELLITE = 10; // 0xa
field public static final int TRANSPORT_THREAD = 9; // 0x9
field public static final int TRANSPORT_USB = 8; // 0x8
field public static final int TRANSPORT_VPN = 4; // 0x4
@@ -418,6 +421,7 @@
method public int describeContents();
method @NonNull public int[] getCapabilities();
method @Nullable public android.net.NetworkSpecifier getNetworkSpecifier();
+ method @FlaggedApi("com.android.net.flags.request_restricted_wifi") @NonNull public java.util.Set<java.lang.Integer> getSubscriptionIds();
method @NonNull public int[] getTransportTypes();
method public boolean hasCapability(int);
method public boolean hasTransport(int);
@@ -437,6 +441,7 @@
method @NonNull public android.net.NetworkRequest.Builder setIncludeOtherUidNetworks(boolean);
method @Deprecated public android.net.NetworkRequest.Builder setNetworkSpecifier(String);
method public android.net.NetworkRequest.Builder setNetworkSpecifier(android.net.NetworkSpecifier);
+ method @FlaggedApi("com.android.net.flags.request_restricted_wifi") @NonNull public android.net.NetworkRequest.Builder setSubscriptionIds(@NonNull java.util.Set<java.lang.Integer>);
}
public class ParseException extends java.lang.RuntimeException {
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index e812024..bef29a4 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -307,7 +307,6 @@
method @NonNull public int[] getAdministratorUids();
method @Nullable public static String getCapabilityCarrierName(int);
method @Nullable public String getSsid();
- method @NonNull public java.util.Set<java.lang.Integer> getSubscriptionIds();
method @NonNull public int[] getTransportTypes();
method @Nullable public java.util.List<android.net.Network> getUnderlyingNetworks();
method public boolean isPrivateDnsBroken();
@@ -373,7 +372,6 @@
public static class NetworkRequest.Builder {
method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP) public android.net.NetworkRequest.Builder setSignalStrength(int);
- method @NonNull public android.net.NetworkRequest.Builder setSubscriptionIds(@NonNull java.util.Set<java.lang.Integer>);
}
public final class NetworkScore implements android.os.Parcelable {
diff --git a/framework/jni/android_net_NetworkUtils.cpp b/framework/jni/android_net_NetworkUtils.cpp
index 5403be7..3779a00 100644
--- a/framework/jni/android_net_NetworkUtils.cpp
+++ b/framework/jni/android_net_NetworkUtils.cpp
@@ -24,6 +24,7 @@
#include <string.h>
#include <bpf/BpfClassic.h>
+#include <bpf/KernelUtils.h>
#include <DnsProxydProtocol.h> // NETID_USE_LOCAL_NAMESERVERS
#include <nativehelper/JNIPlatformHelp.h>
#include <nativehelper/ScopedPrimitiveArray.h>
@@ -250,6 +251,14 @@
}
}
+static jboolean android_net_utils_isKernel64Bit(JNIEnv *env, jclass clazz) {
+ return bpf::isKernel64Bit();
+}
+
+static jboolean android_net_utils_isKernelX86(JNIEnv *env, jclass clazz) {
+ return bpf::isX86();
+}
+
// ----------------------------------------------------------------------------
/*
@@ -272,6 +281,8 @@
{ "getDnsNetwork", "()Landroid/net/Network;", (void*) android_net_utils_getDnsNetwork },
{ "setsockoptBytes", "(Ljava/io/FileDescriptor;II[B)V",
(void*) android_net_utils_setsockoptBytes},
+ { "isKernel64Bit", "()Z", (void*) android_net_utils_isKernel64Bit },
+ { "isKernelX86", "()Z", (void*) android_net_utils_isKernelX86 },
};
// clang-format on
diff --git a/framework/lint-baseline.xml b/framework/lint-baseline.xml
index f68aad7..2c0b15f 100644
--- a/framework/lint-baseline.xml
+++ b/framework/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
<issue
id="NewApi"
@@ -8,78 +8,177 @@
errorLine2=" ~~~~~~~~~~~~~">
<location
file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
- line="2456"
+ line="2490"
column="71"/>
</issue>
<issue
id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.Proxy#setHttpProxyConfiguration`"
- errorLine1=" Proxy.setHttpProxyConfiguration(getInstance().getDefaultProxy());"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
- line="5323"
- column="23"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.os.Build#isDebuggable`"
- errorLine1=" if (!Build.isDebuggable()) {"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/framework/src/android/net/ConnectivitySettingsManager.java"
- line="1072"
- column="24"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.os.UserHandle#getUid`"
- errorLine1=" final int end = nextUser.getUid(0 /* appId */) - 1;"
- errorLine2=" ~~~~~~">
- <location
- file="packages/modules/Connectivity/framework/src/android/net/UidRange.java"
- line="50"
- column="34"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.os.UserHandle#getUid`"
- errorLine1=" final int start = user.getUid(0 /* appId */);"
- errorLine2=" ~~~~~~">
- <location
- file="packages/modules/Connectivity/framework/src/android/net/UidRange.java"
- line="49"
- column="32"/>
- </issue>
-
- <issue
- id="NewApi"
message="Call requires API level 31 (current min is 30): `android.provider.Settings#checkAndNoteWriteSettingsOperation`"
errorLine1=" return Settings.checkAndNoteWriteSettingsOperation(context, uid, callingPackage,"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
- line="2799"
+ line="2853"
column="25"/>
</issue>
<issue
id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.Proxy#setHttpProxyConfiguration`"
+ errorLine1=" Proxy.setHttpProxyConfiguration(getInstance().getDefaultProxy());"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
+ line="5422"
+ column="23"/>
+ </issue>
+
+ <issue
+ id="NewApi"
message="Call requires API level 31 (current min is 30): `java.net.InetAddress#clearDnsCache`"
errorLine1=" InetAddress.clearDnsCache();"
errorLine2=" ~~~~~~~~~~~~~">
<location
file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
- line="5329"
+ line="5428"
column="25"/>
</issue>
<issue
id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.net.event.NetworkEventDispatcher#dispatchNetworkConfigurationChange`"
+ errorLine1=" NetworkEventDispatcher.getInstance().dispatchNetworkConfigurationChange();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
+ line="5431"
+ column="50"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.net.event.NetworkEventDispatcher#getInstance`"
+ errorLine1=" NetworkEventDispatcher.getInstance().dispatchNetworkConfigurationChange();"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
+ line="5431"
+ column="36"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.Build#isDebuggable`"
+ errorLine1=" if (!Build.isDebuggable()) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/ConnectivitySettingsManager.java"
+ line="1095"
+ column="24"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 30): `android.system.OsConstants#ENONET`"
+ errorLine1=' new DnsException(ERROR_SYSTEM, new ErrnoException("resNetworkQuery", ENONET))));'
+ errorLine2=" ~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/DnsResolver.java"
+ line="367"
+ column="90"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(socket);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/util/DnsUtils.java"
+ line="181"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(socket);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/util/DnsUtils.java"
+ line="373"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(is);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
+ line="171"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(zos);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
+ line="178"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(bis);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
+ line="401"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(bos);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
+ line="416"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.net.InetAddressUtils#isNumericAddress`"
+ errorLine1=" return InetAddressUtils.isNumericAddress(address);"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/InetAddresses.java"
+ line="46"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.net.InetAddressUtils#parseNumericAddress`"
+ errorLine1=" return InetAddressUtils.parseNumericAddress(address);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/InetAddresses.java"
+ line="63"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="NewApi"
message="Call requires API level 31 (current min is 30): `java.net.InetAddress#getAllByNameOnNet`"
errorLine1=" return InetAddress.getAllByNameOnNet(host, getNetIdForResolv());"
errorLine2=" ~~~~~~~~~~~~~~~~~">
@@ -103,17 +202,6 @@
<issue
id="NewApi"
message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
- errorLine1=" IoUtils.closeQuietly(is);"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
- line="168"
- column="33"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
errorLine1=" if (failed) IoUtils.closeQuietly(socket);"
errorLine2=" ~~~~~~~~~~~~">
<location
@@ -157,105 +245,6 @@
<issue
id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
- errorLine1=" IoUtils.closeQuietly(bis);"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
- line="391"
- column="21"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
- errorLine1=" IoUtils.closeQuietly(bos);"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
- line="406"
- column="21"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
- errorLine1=" IoUtils.closeQuietly(socket);"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/framework/src/android/net/util/DnsUtils.java"
- line="181"
- column="21"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
- errorLine1=" IoUtils.closeQuietly(socket);"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/framework/src/android/net/util/DnsUtils.java"
- line="373"
- column="21"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
- errorLine1=" IoUtils.closeQuietly(zos);"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
- line="175"
- column="21"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.net.InetAddressUtils#isNumericAddress`"
- errorLine1=" return InetAddressUtils.isNumericAddress(address);"
- errorLine2=" ~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/framework/src/android/net/InetAddresses.java"
- line="46"
- column="33"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.net.InetAddressUtils#parseNumericAddress`"
- errorLine1=" return InetAddressUtils.parseNumericAddress(address);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/framework/src/android/net/InetAddresses.java"
- line="63"
- column="33"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.net.event.NetworkEventDispatcher#dispatchNetworkConfigurationChange`"
- errorLine1=" NetworkEventDispatcher.getInstance().dispatchNetworkConfigurationChange();"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
- line="5332"
- column="50"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.net.event.NetworkEventDispatcher#getInstance`"
- errorLine1=" NetworkEventDispatcher.getInstance().dispatchNetworkConfigurationChange();"
- errorLine2=" ~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
- line="5332"
- column="36"/>
- </issue>
-
- <issue
- id="NewApi"
message="Call requires API level 31 (current min is 30): `libcore.net.http.HttpURLConnectionFactory#createInstance`"
errorLine1=" HttpURLConnectionFactory urlConnectionFactory = HttpURLConnectionFactory.createInstance();"
errorLine2=" ~~~~~~~~~~~~~~">
@@ -267,17 +256,6 @@
<issue
id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.net.http.HttpURLConnectionFactory#openConnection`"
- errorLine1=" return urlConnectionFactory.openConnection(url, socketFactory, proxy);"
- errorLine2=" ~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/framework/src/android/net/Network.java"
- line="372"
- column="37"/>
- </issue>
-
- <issue
- id="NewApi"
message="Call requires API level 31 (current min is 30): `libcore.net.http.HttpURLConnectionFactory#setDns`"
errorLine1=" urlConnectionFactory.setDns(dnsLookup); // Let traffic go via dnsLookup"
errorLine2=" ~~~~~~">
@@ -300,35 +278,13 @@
<issue
id="NewApi"
- message="Call requires API level 31 (current min is 30): `new android.net.EthernetNetworkSpecifier`"
- errorLine1=" return setNetworkSpecifier(new EthernetNetworkSpecifier(networkSpecifier));"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ message="Call requires API level 31 (current min is 30): `libcore.net.http.HttpURLConnectionFactory#openConnection`"
+ errorLine1=" return urlConnectionFactory.openConnection(url, socketFactory, proxy);"
+ errorLine2=" ~~~~~~~~~~~~~~">
<location
- file="packages/modules/Connectivity/framework/src/android/net/NetworkRequest.java"
- line="525"
- column="48"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Cast from `EthernetNetworkSpecifier` to `NetworkSpecifier` requires API level 31 (current min is 30)"
- errorLine1=" return setNetworkSpecifier(new EthernetNetworkSpecifier(networkSpecifier));"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/framework/src/android/net/NetworkRequest.java"
- line="525"
- column="48"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Class requires API level 31 (current min is 30): `android.telephony.data.EpsBearerQosSessionAttributes`"
- errorLine1=" (EpsBearerQosSessionAttributes)attributes));"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/framework/src/android/net/NetworkAgent.java"
- line="1421"
- column="22"/>
+ file="packages/modules/Connectivity/framework/src/android/net/Network.java"
+ line="372"
+ column="37"/>
</issue>
<issue
@@ -338,18 +294,18 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="packages/modules/Connectivity/framework/src/android/net/NetworkAgent.java"
- line="1418"
+ line="1462"
column="35"/>
</issue>
<issue
id="NewApi"
- message="Class requires API level 31 (current min is 30): `android.telephony.data.NrQosSessionAttributes`"
- errorLine1=" (NrQosSessionAttributes)attributes));"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ message="Class requires API level 31 (current min is 30): `android.telephony.data.EpsBearerQosSessionAttributes`"
+ errorLine1=" (EpsBearerQosSessionAttributes)attributes));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="packages/modules/Connectivity/framework/src/android/net/NetworkAgent.java"
- line="1425"
+ line="1465"
column="22"/>
</issue>
@@ -360,8 +316,63 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location
file="packages/modules/Connectivity/framework/src/android/net/NetworkAgent.java"
- line="1422"
+ line="1466"
column="42"/>
</issue>
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.telephony.data.NrQosSessionAttributes`"
+ errorLine1=" (NrQosSessionAttributes)attributes));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/NetworkAgent.java"
+ line="1469"
+ column="22"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `new android.net.EthernetNetworkSpecifier`"
+ errorLine1=" return setNetworkSpecifier(new EthernetNetworkSpecifier(networkSpecifier));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/NetworkRequest.java"
+ line="553"
+ column="48"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Cast from `EthernetNetworkSpecifier` to `NetworkSpecifier` requires API level 31 (current min is 30)"
+ errorLine1=" return setNetworkSpecifier(new EthernetNetworkSpecifier(networkSpecifier));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/NetworkRequest.java"
+ line="553"
+ column="48"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.UserHandle#getUid`"
+ errorLine1=" final int start = user.getUid(0 /* appId */);"
+ errorLine2=" ~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/UidRange.java"
+ line="49"
+ column="32"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.UserHandle#getUid`"
+ errorLine1=" final int end = nextUser.getUid(0 /* appId */) - 1;"
+ errorLine2=" ~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/UidRange.java"
+ line="50"
+ column="34"/>
+ </issue>
+
</issues>
\ No newline at end of file
diff --git a/framework/src/android/net/BpfNetMapsUtils.java b/framework/src/android/net/BpfNetMapsUtils.java
index 11d610c..0be30bb 100644
--- a/framework/src/android/net/BpfNetMapsUtils.java
+++ b/framework/src/android/net/BpfNetMapsUtils.java
@@ -125,13 +125,11 @@
return sj.toString();
}
- public static final boolean PRE_T = !SdkLevel.isAtLeastT();
-
/**
* Throw UnsupportedOperationException if SdkLevel is before T.
*/
public static void throwIfPreT(final String msg) {
- if (PRE_T) {
+ if (!SdkLevel.isAtLeastT()) {
throw new UnsupportedOperationException(msg);
}
}
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index fa27d0e..915ec52 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -74,6 +74,7 @@
import android.util.SparseIntArray;
import com.android.internal.annotations.GuardedBy;
+import com.android.modules.utils.build.SdkLevel;
import libcore.net.event.NetworkEventDispatcher;
@@ -6022,6 +6023,13 @@
/**
* Sets data saver switch.
*
+ * <p>This API configures the bandwidth control, and filling data saver status in BpfMap,
+ * which is intended for internal use by the network stack to optimize performance
+ * when frequently checking data saver status for multiple uids without doing IPC.
+ * It does not directly control the global data saver mode that users manage in settings.
+ * To query the comprehensive data saver status for a specific UID, including allowlist
+ * considerations, use {@link #getRestrictBackgroundStatus}.
+ *
* @param enable True if enable.
* @throws IllegalStateException if failed.
* @hide
@@ -6271,9 +6279,13 @@
// Only the system server process and the network stack have access.
@FlaggedApi(Flags.SUPPORT_IS_UID_NETWORKING_BLOCKED)
@SystemApi(client = MODULE_LIBRARIES)
- @RequiresApi(Build.VERSION_CODES.TIRAMISU) // BPF maps were only mainlined in T
+ // Note b/326143935 kernel bug can trigger crash on some T device.
+ @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
@RequiresPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
public boolean isUidNetworkingBlocked(int uid, boolean isNetworkMetered) {
+ if (!SdkLevel.isAtLeastU()) {
+ Log.wtf(TAG, "isUidNetworkingBlocked is not supported on pre-U devices");
+ }
final BpfNetMapsReader reader = BpfNetMapsReader.getInstance();
// Note that before V, the data saver status in bpf is written by ConnectivityService
// when receiving {@link #ACTION_RESTRICT_BACKGROUND_CHANGED}. Thus,
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index efae754..84a0d29 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -20,6 +20,7 @@
import static com.android.net.module.util.BitUtils.appendStringRepresentationOfBitMaskToStringBuilder;
import static com.android.net.module.util.BitUtils.describeDifferences;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.LongDef;
import android.annotation.NonNull;
@@ -29,9 +30,6 @@
import android.annotation.SystemApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.net.ConnectivityManager.NetworkCallback;
-// Can't be imported because aconfig tooling doesn't exist on udc-mainline-prod yet
-// See inner class Flags which mimics this for the time being
-// import android.net.flags.Flags;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
@@ -130,6 +128,12 @@
public static class Flags {
static final String FLAG_FORBIDDEN_CAPABILITY =
"com.android.net.flags.forbidden_capability";
+ static final String FLAG_NET_CAPABILITY_LOCAL_NETWORK =
+ "com.android.net.flags.net_capability_local_network";
+ static final String REQUEST_RESTRICTED_WIFI =
+ "com.android.net.flags.request_restricted_wifi";
+ static final String SUPPORT_TRANSPORT_SATELLITE =
+ "com.android.net.flags.support_transport_satellite";
}
/**
@@ -716,17 +720,24 @@
public static final int NET_CAPABILITY_PRIORITIZE_BANDWIDTH = 35;
/**
- * This is a local network, e.g. a tethering downstream or a P2P direct network.
+ * Indicates that this network is a local network.
*
- * <p>
- * Note that local networks are not sent to callbacks by default. To receive callbacks about
- * them, the {@link NetworkRequest} instance must be prepared to see them, either by
- * adding the capability with {@link NetworkRequest.Builder#addCapability}, by removing
- * this forbidden capability with {@link NetworkRequest.Builder#removeForbiddenCapability},
- * or by clearing all capabilites with {@link NetworkRequest.Builder#clearCapabilities()}.
- * </p>
- * @hide
+ * Local networks are networks where the device is not obtaining IP addresses from the
+ * network, but advertising IP addresses itself. Examples of local networks are:
+ * <ul>
+ * <li>USB tethering or Wi-Fi hotspot networks to which the device is sharing its Internet
+ * connectivity.
+ * <li>Thread networks where the current device is the Thread Border Router.
+ * <li>Wi-Fi P2P networks where the current device is the Group Owner.
+ * </ul>
+ *
+ * Networks used to obtain Internet access are never local networks.
+ *
+ * Apps that target an SDK before {@link Build.VERSION_CODES.VANILLA_ICE_CREAM} will not see
+ * networks with this capability unless they explicitly set the NET_CAPABILITY_LOCAL_NETWORK
+ * in their NetworkRequests.
*/
+ @FlaggedApi(Flags.FLAG_NET_CAPABILITY_LOCAL_NETWORK)
public static final int NET_CAPABILITY_LOCAL_NETWORK = 36;
private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_LOCAL_NETWORK;
@@ -1257,6 +1268,7 @@
TRANSPORT_TEST,
TRANSPORT_USB,
TRANSPORT_THREAD,
+ TRANSPORT_SATELLITE,
})
public @interface Transport { }
@@ -1313,10 +1325,16 @@
*/
public static final int TRANSPORT_THREAD = 9;
+ /**
+ * Indicates this network uses a Satellite transport.
+ */
+ @FlaggedApi(Flags.SUPPORT_TRANSPORT_SATELLITE)
+ public static final int TRANSPORT_SATELLITE = 10;
+
/** @hide */
public static final int MIN_TRANSPORT = TRANSPORT_CELLULAR;
/** @hide */
- public static final int MAX_TRANSPORT = TRANSPORT_THREAD;
+ public static final int MAX_TRANSPORT = TRANSPORT_SATELLITE;
private static final int ALL_VALID_TRANSPORTS;
static {
@@ -1343,6 +1361,7 @@
"TEST",
"USB",
"THREAD",
+ "SATELLITE",
};
/**
@@ -1751,9 +1770,13 @@
public @NonNull NetworkCapabilities setNetworkSpecifier(
@NonNull NetworkSpecifier networkSpecifier) {
if (networkSpecifier != null
- // Transport can be test, or test + a single other transport
+ // Transport can be test, or test + a single other transport or cellular + satellite
+ // transport. Note: cellular + satellite combination is allowed since both transport
+ // use the same specifier, TelephonyNetworkSpecifier.
&& mTransportTypes != (1L << TRANSPORT_TEST)
- && Long.bitCount(mTransportTypes & ~(1L << TRANSPORT_TEST)) != 1) {
+ && Long.bitCount(mTransportTypes & ~(1L << TRANSPORT_TEST)) != 1
+ && (mTransportTypes & ~(1L << TRANSPORT_TEST))
+ != (1 << TRANSPORT_CELLULAR | 1 << TRANSPORT_SATELLITE)) {
throw new IllegalStateException("Must have a single non-test transport specified to "
+ "use setNetworkSpecifier");
}
@@ -2794,10 +2817,9 @@
* receiver holds the NETWORK_FACTORY permission. In all other cases, it will be the empty set.
*
* @return
- * @hide
*/
@NonNull
- @SystemApi
+ @FlaggedApi(Flags.REQUEST_RESTRICTED_WIFI)
public Set<Integer> getSubscriptionIds() {
return new ArraySet<>(mSubIds);
}
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index 653e41d..4de02ac 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -34,6 +34,7 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -145,6 +146,12 @@
* Look up the specific capability to learn whether its usage requires this self-certification.
*/
public class NetworkRequest implements Parcelable {
+
+ /** @hide */
+ public static class Flags {
+ static final String REQUEST_RESTRICTED_WIFI =
+ "com.android.net.flags.request_restricted_wifi";
+ }
/**
* The first requestId value that will be allocated.
* @hide only used by ConnectivityService.
@@ -630,10 +637,9 @@
* NETWORK_FACTORY permission.
*
* @param subIds A {@code Set} that represents subscription IDs.
- * @hide
*/
@NonNull
- @SystemApi
+ @FlaggedApi(Flags.REQUEST_RESTRICTED_WIFI)
public Builder setSubscriptionIds(@NonNull Set<Integer> subIds) {
mNetworkCapabilities.setSubscriptionIds(subIds);
return this;
@@ -890,4 +896,17 @@
// a new array.
return networkCapabilities.getTransportTypes();
}
+
+ /**
+ * Gets all the subscription ids set on this {@code NetworkRequest} instance.
+ *
+ * @return Set of Integer values for this instance.
+ */
+ @NonNull
+ @FlaggedApi(Flags.REQUEST_RESTRICTED_WIFI)
+ public Set<Integer> getSubscriptionIds() {
+ // No need to make a defensive copy here as NC#getSubscriptionIds() already returns
+ // a new set.
+ return networkCapabilities.getSubscriptionIds();
+ }
}
diff --git a/framework/src/android/net/NetworkUtils.java b/framework/src/android/net/NetworkUtils.java
index fbdc024..18feb84 100644
--- a/framework/src/android/net/NetworkUtils.java
+++ b/framework/src/android/net/NetworkUtils.java
@@ -438,4 +438,9 @@
public static native void setsockoptBytes(FileDescriptor fd, int level, int option,
byte[] value) throws ErrnoException;
+ /** Returns whether the Linux Kernel is 64 bit */
+ public static native boolean isKernel64Bit();
+
+ /** Returns whether the Linux Kernel is x86 */
+ public static native boolean isKernelX86();
}
diff --git a/nearby/README.md b/nearby/README.md
index 0d26563..8dac61c 100644
--- a/nearby/README.md
+++ b/nearby/README.md
@@ -47,8 +47,17 @@
## Build and Install
```sh
-Build unbundled module using banchan
+For master on AOSP (Android) host
+$ source build/envsetup.sh
+$ lunch aosp_oriole-trunk_staging-userdebug
+$ m com.android.tethering
+$ $ANDROID_BUILD_TOP/out/host/linux-x86/bin/deapexer decompress --input $ANDROID_PRODUCT_OUT/system/apex/com.android.tethering.capex --output /tmp/tethering.apex
+$ adb install /tmp/tethering.apex
+$ adb reboot
+NOTE: Developers should use AOSP by default, udc-mainline-prod should not be used unless for Google internal features.
+For udc-mainline-prod on Google internal host
+Build unbundled module using banchan
$ source build/envsetup.sh
$ banchan com.google.android.tethering mainline_modules_arm64
$ m apps_only dist
diff --git a/nearby/apex/Android.bp b/nearby/apex/Android.bp
index d7f063a..5fdf5c9 100644
--- a/nearby/apex/Android.bp
+++ b/nearby/apex/Android.bp
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/nearby/framework/Android.bp b/nearby/framework/Android.bp
index 4bb9efd..4be102c 100644
--- a/nearby/framework/Android.bp
+++ b/nearby/framework/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -49,6 +50,7 @@
"androidx.annotation_annotation",
"framework-annotations-lib",
"framework-bluetooth",
+ "framework-location.stubs.module_lib",
],
static_libs: [
"modules-utils-preconditions",
diff --git a/nearby/framework/java/android/nearby/INearbyManager.aidl b/nearby/framework/java/android/nearby/INearbyManager.aidl
index 7af271e..21ae0ac 100644
--- a/nearby/framework/java/android/nearby/INearbyManager.aidl
+++ b/nearby/framework/java/android/nearby/INearbyManager.aidl
@@ -20,6 +20,7 @@
import android.nearby.IScanListener;
import android.nearby.BroadcastRequestParcelable;
import android.nearby.ScanRequest;
+import android.nearby.PoweredOffFindingEphemeralId;
import android.nearby.aidl.IOffloadCallback;
/**
@@ -40,4 +41,10 @@
void stopBroadcast(in IBroadcastListener callback, String packageName, @nullable String attributionTag);
void queryOffloadCapability(in IOffloadCallback callback) ;
-}
\ No newline at end of file
+
+ void setPoweredOffFindingEphemeralIds(in List<PoweredOffFindingEphemeralId> eids);
+
+ void setPoweredOffModeEnabled(boolean enabled);
+
+ boolean getPoweredOffModeEnabled();
+}
diff --git a/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index 00f1c38..cae653d 100644
--- a/nearby/framework/java/android/nearby/NearbyManager.java
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -18,6 +18,7 @@
import android.Manifest;
import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -25,9 +26,12 @@
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.SystemService;
+import android.bluetooth.BluetoothManager;
import android.content.Context;
+import android.location.LocationManager;
import android.nearby.aidl.IOffloadCallback;
import android.os.RemoteException;
+import android.os.SystemProperties;
import android.provider.Settings;
import android.util.Log;
@@ -37,6 +41,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
+import java.util.List;
import java.util.Objects;
import java.util.WeakHashMap;
import java.util.concurrent.Executor;
@@ -75,8 +80,51 @@
int ERROR = 2;
}
+ /**
+ * Return value of {@link #getPoweredOffFindingMode()} when this powered off finding is not
+ * supported the device.
+ */
+ @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+ public static final int POWERED_OFF_FINDING_MODE_UNSUPPORTED = 0;
+
+ /**
+ * Return value of {@link #getPoweredOffFindingMode()} and argument of {@link
+ * #setPoweredOffFindingMode(int)} when powered off finding is supported but disabled. The
+ * device will not start to advertise when powered off.
+ */
+ @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+ public static final int POWERED_OFF_FINDING_MODE_DISABLED = 1;
+
+ /**
+ * Return value of {@link #getPoweredOffFindingMode()} and argument of {@link
+ * #setPoweredOffFindingMode(int)} when powered off finding is enabled. The device will start to
+ * advertise when powered off.
+ */
+ @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+ public static final int POWERED_OFF_FINDING_MODE_ENABLED = 2;
+
+ /**
+ * Powered off finding modes.
+ *
+ * @hide
+ */
+ @IntDef(
+ prefix = {"POWERED_OFF_FINDING_MODE"},
+ value = {
+ POWERED_OFF_FINDING_MODE_UNSUPPORTED,
+ POWERED_OFF_FINDING_MODE_DISABLED,
+ POWERED_OFF_FINDING_MODE_ENABLED,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PoweredOffFindingMode {}
+
private static final String TAG = "NearbyManager";
+ private static final int POWERED_OFF_FINDING_EID_LENGTH = 20;
+
+ private static final String POWER_OFF_FINDING_SUPPORTED_PROPERTY =
+ "ro.bluetooth.finder.supported";
+
/**
* TODO(b/286137024): Remove this when CTS R5 is rolled out.
* Whether allows Fast Pair to scan.
@@ -456,4 +504,124 @@
"successfully %s Fast Pair scan", enable ? "enables" : "disables"));
}
+ /**
+ * Sets the precomputed EIDs for advertising when the phone is powered off. The Bluetooth
+ * controller will store these EIDs in its memory, and will start advertising them in Find My
+ * Device network EID frames when powered off, only if the powered off finding mode was
+ * previously enabled by calling {@link #setPoweredOffFindingMode(int)}.
+ *
+ * <p>The EIDs are cryptographic ephemeral identifiers that change periodically, based on the
+ * Android clock at the time of the shutdown. They are used as the public part of asymmetric key
+ * pairs. Members of the Find My Device network can use them to encrypt the location of where
+ * they sight the advertising device. Only someone in possession of the private key (the device
+ * owner or someone that the device owner shared the key with) can decrypt this encrypted
+ * location.
+ *
+ * <p>Android will typically call this method during the shutdown process. Even after the
+ * method was called, it is still possible to call {#link setPoweredOffFindingMode() to disable
+ * the advertisement, for example to temporarily disable it for a single shutdown.
+ *
+ * <p>If called more than once, the EIDs of the most recent call overrides the EIDs from any
+ * previous call.
+ *
+ * @throws IllegalArgumentException if the length of one of the EIDs is not 20 bytes
+ */
+ @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public void setPoweredOffFindingEphemeralIds(@NonNull List<byte[]> eids) {
+ Objects.requireNonNull(eids);
+ if (!isPoweredOffFindingSupported()) {
+ throw new UnsupportedOperationException(
+ "Powered off finding is not supported on this device");
+ }
+ List<PoweredOffFindingEphemeralId> ephemeralIdList = eids.stream().map(
+ eid -> {
+ Preconditions.checkArgument(eid.length == POWERED_OFF_FINDING_EID_LENGTH);
+ PoweredOffFindingEphemeralId ephemeralId = new PoweredOffFindingEphemeralId();
+ ephemeralId.bytes = eid;
+ return ephemeralId;
+ }).toList();
+ try {
+ mService.setPoweredOffFindingEphemeralIds(ephemeralIdList);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ }
+
+ /**
+ * Turns the powered off finding on or off. Power off finding will operate only if this method
+ * was called at least once since boot, and the value of the argument {@code
+ * poweredOffFindinMode} was {@link #POWERED_OFF_FINDING_MODE_ENABLED} the last time the method
+ * was called.
+ *
+ * <p>When an Android device with the powered off finding feature is turned off (either as part
+ * of a normal shutdown or due to dead battery), its Bluetooth chip starts to advertise Find My
+ * Device network EID frames with the EID payload that were provided by the last call to {@link
+ * #setPoweredOffFindingEphemeralIds(List)}. These EIDs can be sighted by other Android devices
+ * in BLE range that are part of the Find My Device network. The Android sighters use the EID to
+ * encrypt the location of the Android device and upload it to the server, in a way that only
+ * the owner of the advertising device, or people that the owner shared their encryption key
+ * with, can decrypt the location.
+ *
+ * @param poweredOffFindingMode {@link #POWERED_OFF_FINDING_MODE_ENABLED} or {@link
+ * #POWERED_OFF_FINDING_MODE_DISABLED}
+ *
+ * @throws IllegalStateException if called with {@link #POWERED_OFF_FINDING_MODE_ENABLED} when
+ * Bluetooth or location services are disabled
+ */
+ @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public void setPoweredOffFindingMode(@PoweredOffFindingMode int poweredOffFindingMode) {
+ Preconditions.checkArgument(
+ poweredOffFindingMode == POWERED_OFF_FINDING_MODE_ENABLED
+ || poweredOffFindingMode == POWERED_OFF_FINDING_MODE_DISABLED,
+ "invalid poweredOffFindingMode");
+ if (!isPoweredOffFindingSupported()) {
+ throw new UnsupportedOperationException(
+ "Powered off finding is not supported on this device");
+ }
+ if (poweredOffFindingMode == POWERED_OFF_FINDING_MODE_ENABLED) {
+ Preconditions.checkState(areLocationAndBluetoothEnabled(),
+ "Location services and Bluetooth must be on");
+ }
+ try {
+ mService.setPoweredOffModeEnabled(
+ poweredOffFindingMode == POWERED_OFF_FINDING_MODE_ENABLED);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the state of the powered off finding feature.
+ *
+ * <p>{@link #POWERED_OFF_FINDING_MODE_UNSUPPORTED} if the feature is not supported by the
+ * device, {@link #POWERED_OFF_FINDING_MODE_DISABLED} if this was the last value set by {@link
+ * #setPoweredOffFindingMode(int)} or if no value was set since boot, {@link
+ * #POWERED_OFF_FINDING_MODE_ENABLED} if this was the last value set by {@link
+ * #setPoweredOffFindingMode(int)}
+ */
+ @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public @PoweredOffFindingMode int getPoweredOffFindingMode() {
+ if (!isPoweredOffFindingSupported()) {
+ return POWERED_OFF_FINDING_MODE_UNSUPPORTED;
+ }
+ try {
+ return mService.getPoweredOffModeEnabled()
+ ? POWERED_OFF_FINDING_MODE_ENABLED : POWERED_OFF_FINDING_MODE_DISABLED;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private boolean isPoweredOffFindingSupported() {
+ return Boolean.parseBoolean(SystemProperties.get(POWER_OFF_FINDING_SUPPORTED_PROPERTY));
+ }
+
+ private boolean areLocationAndBluetoothEnabled() {
+ return mContext.getSystemService(BluetoothManager.class).getAdapter().isEnabled()
+ && mContext.getSystemService(LocationManager.class).isLocationEnabled();
+ }
}
diff --git a/nearby/framework/java/android/nearby/PoweredOffFindingEphemeralId.aidl b/nearby/framework/java/android/nearby/PoweredOffFindingEphemeralId.aidl
new file mode 100644
index 0000000..9f4bfef
--- /dev/null
+++ b/nearby/framework/java/android/nearby/PoweredOffFindingEphemeralId.aidl
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+/**
+ * Find My Device network ephemeral ID for powered off finding.
+ *
+ * @hide
+ */
+parcelable PoweredOffFindingEphemeralId {
+ byte[20] bytes;
+}
diff --git a/nearby/service/Android.bp b/nearby/service/Android.bp
index 4630902..749113d 100644
--- a/nearby/service/Android.bp
+++ b/nearby/service/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -30,7 +31,7 @@
srcs: [":nearby-service-srcs"],
defaults: [
- "framework-system-server-module-defaults"
+ "framework-system-server-module-defaults",
],
libs: [
"androidx.annotation_annotation",
@@ -42,6 +43,7 @@
],
static_libs: [
"androidx.core_core",
+ "android.hardware.bluetooth.finder-V1-java",
"guava",
"libprotobuf-java-lite",
"modules-utils-build",
@@ -66,13 +68,16 @@
apex_available: [
"com.android.tethering",
],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
genrule {
name: "statslog-nearby-java-gen",
tools: ["stats-log-api-gen"],
cmd: "$(location stats-log-api-gen) --java $(out) --module nearby " +
- " --javaPackage com.android.server.nearby.proto --javaClass NearbyStatsLog" +
- " --minApiLevel 33",
+ " --javaPackage com.android.server.nearby.proto --javaClass NearbyStatsLog" +
+ " --minApiLevel 33",
out: ["com/android/server/nearby/proto/NearbyStatsLog.java"],
}
diff --git a/nearby/service/java/com/android/server/nearby/NearbyService.java b/nearby/service/java/com/android/server/nearby/NearbyService.java
index 3c183ec..1575f07 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyService.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyService.java
@@ -35,12 +35,14 @@
import android.nearby.INearbyManager;
import android.nearby.IScanListener;
import android.nearby.NearbyManager;
+import android.nearby.PoweredOffFindingEphemeralId;
import android.nearby.ScanRequest;
import android.nearby.aidl.IOffloadCallback;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.managers.BluetoothFinderManager;
import com.android.server.nearby.managers.BroadcastProviderManager;
import com.android.server.nearby.managers.DiscoveryManager;
import com.android.server.nearby.managers.DiscoveryProviderManager;
@@ -50,6 +52,8 @@
import com.android.server.nearby.util.permissions.BroadcastPermissions;
import com.android.server.nearby.util.permissions.DiscoveryPermissions;
+import java.util.List;
+
/** Service implementing nearby functionality. */
public class NearbyService extends INearbyManager.Stub {
public static final String TAG = "NearbyService";
@@ -79,6 +83,7 @@
};
private final DiscoveryManager mDiscoveryProviderManager;
private final BroadcastProviderManager mBroadcastProviderManager;
+ private final BluetoothFinderManager mBluetoothFinderManager;
public NearbyService(Context context) {
mContext = context;
@@ -90,6 +95,7 @@
mNearbyConfiguration.refactorDiscoveryManager()
? new DiscoveryProviderManager(context, mInjector)
: new DiscoveryProviderManagerLegacy(context, mInjector);
+ mBluetoothFinderManager = new BluetoothFinderManager();
}
@VisibleForTesting
@@ -148,6 +154,30 @@
mDiscoveryProviderManager.queryOffloadCapability(callback);
}
+ @Override
+ public void setPoweredOffFindingEphemeralIds(List<PoweredOffFindingEphemeralId> eids) {
+ // Permissions check
+ enforceBluetoothPrivilegedPermission(mContext);
+
+ mBluetoothFinderManager.sendEids(eids);
+ }
+
+ @Override
+ public void setPoweredOffModeEnabled(boolean enabled) {
+ // Permissions check
+ enforceBluetoothPrivilegedPermission(mContext);
+
+ mBluetoothFinderManager.setPoweredOffFinderMode(enabled);
+ }
+
+ @Override
+ public boolean getPoweredOffModeEnabled() {
+ // Permissions check
+ enforceBluetoothPrivilegedPermission(mContext);
+
+ return mBluetoothFinderManager.getPoweredOffFinderMode();
+ }
+
/**
* Called by the service initializer.
*
diff --git a/nearby/service/java/com/android/server/nearby/managers/BluetoothFinderManager.java b/nearby/service/java/com/android/server/nearby/managers/BluetoothFinderManager.java
new file mode 100644
index 0000000..365b099
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/BluetoothFinderManager.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.managers;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.TargetApi;
+import android.hardware.bluetooth.finder.Eid;
+import android.hardware.bluetooth.finder.IBluetoothFinder;
+import android.nearby.PoweredOffFindingEphemeralId;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.ServiceSpecificException;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
+
+import java.util.List;
+
+/** Connects to {@link IBluetoothFinder} HAL and invokes its API. */
+@TargetApi(Build.VERSION_CODES.TIRAMISU)
+public class BluetoothFinderManager {
+
+ private static final String HAL_INSTANCE_NAME = IBluetoothFinder.DESCRIPTOR + "/default";
+
+ private IBluetoothFinder mBluetoothFinder;
+ private IBinder.DeathRecipient mServiceDeathRecipient;
+ private final Object mLock = new Object();
+
+ private boolean initBluetoothFinderHal() {
+ final String methodStr = "initBluetoothFinderHal";
+ if (!SdkLevel.isAtLeastV()) return false;
+ synchronized (mLock) {
+ if (mBluetoothFinder != null) {
+ Log.i(TAG, "Bluetooth Finder HAL is already initialized");
+ return true;
+ }
+ try {
+ mBluetoothFinder = getServiceMockable();
+ if (mBluetoothFinder == null) {
+ Log.e(TAG, "Unable to obtain IBluetoothFinder");
+ return false;
+ }
+ Log.i(TAG, "Obtained IBluetoothFinder. Local ver: " + IBluetoothFinder.VERSION
+ + ", Remote ver: " + mBluetoothFinder.getInterfaceVersion());
+
+ IBinder serviceBinder = getServiceBinderMockable();
+ if (serviceBinder == null) {
+ Log.e(TAG, "Unable to obtain the service binder for IBluetoothFinder");
+ return false;
+ }
+ mServiceDeathRecipient = new BluetoothFinderDeathRecipient();
+ serviceBinder.linkToDeath(mServiceDeathRecipient, /* flags= */ 0);
+
+ Log.i(TAG, "Bluetooth Finder HAL initialization was successful");
+ return true;
+ } catch (RemoteException e) {
+ handleRemoteException(e, methodStr);
+ } catch (Exception e) {
+ Log.e(TAG, methodStr + " encountered an exception: " + e);
+ }
+ return false;
+ }
+ }
+
+ @VisibleForTesting
+ protected IBluetoothFinder getServiceMockable() {
+ return IBluetoothFinder.Stub.asInterface(
+ ServiceManager.waitForDeclaredService(HAL_INSTANCE_NAME));
+ }
+
+ @VisibleForTesting
+ protected IBinder getServiceBinderMockable() {
+ return mBluetoothFinder.asBinder();
+ }
+
+ private class BluetoothFinderDeathRecipient implements IBinder.DeathRecipient {
+ @Override
+ public void binderDied() {
+ Log.e(TAG, "BluetoothFinder service died.");
+ synchronized (mLock) {
+ mBluetoothFinder = null;
+ }
+ }
+ }
+
+ /** See comments for {@link IBluetoothFinder#sendEids(Eid[])} */
+ public void sendEids(List<PoweredOffFindingEphemeralId> eids) {
+ final String methodStr = "sendEids";
+ if (!checkHalAndLogFailure(methodStr)) return;
+ Eid[] eidArray = eids.stream().map(
+ ephmeralId -> {
+ Eid eid = new Eid();
+ eid.bytes = ephmeralId.bytes;
+ return eid;
+ }).toArray(Eid[]::new);
+ try {
+ mBluetoothFinder.sendEids(eidArray);
+ } catch (RemoteException e) {
+ handleRemoteException(e, methodStr);
+ } catch (ServiceSpecificException e) {
+ handleServiceSpecificException(e, methodStr);
+ }
+ }
+
+ /** See comments for {@link IBluetoothFinder#setPoweredOffFinderMode(boolean)} */
+ public void setPoweredOffFinderMode(boolean enable) {
+ final String methodStr = "setPoweredOffMode";
+ if (!checkHalAndLogFailure(methodStr)) return;
+ try {
+ mBluetoothFinder.setPoweredOffFinderMode(enable);
+ } catch (RemoteException e) {
+ handleRemoteException(e, methodStr);
+ } catch (ServiceSpecificException e) {
+ handleServiceSpecificException(e, methodStr);
+ }
+ }
+
+ /** See comments for {@link IBluetoothFinder#getPoweredOffFinderMode()} */
+ public boolean getPoweredOffFinderMode() {
+ final String methodStr = "getPoweredOffMode";
+ if (!checkHalAndLogFailure(methodStr)) return false;
+ try {
+ return mBluetoothFinder.getPoweredOffFinderMode();
+ } catch (RemoteException e) {
+ handleRemoteException(e, methodStr);
+ } catch (ServiceSpecificException e) {
+ handleServiceSpecificException(e, methodStr);
+ }
+ return false;
+ }
+
+ private boolean checkHalAndLogFailure(String methodStr) {
+ if ((mBluetoothFinder == null) && !initBluetoothFinderHal()) {
+ Log.e(TAG, "Unable to call " + methodStr + " because IBluetoothFinder is null.");
+ return false;
+ }
+ return true;
+ }
+
+ private void handleRemoteException(RemoteException e, String methodStr) {
+ mBluetoothFinder = null;
+ Log.e(TAG, methodStr + " failed with remote exception: " + e);
+ }
+
+ private void handleServiceSpecificException(ServiceSpecificException e, String methodStr) {
+ Log.e(TAG, methodStr + " failed with service-specific exception: " + e);
+ }
+}
diff --git a/nearby/service/lint-baseline.xml b/nearby/service/lint-baseline.xml
index a4761ab..3477594 100644
--- a/nearby/service/lint-baseline.xml
+++ b/nearby/service/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
<issue
id="NewApi"
@@ -8,7 +8,7 @@
errorLine2=" ~~~~~~~~~~~~">
<location
file="packages/modules/Connectivity/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java"
- line="263"
+ line="289"
column="54"/>
</issue>
diff --git a/nearby/service/proto/Android.bp b/nearby/service/proto/Android.bp
index 1b00cf6..be5a0b3 100644
--- a/nearby/service/proto/Android.bp
+++ b/nearby/service/proto/Android.bp
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -41,4 +42,4 @@
apex_available: [
"com.android.tethering",
],
-}
\ No newline at end of file
+}
diff --git a/nearby/tests/cts/fastpair/Android.bp b/nearby/tests/cts/fastpair/Android.bp
index 66a1ffe..8009303 100644
--- a/nearby/tests/cts/fastpair/Android.bp
+++ b/nearby/tests/cts/fastpair/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -33,12 +34,14 @@
"framework-bluetooth.stubs.module_lib",
"framework-configinfrastructure",
"framework-connectivity-t.impl",
+ "framework-location.stubs.module_lib",
],
srcs: ["src/**/*.java"],
test_suites: [
"cts",
"general-tests",
"mts-tethering",
+ "mcts-tethering",
],
certificate: "platform",
sdk_version: "module_current",
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
index bc9691d..832ac03 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
@@ -25,12 +25,14 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeTrue;
import android.app.UiAutomation;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.bluetooth.cts.BTAdapterUtils;
import android.content.Context;
+import android.location.LocationManager;
import android.nearby.BroadcastCallback;
import android.nearby.BroadcastRequest;
import android.nearby.NearbyDevice;
@@ -42,6 +44,8 @@
import android.nearby.ScanCallback;
import android.nearby.ScanRequest;
import android.os.Build;
+import android.os.Process;
+import android.os.UserHandle;
import android.provider.DeviceConfig;
import androidx.annotation.NonNull;
@@ -50,6 +54,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
+import com.android.compatibility.common.util.SystemUtil;
import com.android.modules.utils.build.SdkLevel;
import org.junit.Before;
@@ -57,6 +62,7 @@
import org.junit.runner.RunWith;
import java.util.Collections;
+import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@@ -189,6 +195,92 @@
mScanCallback.onError(ERROR_UNSUPPORTED);
}
+ @Test
+ public void testsetPoweredOffFindingEphemeralIds() {
+ // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
+ assumeTrue(SdkLevel.isAtLeastV());
+ // Only test supporting devices.
+ if (mNearbyManager.getPoweredOffFindingMode()
+ == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
+
+ mNearbyManager.setPoweredOffFindingEphemeralIds(List.of(new byte[20], new byte[20]));
+ }
+
+ @Test
+ public void testsetPoweredOffFindingEphemeralIds_noPrivilegedPermission() {
+ // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
+ assumeTrue(SdkLevel.isAtLeastV());
+ // Only test supporting devices.
+ if (mNearbyManager.getPoweredOffFindingMode()
+ == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
+
+ mUiAutomation.dropShellPermissionIdentity();
+
+ assertThrows(SecurityException.class,
+ () -> mNearbyManager.setPoweredOffFindingEphemeralIds(List.of(new byte[20])));
+ }
+
+
+ @Test
+ public void testSetAndGetPoweredOffFindingMode_enabled() {
+ // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
+ assumeTrue(SdkLevel.isAtLeastV());
+ // Only test supporting devices.
+ if (mNearbyManager.getPoweredOffFindingMode()
+ == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
+
+ enableLocation();
+ // enableLocation() has dropped shell permission identity.
+ mUiAutomation.adoptShellPermissionIdentity(BLUETOOTH_PRIVILEGED);
+
+ mNearbyManager.setPoweredOffFindingMode(
+ NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED);
+ assertThat(mNearbyManager.getPoweredOffFindingMode())
+ .isEqualTo(NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED);
+ }
+
+ @Test
+ public void testSetAndGetPoweredOffFindingMode_disabled() {
+ // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
+ assumeTrue(SdkLevel.isAtLeastV());
+ // Only test supporting devices.
+ if (mNearbyManager.getPoweredOffFindingMode()
+ == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
+
+ mNearbyManager.setPoweredOffFindingMode(
+ NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED);
+ assertThat(mNearbyManager.getPoweredOffFindingMode())
+ .isEqualTo(NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED);
+ }
+
+ @Test
+ public void testSetPoweredOffFindingMode_noPrivilegedPermission() {
+ // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
+ assumeTrue(SdkLevel.isAtLeastV());
+ // Only test supporting devices.
+ if (mNearbyManager.getPoweredOffFindingMode()
+ == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
+
+ enableLocation();
+ mUiAutomation.dropShellPermissionIdentity();
+
+ assertThrows(SecurityException.class, () -> mNearbyManager
+ .setPoweredOffFindingMode(NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED));
+ }
+
+ @Test
+ public void testGetPoweredOffFindingMode_noPrivilegedPermission() {
+ // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
+ assumeTrue(SdkLevel.isAtLeastV());
+ // Only test supporting devices.
+ if (mNearbyManager.getPoweredOffFindingMode()
+ == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
+
+ mUiAutomation.dropShellPermissionIdentity();
+
+ assertThrows(SecurityException.class, () -> mNearbyManager.getPoweredOffFindingMode());
+ }
+
private void enableBluetooth() {
BluetoothManager manager = mContext.getSystemService(BluetoothManager.class);
BluetoothAdapter bluetoothAdapter = manager.getAdapter();
@@ -197,6 +289,13 @@
}
}
+ private void enableLocation() {
+ LocationManager locationManager = mContext.getSystemService(LocationManager.class);
+ UserHandle user = Process.myUserHandle();
+ SystemUtil.runWithShellPermissionIdentity(
+ mUiAutomation, () -> locationManager.setLocationEnabledForUser(true, user));
+ }
+
private static class OffloadCallback implements Consumer<OffloadCapability> {
@Override
public void accept(OffloadCapability aBoolean) {
diff --git a/nearby/tests/integration/privileged/Android.bp b/nearby/tests/integration/privileged/Android.bp
index 9b6e488..5e64009 100644
--- a/nearby/tests/integration/privileged/Android.bp
+++ b/nearby/tests/integration/privileged/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt b/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt
index 506b4e2..b949720 100644
--- a/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt
+++ b/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt
@@ -29,6 +29,7 @@
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -96,4 +97,49 @@
)
nearbyManager.stopBroadcast(broadcastCallback)
}
+
+ /** Verify privileged app can set powered off finding ephemeral IDs without exception. */
+ @Test
+ fun testNearbyManagerSetPoweredOffFindingEphemeralIds_fromPrivilegedApp_succeed() {
+ val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
+ // Only test supporting devices.
+ if (nearbyManager.getPoweredOffFindingMode()
+ == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return
+
+ val eid = ByteArray(20)
+
+ nearbyManager.setPoweredOffFindingEphemeralIds(listOf(eid))
+ }
+
+ /**
+ * Verifies that [NearbyManager.setPoweredOffFindingEphemeralIds] checkes the ephemeral ID
+ * length.
+ */
+ @Test
+ fun testNearbyManagerSetPoweredOffFindingEphemeralIds_wrongSize_throwsException() {
+ val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
+ // Only test supporting devices.
+ if (nearbyManager.getPoweredOffFindingMode()
+ == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return
+
+ assertThrows(IllegalArgumentException::class.java) {
+ nearbyManager.setPoweredOffFindingEphemeralIds(listOf(ByteArray(21)))
+ }
+ assertThrows(IllegalArgumentException::class.java) {
+ nearbyManager.setPoweredOffFindingEphemeralIds(listOf(ByteArray(19)))
+ }
+ }
+
+ /** Verify privileged app can set and get powered off finding mode without exception. */
+ @Test
+ fun testNearbyManagerSetGetPoweredOffMode_fromPrivilegedApp_succeed() {
+ val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
+ // Only test supporting devices.
+ if (nearbyManager.getPoweredOffFindingMode()
+ == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return
+
+ nearbyManager.setPoweredOffFindingMode(NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED)
+ assertThat(nearbyManager.getPoweredOffFindingMode())
+ .isEqualTo(NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED)
+ }
}
diff --git a/nearby/tests/integration/untrusted/Android.bp b/nearby/tests/integration/untrusted/Android.bp
index 75f765b..e6259c5 100644
--- a/nearby/tests/integration/untrusted/Android.bp
+++ b/nearby/tests/integration/untrusted/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/nearby/tests/integration/untrusted/src/android/nearby/integration/untrusted/NearbyManagerTest.kt b/nearby/tests/integration/untrusted/src/android/nearby/integration/untrusted/NearbyManagerTest.kt
index 7bf9f63..015d022 100644
--- a/nearby/tests/integration/untrusted/src/android/nearby/integration/untrusted/NearbyManagerTest.kt
+++ b/nearby/tests/integration/untrusted/src/android/nearby/integration/untrusted/NearbyManagerTest.kt
@@ -30,12 +30,12 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.uiautomator.LogcatWaitMixin
import com.google.common.truth.Truth.assertThat
+import java.time.Duration
+import java.util.Calendar
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import java.time.Duration
-import java.util.Calendar
@RunWith(AndroidJUnit4::class)
class NearbyManagerTest {
@@ -151,6 +151,46 @@
).isTrue()
}
+ /**
+ * Verify untrusted app can't set powered off finding ephemeral IDs because it needs
+ * BLUETOOTH_PRIVILEGED permission which is not for use by third-party applications.
+ */
+ @Test
+ fun testNearbyManagerSetPoweredOffFindingEphemeralIds_fromUnTrustedApp_throwsException() {
+ val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
+ val eid = ByteArray(20)
+
+ assertThrows(SecurityException::class.java) {
+ nearbyManager.setPoweredOffFindingEphemeralIds(listOf(eid))
+ }
+ }
+
+ /**
+ * Verify untrusted app can't set powered off finding mode because it needs BLUETOOTH_PRIVILEGED
+ * permission which is not for use by third-party applications.
+ */
+ @Test
+ fun testNearbyManagerSetPoweredOffFindingMode_fromUnTrustedApp_throwsException() {
+ val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
+
+ assertThrows(SecurityException::class.java) {
+ nearbyManager.setPoweredOffFindingMode(NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED)
+ }
+ }
+
+ /**
+ * Verify untrusted app can't get powered off finding mode because it needs BLUETOOTH_PRIVILEGED
+ * permission which is not for use by third-party applications.
+ */
+ @Test
+ fun testNearbyManagerGetPoweredOffFindingMode_fromUnTrustedApp_throwsException() {
+ val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
+
+ assertThrows(SecurityException::class.java) {
+ nearbyManager.getPoweredOffFindingMode()
+ }
+ }
+
companion object {
private val WAIT_INVALID_OPERATIONS_LOGS_TIMEOUT = Duration.ofSeconds(5)
}
diff --git a/nearby/tests/unit/Android.bp b/nearby/tests/unit/Android.bp
index 112c751..2950568 100644
--- a/nearby/tests/unit/Android.bp
+++ b/nearby/tests/unit/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -43,7 +44,6 @@
"platform-test-annotations",
"service-nearby-pre-jarjar",
"truth",
- // "Robolectric_all-target",
],
// these are needed for Extended Mockito
jni_libs: [
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java
new file mode 100644
index 0000000..671b5c5
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.managers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.bluetooth.finder.Eid;
+import android.hardware.bluetooth.finder.IBluetoothFinder;
+import android.nearby.PoweredOffFindingEphemeralId;
+import android.os.IBinder;
+import android.os.IBinder.DeathRecipient;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.List;
+
+public class BluetoothFinderManagerTest {
+ private BluetoothFinderManager mBluetoothFinderManager;
+ private boolean mGetServiceCalled = false;
+
+ @Mock private IBluetoothFinder mIBluetoothFinderMock;
+ @Mock private IBinder mServiceBinderMock;
+
+ private ArgumentCaptor<DeathRecipient> mDeathRecipientCaptor =
+ ArgumentCaptor.forClass(DeathRecipient.class);
+
+ private ArgumentCaptor<Eid[]> mEidArrayCaptor = ArgumentCaptor.forClass(Eid[].class);
+
+ private class BluetoothFinderManagerSpy extends BluetoothFinderManager {
+ @Override
+ protected IBluetoothFinder getServiceMockable() {
+ mGetServiceCalled = true;
+ return mIBluetoothFinderMock;
+ }
+
+ @Override
+ protected IBinder getServiceBinderMockable() {
+ return mServiceBinderMock;
+ }
+ }
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mBluetoothFinderManager = new BluetoothFinderManagerSpy();
+ }
+
+ @Test
+ public void testSendEids() throws Exception {
+ byte[] eidBytes1 = {
+ (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
+ (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
+ (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
+ (byte) 0xe1, (byte) 0xde
+ };
+ byte[] eidBytes2 = {
+ (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
+ (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
+ (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
+ (byte) 0xf2, (byte) 0xef
+ };
+ PoweredOffFindingEphemeralId ephemeralId1 = new PoweredOffFindingEphemeralId();
+ PoweredOffFindingEphemeralId ephemeralId2 = new PoweredOffFindingEphemeralId();
+ ephemeralId1.bytes = eidBytes1;
+ ephemeralId2.bytes = eidBytes2;
+
+ mBluetoothFinderManager.sendEids(List.of(ephemeralId1, ephemeralId2));
+
+ verify(mIBluetoothFinderMock).sendEids(mEidArrayCaptor.capture());
+ assertThat(mEidArrayCaptor.getValue()[0].bytes).isEqualTo(eidBytes1);
+ assertThat(mEidArrayCaptor.getValue()[1].bytes).isEqualTo(eidBytes2);
+ }
+
+ @Test
+ public void testSendEids_remoteException() throws Exception {
+ doThrow(new RemoteException())
+ .when(mIBluetoothFinderMock).sendEids(any());
+ mBluetoothFinderManager.sendEids(List.of());
+
+ // Verify that we get the service again following a RemoteException.
+ mGetServiceCalled = false;
+ mBluetoothFinderManager.sendEids(List.of());
+ assertThat(mGetServiceCalled).isTrue();
+ }
+
+ @Test
+ public void testSendEids_serviceSpecificException() throws Exception {
+ doThrow(new ServiceSpecificException(1))
+ .when(mIBluetoothFinderMock).sendEids(any());
+ mBluetoothFinderManager.sendEids(List.of());
+ }
+
+ @Test
+ public void testSetPoweredOffFinderMode() throws Exception {
+ mBluetoothFinderManager.setPoweredOffFinderMode(true);
+ verify(mIBluetoothFinderMock).setPoweredOffFinderMode(true);
+
+ mBluetoothFinderManager.setPoweredOffFinderMode(false);
+ verify(mIBluetoothFinderMock).setPoweredOffFinderMode(false);
+ }
+
+ @Test
+ public void testSetPoweredOffFinderMode_remoteException() throws Exception {
+ doThrow(new RemoteException())
+ .when(mIBluetoothFinderMock).setPoweredOffFinderMode(anyBoolean());
+ mBluetoothFinderManager.setPoweredOffFinderMode(true);
+
+ // Verify that we get the service again following a RemoteException.
+ mGetServiceCalled = false;
+ mBluetoothFinderManager.setPoweredOffFinderMode(true);
+ assertThat(mGetServiceCalled).isTrue();
+ }
+
+ @Test
+ public void testSetPoweredOffFinderMode_serviceSpecificException() throws Exception {
+ doThrow(new ServiceSpecificException(1))
+ .when(mIBluetoothFinderMock).setPoweredOffFinderMode(anyBoolean());
+ mBluetoothFinderManager.setPoweredOffFinderMode(true);
+ }
+
+ @Test
+ public void testGetPoweredOffFinderMode() throws Exception {
+ when(mIBluetoothFinderMock.getPoweredOffFinderMode()).thenReturn(true);
+ assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isTrue();
+
+ when(mIBluetoothFinderMock.getPoweredOffFinderMode()).thenReturn(false);
+ assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isFalse();
+ }
+
+ @Test
+ public void testGetPoweredOffFinderMode_remoteException() throws Exception {
+ when(mIBluetoothFinderMock.getPoweredOffFinderMode()).thenThrow(new RemoteException());
+ assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isFalse();
+
+ // Verify that we get the service again following a RemoteException.
+ mGetServiceCalled = false;
+ assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isFalse();
+ assertThat(mGetServiceCalled).isTrue();
+ }
+
+ @Test
+ public void testGetPoweredOffFinderMode_serviceSpecificException() throws Exception {
+ when(mIBluetoothFinderMock.getPoweredOffFinderMode())
+ .thenThrow(new ServiceSpecificException(1));
+ assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isFalse();
+ }
+
+ @Test
+ public void testDeathRecipient() throws Exception {
+ mBluetoothFinderManager.setPoweredOffFinderMode(true);
+ verify(mServiceBinderMock).linkToDeath(mDeathRecipientCaptor.capture(), anyInt());
+ mDeathRecipientCaptor.getValue().binderDied();
+
+ // Verify that we get the service again following a binder death.
+ mGetServiceCalled = false;
+ mBluetoothFinderManager.setPoweredOffFinderMode(true);
+ assertThat(mGetServiceCalled).isTrue();
+ }
+}
diff --git a/netbpfload/Android.bp b/netbpfload/Android.bp
index 1f92374..b71890e 100644
--- a/netbpfload/Android.bp
+++ b/netbpfload/Android.bp
@@ -14,6 +14,10 @@
// limitations under the License.
//
+package {
+ default_team: "trendy_team_fwk_core_networking",
+}
+
cc_binary {
name: "netbpfload",
@@ -40,7 +44,7 @@
"com.android.tethering",
"//apex_available:platform",
],
- // really should be Android 14/U (34), but we cannot include binaries built
+ // really should be Android 13/T (33), but we cannot include binaries built
// against newer sdk in the apex, which still targets 30(R):
// module "netbpfload" variant "android_x86_apex30": should support
// min_sdk_version(30) for "com.android.tethering": newer SDK(34).
@@ -49,3 +53,15 @@
init_rc: ["netbpfload.rc"],
required: ["bpfloader"],
}
+
+// Versioned netbpfload init rc: init system will process it only on api T/33+ devices
+// Note: R[30] S[31] Sv2[32] T[33] U[34] V[35])
+//
+// For details of versioned rc files see:
+// https://android.googlesource.com/platform/system/core/+/HEAD/init/README.md#versioned-rc-files-within-apexs
+prebuilt_etc {
+ name: "netbpfload.mainline.rc",
+ src: "netbpfload.mainline.rc",
+ filename: "netbpfload.33rc",
+ installable: false,
+}
diff --git a/netbpfload/NetBpfLoad.cpp b/netbpfload/NetBpfLoad.cpp
index 9df6877..8f25593 100644
--- a/netbpfload/NetBpfLoad.cpp
+++ b/netbpfload/NetBpfLoad.cpp
@@ -169,16 +169,141 @@
return 0;
}
+#define APEX_MOUNT_POINT "/apex/com.android.tethering"
+const char * const platformBpfLoader = "/system/bin/bpfloader";
+const char * const platformNetBpfLoad = "/system/bin/netbpfload";
+const char * const apexNetBpfLoad = APEX_MOUNT_POINT "/bin/netbpfload";
+
+int logTetheringApexVersion(void) {
+ char * found_blockdev = NULL;
+ FILE * f = NULL;
+ char buf[4096];
+
+ f = fopen("/proc/mounts", "re");
+ if (!f) return 1;
+
+ // /proc/mounts format: block_device [space] mount_point [space] other stuff... newline
+ while (fgets(buf, sizeof(buf), f)) {
+ char * blockdev = buf;
+ char * space = strchr(blockdev, ' ');
+ if (!space) continue;
+ *space = '\0';
+ char * mntpath = space + 1;
+ space = strchr(mntpath, ' ');
+ if (!space) continue;
+ *space = '\0';
+ if (strcmp(mntpath, APEX_MOUNT_POINT)) continue;
+ found_blockdev = strdup(blockdev);
+ break;
+ }
+ fclose(f);
+ f = NULL;
+
+ if (!found_blockdev) return 2;
+ ALOGD("Found Tethering Apex mounted from blockdev %s", found_blockdev);
+
+ f = fopen("/proc/mounts", "re");
+ if (!f) { free(found_blockdev); return 3; }
+
+ while (fgets(buf, sizeof(buf), f)) {
+ char * blockdev = buf;
+ char * space = strchr(blockdev, ' ');
+ if (!space) continue;
+ *space = '\0';
+ char * mntpath = space + 1;
+ space = strchr(mntpath, ' ');
+ if (!space) continue;
+ *space = '\0';
+ if (strcmp(blockdev, found_blockdev)) continue;
+ if (strncmp(mntpath, APEX_MOUNT_POINT "@", strlen(APEX_MOUNT_POINT "@"))) continue;
+ char * at = strchr(mntpath, '@');
+ if (!at) continue;
+ char * ver = at + 1;
+ ALOGI("Tethering APEX version %s", ver);
+ }
+ fclose(f);
+ free(found_blockdev);
+ return 0;
+}
+
int main(int argc, char** argv, char * const envp[]) {
(void)argc;
android::base::InitLogging(argv, &android::base::KernelLogger);
- const int device_api_level = android_get_device_api_level();
- const bool isAtLeastU = (device_api_level >= __ANDROID_API_U__);
+ ALOGI("NetBpfLoad '%s' starting...", argv[0]);
- if (!android::bpf::isAtLeastKernelVersion(4, 19, 0) &&
+ // true iff we are running from the module
+ const bool is_mainline = !strcmp(argv[0], apexNetBpfLoad);
+
+ // true iff we are running from the platform
+ const bool is_platform = !strcmp(argv[0], platformNetBpfLoad);
+
+ const int device_api_level = android_get_device_api_level();
+ const bool isAtLeastT = (device_api_level >= __ANDROID_API_T__);
+ const bool isAtLeastU = (device_api_level >= __ANDROID_API_U__);
+ const bool isAtLeastV = (device_api_level >= __ANDROID_API_V__);
+
+ // last in U QPR2 beta1
+ const bool has_platform_bpfloader_rc = exists("/system/etc/init/bpfloader.rc");
+ // first in U QPR2 beta~2
+ const bool has_platform_netbpfload_rc = exists("/system/etc/init/netbpfload.rc");
+
+ ALOGI("NetBpfLoad api:%d/%d kver:%07x platform:%d mainline:%d rc:%d%d",
+ android_get_application_target_sdk_version(), device_api_level,
+ android::bpf::kernelVersion(), is_platform, is_mainline,
+ has_platform_bpfloader_rc, has_platform_netbpfload_rc);
+
+ if (!is_platform && !is_mainline) {
+ ALOGE("Unable to determine if we're platform or mainline netbpfload.");
+ return 1;
+ }
+
+ if (is_platform) {
+ ALOGI("Executing apex netbpfload...");
+ const char * args[] = { apexNetBpfLoad, NULL, };
+ execve(args[0], (char**)args, envp);
+ ALOGE("exec '%s' fail: %d[%s]", apexNetBpfLoad, errno, strerror(errno));
+ return 1;
+ }
+
+ if (!has_platform_bpfloader_rc && !has_platform_netbpfload_rc) {
+ ALOGE("Unable to find platform's bpfloader & netbpfload init scripts.");
+ return 1;
+ }
+
+ if (has_platform_bpfloader_rc && has_platform_netbpfload_rc) {
+ ALOGE("Platform has *both* bpfloader & netbpfload init scripts.");
+ return 1;
+ }
+
+ logTetheringApexVersion();
+
+ if (is_mainline && has_platform_bpfloader_rc && !has_platform_netbpfload_rc) {
+ // Tethering apex shipped initrc file causes us to reach here
+ // but we're not ready to correctly handle anything before U QPR2
+ // in which the 'bpfloader' vs 'netbpfload' split happened
+ const char * args[] = { platformBpfLoader, NULL, };
+ execve(args[0], (char**)args, envp);
+ ALOGE("exec '%s' fail: %d[%s]", platformBpfLoader, errno, strerror(errno));
+ return 1;
+ }
+
+ if (isAtLeastT && !android::bpf::isAtLeastKernelVersion(4, 9, 0)) {
+ ALOGW("Android T requires kernel 4.9.");
+ }
+
+ if (isAtLeastU && !android::bpf::isAtLeastKernelVersion(4, 14, 0)) {
+ ALOGW("Android U requires kernel 4.14.");
+ }
+
+ if (isAtLeastV && !android::bpf::isAtLeastKernelVersion(4, 19, 0) &&
android::bpf::isAtLeastKernelVersion(4, 14, 0)) {
- ALOGE("Android U QPR2 requires kernel 4.19.");
+ ALOGE("Android V requires kernel 4.19.");
+ return 1;
+ }
+
+ if (isAtLeastV && android::bpf::isX86() && !android::bpf::isKernel64Bit()) {
+ ALOGE("Android V requires X86 kernel to be 64-bit.");
return 1;
}
@@ -245,6 +370,13 @@
if (createSysFsBpfSubDir(location.prefix)) return 1;
}
+ // Note: there's no actual src dir for fs_bpf_loader .o's,
+ // so it is not listed in 'locations[].prefix'.
+ // This is because this is primarily meant for triggering genfscon rules,
+ // and as such this will likely always be the case.
+ // Thus we need to manually create the /sys/fs/bpf/loader subdirectory.
+ if (createSysFsBpfSubDir("loader")) return 1;
+
// Load all ELF objects, create programs and maps, and pin them
for (const auto& location : locations) {
if (loadAllElfObjects(location) != 0) {
@@ -269,10 +401,8 @@
ALOGI("done, transferring control to platform bpfloader.");
- const char * args[] = { "/system/bin/bpfloader", NULL, };
- if (execve(args[0], (char**)args, envp)) {
- ALOGE("FATAL: execve('/system/bin/bpfloader'): %d[%s]", errno, strerror(errno));
- }
-
+ const char * args[] = { platformBpfLoader, NULL, };
+ execve(args[0], (char**)args, envp);
+ ALOGE("FATAL: execve('%s'): %d[%s]", platformBpfLoader, errno, strerror(errno));
return 1;
}
diff --git a/netbpfload/netbpfload.mainline.rc b/netbpfload/netbpfload.mainline.rc
new file mode 100644
index 0000000..0ac5de8
--- /dev/null
+++ b/netbpfload/netbpfload.mainline.rc
@@ -0,0 +1,8 @@
+service bpfloader /apex/com.android.tethering/bin/netbpfload
+ capabilities CHOWN SYS_ADMIN NET_ADMIN
+ group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
+ user root
+ rlimit memlock 1073741824 1073741824
+ oneshot
+ reboot_on_failure reboot,bpfloader-failed
+ override
diff --git a/netd/Android.bp b/netd/Android.bp
index 4325d89..eedbdae 100644
--- a/netd/Android.bp
+++ b/netd/Android.bp
@@ -14,6 +14,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -58,21 +59,24 @@
cc_test {
name: "netd_updatable_unit_test",
defaults: ["netd_defaults"],
- test_suites: ["general-tests", "mts-tethering"],
+ test_suites: [
+ "general-tests",
+ "mts-tethering",
+ ],
test_config_template: ":net_native_test_config_template",
- require_root: true, // required by setrlimitForTest()
+ require_root: true, // required by setrlimitForTest()
header_libs: [
"bpf_connectivity_headers",
],
srcs: [
"BpfHandlerTest.cpp",
- "BpfBaseTest.cpp"
+ "BpfBaseTest.cpp",
],
static_libs: [
+ "libbase",
"libnetd_updatable",
],
shared_libs: [
- "libbase",
"libcutils",
"liblog",
"libnetdutils",
diff --git a/netd/NetdUpdatable.cpp b/netd/NetdUpdatable.cpp
index 8b9e5a7..3b15916 100644
--- a/netd/NetdUpdatable.cpp
+++ b/netd/NetdUpdatable.cpp
@@ -31,8 +31,8 @@
android::netdutils::Status ret = sBpfHandler.init(cg2_path);
if (!android::netdutils::isOk(ret)) {
- LOG(ERROR) << __func__ << ": Failed. " << ret.code() << " " << ret.msg();
- return -ret.code();
+ LOG(ERROR) << __func__ << ": Failed: (" << ret.code() << ") " << ret.msg();
+ abort();
}
return 0;
}
diff --git a/remoteauth/framework/Android.bp b/remoteauth/framework/Android.bp
index 71b621a..2f1737f 100644
--- a/remoteauth/framework/Android.bp
+++ b/remoteauth/framework/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/remoteauth/service/Android.bp b/remoteauth/service/Android.bp
index 8330efc..32ae54f 100644
--- a/remoteauth/service/Android.bp
+++ b/remoteauth/service/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/remoteauth/service/jni/Android.bp b/remoteauth/service/jni/Android.bp
index a95a8fb..c0ac779 100644
--- a/remoteauth/service/jni/Android.bp
+++ b/remoteauth/service/jni/Android.bp
@@ -1,4 +1,5 @@
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs b/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
index 0a189f2..421fe7e 100644
--- a/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
+++ b/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
@@ -67,7 +67,7 @@
logger::init(
logger::Config::default()
.with_tag_on_device("remoteauth")
- .with_min_level(log::Level::Trace)
+ .with_max_level(log::LevelFilter::Trace)
.with_filter("trace,jni=info"),
);
}
diff --git a/remoteauth/service/jni/src/remoteauth_jni_android_protocol.rs b/remoteauth/service/jni/src/remoteauth_jni_android_protocol.rs
index ac2eb8c..e44ab8b 100644
--- a/remoteauth/service/jni/src/remoteauth_jni_android_protocol.rs
+++ b/remoteauth/service/jni/src/remoteauth_jni_android_protocol.rs
@@ -30,7 +30,7 @@
logger::init(
logger::Config::default()
.with_tag_on_device("remoteauth")
- .with_min_level(log::Level::Trace)
+ .with_max_level(log::LevelFilter::Trace)
.with_filter("trace,jni=info"),
);
get_boolean_result(native_init(env), "native_init")
diff --git a/remoteauth/tests/unit/Android.bp b/remoteauth/tests/unit/Android.bp
index 77e6f19..47b9e31 100644
--- a/remoteauth/tests/unit/Android.bp
+++ b/remoteauth/tests/unit/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/service-t/Android.bp b/service-t/Android.bp
index bc49f0e..779f354 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -31,6 +32,7 @@
],
visibility: ["//visibility:private"],
}
+
// The above filegroup can be used to specify different sources depending
// on the branch, while minimizing merge conflicts in the rest of the
// build rules.
@@ -73,11 +75,15 @@
"com.android.tethering",
],
visibility: [
+ "//frameworks/base/services/tests/VpnTests",
"//frameworks/base/tests/vcn",
"//packages/modules/Connectivity/service",
"//packages/modules/Connectivity/tests:__subpackages__",
"//packages/modules/IPsec/tests/iketests",
],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
// Test building mDNS as a standalone, so that it can be imported into other repositories as-is.
@@ -94,11 +100,12 @@
min_sdk_version: "21",
lint: {
error_checks: ["NewApi"],
+
},
srcs: [
"src/com/android/server/connectivity/mdns/**/*.java",
":framework-connectivity-t-mdns-standalone-build-sources",
- ":service-mdns-droidstubs"
+ ":service-mdns-droidstubs",
],
exclude_srcs: [
"src/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitor.java",
@@ -127,7 +134,7 @@
srcs: ["src/com/android/server/connectivity/mdns/SocketNetLinkMonitorFactory.java"],
libs: [
"net-utils-device-common-mdns-standalone-build-test",
- "service-connectivity-tiramisu-pre-jarjar"
+ "service-connectivity-tiramisu-pre-jarjar",
],
visibility: [
"//visibility:private",
diff --git a/service-t/jni/com_android_server_net_NetworkStatsService.cpp b/service-t/jni/com_android_server_net_NetworkStatsService.cpp
index bdbb655..c999398 100644
--- a/service-t/jni/com_android_server_net_NetworkStatsService.cpp
+++ b/service-t/jni/com_android_server_net_NetworkStatsService.cpp
@@ -34,77 +34,77 @@
using android::bpf::bpfGetUidStats;
using android::bpf::bpfGetIfaceStats;
-using android::bpf::bpfGetIfIndexStats;
+using android::bpf::bpfRegisterIface;
using android::bpf::NetworkTraceHandler;
namespace android {
-// NOTE: keep these in sync with TrafficStats.java
-static const uint64_t UNKNOWN = -1;
-
-enum StatsType {
- RX_BYTES = 0,
- RX_PACKETS = 1,
- TX_BYTES = 2,
- TX_PACKETS = 3,
-};
-
-static uint64_t getStatsType(StatsValue* stats, StatsType type) {
- switch (type) {
- case RX_BYTES:
- return stats->rxBytes;
- case RX_PACKETS:
- return stats->rxPackets;
- case TX_BYTES:
- return stats->txBytes;
- case TX_PACKETS:
- return stats->txPackets;
- default:
- return UNKNOWN;
- }
+static void nativeRegisterIface(JNIEnv* env, jclass clazz, jstring iface) {
+ ScopedUtfChars iface8(env, iface);
+ if (iface8.c_str() == nullptr) return;
+ bpfRegisterIface(iface8.c_str());
}
-static jlong nativeGetTotalStat(JNIEnv* env, jclass clazz, jint type) {
+static jobject statsValueToEntry(JNIEnv* env, StatsValue* stats) {
+ // Find the Java class that represents the structure
+ jclass gEntryClass = env->FindClass("android/net/NetworkStats$Entry");
+ if (gEntryClass == nullptr) {
+ return nullptr;
+ }
+
+ // Find the constructor.
+ jmethodID constructorID = env->GetMethodID(gEntryClass, "<init>", "()V");
+ if (constructorID == nullptr) {
+ return nullptr;
+ }
+
+ // Create a new instance of the Java class
+ jobject result = env->NewObject(gEntryClass, constructorID);
+ if (result == nullptr) {
+ return nullptr;
+ }
+
+ // Set the values of the structure fields in the Java object
+ env->SetLongField(result, env->GetFieldID(gEntryClass, "rxBytes", "J"), stats->rxBytes);
+ env->SetLongField(result, env->GetFieldID(gEntryClass, "txBytes", "J"), stats->txBytes);
+ env->SetLongField(result, env->GetFieldID(gEntryClass, "rxPackets", "J"), stats->rxPackets);
+ env->SetLongField(result, env->GetFieldID(gEntryClass, "txPackets", "J"), stats->txPackets);
+
+ return result;
+}
+
+static jobject nativeGetTotalStat(JNIEnv* env, jclass clazz) {
StatsValue stats = {};
- if (bpfGetIfaceStats(NULL, &stats) == 0) {
- return getStatsType(&stats, (StatsType) type);
+ if (bpfGetIfaceStats(nullptr, &stats) == 0) {
+ return statsValueToEntry(env, &stats);
} else {
- return UNKNOWN;
+ return nullptr;
}
}
-static jlong nativeGetIfaceStat(JNIEnv* env, jclass clazz, jstring iface, jint type) {
+static jobject nativeGetIfaceStat(JNIEnv* env, jclass clazz, jstring iface) {
ScopedUtfChars iface8(env, iface);
- if (iface8.c_str() == NULL) {
- return UNKNOWN;
+ if (iface8.c_str() == nullptr) {
+ return nullptr;
}
StatsValue stats = {};
if (bpfGetIfaceStats(iface8.c_str(), &stats) == 0) {
- return getStatsType(&stats, (StatsType) type);
+ return statsValueToEntry(env, &stats);
} else {
- return UNKNOWN;
+ return nullptr;
}
}
-static jlong nativeGetIfIndexStat(JNIEnv* env, jclass clazz, jint ifindex, jint type) {
- StatsValue stats = {};
- if (bpfGetIfIndexStats(ifindex, &stats) == 0) {
- return getStatsType(&stats, (StatsType) type);
- } else {
- return UNKNOWN;
- }
-}
-
-static jlong nativeGetUidStat(JNIEnv* env, jclass clazz, jint uid, jint type) {
+static jobject nativeGetUidStat(JNIEnv* env, jclass clazz, jint uid) {
StatsValue stats = {};
if (bpfGetUidStats(uid, &stats) == 0) {
- return getStatsType(&stats, (StatsType) type);
+ return statsValueToEntry(env, &stats);
} else {
- return UNKNOWN;
+ return nullptr;
}
}
@@ -113,11 +113,31 @@
}
static const JNINativeMethod gMethods[] = {
- {"nativeGetTotalStat", "(I)J", (void*)nativeGetTotalStat},
- {"nativeGetIfaceStat", "(Ljava/lang/String;I)J", (void*)nativeGetIfaceStat},
- {"nativeGetIfIndexStat", "(II)J", (void*)nativeGetIfIndexStat},
- {"nativeGetUidStat", "(II)J", (void*)nativeGetUidStat},
- {"nativeInitNetworkTracing", "()V", (void*)nativeInitNetworkTracing},
+ {
+ "nativeRegisterIface",
+ "(Ljava/lang/String;)V",
+ (void*)nativeRegisterIface
+ },
+ {
+ "nativeGetTotalStat",
+ "()Landroid/net/NetworkStats$Entry;",
+ (void*)nativeGetTotalStat
+ },
+ {
+ "nativeGetIfaceStat",
+ "(Ljava/lang/String;)Landroid/net/NetworkStats$Entry;",
+ (void*)nativeGetIfaceStat
+ },
+ {
+ "nativeGetUidStat",
+ "(I)Landroid/net/NetworkStats$Entry;",
+ (void*)nativeGetUidStat
+ },
+ {
+ "nativeInitNetworkTracing",
+ "()V",
+ (void*)nativeInitNetworkTracing
+ },
};
int register_android_server_net_NetworkStatsService(JNIEnv* env) {
diff --git a/service-t/lint-baseline.xml b/service-t/lint-baseline.xml
index 38d3ab0..e4b92d6 100644
--- a/service-t/lint-baseline.xml
+++ b/service-t/lint-baseline.xml
@@ -1,104 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.EthernetNetworkSpecifier#getInterfaceName`"
- errorLine1=" if (!((EthernetNetworkSpecifier) spec).getInterfaceName().matches(iface)) {"
- errorLine2=" ~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java"
- line="224"
- column="48"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.UnderlyingNetworkInfo#getInterface`"
- errorLine1=" delta.migrateTun(info.getOwnerUid(), info.getInterface(),"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
- line="276"
- column="55"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.UnderlyingNetworkInfo#getOwnerUid`"
- errorLine1=" delta.migrateTun(info.getOwnerUid(), info.getInterface(),"
- errorLine2=" ~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
- line="276"
- column="35"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.UnderlyingNetworkInfo#getUnderlyingInterfaces`"
- errorLine1=" info.getUnderlyingInterfaces());"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
- line="277"
- column="26"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
- errorLine1=" dnsAddresses.add(InetAddress.parseNumericAddress(address));"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetTracker.java"
- line="875"
- column="54"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
- errorLine1=" staticIpConfigBuilder.setGateway(InetAddress.parseNumericAddress(value));"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetTracker.java"
- line="870"
- column="66"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
- errorLine1=" IoUtils.closeQuietly(os);"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsRecorder.java"
- line="556"
- column="25"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
- errorLine1=" IoUtils.closeQuietly(sockFd);"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service-t/src/com/android/server/IpSecService.java"
- line="1309"
- column="25"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
- errorLine1=" IoUtils.closeQuietly(mSocket);"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service-t/src/com/android/server/IpSecService.java"
- line="1034"
- column="21"/>
- </issue>
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
<issue
id="NewApi"
@@ -113,6 +14,17 @@
<issue
id="NewApi"
+ message="Cast from `EthernetNetworkSpecifier` to `NetworkSpecifier` requires API level 31 (current min is 30)"
+ errorLine1=" .setNetworkSpecifier(new EthernetNetworkSpecifier(ifaceName))"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java"
+ line="156"
+ column="38"/>
+ </issue>
+
+ <issue
+ id="NewApi"
message="Call requires API level 31 (current min is 30): `new android.net.EthernetNetworkSpecifier`"
errorLine1=" nc.setNetworkSpecifier(new EthernetNetworkSpecifier(iface));"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -124,39 +36,6 @@
<issue
id="NewApi"
- message="Call requires API level 31 (current min is 30): `new android.util.AtomicFile`"
- errorLine1=" mFile = new AtomicFile(new File(path), logger);"
- errorLine2=" ~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service-t/src/com/android/server/net/PersistentInt.java"
- line="53"
- column="17"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `new java.net.InetSocketAddress`"
- errorLine1=" super(handler, new RecvBuffer(buffer, new InetSocketAddress()));"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java"
- line="66"
- column="47"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Cast from `EthernetNetworkSpecifier` to `NetworkSpecifier` requires API level 31 (current min is 30)"
- errorLine1=" .setNetworkSpecifier(new EthernetNetworkSpecifier(ifaceName))"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java"
- line="156"
- column="38"/>
- </issue>
-
- <issue
- id="NewApi"
message="Cast from `EthernetNetworkSpecifier` to `NetworkSpecifier` requires API level 31 (current min is 30)"
errorLine1=" nc.setNetworkSpecifier(new EthernetNetworkSpecifier(iface));"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -169,6 +48,28 @@
<issue
id="NewApi"
message="Class requires API level 31 (current min is 30): `android.net.EthernetNetworkSpecifier`"
+ errorLine1=" if (!(spec instanceof EthernetNetworkSpecifier)) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java"
+ line="221"
+ column="31"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.EthernetNetworkSpecifier#getInterfaceName`"
+ errorLine1=" if (!((EthernetNetworkSpecifier) spec).getInterfaceName().matches(iface)) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java"
+ line="224"
+ column="48"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.net.EthernetNetworkSpecifier`"
errorLine1=" if (!((EthernetNetworkSpecifier) spec).getInterfaceName().matches(iface)) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
<location
@@ -179,13 +80,178 @@
<issue
id="NewApi"
- message="Class requires API level 31 (current min is 30): `android.net.EthernetNetworkSpecifier`"
- errorLine1=" if (!(spec instanceof EthernetNetworkSpecifier)) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
+ errorLine1=" staticIpConfigBuilder.setGateway(InetAddress.parseNumericAddress(value));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
<location
- file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java"
- line="221"
- column="31"/>
+ file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetTracker.java"
+ line="885"
+ column="66"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
+ errorLine1=" dnsAddresses.add(InetAddress.parseNumericAddress(address));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetTracker.java"
+ line="890"
+ column="54"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(mSocket);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/IpSecService.java"
+ line="1042"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(sockFd);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/IpSecService.java"
+ line="1318"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 30): `android.system.OsConstants#UDP_ENCAP`"
+ errorLine1=" OsConstants.UDP_ENCAP,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/IpSecService.java"
+ line="1326"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 30): `android.system.OsConstants#UDP_ENCAP_ESPINUDP`"
+ errorLine1=" OsConstants.UDP_ENCAP_ESPINUDP);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/IpSecService.java"
+ line="1327"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `BpfNetMaps`"
+ errorLine1=" return new BpfNetMaps(ctx);"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
+ line="111"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `swapActiveStatsMap`"
+ errorLine1=" mBpfNetMaps.swapActiveStatsMap();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
+ line="185"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.UnderlyingNetworkInfo#getInterface`"
+ errorLine1=" delta.migrateTun(info.getOwnerUid(), info.getInterface(),"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
+ line="240"
+ column="55"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.UnderlyingNetworkInfo#getOwnerUid`"
+ errorLine1=" delta.migrateTun(info.getOwnerUid(), info.getInterface(),"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
+ line="240"
+ column="35"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.UnderlyingNetworkInfo#getUnderlyingInterfaces`"
+ errorLine1=" info.getUnderlyingInterfaces());"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
+ line="241"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(os);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsRecorder.java"
+ line="580"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 35 (current min is 34): `newInstance`"
+ errorLine1=" opts = BroadcastOptionsShimImpl.newInstance("
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsService.java"
+ line="562"
+ column="61"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `new android.util.AtomicFile`"
+ errorLine1=" mFile = new AtomicFile(new File(path), logger);"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/net/PersistentInt.java"
+ line="53"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `addOrUpdateInterfaceAddress`"
+ errorLine1=" mCb.addOrUpdateInterfaceAddress(ifaddrMsg.index, la);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitor.java"
+ line="69"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `deleteInterfaceAddress`"
+ errorLine1=" mCb.deleteInterfaceAddress(ifaddrMsg.index, la);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitor.java"
+ line="73"
+ column="21"/>
</issue>
</issues>
\ No newline at end of file
diff --git a/service-t/native/libs/libnetworkstats/Android.bp b/service-t/native/libs/libnetworkstats/Android.bp
index 0dfd0af..c620634 100644
--- a/service-t/native/libs/libnetworkstats/Android.bp
+++ b/service-t/native/libs/libnetworkstats/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -57,9 +58,12 @@
cc_test {
name: "libnetworkstats_test",
- test_suites: ["general-tests", "mts-tethering"],
+ test_suites: [
+ "general-tests",
+ "mts-tethering",
+ ],
test_config_template: ":net_native_test_config_template",
- require_root: true, // required by setrlimitForTest()
+ require_root: true, // required by setrlimitForTest()
header_libs: ["bpf_connectivity_headers"],
srcs: [
"BpfNetworkStatsTest.cpp",
@@ -73,6 +77,7 @@
"-Wthread-safety",
],
static_libs: [
+ "libbase",
"libgmock",
"libnetworkstats",
"libperfetto_client_experimental",
@@ -80,7 +85,6 @@
"perfetto_trace_protos",
],
shared_libs: [
- "libbase",
"liblog",
"libcutils",
"libandroid_net",
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
index 3101397..d3e331e 100644
--- a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
@@ -40,6 +40,35 @@
using base::Result;
+BpfMap<uint32_t, IfaceValue>& getIfaceIndexNameMap() {
+ static BpfMap<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
+ return ifaceIndexNameMap;
+}
+
+const BpfMapRO<uint32_t, StatsValue>& getIfaceStatsMap() {
+ static BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
+ return ifaceStatsMap;
+}
+
+Result<IfaceValue> ifindex2name(const uint32_t ifindex) {
+ Result<IfaceValue> v = getIfaceIndexNameMap().readValue(ifindex);
+ if (v.ok()) return v;
+ IfaceValue iv = {};
+ if (!if_indextoname(ifindex, iv.name)) return v;
+ getIfaceIndexNameMap().writeValue(ifindex, iv, BPF_ANY);
+ return iv;
+}
+
+void bpfRegisterIface(const char* iface) {
+ if (!iface) return;
+ if (strlen(iface) >= sizeof(IfaceValue)) return;
+ uint32_t ifindex = if_nametoindex(iface);
+ if (!ifindex) return;
+ IfaceValue ifname = {};
+ strlcpy(ifname.name, iface, sizeof(ifname.name));
+ getIfaceIndexNameMap().writeValue(ifindex, ifname, BPF_ANY);
+}
+
int bpfGetUidStatsInternal(uid_t uid, StatsValue* stats,
const BpfMapRO<uint32_t, StatsValue>& appUidStatsMap) {
auto statsEntry = appUidStatsMap.readValue(uid);
@@ -58,19 +87,19 @@
int bpfGetIfaceStatsInternal(const char* iface, StatsValue* stats,
const BpfMapRO<uint32_t, StatsValue>& ifaceStatsMap,
- const BpfMapRO<uint32_t, IfaceValue>& ifaceNameMap) {
+ const IfIndexToNameFunc ifindex2name) {
*stats = {};
int64_t unknownIfaceBytesTotal = 0;
const auto processIfaceStats =
- [iface, stats, &ifaceNameMap, &unknownIfaceBytesTotal](
+ [iface, stats, ifindex2name, &unknownIfaceBytesTotal](
const uint32_t& key,
const BpfMapRO<uint32_t, StatsValue>& ifaceStatsMap) -> Result<void> {
- char ifname[IFNAMSIZ];
- if (getIfaceNameFromMap(ifaceNameMap, ifaceStatsMap, key, ifname, key,
- &unknownIfaceBytesTotal)) {
+ Result<IfaceValue> ifname = ifindex2name(key);
+ if (!ifname.ok()) {
+ maybeLogUnknownIface(key, ifaceStatsMap, key, &unknownIfaceBytesTotal);
return Result<void>();
}
- if (!iface || !strcmp(iface, ifname)) {
+ if (!iface || !strcmp(iface, ifname.value().name)) {
Result<StatsValue> statsEntry = ifaceStatsMap.readValue(key);
if (!statsEntry.ok()) {
return statsEntry.error();
@@ -84,9 +113,7 @@
}
int bpfGetIfaceStats(const char* iface, StatsValue* stats) {
- static BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
- static BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
- return bpfGetIfaceStatsInternal(iface, stats, ifaceStatsMap, ifaceIndexNameMap);
+ return bpfGetIfaceStatsInternal(iface, stats, getIfaceStatsMap(), ifindex2name);
}
int bpfGetIfIndexStatsInternal(uint32_t ifindex, StatsValue* stats,
@@ -101,14 +128,13 @@
}
int bpfGetIfIndexStats(int ifindex, StatsValue* stats) {
- static BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
- return bpfGetIfIndexStatsInternal(ifindex, stats, ifaceStatsMap);
+ return bpfGetIfIndexStatsInternal(ifindex, stats, getIfaceStatsMap());
}
stats_line populateStatsEntry(const StatsKey& statsKey, const StatsValue& statsEntry,
- const char* ifname) {
+ const IfaceValue& ifname) {
stats_line newLine;
- strlcpy(newLine.iface, ifname, sizeof(newLine.iface));
+ strlcpy(newLine.iface, ifname.name, sizeof(newLine.iface));
newLine.uid = (int32_t)statsKey.uid;
newLine.set = (int32_t)statsKey.counterSet;
newLine.tag = (int32_t)statsKey.tag;
@@ -121,22 +147,22 @@
int parseBpfNetworkStatsDetailInternal(std::vector<stats_line>& lines,
const BpfMapRO<StatsKey, StatsValue>& statsMap,
- const BpfMapRO<uint32_t, IfaceValue>& ifaceMap) {
+ const IfIndexToNameFunc ifindex2name) {
int64_t unknownIfaceBytesTotal = 0;
const auto processDetailUidStats =
- [&lines, &unknownIfaceBytesTotal, &ifaceMap](
+ [&lines, &unknownIfaceBytesTotal, &ifindex2name](
const StatsKey& key,
const BpfMapRO<StatsKey, StatsValue>& statsMap) -> Result<void> {
- char ifname[IFNAMSIZ];
- if (getIfaceNameFromMap(ifaceMap, statsMap, key.ifaceIndex, ifname, key,
- &unknownIfaceBytesTotal)) {
+ Result<IfaceValue> ifname = ifindex2name(key.ifaceIndex);
+ if (!ifname.ok()) {
+ maybeLogUnknownIface(key.ifaceIndex, statsMap, key, &unknownIfaceBytesTotal);
return Result<void>();
}
Result<StatsValue> statsEntry = statsMap.readValue(key);
if (!statsEntry.ok()) {
return base::ResultError(statsEntry.error().message(), statsEntry.error().code());
}
- stats_line newLine = populateStatsEntry(key, statsEntry.value(), ifname);
+ stats_line newLine = populateStatsEntry(key, statsEntry.value(), ifname.value());
lines.push_back(newLine);
if (newLine.tag) {
// account tagged traffic in the untagged stats (for historical reasons?)
@@ -166,7 +192,6 @@
}
int parseBpfNetworkStatsDetail(std::vector<stats_line>* lines) {
- static BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
static BpfMapRO<uint32_t, uint32_t> configurationMap(CONFIGURATION_MAP_PATH);
static BpfMap<StatsKey, StatsValue> statsMapA(STATS_MAP_A_PATH);
static BpfMap<StatsKey, StatsValue> statsMapB(STATS_MAP_B_PATH);
@@ -196,7 +221,7 @@
// TODO: the above comment feels like it may be obsolete / out of date,
// since we no longer swap the map via netd binder rpc - though we do
// still swap it.
- int ret = parseBpfNetworkStatsDetailInternal(*lines, *inactiveStatsMap, ifaceIndexNameMap);
+ int ret = parseBpfNetworkStatsDetailInternal(*lines, *inactiveStatsMap, ifindex2name);
if (ret) {
ALOGE("parse detail network stats failed: %s", strerror(errno));
return ret;
@@ -213,13 +238,14 @@
int parseBpfNetworkStatsDevInternal(std::vector<stats_line>& lines,
const BpfMapRO<uint32_t, StatsValue>& statsMap,
- const BpfMapRO<uint32_t, IfaceValue>& ifaceMap) {
+ const IfIndexToNameFunc ifindex2name) {
int64_t unknownIfaceBytesTotal = 0;
- const auto processDetailIfaceStats = [&lines, &unknownIfaceBytesTotal, &ifaceMap, &statsMap](
+ const auto processDetailIfaceStats = [&lines, &unknownIfaceBytesTotal, ifindex2name, &statsMap](
const uint32_t& key, const StatsValue& value,
const BpfMapRO<uint32_t, StatsValue>&) {
- char ifname[IFNAMSIZ];
- if (getIfaceNameFromMap(ifaceMap, statsMap, key, ifname, key, &unknownIfaceBytesTotal)) {
+ Result<IfaceValue> ifname = ifindex2name(key);
+ if (!ifname.ok()) {
+ maybeLogUnknownIface(key, statsMap, key, &unknownIfaceBytesTotal);
return Result<void>();
}
StatsKey fakeKey = {
@@ -227,7 +253,7 @@
.tag = (uint32_t)TAG_NONE,
.counterSet = (uint32_t)SET_ALL,
};
- lines.push_back(populateStatsEntry(fakeKey, value, ifname));
+ lines.push_back(populateStatsEntry(fakeKey, value, ifname.value()));
return Result<void>();
};
Result<void> res = statsMap.iterateWithValue(processDetailIfaceStats);
@@ -242,9 +268,7 @@
}
int parseBpfNetworkStatsDev(std::vector<stats_line>* lines) {
- static BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
- static BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
- return parseBpfNetworkStatsDevInternal(*lines, ifaceStatsMap, ifaceIndexNameMap);
+ return parseBpfNetworkStatsDevInternal(*lines, getIfaceStatsMap(), ifindex2name);
}
void groupNetworkStats(std::vector<stats_line>& lines) {
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
index bcc4550..484c166 100644
--- a/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
@@ -77,6 +77,10 @@
BpfMap<uint32_t, IfaceValue> mFakeIfaceIndexNameMap;
BpfMap<uint32_t, StatsValue> mFakeIfaceStatsMap;
+ IfIndexToNameFunc mIfIndex2Name = [this](const uint32_t ifindex){
+ return mFakeIfaceIndexNameMap.readValue(ifindex);
+ };
+
void SetUp() {
ASSERT_EQ(0, setrlimitForTest());
@@ -228,7 +232,7 @@
populateFakeStats(TEST_UID1, 0, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
populateFakeStats(TEST_UID1, 0, IFACE_INDEX2, TEST_COUNTERSET1, value1, mFakeStatsMap);
populateFakeStats(TEST_UID2, 0, IFACE_INDEX3, TEST_COUNTERSET1, value1, mFakeStatsMap);
- ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mIfIndex2Name));
ASSERT_EQ((unsigned long)3, lines.size());
}
@@ -256,16 +260,15 @@
EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
StatsValue result1 = {};
- ASSERT_EQ(0, bpfGetIfaceStatsInternal(IFACE_NAME1, &result1, mFakeIfaceStatsMap,
- mFakeIfaceIndexNameMap));
+ ASSERT_EQ(0,
+ bpfGetIfaceStatsInternal(IFACE_NAME1, &result1, mFakeIfaceStatsMap, mIfIndex2Name));
expectStatsEqual(value1, result1);
StatsValue result2 = {};
- ASSERT_EQ(0, bpfGetIfaceStatsInternal(IFACE_NAME2, &result2, mFakeIfaceStatsMap,
- mFakeIfaceIndexNameMap));
+ ASSERT_EQ(0,
+ bpfGetIfaceStatsInternal(IFACE_NAME2, &result2, mFakeIfaceStatsMap, mIfIndex2Name));
expectStatsEqual(value2, result2);
StatsValue totalResult = {};
- ASSERT_EQ(0, bpfGetIfaceStatsInternal(NULL, &totalResult, mFakeIfaceStatsMap,
- mFakeIfaceIndexNameMap));
+ ASSERT_EQ(0, bpfGetIfaceStatsInternal(NULL, &totalResult, mFakeIfaceStatsMap, mIfIndex2Name));
StatsValue totalValue = {
.rxPackets = TEST_PACKET0 * 2 + TEST_PACKET1,
.rxBytes = TEST_BYTES0 * 2 + TEST_BYTES1,
@@ -304,7 +307,7 @@
mFakeStatsMap);
populateFakeStats(TEST_UID2, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
std::vector<stats_line> lines;
- ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mIfIndex2Name));
ASSERT_EQ((unsigned long)7, lines.size());
}
@@ -324,7 +327,7 @@
populateFakeStats(TEST_UID1, 0, IFACE_INDEX1, TEST_COUNTERSET1, value1, mFakeStatsMap);
populateFakeStats(TEST_UID2, 0, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
std::vector<stats_line> lines;
- ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mIfIndex2Name));
ASSERT_EQ((unsigned long)4, lines.size());
}
@@ -352,18 +355,20 @@
.counterSet = TEST_COUNTERSET0,
.ifaceIndex = ifaceIndex,
};
- char ifname[IFNAMSIZ];
int64_t unknownIfaceBytesTotal = 0;
- ASSERT_EQ(-ENODEV, getIfaceNameFromMap(mFakeIfaceIndexNameMap, mFakeStatsMap, ifaceIndex,
- ifname, curKey, &unknownIfaceBytesTotal));
+ ASSERT_EQ(false, mFakeIfaceIndexNameMap.readValue(ifaceIndex).ok());
+ maybeLogUnknownIface(ifaceIndex, mFakeStatsMap, curKey, &unknownIfaceBytesTotal);
+
ASSERT_EQ(((int64_t)(TEST_BYTES0 * 20 + TEST_BYTES1 * 20)), unknownIfaceBytesTotal);
curKey.ifaceIndex = IFACE_INDEX2;
- ASSERT_EQ(-ENODEV, getIfaceNameFromMap(mFakeIfaceIndexNameMap, mFakeStatsMap, ifaceIndex,
- ifname, curKey, &unknownIfaceBytesTotal));
+
+ ASSERT_EQ(false, mFakeIfaceIndexNameMap.readValue(ifaceIndex).ok());
+ maybeLogUnknownIface(ifaceIndex, mFakeStatsMap, curKey, &unknownIfaceBytesTotal);
+
ASSERT_EQ(-1, unknownIfaceBytesTotal);
std::vector<stats_line> lines;
// TODO: find a way to test the total of unknown Iface Bytes go above limit.
- ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mIfIndex2Name));
ASSERT_EQ((unsigned long)1, lines.size());
expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, 0, lines.front());
}
@@ -394,8 +399,7 @@
ifaceStatsKey = IFACE_INDEX4;
EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value2, BPF_ANY));
std::vector<stats_line> lines;
- ASSERT_EQ(0,
- parseBpfNetworkStatsDevInternal(lines, mFakeIfaceStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ(0, parseBpfNetworkStatsDevInternal(lines, mFakeIfaceStatsMap, mIfIndex2Name));
ASSERT_EQ((unsigned long)4, lines.size());
expectStatsLineEqual(value1, IFACE_NAME1, UID_ALL, SET_ALL, TAG_NONE, lines[0]);
@@ -439,13 +443,13 @@
std::vector<stats_line> lines;
// Test empty stats.
- ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mIfIndex2Name));
ASSERT_EQ((size_t) 0, lines.size());
lines.clear();
// Test 1 line stats.
populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
- ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mIfIndex2Name));
ASSERT_EQ((size_t) 2, lines.size()); // TEST_TAG != 0 -> 1 entry becomes 2 lines
expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, 0, lines[0]);
expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, TEST_TAG, lines[1]);
@@ -457,7 +461,7 @@
populateFakeStats(TEST_UID1, TEST_TAG + 1, IFACE_INDEX1, TEST_COUNTERSET0, value2,
mFakeStatsMap);
populateFakeStats(TEST_UID2, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
- ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mIfIndex2Name));
ASSERT_EQ((size_t) 9, lines.size());
lines.clear();
@@ -465,7 +469,7 @@
populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX3, TEST_COUNTERSET0, value1, mFakeStatsMap);
populateFakeStats(TEST_UID2, TEST_TAG, IFACE_INDEX3, TEST_COUNTERSET0, value1, mFakeStatsMap);
- ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mIfIndex2Name));
ASSERT_EQ((size_t) 9, lines.size());
// Verify Sorted & Grouped.
@@ -490,8 +494,7 @@
ifaceStatsKey = IFACE_INDEX3;
EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
- ASSERT_EQ(0,
- parseBpfNetworkStatsDevInternal(lines, mFakeIfaceStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ(0, parseBpfNetworkStatsDevInternal(lines, mFakeIfaceStatsMap, mIfIndex2Name));
ASSERT_EQ((size_t) 2, lines.size());
expectStatsLineEqual(value3, IFACE_NAME1, UID_ALL, SET_ALL, TAG_NONE, lines[0]);
@@ -532,7 +535,7 @@
// TODO: Mutate counterSet and enlarge TEST_MAP_SIZE if overflow on counterSet is possible.
std::vector<stats_line> lines;
- ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mIfIndex2Name));
ASSERT_EQ((size_t) 12, lines.size());
// Uid 0 first
diff --git a/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h b/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h
index 8058d05..59eb195 100644
--- a/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h
+++ b/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h
@@ -55,36 +55,27 @@
bool operator==(const stats_line& lhs, const stats_line& rhs);
bool operator<(const stats_line& lhs, const stats_line& rhs);
+// This mirrors BpfMap.h's:
+// Result<Value> readValue(const Key key) const
+// for a BpfMap<uint32_t, IfaceValue>
+using IfIndexToNameFunc = std::function<Result<IfaceValue>(const uint32_t)>;
+
// For test only
int bpfGetUidStatsInternal(uid_t uid, StatsValue* stats,
const BpfMapRO<uint32_t, StatsValue>& appUidStatsMap);
// For test only
int bpfGetIfaceStatsInternal(const char* iface, StatsValue* stats,
const BpfMapRO<uint32_t, StatsValue>& ifaceStatsMap,
- const BpfMapRO<uint32_t, IfaceValue>& ifaceNameMap);
+ const IfIndexToNameFunc ifindex2name);
// For test only
int bpfGetIfIndexStatsInternal(uint32_t ifindex, StatsValue* stats,
const BpfMapRO<uint32_t, StatsValue>& ifaceStatsMap);
// For test only
int parseBpfNetworkStatsDetailInternal(std::vector<stats_line>& lines,
const BpfMapRO<StatsKey, StatsValue>& statsMap,
- const BpfMapRO<uint32_t, IfaceValue>& ifaceMap);
+ const IfIndexToNameFunc ifindex2name);
// For test only
int cleanStatsMapInternal(const base::unique_fd& cookieTagMap, const base::unique_fd& tagStatsMap);
-// For test only
-template <class Key>
-int getIfaceNameFromMap(const BpfMapRO<uint32_t, IfaceValue>& ifaceMap,
- const BpfMapRO<Key, StatsValue>& statsMap,
- uint32_t ifaceIndex, char* ifname,
- const Key& curKey, int64_t* unknownIfaceBytesTotal) {
- auto iface = ifaceMap.readValue(ifaceIndex);
- if (!iface.ok()) {
- maybeLogUnknownIface(ifaceIndex, statsMap, curKey, unknownIfaceBytesTotal);
- return -ENODEV;
- }
- strlcpy(ifname, iface.value().name, sizeof(IfaceValue));
- return 0;
-}
template <class Key>
void maybeLogUnknownIface(int ifaceIndex, const BpfMapRO<Key, StatsValue>& statsMap,
@@ -112,8 +103,9 @@
// For test only
int parseBpfNetworkStatsDevInternal(std::vector<stats_line>& lines,
const BpfMapRO<uint32_t, StatsValue>& statsMap,
- const BpfMapRO<uint32_t, IfaceValue>& ifaceMap);
+ const IfIndexToNameFunc ifindex2name);
+void bpfRegisterIface(const char* iface);
int bpfGetUidStats(uid_t uid, StatsValue* stats);
int bpfGetIfaceStats(const char* iface, StatsValue* stats);
int bpfGetIfIndexStats(int ifindex, StatsValue* stats);
diff --git a/service-t/src/com/android/metrics/NetworkStatsMetricsLogger.java b/service-t/src/com/android/metrics/NetworkStatsMetricsLogger.java
new file mode 100644
index 0000000..08a8603
--- /dev/null
+++ b/service-t/src/com/android/metrics/NetworkStatsMetricsLogger.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.metrics;
+
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID_TAG;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_XT;
+
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__FAST_DATA_INPUT_STATE__FDIS_DISABLED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__FAST_DATA_INPUT_STATE__FDIS_ENABLED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__OPERATION_TYPE__ROT_READ;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UID;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UIDTAG;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UNKNOWN;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_XT;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.NetworkStatsCollection;
+import android.net.NetworkStatsHistory;
+import android.util.Pair;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.ConnectivityStatsLog;
+
+import java.io.File;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * Helper class to log NetworkStats related metrics.
+ *
+ * This class does not provide thread-safe.
+ */
+public class NetworkStatsMetricsLogger {
+ final Dependencies mDeps;
+ int mReadIndex = 1;
+
+ /** Dependency class */
+ @VisibleForTesting
+ public static class Dependencies {
+ /**
+ * Writes a NETWORK_STATS_RECORDER_FILE_OPERATION_REPORTED event to ConnectivityStatsLog.
+ */
+ public void writeRecorderFileReadingStats(int recorderType, int readIndex,
+ int readLatencyMillis,
+ int fileCount, int totalFileSize,
+ int keys, int uids, int totalHistorySize,
+ boolean useFastDataInput) {
+ ConnectivityStatsLog.write(NETWORK_STATS_RECORDER_FILE_OPERATED,
+ NETWORK_STATS_RECORDER_FILE_OPERATED__OPERATION_TYPE__ROT_READ,
+ recorderType,
+ readIndex,
+ readLatencyMillis,
+ fileCount,
+ totalFileSize,
+ keys,
+ uids,
+ totalHistorySize,
+ useFastDataInput
+ ? NETWORK_STATS_RECORDER_FILE_OPERATED__FAST_DATA_INPUT_STATE__FDIS_ENABLED
+ : NETWORK_STATS_RECORDER_FILE_OPERATED__FAST_DATA_INPUT_STATE__FDIS_DISABLED);
+ }
+ }
+
+ public NetworkStatsMetricsLogger() {
+ mDeps = new Dependencies();
+ }
+
+ @VisibleForTesting
+ public NetworkStatsMetricsLogger(Dependencies deps) {
+ mDeps = deps;
+ }
+
+ private static int prefixToRecorderType(@NonNull String prefix) {
+ switch (prefix) {
+ case PREFIX_XT:
+ return NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_XT;
+ case PREFIX_UID:
+ return NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UID;
+ case PREFIX_UID_TAG:
+ return NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UIDTAG;
+ default:
+ return NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UNKNOWN;
+ }
+ }
+
+ /**
+ * Get file count and total byte count for the given directory and prefix.
+ *
+ * @return File count and total byte count as a pair, or 0s if met errors.
+ */
+ private static Pair<Integer, Integer> getStatsFilesAttributes(
+ @Nullable File statsDir, @NonNull String prefix) {
+ if (statsDir == null || !statsDir.isDirectory()) return new Pair<>(0, 0);
+
+ // Only counts the matching files.
+ // The files are named in the following format:
+ // <prefix>.<startTimestamp>-[<endTimestamp>]
+ // e.g. uid_tag.12345-
+ // See FileRotator#FileInfo for more detail.
+ final Pattern pattern = Pattern.compile("^" + prefix + "\\.[0-9]+-[0-9]*$");
+
+ int totalFiles = 0;
+ int totalBytes = 0;
+ for (String name : emptyIfNull(statsDir.list())) {
+ if (!pattern.matcher(name).matches()) continue;
+
+ totalFiles++;
+ // Cast to int is safe since stats persistent files are several MBs in total.
+ totalBytes += (int) (new File(statsDir, name).length());
+
+ }
+ return new Pair<>(totalFiles, totalBytes);
+ }
+
+ private static String [] emptyIfNull(@Nullable String [] array) {
+ return (array == null) ? new String[0] : array;
+ }
+
+ /**
+ * Log statistics from the NetworkStatsRecorder file reading process into statsd.
+ */
+ public void logRecorderFileReading(@NonNull String prefix, int readLatencyMillis,
+ @Nullable File statsDir, @NonNull NetworkStatsCollection collection,
+ boolean useFastDataInput) {
+ final Set<Integer> uids = new HashSet<>();
+ final Map<NetworkStatsCollection.Key, NetworkStatsHistory> entries =
+ collection.getEntries();
+
+ for (final NetworkStatsCollection.Key key : entries.keySet()) {
+ uids.add(key.uid);
+ }
+
+ int totalHistorySize = 0;
+ for (final NetworkStatsHistory history : entries.values()) {
+ totalHistorySize += history.size();
+ }
+
+ final Pair<Integer, Integer> fileAttributes = getStatsFilesAttributes(statsDir, prefix);
+ mDeps.writeRecorderFileReadingStats(prefixToRecorderType(prefix),
+ mReadIndex++,
+ readLatencyMillis,
+ fileAttributes.first /* fileCount */,
+ fileAttributes.second /* totalFileSize */,
+ entries.size(),
+ uids.size(),
+ totalHistorySize,
+ useFastDataInput);
+ }
+}
diff --git a/service-t/src/com/android/server/IpSecXfrmController.java b/service-t/src/com/android/server/IpSecXfrmController.java
index c8abd40..3cfbf83 100644
--- a/service-t/src/com/android/server/IpSecXfrmController.java
+++ b/service-t/src/com/android/server/IpSecXfrmController.java
@@ -15,6 +15,7 @@
*/
package com.android.server;
+import static com.android.net.module.util.netlink.NetlinkUtils.SOCKET_RECV_BUFSIZE;
import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.IPPROTO_ESP;
import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.NETLINK_XFRM;
import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.XFRM_MSG_NEWSA;
@@ -106,7 +107,8 @@
public static class Dependencies {
/** Get a new XFRM netlink socket and connect it */
public FileDescriptor newNetlinkSocket() throws ErrnoException, SocketException {
- final FileDescriptor fd = NetlinkUtils.netlinkSocketForProto(NETLINK_XFRM);
+ final FileDescriptor fd =
+ NetlinkUtils.netlinkSocketForProto(NETLINK_XFRM, SOCKET_RECV_BUFSIZE);
NetlinkUtils.connectToKernel(fd);
return fd;
}
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 2640332..cfb1a33 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -26,6 +26,9 @@
import static android.net.nsd.NsdManager.MDNS_DISCOVERY_MANAGER_EVENT;
import static android.net.nsd.NsdManager.MDNS_SERVICE_EVENT;
import static android.net.nsd.NsdManager.RESOLVE_SERVICE_SUCCEEDED;
+import static android.net.nsd.NsdManager.SUBTYPE_LABEL_REGEX;
+import static android.net.nsd.NsdManager.TYPE_REGEX;
+import static android.os.Process.SYSTEM_UID;
import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
import static com.android.modules.utils.build.SdkLevel.isAtLeastU;
@@ -33,6 +36,8 @@
import static com.android.server.connectivity.mdns.MdnsAdvertiser.AdvertiserMetrics;
import static com.android.server.connectivity.mdns.MdnsConstants.NO_PACKET;
import static com.android.server.connectivity.mdns.MdnsRecord.MAX_LABEL_LENGTH;
+import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE;
+import static com.android.server.connectivity.mdns.MdnsSearchOptions.PASSIVE_QUERY_MODE;
import static com.android.server.connectivity.mdns.util.MdnsUtils.Clock;
import android.annotation.NonNull;
@@ -51,6 +56,8 @@
import android.net.mdns.aidl.IMDnsEventListener;
import android.net.mdns.aidl.RegistrationInfo;
import android.net.mdns.aidl.ResolutionInfo;
+import android.net.nsd.AdvertisingRequest;
+import android.net.nsd.DiscoveryRequest;
import android.net.nsd.INsdManager;
import android.net.nsd.INsdManagerCallback;
import android.net.nsd.INsdServiceConnector;
@@ -86,6 +93,7 @@
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.HandlerUtils;
import com.android.net.module.util.InetAddressUtils;
import com.android.net.module.util.PermissionUtils;
import com.android.net.module.util.SharedLog;
@@ -109,9 +117,13 @@
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -170,6 +182,8 @@
"mdns_advertiser_allowlist_";
private static final String MDNS_ALLOWLIST_FLAG_SUFFIX = "_version";
+ private static final String FORCE_ENABLE_FLAG_FOR_TEST_PREFIX = "test_";
+
@VisibleForTesting
static final String MDNS_CONFIG_RUNNING_APP_ACTIVE_IMPORTANCE_CUTOFF =
"mdns_config_running_app_active_importance_cutoff";
@@ -186,11 +200,13 @@
static final int NO_TRANSACTION = -1;
private static final int NO_SENT_QUERY_COUNT = 0;
private static final int DISCOVERY_QUERY_SENT_CALLBACK = 1000;
+ private static final int MAX_SUBTYPE_COUNT = 100;
private static final SharedLog LOGGER = new SharedLog("serviceDiscovery");
private final Context mContext;
private final NsdStateMachine mNsdStateMachine;
- private final MDnsManager mMDnsManager;
+ // It can be null on V+ device since mdns native service provided by netd is removed.
+ private final @Nullable MDnsManager mMDnsManager;
private final MDnsEventCallback mMDnsEventCallback;
@NonNull
private final Dependencies mDeps;
@@ -240,6 +256,8 @@
private final RemoteCallbackList<IOffloadEngine> mOffloadEngines =
new RemoteCallbackList<>();
+ @NonNull
+ private final MdnsFeatureFlags mMdnsFeatureFlags;
private static class OffloadEngineInfo {
@NonNull final String mInterfaceName;
@@ -261,15 +279,11 @@
protected final int mClientRequestId;
protected final int mTransactionId;
@NonNull
- protected final NsdServiceInfo mReqServiceInfo;
- @NonNull
protected final String mListenedServiceType;
- MdnsListener(int clientRequestId, int transactionId, @NonNull NsdServiceInfo reqServiceInfo,
- @NonNull String listenedServiceType) {
+ MdnsListener(int clientRequestId, int transactionId, @NonNull String listenedServiceType) {
mClientRequestId = clientRequestId;
mTransactionId = transactionId;
- mReqServiceInfo = reqServiceInfo;
mListenedServiceType = listenedServiceType;
}
@@ -312,8 +326,8 @@
private class DiscoveryListener extends MdnsListener {
DiscoveryListener(int clientRequestId, int transactionId,
- @NonNull NsdServiceInfo reqServiceInfo, @NonNull String listenServiceType) {
- super(clientRequestId, transactionId, reqServiceInfo, listenServiceType);
+ @NonNull String listenServiceType) {
+ super(clientRequestId, transactionId, listenServiceType);
}
@Override
@@ -342,8 +356,8 @@
private class ResolutionListener extends MdnsListener {
ResolutionListener(int clientRequestId, int transactionId,
- @NonNull NsdServiceInfo reqServiceInfo, @NonNull String listenServiceType) {
- super(clientRequestId, transactionId, reqServiceInfo, listenServiceType);
+ @NonNull String listenServiceType) {
+ super(clientRequestId, transactionId, listenServiceType);
}
@Override
@@ -364,8 +378,8 @@
private class ServiceInfoListener extends MdnsListener {
ServiceInfoListener(int clientRequestId, int transactionId,
- @NonNull NsdServiceInfo reqServiceInfo, @NonNull String listenServiceType) {
- super(clientRequestId, transactionId, reqServiceInfo, listenServiceType);
+ @NonNull String listenServiceType) {
+ super(clientRequestId, transactionId, listenServiceType);
}
@Override
@@ -536,6 +550,11 @@
if (DBG) Log.d(TAG, "Daemon is already started.");
return;
}
+
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "maybeStartDaemon: mMDnsManager is null");
+ return;
+ }
mMDnsManager.registerEventListener(mMDnsEventCallback);
mMDnsManager.startDaemon();
mIsDaemonStarted = true;
@@ -548,6 +567,11 @@
if (DBG) Log.d(TAG, "Daemon has not been started.");
return;
}
+
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "maybeStopDaemon: mMDnsManager is null");
+ return;
+ }
mMDnsManager.unregisterEventListener(mMDnsEventCallback);
mMDnsManager.stopDaemon();
mIsDaemonStarted = false;
@@ -688,18 +712,73 @@
return mClients.get(args.connector);
}
+ /**
+ * Returns {@code false} if {@code subtypes} exceeds the maximum number limit or
+ * contains invalid subtype label.
+ */
+ private boolean checkSubtypeLabels(Set<String> subtypes) {
+ if (subtypes.size() > MAX_SUBTYPE_COUNT) {
+ mServiceLogs.e(
+ "Too many subtypes: " + subtypes.size() + " (max = "
+ + MAX_SUBTYPE_COUNT + ")");
+ return false;
+ }
+
+ for (String subtype : subtypes) {
+ if (!checkSubtypeLabel(subtype)) {
+ mServiceLogs.e("Subtype " + subtype + " is invalid");
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private Set<String> dedupSubtypeLabels(Collection<String> subtypes) {
+ final Map<String, String> subtypeMap = new LinkedHashMap<>(subtypes.size());
+ for (String subtype : subtypes) {
+ subtypeMap.put(MdnsUtils.toDnsLowerCase(subtype), subtype);
+ }
+ return new ArraySet<>(subtypeMap.values());
+ }
+
+ private boolean checkTtl(
+ @Nullable Duration ttl, @NonNull ClientInfo clientInfo) {
+ if (ttl == null) {
+ return true;
+ }
+
+ final long ttlSeconds = ttl.toSeconds();
+ final int uid = clientInfo.getUid();
+
+ // Allows Thread module in the system_server to register TTL that is smaller than
+ // 30 seconds
+ final long minTtlSeconds = uid == SYSTEM_UID ? 0 : NsdManager.TTL_SECONDS_MIN;
+
+ // Allows Thread module in the system_server to register TTL that is larger than
+ // 10 hours
+ final long maxTtlSeconds =
+ uid == SYSTEM_UID ? 0xffffffffL : NsdManager.TTL_SECONDS_MAX;
+
+ if (ttlSeconds < minTtlSeconds || ttlSeconds > maxTtlSeconds) {
+ mServiceLogs.e("ttlSeconds exceeds allowed range (value = "
+ + ttlSeconds + ", allowedRange = [" + minTtlSeconds
+ + ", " + maxTtlSeconds + " ])");
+ return false;
+ }
+ return true;
+ }
+
@Override
public boolean processMessage(Message msg) {
final ClientInfo clientInfo;
final int transactionId;
final int clientRequestId = msg.arg2;
- final ListenerArgs args;
final OffloadEngineInfo offloadEngineInfo;
switch (msg.what) {
case NsdManager.DISCOVER_SERVICES: {
if (DBG) Log.d(TAG, "Discover services");
- args = (ListenerArgs) msg.obj;
- clientInfo = mClients.get(args.connector);
+ final DiscoveryArgs discoveryArgs = (DiscoveryArgs) msg.obj;
+ clientInfo = mClients.get(discoveryArgs.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
// cleared and cause NPE. Add a null check here to prevent this corner case.
@@ -714,55 +793,70 @@
break;
}
- final NsdServiceInfo info = args.serviceInfo;
+ final DiscoveryRequest discoveryRequest = discoveryArgs.discoveryRequest;
transactionId = getUniqueId();
- final Pair<String, String> typeAndSubtype =
- parseTypeAndSubtype(info.getServiceType());
+ final Pair<String, List<String>> typeAndSubtype =
+ parseTypeAndSubtype(discoveryRequest.getServiceType());
final String serviceType = typeAndSubtype == null
? null : typeAndSubtype.first;
if (clientInfo.mUseJavaBackend
|| mDeps.isMdnsDiscoveryManagerEnabled(mContext)
|| useDiscoveryManagerForType(serviceType)) {
- if (serviceType == null) {
+ if (serviceType == null || typeAndSubtype.second.size() > 1) {
clientInfo.onDiscoverServicesFailedImmediately(clientRequestId,
NsdManager.FAILURE_INTERNAL_ERROR, false /* isLegacy */);
break;
}
+ String subtype = discoveryRequest.getSubtype();
+ if (subtype == null && !typeAndSubtype.second.isEmpty()) {
+ subtype = typeAndSubtype.second.get(0);
+ }
+
+ if (subtype != null && !checkSubtypeLabel(subtype)) {
+ clientInfo.onDiscoverServicesFailedImmediately(clientRequestId,
+ NsdManager.FAILURE_BAD_PARAMETERS, false /* isLegacy */);
+ break;
+ }
+
final String listenServiceType = serviceType + ".local";
maybeStartMonitoringSockets();
final MdnsListener listener = new DiscoveryListener(clientRequestId,
- transactionId, info, listenServiceType);
+ transactionId, listenServiceType);
final MdnsSearchOptions.Builder optionsBuilder =
MdnsSearchOptions.newBuilder()
- .setNetwork(info.getNetwork())
+ .setNetwork(discoveryRequest.getNetwork())
.setRemoveExpiredService(true)
- .setIsPassiveMode(true);
- if (typeAndSubtype.second != null) {
- // The parsing ensures subtype starts with an underscore.
+ .setQueryMode(
+ mMdnsFeatureFlags.isAggressiveQueryModeEnabled()
+ ? AGGRESSIVE_QUERY_MODE
+ : PASSIVE_QUERY_MODE);
+ if (subtype != null) {
+ // checkSubtypeLabels() ensures that subtypes start with '_' but
// MdnsSearchOptions expects the underscore to not be present.
- optionsBuilder.addSubtype(typeAndSubtype.second.substring(1));
+ optionsBuilder.addSubtype(subtype.substring(1));
}
mMdnsDiscoveryManager.registerListener(
listenServiceType, listener, optionsBuilder.build());
final ClientRequest request = storeDiscoveryManagerRequestMap(
clientRequestId, transactionId, listener, clientInfo,
- info.getNetwork());
- clientInfo.onDiscoverServicesStarted(clientRequestId, info, request);
+ discoveryRequest.getNetwork());
+ clientInfo.onDiscoverServicesStarted(
+ clientRequestId, discoveryRequest, request);
clientInfo.log("Register a DiscoveryListener " + transactionId
+ " for service type:" + listenServiceType);
} else {
maybeStartDaemon();
- if (discoverServices(transactionId, info)) {
+ if (discoverServices(transactionId, discoveryRequest)) {
if (DBG) {
Log.d(TAG, "Discover " + msg.arg2 + " " + transactionId
- + info.getServiceType());
+ + discoveryRequest.getServiceType());
}
final ClientRequest request = storeLegacyRequestMap(clientRequestId,
transactionId, clientInfo, msg.what,
mClock.elapsedRealtime());
clientInfo.onDiscoverServicesStarted(
- clientRequestId, info, request);
+ clientRequestId, discoveryRequest, request);
} else {
stopServiceDiscovery(transactionId);
clientInfo.onDiscoverServicesFailedImmediately(clientRequestId,
@@ -773,7 +867,7 @@
}
case NsdManager.STOP_DISCOVERY: {
if (DBG) Log.d(TAG, "Stop service discovery");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -811,7 +905,7 @@
}
case NsdManager.REGISTER_SERVICE: {
if (DBG) Log.d(TAG, "Register service");
- args = (ListenerArgs) msg.obj;
+ final AdvertisingArgs args = (AdvertisingArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -826,38 +920,100 @@
NsdManager.FAILURE_MAX_LIMIT, true /* isLegacy */);
break;
}
-
- transactionId = getUniqueId();
- final NsdServiceInfo serviceInfo = args.serviceInfo;
+ final AdvertisingRequest advertisingRequest = args.advertisingRequest;
+ if (advertisingRequest == null) {
+ Log.e(TAG, "Unknown advertisingRequest in registration");
+ break;
+ }
+ final NsdServiceInfo serviceInfo = advertisingRequest.getServiceInfo();
final String serviceType = serviceInfo.getServiceType();
- final Pair<String, String> typeSubtype = parseTypeAndSubtype(serviceType);
+ final Pair<String, List<String>> typeSubtype = parseTypeAndSubtype(
+ serviceType);
final String registerServiceType = typeSubtype == null
? null : typeSubtype.first;
+ final String hostname = serviceInfo.getHostname();
+ // Keep compatible with the legacy behavior: It's allowed to set host
+ // addresses for a service registration although the host addresses
+ // won't be registered. To register the addresses for a host, the
+ // hostname must be specified.
+ if (hostname == null) {
+ serviceInfo.setHostAddresses(Collections.emptyList());
+ }
if (clientInfo.mUseJavaBackend
|| mDeps.isMdnsAdvertiserEnabled(mContext)
|| useAdvertiserForType(registerServiceType)) {
- if (registerServiceType == null) {
+ if (serviceType != null && registerServiceType == null) {
Log.e(TAG, "Invalid service type: " + serviceType);
clientInfo.onRegisterServiceFailedImmediately(clientRequestId,
NsdManager.FAILURE_INTERNAL_ERROR, false /* isLegacy */);
break;
}
- serviceInfo.setServiceType(registerServiceType);
- serviceInfo.setServiceName(truncateServiceName(
- serviceInfo.getServiceName()));
+ boolean isUpdateOnly = (advertisingRequest.getAdvertisingConfig()
+ & AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY) > 0;
+ // If it is an update request, then reuse the old transactionId
+ if (isUpdateOnly) {
+ final ClientRequest existingClientRequest =
+ clientInfo.mClientRequests.get(clientRequestId);
+ if (existingClientRequest == null) {
+ Log.e(TAG, "Invalid update on requestId: " + clientRequestId);
+ clientInfo.onRegisterServiceFailedImmediately(clientRequestId,
+ NsdManager.FAILURE_INTERNAL_ERROR,
+ false /* isLegacy */);
+ break;
+ }
+ transactionId = existingClientRequest.mTransactionId;
+ } else {
+ transactionId = getUniqueId();
+ }
+ if (registerServiceType != null) {
+ serviceInfo.setServiceType(registerServiceType);
+ serviceInfo.setServiceName(
+ truncateServiceName(serviceInfo.getServiceName()));
+ }
+
+ if (!checkHostname(hostname)) {
+ clientInfo.onRegisterServiceFailedImmediately(clientRequestId,
+ NsdManager.FAILURE_BAD_PARAMETERS, false /* isLegacy */);
+ break;
+ }
+
+ Set<String> subtypes = new ArraySet<>(serviceInfo.getSubtypes());
+ if (typeSubtype != null && typeSubtype.second != null) {
+ for (String subType : typeSubtype.second) {
+ if (!TextUtils.isEmpty(subType)) {
+ subtypes.add(subType);
+ }
+ }
+ }
+ subtypes = dedupSubtypeLabels(subtypes);
+
+ if (!checkSubtypeLabels(subtypes)) {
+ clientInfo.onRegisterServiceFailedImmediately(clientRequestId,
+ NsdManager.FAILURE_BAD_PARAMETERS, false /* isLegacy */);
+ break;
+ }
+
+ if (!checkTtl(advertisingRequest.getTtl(), clientInfo)) {
+ clientInfo.onRegisterServiceFailedImmediately(clientRequestId,
+ NsdManager.FAILURE_BAD_PARAMETERS, false /* isLegacy */);
+ break;
+ }
+
+ serviceInfo.setSubtypes(subtypes);
maybeStartMonitoringSockets();
- // TODO: pass in the subtype as well. Including the subtype in the
- // service type would generate service instance names like
- // Name._subtype._sub._type._tcp, which is incorrect
- // (it should be Name._type._tcp).
+ final MdnsAdvertisingOptions mdnsAdvertisingOptions =
+ MdnsAdvertisingOptions.newBuilder()
+ .setIsOnlyUpdate(isUpdateOnly)
+ .setTtl(advertisingRequest.getTtl())
+ .build();
mAdvertiser.addOrUpdateService(transactionId, serviceInfo,
- typeSubtype.second,
- MdnsAdvertisingOptions.newBuilder().build());
+ mdnsAdvertisingOptions, clientInfo.mUid);
storeAdvertiserRequestMap(clientRequestId, transactionId, clientInfo,
serviceInfo.getNetwork());
} else {
maybeStartDaemon();
+ transactionId = getUniqueId();
if (registerService(transactionId, serviceInfo)) {
if (DBG) {
Log.d(TAG, "Register " + clientRequestId
@@ -877,7 +1033,7 @@
}
case NsdManager.UNREGISTER_SERVICE: {
if (DBG) Log.d(TAG, "unregister service");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -920,7 +1076,7 @@
}
case NsdManager.RESOLVE_SERVICE: {
if (DBG) Log.d(TAG, "Resolve service");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -932,7 +1088,7 @@
final NsdServiceInfo info = args.serviceInfo;
transactionId = getUniqueId();
- final Pair<String, String> typeSubtype =
+ final Pair<String, List<String>> typeSubtype =
parseTypeAndSubtype(info.getServiceType());
final String serviceType = typeSubtype == null
? null : typeSubtype.first;
@@ -948,10 +1104,12 @@
maybeStartMonitoringSockets();
final MdnsListener listener = new ResolutionListener(clientRequestId,
- transactionId, info, resolveServiceType);
+ transactionId, resolveServiceType);
final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
.setNetwork(info.getNetwork())
- .setIsPassiveMode(true)
+ .setQueryMode(mMdnsFeatureFlags.isAggressiveQueryModeEnabled()
+ ? AGGRESSIVE_QUERY_MODE
+ : PASSIVE_QUERY_MODE)
.setResolveInstanceName(info.getServiceName())
.setRemoveExpiredService(true)
.build();
@@ -982,7 +1140,7 @@
}
case NsdManager.STOP_RESOLUTION: {
if (DBG) Log.d(TAG, "Stop service resolution");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -1021,7 +1179,7 @@
}
case NsdManager.REGISTER_SERVICE_CALLBACK: {
if (DBG) Log.d(TAG, "Register a service callback");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -1033,7 +1191,7 @@
final NsdServiceInfo info = args.serviceInfo;
transactionId = getUniqueId();
- final Pair<String, String> typeAndSubtype =
+ final Pair<String, List<String>> typeAndSubtype =
parseTypeAndSubtype(info.getServiceType());
final String serviceType = typeAndSubtype == null
? null : typeAndSubtype.first;
@@ -1046,10 +1204,12 @@
maybeStartMonitoringSockets();
final MdnsListener listener = new ServiceInfoListener(clientRequestId,
- transactionId, info, resolveServiceType);
+ transactionId, resolveServiceType);
final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
.setNetwork(info.getNetwork())
- .setIsPassiveMode(true)
+ .setQueryMode(mMdnsFeatureFlags.isAggressiveQueryModeEnabled()
+ ? AGGRESSIVE_QUERY_MODE
+ : PASSIVE_QUERY_MODE)
.setResolveInstanceName(info.getServiceName())
.setRemoveExpiredService(true)
.build();
@@ -1064,7 +1224,7 @@
}
case NsdManager.UNREGISTER_SERVICE_CALLBACK: {
if (DBG) Log.d(TAG, "Unregister a service callback");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -1388,6 +1548,8 @@
servInfo,
network == null ? INetd.LOCAL_NET_ID : network.netId,
serviceInfo.getInterfaceIndex());
+ servInfo.setSubtypes(dedupSubtypeLabels(serviceInfo.getSubtypes()));
+ servInfo.setExpirationTime(serviceInfo.getExpirationTime());
return servInfo;
}
@@ -1441,6 +1603,7 @@
Log.e(TAG, "Invalid attribute", e);
}
}
+ info.setHostname(getHostname(serviceInfo));
final List<InetAddress> addresses = getInetAddresses(serviceInfo);
if (addresses.size() != 0) {
info.setHostAddresses(addresses);
@@ -1477,6 +1640,7 @@
}
}
+ info.setHostname(getHostname(serviceInfo));
final List<InetAddress> addresses = getInetAddresses(serviceInfo);
info.setHostAddresses(addresses);
clientInfo.onServiceUpdated(clientRequestId, info, request);
@@ -1523,6 +1687,16 @@
return addresses;
}
+ @NonNull
+ private static String getHostname(@NonNull MdnsServiceInfo serviceInfo) {
+ String[] hostname = serviceInfo.getHostName();
+ // Strip the "local" top-level domain.
+ if (hostname.length >= 2 && hostname[hostname.length - 1].equals("local")) {
+ hostname = Arrays.copyOf(hostname, hostname.length - 1);
+ }
+ return String.join(".", hostname);
+ }
+
private static void setServiceNetworkForCallback(NsdServiceInfo info, int netId, int ifaceIdx) {
switch (netId) {
case NETID_UNSET:
@@ -1581,34 +1755,51 @@
* underscore; they are alphanumerical characters or dashes or underscore, except the
* last one that is just alphanumerical. The last label must be _tcp or _udp.
*
- * <p>The subtype may also be specified with a comma after the service type, for example
- * _type._tcp,_subtype.
+ * <p>The subtypes may also be specified with a comma after the service type, for example
+ * _type._tcp,_subtype1,_subtype2
*
* @param serviceType the request service type for discovery / resolution service
* @return constructed service type or null if the given service type is invalid.
*/
@Nullable
- public static Pair<String, String> parseTypeAndSubtype(String serviceType) {
+ public static Pair<String, List<String>> parseTypeAndSubtype(String serviceType) {
if (TextUtils.isEmpty(serviceType)) return null;
-
- final String typeOrSubtypePattern = "_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]";
- final Pattern serviceTypePattern = Pattern.compile(
- // Optional leading subtype (_subtype._type._tcp)
- // (?: xxx) is a non-capturing parenthesis, don't capture the dot
- "^(?:(" + typeOrSubtypePattern + ")\\.)?"
- // Actual type (_type._tcp.local)
- + "(" + typeOrSubtypePattern + "\\._(?:tcp|udp))"
- // Drop '.' at the end of service type that is compatible with old backend.
- // e.g. allow "_type._tcp.local."
- + "\\.?"
- // Optional subtype after comma, for "_type._tcp,_subtype" format
- + "(?:,(" + typeOrSubtypePattern + "))?"
- + "$");
+ final Pattern serviceTypePattern = Pattern.compile(TYPE_REGEX);
final Matcher matcher = serviceTypePattern.matcher(serviceType);
if (!matcher.matches()) return null;
- // Use the subtype either at the beginning or after the comma
- final String subtype = matcher.group(1) != null ? matcher.group(1) : matcher.group(3);
- return new Pair<>(matcher.group(2), subtype);
+ final String queryType = matcher.group(2);
+ // Use the subtype at the beginning
+ if (matcher.group(1) != null) {
+ return new Pair<>(queryType, List.of(matcher.group(1)));
+ }
+ // Use the subtypes at the end
+ final String subTypesStr = matcher.group(3);
+ if (subTypesStr != null && !subTypesStr.isEmpty()) {
+ final String[] subTypes = subTypesStr.substring(1).split(",");
+ return new Pair<>(queryType, List.of(subTypes));
+ }
+
+ return new Pair<>(queryType, Collections.emptyList());
+ }
+
+ /**
+ * Checks if the hostname is valid.
+ *
+ * <p>For now NsdService only allows single-label hostnames conforming to RFC 1035. In other
+ * words, the hostname should be at most 63 characters long and it only contains letters, digits
+ * and hyphens.
+ */
+ public static boolean checkHostname(@Nullable String hostname) {
+ if (hostname == null) {
+ return true;
+ }
+ String HOSTNAME_REGEX = "^[a-zA-Z]([a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$";
+ return Pattern.compile(HOSTNAME_REGEX).matcher(hostname).matches();
+ }
+
+ /** Returns {@code true} if {@code subtype} is a valid DNS-SD subtype label. */
+ private static boolean checkSubtypeLabel(String subtype) {
+ return Pattern.compile("^" + SUBTYPE_LABEL_REGEX + "$").matcher(subtype).matches();
}
@VisibleForTesting
@@ -1622,7 +1813,8 @@
mContext = ctx;
mNsdStateMachine = new NsdStateMachine(TAG, handler);
mNsdStateMachine.start();
- mMDnsManager = ctx.getSystemService(MDnsManager.class);
+ // It can fail on V+ device since mdns native service provided by netd is removed.
+ mMDnsManager = SdkLevel.isAtLeastV() ? null : ctx.getSystemService(MDnsManager.class);
mMDnsEventCallback = new MDnsEventCallback(mNsdStateMachine);
mDeps = deps;
@@ -1644,7 +1836,7 @@
am.addOnUidImportanceListener(new UidImportanceListener(handler),
mRunningAppActiveImportanceCutoff);
- final MdnsFeatureFlags flags = new MdnsFeatureFlags.Builder()
+ mMdnsFeatureFlags = new MdnsFeatureFlags.Builder()
.setIsMdnsOffloadFeatureEnabled(mDeps.isTetheringFeatureNotChickenedOut(
mContext, MdnsFeatureFlags.NSD_FORCE_DISABLE_MDNS_OFFLOAD))
.setIncludeInetAddressRecordsInProbing(mDeps.isFeatureEnabled(
@@ -1653,15 +1845,25 @@
mContext, MdnsFeatureFlags.NSD_EXPIRED_SERVICES_REMOVAL))
.setIsLabelCountLimitEnabled(mDeps.isTetheringFeatureNotChickenedOut(
mContext, MdnsFeatureFlags.NSD_LIMIT_LABEL_COUNT))
+ .setIsKnownAnswerSuppressionEnabled(mDeps.isFeatureEnabled(
+ mContext, MdnsFeatureFlags.NSD_KNOWN_ANSWER_SUPPRESSION))
+ .setIsUnicastReplyEnabled(mDeps.isFeatureEnabled(
+ mContext, MdnsFeatureFlags.NSD_UNICAST_REPLY_ENABLED))
+ .setIsAggressiveQueryModeEnabled(mDeps.isFeatureEnabled(
+ mContext, MdnsFeatureFlags.NSD_AGGRESSIVE_QUERY_MODE))
+ .setOverrideProvider(flag -> mDeps.isFeatureEnabled(
+ mContext, FORCE_ENABLE_FLAG_FOR_TEST_PREFIX + flag))
.build();
mMdnsSocketClient =
new MdnsMultinetworkSocketClient(handler.getLooper(), mMdnsSocketProvider,
- LOGGER.forSubComponent("MdnsMultinetworkSocketClient"), flags);
+ LOGGER.forSubComponent("MdnsMultinetworkSocketClient"), mMdnsFeatureFlags);
mMdnsDiscoveryManager = deps.makeMdnsDiscoveryManager(new ExecutorProvider(),
- mMdnsSocketClient, LOGGER.forSubComponent("MdnsDiscoveryManager"), flags);
+ mMdnsSocketClient, LOGGER.forSubComponent("MdnsDiscoveryManager"),
+ mMdnsFeatureFlags);
handler.post(() -> mMdnsSocketClient.setCallback(mMdnsDiscoveryManager));
mAdvertiser = deps.makeMdnsAdvertiser(handler.getLooper(), mMdnsSocketProvider,
- new AdvertiserCallback(), LOGGER.forSubComponent("MdnsAdvertiser"), flags);
+ new AdvertiserCallback(), LOGGER.forSubComponent("MdnsAdvertiser"),
+ mMdnsFeatureFlags, mContext);
mClock = deps.makeClock();
}
@@ -1717,13 +1919,6 @@
}
/**
- * @see DeviceConfigUtils#isTrunkStableFeatureEnabled
- */
- public boolean isTrunkStableFeatureEnabled(String feature) {
- return DeviceConfigUtils.isTrunkStableFeatureEnabled(feature);
- }
-
- /**
* @see MdnsDiscoveryManager
*/
public MdnsDiscoveryManager makeMdnsDiscoveryManager(
@@ -1740,8 +1935,8 @@
public MdnsAdvertiser makeMdnsAdvertiser(
@NonNull Looper looper, @NonNull MdnsSocketProvider socketProvider,
@NonNull MdnsAdvertiser.AdvertiserCallback cb, @NonNull SharedLog sharedLog,
- MdnsFeatureFlags featureFlags) {
- return new MdnsAdvertiser(looper, socketProvider, cb, sharedLog, featureFlags);
+ MdnsFeatureFlags featureFlags, Context context) {
+ return new MdnsAdvertiser(looper, socketProvider, cb, sharedLog, featureFlags, context);
}
/**
@@ -1927,9 +2122,10 @@
final int clientRequestId = getClientRequestIdOrLog(clientInfo, transactionId);
if (clientRequestId < 0) return;
- // onRegisterServiceSucceeded only has the service name in its info. This aligns with
- // historical behavior.
+ // onRegisterServiceSucceeded only has the service name and hostname in its info. This
+ // aligns with historical behavior.
final NsdServiceInfo cbInfo = new NsdServiceInfo(registeredInfo.getServiceName(), null);
+ cbInfo.setHostname(registeredInfo.getHostname());
final ClientRequest request = clientInfo.mClientRequests.get(clientRequestId);
clientInfo.onRegisterServiceSucceeded(clientRequestId, cbInfo, request);
}
@@ -2014,33 +2210,56 @@
}
}
+ private static class AdvertisingArgs {
+ public final NsdServiceConnector connector;
+ public final AdvertisingRequest advertisingRequest;
+
+ AdvertisingArgs(NsdServiceConnector connector, AdvertisingRequest advertisingRequest) {
+ this.connector = connector;
+ this.advertisingRequest = advertisingRequest;
+ }
+ }
+
+ private static final class DiscoveryArgs {
+ public final NsdServiceConnector connector;
+ public final DiscoveryRequest discoveryRequest;
+ DiscoveryArgs(NsdServiceConnector connector, DiscoveryRequest discoveryRequest) {
+ this.connector = connector;
+ this.discoveryRequest = discoveryRequest;
+ }
+ }
+
private class NsdServiceConnector extends INsdServiceConnector.Stub
implements IBinder.DeathRecipient {
+
@Override
- public void registerService(int listenerKey, NsdServiceInfo serviceInfo) {
+ public void registerService(int listenerKey, AdvertisingRequest advertisingRequest)
+ throws RemoteException {
+ NsdManager.checkServiceInfoForRegistration(advertisingRequest.getServiceInfo());
mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
NsdManager.REGISTER_SERVICE, 0, listenerKey,
- new ListenerArgs(this, serviceInfo)));
+ new AdvertisingArgs(this, advertisingRequest)
+ ));
}
@Override
public void unregisterService(int listenerKey) {
mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
NsdManager.UNREGISTER_SERVICE, 0, listenerKey,
- new ListenerArgs(this, null)));
+ new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
- public void discoverServices(int listenerKey, NsdServiceInfo serviceInfo) {
+ public void discoverServices(int listenerKey, DiscoveryRequest discoveryRequest) {
mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
NsdManager.DISCOVER_SERVICES, 0, listenerKey,
- new ListenerArgs(this, serviceInfo)));
+ new DiscoveryArgs(this, discoveryRequest)));
}
@Override
public void stopDiscovery(int listenerKey) {
- mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
- NsdManager.STOP_DISCOVERY, 0, listenerKey, new ListenerArgs(this, null)));
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(NsdManager.STOP_DISCOVERY,
+ 0, listenerKey, new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
@@ -2052,8 +2271,8 @@
@Override
public void stopResolution(int listenerKey) {
- mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
- NsdManager.STOP_RESOLUTION, 0, listenerKey, new ListenerArgs(this, null)));
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(NsdManager.STOP_RESOLUTION,
+ 0, listenerKey, new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
@@ -2067,13 +2286,13 @@
public void unregisterServiceInfoCallback(int listenerKey) {
mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
NsdManager.UNREGISTER_SERVICE_CALLBACK, 0, listenerKey,
- new ListenerArgs(this, null)));
+ new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
public void startDaemon() {
- mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
- NsdManager.DAEMON_STARTUP, new ListenerArgs(this, null)));
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(NsdManager.DAEMON_STARTUP,
+ new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
@@ -2109,25 +2328,24 @@
throw new SecurityException("API is not available in before API level 33");
}
- // REGISTER_NSD_OFFLOAD_ENGINE was only added to the SDK in V.
- if (SdkLevel.isAtLeastV() && PermissionUtils.checkAnyPermissionOf(context,
- REGISTER_NSD_OFFLOAD_ENGINE)) {
- return;
+ final ArrayList<String> permissionsList = new ArrayList<>(Arrays.asList(NETWORK_STACK,
+ PERMISSION_MAINLINE_NETWORK_STACK, NETWORK_SETTINGS));
+
+ if (SdkLevel.isAtLeastV()) {
+ // REGISTER_NSD_OFFLOAD_ENGINE was only added to the SDK in V.
+ permissionsList.add(REGISTER_NSD_OFFLOAD_ENGINE);
+ } else if (SdkLevel.isAtLeastU()) {
+ // REGISTER_NSD_OFFLOAD_ENGINE cannot be backport to U. In U, check the DEVICE_POWER
+ // permission instead.
+ permissionsList.add(DEVICE_POWER);
}
- // REGISTER_NSD_OFFLOAD_ENGINE cannot be backport to U. In U, check the DEVICE_POWER
- // permission instead.
- if (!SdkLevel.isAtLeastV() && SdkLevel.isAtLeastU()
- && PermissionUtils.checkAnyPermissionOf(context, DEVICE_POWER)) {
- return;
- }
- if (PermissionUtils.checkAnyPermissionOf(context, NETWORK_STACK,
- PERMISSION_MAINLINE_NETWORK_STACK, NETWORK_SETTINGS)) {
+ if (PermissionUtils.hasAnyPermissionOf(context,
+ permissionsList.toArray(new String[0]))) {
return;
}
throw new SecurityException("Requires one of the following permissions: "
- + String.join(", ", List.of(REGISTER_NSD_OFFLOAD_ENGINE, NETWORK_STACK,
- PERMISSION_MAINLINE_NETWORK_STACK, NETWORK_SETTINGS)) + ".");
+ + String.join(", ", permissionsList) + ".");
}
}
@@ -2145,6 +2363,11 @@
}
private boolean registerService(int transactionId, NsdServiceInfo service) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "registerService: mMDnsManager is null");
+ return false;
+ }
+
if (DBG) {
Log.d(TAG, "registerService: " + transactionId + " " + service);
}
@@ -2162,13 +2385,22 @@
}
private boolean unregisterService(int transactionId) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "unregisterService: mMDnsManager is null");
+ return false;
+ }
return mMDnsManager.stopOperation(transactionId);
}
- private boolean discoverServices(int transactionId, NsdServiceInfo serviceInfo) {
- final String type = serviceInfo.getServiceType();
- final int discoverInterface = getNetworkInterfaceIndex(serviceInfo);
- if (serviceInfo.getNetwork() != null && discoverInterface == IFACE_IDX_ANY) {
+ private boolean discoverServices(int transactionId, DiscoveryRequest discoveryRequest) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "discoverServices: mMDnsManager is null");
+ return false;
+ }
+
+ final String type = discoveryRequest.getServiceType();
+ final int discoverInterface = getNetworkInterfaceIndex(discoveryRequest);
+ if (discoveryRequest.getNetwork() != null && discoverInterface == IFACE_IDX_ANY) {
Log.e(TAG, "Interface to discover service on not found");
return false;
}
@@ -2176,10 +2408,18 @@
}
private boolean stopServiceDiscovery(int transactionId) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "stopServiceDiscovery: mMDnsManager is null");
+ return false;
+ }
return mMDnsManager.stopOperation(transactionId);
}
private boolean resolveService(int transactionId, NsdServiceInfo service) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "resolveService: mMDnsManager is null");
+ return false;
+ }
final String name = service.getServiceName();
final String type = service.getServiceType();
final int resolveInterface = getNetworkInterfaceIndex(service);
@@ -2210,7 +2450,26 @@
}
return IFACE_IDX_ANY;
}
+ return getNetworkInterfaceIndex(network);
+ }
+ /**
+ * Returns the interface to use to discover a service on a specific network, or {@link
+ * IFACE_IDX_ANY} if no network is specified.
+ */
+ private int getNetworkInterfaceIndex(DiscoveryRequest discoveryRequest) {
+ final Network network = discoveryRequest.getNetwork();
+ if (network == null) {
+ return IFACE_IDX_ANY;
+ }
+ return getNetworkInterfaceIndex(network);
+ }
+
+ /**
+ * Returns the interface of a specific network, or {@link IFACE_IDX_ANY} if no interface is
+ * associated with {@code network}.
+ */
+ private int getNetworkInterfaceIndex(@NonNull Network network) {
String interfaceName = getNetworkInterfaceName(network);
if (interfaceName == null) {
return IFACE_IDX_ANY;
@@ -2253,20 +2512,32 @@
}
private boolean stopResolveService(int transactionId) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "stopResolveService: mMDnsManager is null");
+ return false;
+ }
return mMDnsManager.stopOperation(transactionId);
}
private boolean getAddrInfo(int transactionId, String hostname, int interfaceIdx) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "getAddrInfo: mMDnsManager is null");
+ return false;
+ }
return mMDnsManager.getServiceAddress(transactionId, hostname, interfaceIdx);
}
private boolean stopGetAddrInfo(int transactionId) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "stopGetAddrInfo: mMDnsManager is null");
+ return false;
+ }
return mMDnsManager.stopOperation(transactionId);
}
@Override
public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
- if (!PermissionUtils.checkDumpPermission(mContext, TAG, writer)) return;
+ if (!PermissionUtils.hasDumpPermission(mContext, TAG, writer)) return;
final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ");
// Dump state machine logs
@@ -2278,6 +2549,14 @@
pw.increaseIndent();
mServiceLogs.reverseDump(pw);
pw.decreaseIndent();
+
+ //Dump DiscoveryManager
+ pw.println();
+ pw.println("DiscoveryManager:");
+ pw.increaseIndent();
+ HandlerUtils.runWithScissorsForDump(
+ mNsdStateMachine.getHandler(), () -> mMdnsDiscoveryManager.dump(pw), 10_000);
+ pw.decreaseIndent();
}
private abstract static class ClientRequest {
@@ -2384,7 +2663,15 @@
/* Information tracked per client */
private class ClientInfo {
- private static final int MAX_LIMIT = 10;
+ /**
+ * Maximum number of requests (callbacks) for a client.
+ *
+ * 200 listeners should be more than enough for most use-cases: even if a client tries to
+ * file callbacks for every service on a local network, there are generally much less than
+ * 200 devices on a local network (a /24 only allows 255 IPv4 devices), and while some
+ * devices may have multiple services, many devices do not advertise any.
+ */
+ private static final int MAX_LIMIT = 200;
private final INsdManagerCallback mCb;
/* Remembers a resolved service until getaddrinfo completes */
private NsdServiceInfo mResolvedService;
@@ -2431,6 +2718,10 @@
return sb.toString();
}
+ public int getUid() {
+ return mUid;
+ }
+
private boolean isPreSClient() {
return mIsPreSClient;
}
@@ -2573,12 +2864,12 @@
&& !(request instanceof AdvertiserClientRequest);
}
- void onDiscoverServicesStarted(int listenerKey, NsdServiceInfo info,
+ void onDiscoverServicesStarted(int listenerKey, DiscoveryRequest discoveryRequest,
ClientRequest request) {
mMetrics.reportServiceDiscoveryStarted(
isLegacyClientRequest(request), request.mTransactionId);
try {
- mCb.onDiscoverServicesStarted(listenerKey, info);
+ mCb.onDiscoverServicesStarted(listenerKey, discoveryRequest);
} catch (RemoteException e) {
Log.e(TAG, "Error calling onDiscoverServicesStarted", e);
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
index fc0e11b..c162bcc 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -17,11 +17,14 @@
package com.android.server.connectivity.mdns;
import static com.android.server.connectivity.mdns.MdnsConstants.NO_PACKET;
+import static com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_HOST;
+import static com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_SERVICE;
import static com.android.server.connectivity.mdns.MdnsRecord.MAX_LABEL_LENGTH;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresApi;
+import android.content.Context;
import android.net.LinkAddress;
import android.net.Network;
import android.net.nsd.NsdManager;
@@ -30,13 +33,16 @@
import android.net.nsd.OffloadServiceInfo;
import android.os.Build;
import android.os.Looper;
+import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import android.util.SparseArray;
+import com.android.connectivity.resources.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.SharedLog;
+import com.android.server.connectivity.ConnectivityResources;
import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.util.ArrayList;
@@ -44,6 +50,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.UUID;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
@@ -83,6 +90,7 @@
private final Map<String, List<OffloadServiceInfoWrapper>> mInterfaceOffloadServices =
new ArrayMap<>();
private final MdnsFeatureFlags mMdnsFeatureFlags;
+ private final Map<String, Integer> mServiceTypeToOffloadPriority;
/**
* Dependencies for {@link MdnsAdvertiser}, useful for testing.
@@ -146,7 +154,9 @@
mSharedLog.wtf("Register succeeded for unknown registration");
return;
}
- if (mMdnsFeatureFlags.mIsMdnsOffloadFeatureEnabled) {
+ if (mMdnsFeatureFlags.mIsMdnsOffloadFeatureEnabled
+ // TODO: Enable offload when the serviceInfo contains a custom host.
+ && TextUtils.isEmpty(registration.getServiceInfo().getHostname())) {
final String interfaceName = advertiser.getSocketInterfaceName();
final List<OffloadServiceInfoWrapper> existingOffloadServiceInfoWrappers =
mInterfaceOffloadServices.computeIfAbsent(interfaceName,
@@ -174,8 +184,11 @@
}
@Override
- public void onServiceConflict(@NonNull MdnsInterfaceAdvertiser advertiser, int serviceId) {
- mSharedLog.i("Found conflict, restarted probing for service " + serviceId);
+ public void onServiceConflict(@NonNull MdnsInterfaceAdvertiser advertiser, int serviceId,
+ int conflictType) {
+ mSharedLog.i("Found conflict, restarted probing for service "
+ + serviceId + " "
+ + conflictType);
final Registration registration = mRegistrations.get(serviceId);
if (registration == null) return;
@@ -200,10 +213,22 @@
return;
}
- // Conflict was found during probing; rename once to find a name that has no conflict
- registration.updateForConflict(
- registration.makeNewServiceInfoForConflict(1 /* renameCount */),
- 1 /* renameCount */);
+ if ((conflictType & CONFLICT_SERVICE) != 0) {
+ // Service conflict was found during probing; rename once to find a name that has no
+ // conflict
+ registration.updateForServiceConflict(
+ registration.makeNewServiceInfoForServiceConflict(1 /* renameCount */),
+ 1 /* renameCount */);
+ }
+
+ if ((conflictType & CONFLICT_HOST) != 0) {
+ // Host conflict was found during probing; rename once to find a name that has no
+ // conflict
+ registration.updateForHostConflict(
+ registration.makeNewServiceInfoForHostConflict(1 /* renameCount */),
+ 1 /* renameCount */);
+ }
+
registration.mConflictDuringProbingCount++;
// Keep renaming if the new name conflicts in local registrations
@@ -226,23 +251,54 @@
}
};
- private boolean hasAnyConflict(
+ private boolean hasAnyServiceConflict(
@NonNull BiPredicate<Network, InterfaceAdvertiserRequest> applicableAdvertiserFilter,
- @NonNull NsdServiceInfo newInfo) {
- return any(mAdvertiserRequests, (network, adv) ->
- applicableAdvertiserFilter.test(network, adv) && adv.hasConflict(newInfo));
+ @NonNull NsdServiceInfo newInfo,
+ @NonNull Registration originalRegistration) {
+ return any(
+ mAdvertiserRequests,
+ (network, adv) ->
+ applicableAdvertiserFilter.test(network, adv)
+ && adv.hasServiceConflict(newInfo, originalRegistration));
+ }
+
+ private boolean hasAnyHostConflict(
+ @NonNull BiPredicate<Network, InterfaceAdvertiserRequest> applicableAdvertiserFilter,
+ @NonNull NsdServiceInfo newInfo,
+ int clientUid) {
+ // Check if it conflicts with custom hosts.
+ if (any(
+ mAdvertiserRequests,
+ (network, adv) ->
+ applicableAdvertiserFilter.test(network, adv)
+ && adv.hasHostConflict(newInfo, clientUid))) {
+ return true;
+ }
+ // Check if it conflicts with the default hostname.
+ return MdnsUtils.equalsIgnoreDnsCase(newInfo.getHostname(), mDeviceHostName[0]);
}
private void updateRegistrationUntilNoConflict(
@NonNull BiPredicate<Network, InterfaceAdvertiserRequest> applicableAdvertiserFilter,
@NonNull Registration registration) {
- int renameCount = 0;
NsdServiceInfo newInfo = registration.getServiceInfo();
- while (hasAnyConflict(applicableAdvertiserFilter, newInfo)) {
- renameCount++;
- newInfo = registration.makeNewServiceInfoForConflict(renameCount);
+
+ int renameServiceCount = 0;
+ while (hasAnyServiceConflict(applicableAdvertiserFilter, newInfo, registration)) {
+ renameServiceCount++;
+ newInfo = registration.makeNewServiceInfoForServiceConflict(renameServiceCount);
}
- registration.updateForConflict(newInfo, renameCount);
+ registration.updateForServiceConflict(newInfo, renameServiceCount);
+
+ if (!TextUtils.isEmpty(registration.getServiceInfo().getHostname())) {
+ int renameHostCount = 0;
+ while (hasAnyHostConflict(
+ applicableAdvertiserFilter, newInfo, registration.mClientUid)) {
+ renameHostCount++;
+ newInfo = registration.makeNewServiceInfoForHostConflict(renameHostCount);
+ }
+ registration.updateForHostConflict(newInfo, renameHostCount);
+ }
}
private void maybeSendOffloadStop(final String interfaceName, int serviceId) {
@@ -321,17 +377,34 @@
/**
* Return whether using the proposed new {@link NsdServiceInfo} to add a registration would
- * cause a conflict in this {@link InterfaceAdvertiserRequest}.
+ * cause a conflict of the service in this {@link InterfaceAdvertiserRequest}.
*/
- boolean hasConflict(@NonNull NsdServiceInfo newInfo) {
- return getConflictingService(newInfo) >= 0;
+ boolean hasServiceConflict(
+ @NonNull NsdServiceInfo newInfo, @NonNull Registration originalRegistration) {
+ return getConflictingRegistrationDueToService(newInfo, originalRegistration) >= 0;
}
/**
- * Get the ID of a conflicting service, or -1 if none.
+ * Return whether using the proposed new {@link NsdServiceInfo} to add a registration would
+ * cause a conflict of the host in this {@link InterfaceAdvertiserRequest}.
+ *
+ * @param clientUid UID of the user who wants to advertise the serviceInfo.
*/
- int getConflictingService(@NonNull NsdServiceInfo info) {
+ boolean hasHostConflict(@NonNull NsdServiceInfo newInfo, int clientUid) {
+ return getConflictingRegistrationDueToHost(newInfo, clientUid) >= 0;
+ }
+
+ /** Get the ID of a conflicting registration due to service, or -1 if none. */
+ int getConflictingRegistrationDueToService(
+ @NonNull NsdServiceInfo info, @NonNull Registration originalRegistration) {
+ if (TextUtils.isEmpty(info.getServiceName())) {
+ return -1;
+ }
for (int i = 0; i < mPendingRegistrations.size(); i++) {
+ // Never conflict with itself
+ if (mPendingRegistrations.valueAt(i) == originalRegistration) {
+ continue;
+ }
final NsdServiceInfo other = mPendingRegistrations.valueAt(i).getServiceInfo();
if (MdnsUtils.equalsIgnoreDnsCase(info.getServiceName(), other.getServiceName())
&& MdnsUtils.equalsIgnoreDnsCase(info.getServiceType(),
@@ -343,16 +416,41 @@
}
/**
+ * Get the ID of a conflicting registration due to host, or -1 if none.
+ *
+ * <p>It's valid that multiple registrations from the same user are using the same hostname.
+ *
+ * <p>If there's already another registration with the same hostname requested by another
+ * user, this is considered a conflict.
+ */
+ int getConflictingRegistrationDueToHost(@NonNull NsdServiceInfo info, int clientUid) {
+ if (TextUtils.isEmpty(info.getHostname())) {
+ return -1;
+ }
+ for (int i = 0; i < mPendingRegistrations.size(); i++) {
+ final Registration otherRegistration = mPendingRegistrations.valueAt(i);
+ final NsdServiceInfo otherInfo = otherRegistration.getServiceInfo();
+ if (clientUid != otherRegistration.mClientUid
+ && MdnsUtils.equalsIgnoreDnsCase(
+ info.getHostname(), otherInfo.getHostname())) {
+ return mPendingRegistrations.keyAt(i);
+ }
+ }
+ return -1;
+ }
+
+ /**
* Add a service to advertise.
*
- * Conflicts must be checked via {@link #getConflictingService} before attempting to add.
+ * <p>Conflicts must be checked via {@link #getConflictingRegistrationDueToService} and
+ * {@link #getConflictingRegistrationDueToHost} before attempting to add.
*/
void addService(int id, @NonNull Registration registration) {
mPendingRegistrations.put(id, registration);
for (int i = 0; i < mAdvertisers.size(); i++) {
try {
mAdvertisers.valueAt(i).addService(id, registration.getServiceInfo(),
- registration.getSubtype());
+ registration.getAdvertisingOptions());
} catch (NameConflictException e) {
mSharedLog.wtf("Name conflict adding services that should have unique names",
e);
@@ -367,7 +465,8 @@
void updateService(int id, @NonNull Registration registration) {
mPendingRegistrations.put(id, registration);
for (int i = 0; i < mAdvertisers.size(); i++) {
- mAdvertisers.valueAt(i).updateService(id, registration.getSubtype());
+ mAdvertisers.valueAt(i).updateService(
+ id, registration.getServiceInfo().getSubtypes());
}
}
@@ -417,7 +516,7 @@
final Registration registration = mPendingRegistrations.valueAt(i);
try {
advertiser.addService(mPendingRegistrations.keyAt(i),
- registration.getServiceInfo(), registration.getSubtype());
+ registration.getServiceInfo(), registration.getAdvertisingOptions());
} catch (NameConflictException e) {
mSharedLog.wtf("Name conflict adding services that should have unique names",
e);
@@ -479,38 +578,45 @@
}
private static class Registration {
- @NonNull
- final String mOriginalName;
+ @Nullable
+ final String mOriginalServiceName;
+ @Nullable
+ final String mOriginalHostname;
boolean mNotifiedRegistrationSuccess;
- private int mConflictCount;
+ private int mServiceNameConflictCount;
+ private int mHostnameConflictCount;
@NonNull
private NsdServiceInfo mServiceInfo;
- @Nullable
- private String mSubtype;
-
+ final int mClientUid;
+ private final MdnsAdvertisingOptions mAdvertisingOptions;
int mConflictDuringProbingCount;
int mConflictAfterProbingCount;
- private Registration(@NonNull NsdServiceInfo serviceInfo, @Nullable String subtype) {
- this.mOriginalName = serviceInfo.getServiceName();
+ private Registration(@NonNull NsdServiceInfo serviceInfo, int clientUid,
+ @NonNull MdnsAdvertisingOptions advertisingOptions) {
+ this.mOriginalServiceName = serviceInfo.getServiceName();
+ this.mOriginalHostname = serviceInfo.getHostname();
this.mServiceInfo = serviceInfo;
- this.mSubtype = subtype;
+ this.mClientUid = clientUid;
+ this.mAdvertisingOptions = advertisingOptions;
+ }
+
+ /** Check if the new {@link NsdServiceInfo} doesn't update any data other than subtypes. */
+ public boolean isSubtypeOnlyUpdate(@NonNull NsdServiceInfo newInfo) {
+ return Objects.equals(newInfo.getServiceName(), mOriginalServiceName)
+ && Objects.equals(newInfo.getServiceType(), mServiceInfo.getServiceType())
+ && newInfo.getPort() == mServiceInfo.getPort()
+ && Objects.equals(newInfo.getHostname(), mOriginalHostname)
+ && Objects.equals(newInfo.getHostAddresses(), mServiceInfo.getHostAddresses())
+ && Objects.equals(newInfo.getNetwork(), mServiceInfo.getNetwork());
}
/**
- * Matches between the NsdServiceInfo in the Registration and the provided argument.
+ * Update subTypes for the registration.
*/
- public boolean matches(@Nullable NsdServiceInfo newInfo) {
- return Objects.equals(newInfo.getServiceName(), mOriginalName) && Objects.equals(
- newInfo.getServiceType(), mServiceInfo.getServiceType()) && Objects.equals(
- newInfo.getNetwork(), mServiceInfo.getNetwork());
- }
-
- /**
- * Update subType for the registration.
- */
- public void updateSubtype(@Nullable String subtype) {
- this.mSubtype = subtype;
+ public void updateSubtypes(@NonNull Set<String> subtypes) {
+ mServiceInfo = new NsdServiceInfo(mServiceInfo);
+ mServiceInfo.setSubtypes(subtypes);
}
/**
@@ -519,8 +625,19 @@
* @param newInfo New service info to use.
* @param renameCount How many renames were done before reaching the current name.
*/
- private void updateForConflict(@NonNull NsdServiceInfo newInfo, int renameCount) {
- mConflictCount += renameCount;
+ private void updateForServiceConflict(@NonNull NsdServiceInfo newInfo, int renameCount) {
+ mServiceNameConflictCount += renameCount;
+ mServiceInfo = newInfo;
+ }
+
+ /**
+ * Update the registration to use a different host name, after a conflict was found.
+ *
+ * @param newInfo New service info to use.
+ * @param renameCount How many renames were done before reaching the current name.
+ */
+ private void updateForHostConflict(@NonNull NsdServiceInfo newInfo, int renameCount) {
+ mHostnameConflictCount += renameCount;
mServiceInfo = newInfo;
}
@@ -536,39 +653,57 @@
* @param renameCount How much to increase the number suffix for this conflict.
*/
@NonNull
- public NsdServiceInfo makeNewServiceInfoForConflict(int renameCount) {
+ public NsdServiceInfo makeNewServiceInfoForServiceConflict(int renameCount) {
// In case of conflict choose a different service name. After the first conflict use
// "Name (2)", then "Name (3)" etc.
// TODO: use a hidden method in NsdServiceInfo once MdnsAdvertiser is moved to service-t
- final NsdServiceInfo newInfo = new NsdServiceInfo();
+ final NsdServiceInfo newInfo = new NsdServiceInfo(mServiceInfo);
newInfo.setServiceName(getUpdatedServiceName(renameCount));
- newInfo.setServiceType(mServiceInfo.getServiceType());
- for (Map.Entry<String, byte[]> attr : mServiceInfo.getAttributes().entrySet()) {
- newInfo.setAttribute(attr.getKey(),
- attr.getValue() == null ? null : new String(attr.getValue()));
- }
- newInfo.setHost(mServiceInfo.getHost());
- newInfo.setPort(mServiceInfo.getPort());
- newInfo.setNetwork(mServiceInfo.getNetwork());
- // interfaceIndex is not set when registering
+ return newInfo;
+ }
+
+ /**
+ * Make a new hostname for the registration, after a conflict was found.
+ *
+ * <p>If a name conflict was found during probing or because different advertising requests
+ * used the same name, the registration is attempted again with a new name (here using a
+ * number suffix, -1, -2, etc). Registration success is notified once probing succeeds with
+ * a new name.
+ *
+ * @param renameCount How much to increase the number suffix for this conflict.
+ */
+ @NonNull
+ public NsdServiceInfo makeNewServiceInfoForHostConflict(int renameCount) {
+ // In case of conflict choose a different hostname. After the first conflict use
+ // "Name-2", then "Name-3" etc.
+ final NsdServiceInfo newInfo = new NsdServiceInfo(mServiceInfo);
+ newInfo.setHostname(getUpdatedHostname(renameCount));
return newInfo;
}
private String getUpdatedServiceName(int renameCount) {
- final String suffix = " (" + (mConflictCount + renameCount + 1) + ")";
- final String truncatedServiceName = MdnsUtils.truncateServiceName(mOriginalName,
+ final String suffix = " (" + (mServiceNameConflictCount + renameCount + 1) + ")";
+ final String truncatedServiceName = MdnsUtils.truncateServiceName(mOriginalServiceName,
MAX_LABEL_LENGTH - suffix.length());
return truncatedServiceName + suffix;
}
+ private String getUpdatedHostname(int renameCount) {
+ final String suffix = "-" + (mHostnameConflictCount + renameCount + 1);
+ final String truncatedHostname =
+ MdnsUtils.truncateServiceName(
+ mOriginalHostname, MAX_LABEL_LENGTH - suffix.length());
+ return truncatedHostname + suffix;
+ }
+
@NonNull
public NsdServiceInfo getServiceInfo() {
return mServiceInfo;
}
- @Nullable
- public String getSubtype() {
- return mSubtype;
+ @NonNull
+ public MdnsAdvertisingOptions getAdvertisingOptions() {
+ return mAdvertisingOptions;
}
}
@@ -637,14 +772,16 @@
public MdnsAdvertiser(@NonNull Looper looper, @NonNull MdnsSocketProvider socketProvider,
@NonNull AdvertiserCallback cb, @NonNull SharedLog sharedLog,
- @NonNull MdnsFeatureFlags mDnsFeatureFlags) {
- this(looper, socketProvider, cb, new Dependencies(), sharedLog, mDnsFeatureFlags);
+ @NonNull MdnsFeatureFlags mDnsFeatureFlags, @NonNull Context context) {
+ this(looper, socketProvider, cb, new Dependencies(), sharedLog, mDnsFeatureFlags,
+ context);
}
@VisibleForTesting
MdnsAdvertiser(@NonNull Looper looper, @NonNull MdnsSocketProvider socketProvider,
@NonNull AdvertiserCallback cb, @NonNull Dependencies deps,
- @NonNull SharedLog sharedLog, @NonNull MdnsFeatureFlags mDnsFeatureFlags) {
+ @NonNull SharedLog sharedLog, @NonNull MdnsFeatureFlags mDnsFeatureFlags,
+ @NonNull Context context) {
mLooper = looper;
mCb = cb;
mSocketProvider = socketProvider;
@@ -652,6 +789,31 @@
mDeviceHostName = deps.generateHostname();
mSharedLog = sharedLog;
mMdnsFeatureFlags = mDnsFeatureFlags;
+ final ConnectivityResources res = new ConnectivityResources(context);
+ mServiceTypeToOffloadPriority = parseOffloadPriorityList(
+ res.get().getStringArray(R.array.config_nsdOffloadServicesPriority), sharedLog);
+ }
+
+ private static Map<String, Integer> parseOffloadPriorityList(
+ @NonNull String[] resValues, SharedLog sharedLog) {
+ final Map<String, Integer> priorities = new ArrayMap<>(resValues.length);
+ for (String entry : resValues) {
+ final String[] priorityAndType = entry.split(":", 2);
+ if (priorityAndType.length != 2) {
+ sharedLog.wtf("Invalid config_nsdOffloadServicesPriority ignored: " + entry);
+ continue;
+ }
+
+ final int priority;
+ try {
+ priority = Integer.parseInt(priorityAndType[0]);
+ } catch (NumberFormatException e) {
+ sharedLog.wtf("Invalid priority in config_nsdOffloadServicesPriority: " + entry);
+ continue;
+ }
+ priorities.put(MdnsUtils.toDnsLowerCase(priorityAndType[1]), priority);
+ }
+ return priorities;
}
private void checkThread() {
@@ -665,14 +827,15 @@
*
* @param id A unique ID for the service.
* @param service The service info to advertise.
- * @param subtype An optional subtype to advertise the service with.
* @param advertisingOptions The advertising options.
+ * @param clientUid The UID who wants to advertise the service.
*/
- public void addOrUpdateService(int id, NsdServiceInfo service, @Nullable String subtype,
- MdnsAdvertisingOptions advertisingOptions) {
+ public void addOrUpdateService(int id, NsdServiceInfo service,
+ MdnsAdvertisingOptions advertisingOptions, int clientUid) {
checkThread();
final Registration existingRegistration = mRegistrations.get(id);
final Network network = service.getNetwork();
+ final Set<String> subtypes = service.getSubtypes();
Registration registration;
if (advertisingOptions.isOnlyUpdate()) {
if (existingRegistration == null) {
@@ -680,17 +843,17 @@
mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR);
return;
}
- if (!(existingRegistration.matches(service))) {
+ if (!(existingRegistration.isSubtypeOnlyUpdate(service))) {
mSharedLog.e("Update request can only update subType, serviceInfo: " + service
+ ", existing serviceInfo: " + existingRegistration.getServiceInfo());
mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR);
return;
}
- mSharedLog.i("Update service " + service + " with ID " + id + " and subtype " + subtype
- + " advertisingOptions " + advertisingOptions);
+ mSharedLog.i("Update service " + service + " with ID " + id + " and subtypes "
+ + subtypes + " advertisingOptions " + advertisingOptions);
registration = existingRegistration;
- registration.updateSubtype(subtype);
+ registration.updateSubtypes(subtypes);
} else {
if (existingRegistration != null) {
mSharedLog.e("Adding duplicate registration for " + service);
@@ -698,9 +861,9 @@
mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR);
return;
}
- mSharedLog.i("Adding service " + service + " with ID " + id + " and subtype " + subtype
- + " advertisingOptions " + advertisingOptions);
- registration = new Registration(service, subtype);
+ mSharedLog.i("Adding service " + service + " with ID " + id + " and subtypes "
+ + subtypes + " advertisingOptions " + advertisingOptions);
+ registration = new Registration(service, clientUid, advertisingOptions);
final BiPredicate<Network, InterfaceAdvertiserRequest> checkConflictFilter;
if (network == null) {
// If registering on all networks, no advertiser must have conflicts
@@ -793,21 +956,17 @@
private OffloadServiceInfoWrapper createOffloadService(int serviceId,
@NonNull Registration registration, byte[] rawOffloadPacket) {
final NsdServiceInfo nsdServiceInfo = registration.getServiceInfo();
- final List<String> subTypes = new ArrayList<>();
- String subType = registration.getSubtype();
- if (subType != null) {
- subTypes.add(subType);
- }
+ final Integer mapPriority = mServiceTypeToOffloadPriority.get(
+ MdnsUtils.toDnsLowerCase(nsdServiceInfo.getServiceType()));
+ // Higher values of priority are less prioritized
+ final int priority = mapPriority == null ? Integer.MAX_VALUE : mapPriority;
final OffloadServiceInfo offloadServiceInfo = new OffloadServiceInfo(
new OffloadServiceInfo.Key(nsdServiceInfo.getServiceName(),
nsdServiceInfo.getServiceType()),
- subTypes,
+ new ArrayList<>(nsdServiceInfo.getSubtypes()),
String.join(".", mDeviceHostName),
rawOffloadPacket,
- // TODO: define overlayable resources in
- // ServiceConnectivityResources that set the priority based on
- // service type.
- 0 /* priority */,
+ priority,
// TODO: set the offloadType based on the callback timing.
OffloadEngine.OFFLOAD_TYPE_REPLY);
return new OffloadServiceInfoWrapper(serviceId, offloadServiceInfo);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
index e7a6ca7..a81d1e4 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
@@ -16,6 +16,11 @@
package com.android.server.connectivity.mdns;
+import android.annotation.Nullable;
+
+import java.time.Duration;
+import java.util.Objects;
+
/**
* API configuration parameters for advertising the mDNS service.
*
@@ -27,13 +32,15 @@
private static MdnsAdvertisingOptions sDefaultOptions;
private final boolean mIsOnlyUpdate;
+ @Nullable
+ private final Duration mTtl;
/**
* Parcelable constructs for a {@link MdnsAdvertisingOptions}.
*/
- MdnsAdvertisingOptions(
- boolean isOnlyUpdate) {
+ MdnsAdvertisingOptions(boolean isOnlyUpdate, @Nullable Duration ttl) {
this.mIsOnlyUpdate = isOnlyUpdate;
+ this.mTtl = ttl;
}
/**
@@ -60,9 +67,36 @@
return mIsOnlyUpdate;
}
+ /**
+ * Returns the TTL for all records in a service.
+ */
+ @Nullable
+ public Duration getTtl() {
+ return mTtl;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ } else if (!(other instanceof MdnsAdvertisingOptions)) {
+ return false;
+ } else {
+ final MdnsAdvertisingOptions otherOptions = (MdnsAdvertisingOptions) other;
+ return mIsOnlyUpdate == otherOptions.mIsOnlyUpdate
+ && Objects.equals(mTtl, otherOptions.mTtl);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mIsOnlyUpdate, mTtl);
+ }
+
@Override
public String toString() {
- return "MdnsAdvertisingOptions{" + "mIsOnlyUpdate=" + mIsOnlyUpdate + '}';
+ return "MdnsAdvertisingOptions{" + "mIsOnlyUpdate=" + mIsOnlyUpdate + ", mTtl=" + mTtl
+ + '}';
}
/**
@@ -70,6 +104,8 @@
*/
public static final class Builder {
private boolean mIsOnlyUpdate = false;
+ @Nullable
+ private Duration mTtl;
private Builder() {
}
@@ -83,10 +119,18 @@
}
/**
+ * Sets the TTL duration for all records of the service.
+ */
+ public Builder setTtl(@Nullable Duration ttl) {
+ this.mTtl = ttl;
+ return this;
+ }
+
+ /**
* Builds a {@link MdnsAdvertisingOptions} with the arguments supplied to this builder.
*/
public MdnsAdvertisingOptions build() {
- return new MdnsAdvertisingOptions(mIsOnlyUpdate);
+ return new MdnsAdvertisingOptions(mIsOnlyUpdate, mTtl);
}
}
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
index 766f999..21b7069 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -16,26 +16,29 @@
package com.android.server.connectivity.mdns;
-import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
-
import android.Manifest.permission;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.os.Handler;
import android.os.HandlerThread;
+import android.os.Looper;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
+import androidx.annotation.GuardedBy;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.IOException;
+import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
+import java.util.concurrent.Executor;
/**
* This class keeps tracking the set of registered {@link MdnsServiceBrowserListener} instances, and
@@ -50,11 +53,13 @@
@NonNull private final SharedLog sharedLog;
@NonNull private final PerSocketServiceTypeClients perSocketServiceTypeClients;
- @NonNull private final Handler handler;
- @Nullable private final HandlerThread handlerThread;
- @NonNull private final MdnsServiceCache serviceCache;
+ @NonNull private final DiscoveryExecutor discoveryExecutor;
@NonNull private final MdnsFeatureFlags mdnsFeatureFlags;
+ // Only accessed on the handler thread, initialized before first use
+ @Nullable
+ private MdnsServiceCache serviceCache;
+
private static class PerSocketServiceTypeClients {
private final ArrayMap<Pair<String, SocketKey>, MdnsServiceTypeClient> clients =
new ArrayMap<>();
@@ -125,33 +130,82 @@
this.sharedLog = sharedLog;
this.perSocketServiceTypeClients = new PerSocketServiceTypeClients();
this.mdnsFeatureFlags = mdnsFeatureFlags;
- if (socketClient.getLooper() != null) {
- this.handlerThread = null;
- this.handler = new Handler(socketClient.getLooper());
- this.serviceCache = new MdnsServiceCache(socketClient.getLooper(), mdnsFeatureFlags);
- } else {
- this.handlerThread = new HandlerThread(MdnsDiscoveryManager.class.getSimpleName());
- this.handlerThread.start();
- this.handler = new Handler(handlerThread.getLooper());
- this.serviceCache = new MdnsServiceCache(handlerThread.getLooper(), mdnsFeatureFlags);
- }
+ this.discoveryExecutor = new DiscoveryExecutor(socketClient.getLooper());
}
- private void checkAndRunOnHandlerThread(@NonNull Runnable function) {
- if (this.handlerThread == null) {
- function.run();
- } else {
+ private static class DiscoveryExecutor implements Executor {
+ private final HandlerThread handlerThread;
+
+ @GuardedBy("pendingTasks")
+ @Nullable private Handler handler;
+ @GuardedBy("pendingTasks")
+ @NonNull private final ArrayList<Runnable> pendingTasks = new ArrayList<>();
+
+ DiscoveryExecutor(@Nullable Looper defaultLooper) {
+ if (defaultLooper != null) {
+ this.handlerThread = null;
+ synchronized (pendingTasks) {
+ this.handler = new Handler(defaultLooper);
+ }
+ } else {
+ this.handlerThread = new HandlerThread(MdnsDiscoveryManager.class.getSimpleName()) {
+ @Override
+ protected void onLooperPrepared() {
+ synchronized (pendingTasks) {
+ handler = new Handler(getLooper());
+ for (Runnable pendingTask : pendingTasks) {
+ handler.post(pendingTask);
+ }
+ pendingTasks.clear();
+ }
+ }
+ };
+ this.handlerThread.start();
+ }
+ }
+
+ public void checkAndRunOnHandlerThread(@NonNull Runnable function) {
+ if (this.handlerThread == null) {
+ // Callers are expected to already be running on the handler when a defaultLooper
+ // was provided
+ function.run();
+ } else {
+ execute(function);
+ }
+ }
+
+ @Override
+ public void execute(Runnable function) {
+ final Handler handler;
+ synchronized (pendingTasks) {
+ if (this.handler == null) {
+ pendingTasks.add(function);
+ return;
+ } else {
+ handler = this.handler;
+ }
+ }
handler.post(function);
}
+
+ void shutDown() {
+ if (this.handlerThread != null) {
+ this.handlerThread.quitSafely();
+ }
+ }
+
+ void ensureRunningOnHandlerThread() {
+ synchronized (pendingTasks) {
+ MdnsUtils.ensureRunningOnHandlerThread(handler);
+ }
+ }
}
/**
* Do the cleanup of the MdnsDiscoveryManager
*/
public void shutDown() {
- if (this.handlerThread != null) {
- this.handlerThread.quitSafely();
- }
+ discoveryExecutor.shutDown();
}
/**
@@ -169,7 +223,7 @@
@NonNull MdnsServiceBrowserListener listener,
@NonNull MdnsSearchOptions searchOptions) {
sharedLog.i("Registering listener for serviceType: " + serviceType);
- checkAndRunOnHandlerThread(() ->
+ discoveryExecutor.checkAndRunOnHandlerThread(() ->
handleRegisterListener(serviceType, listener, searchOptions));
}
@@ -191,7 +245,7 @@
new MdnsSocketClientBase.SocketCreationCallback() {
@Override
public void onSocketCreated(@NonNull SocketKey socketKey) {
- ensureRunningOnHandlerThread(handler);
+ discoveryExecutor.ensureRunningOnHandlerThread();
// All listeners of the same service types shares the same
// MdnsServiceTypeClient.
MdnsServiceTypeClient serviceTypeClient =
@@ -206,7 +260,7 @@
@Override
public void onSocketDestroyed(@NonNull SocketKey socketKey) {
- ensureRunningOnHandlerThread(handler);
+ discoveryExecutor.ensureRunningOnHandlerThread();
final MdnsServiceTypeClient serviceTypeClient =
perSocketServiceTypeClients.get(serviceType, socketKey);
if (serviceTypeClient == null) return;
@@ -229,7 +283,8 @@
public void unregisterListener(
@NonNull String serviceType, @NonNull MdnsServiceBrowserListener listener) {
sharedLog.i("Unregistering listener for serviceType:" + serviceType);
- checkAndRunOnHandlerThread(() -> handleUnregisterListener(serviceType, listener));
+ discoveryExecutor.checkAndRunOnHandlerThread(() ->
+ handleUnregisterListener(serviceType, listener));
}
private void handleUnregisterListener(
@@ -260,7 +315,7 @@
@Override
public void onResponseReceived(@NonNull MdnsPacket packet, @NonNull SocketKey socketKey) {
- checkAndRunOnHandlerThread(() ->
+ discoveryExecutor.checkAndRunOnHandlerThread(() ->
handleOnResponseReceived(packet, socketKey));
}
@@ -282,7 +337,7 @@
@Override
public void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode,
@NonNull SocketKey socketKey) {
- checkAndRunOnHandlerThread(() ->
+ discoveryExecutor.checkAndRunOnHandlerThread(() ->
handleOnFailedToParseMdnsResponse(receivedPacketNumber, errorCode, socketKey));
}
@@ -296,12 +351,31 @@
@VisibleForTesting
MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType,
@NonNull SocketKey socketKey) {
+ discoveryExecutor.ensureRunningOnHandlerThread();
sharedLog.log("createServiceTypeClient for type:" + serviceType + " " + socketKey);
final String tag = serviceType + "-" + socketKey.getNetwork()
+ "/" + socketKey.getInterfaceIndex();
+ final Looper looper = Looper.myLooper();
+ if (serviceCache == null) {
+ serviceCache = new MdnsServiceCache(looper, mdnsFeatureFlags);
+ }
return new MdnsServiceTypeClient(
serviceType, socketClient,
executorProvider.newServiceTypeClientSchedulerExecutor(), socketKey,
- sharedLog.forSubComponent(tag), handler.getLooper(), serviceCache);
+ sharedLog.forSubComponent(tag), looper, serviceCache);
+ }
+
+ /**
+ * Dump DiscoveryManager state.
+ */
+ public void dump(PrintWriter pw) {
+ discoveryExecutor.checkAndRunOnHandlerThread(() -> {
+ pw.println();
+ // Dump ServiceTypeClients
+ for (MdnsServiceTypeClient serviceTypeClient
+ : perSocketServiceTypeClients.getAllMdnsServiceTypeClient()) {
+ serviceTypeClient.dump(pw);
+ }
+ });
}
}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
index 0a6d8c1..56202fd 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -15,6 +15,9 @@
*/
package com.android.server.connectivity.mdns;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
/**
* The class that contains mDNS feature flags;
*/
@@ -41,6 +44,24 @@
*/
public static final String NSD_LIMIT_LABEL_COUNT = "nsd_limit_label_count";
+ /**
+ * A feature flag to control whether the known-answer suppression should be enabled.
+ */
+ public static final String NSD_KNOWN_ANSWER_SUPPRESSION = "nsd_known_answer_suppression";
+
+ /**
+ * A feature flag to control whether unicast replies should be enabled.
+ *
+ * <p>Enabling this feature causes replies to queries with the Query Unicast (QU) flag set to be
+ * sent unicast instead of multicast, as per RFC6762 5.4.
+ */
+ public static final String NSD_UNICAST_REPLY_ENABLED = "nsd_unicast_reply_enabled";
+
+ /**
+ * A feature flag to control whether the aggressive query mode should be enabled.
+ */
+ public static final String NSD_AGGRESSIVE_QUERY_MODE = "nsd_aggressive_query_mode";
+
// Flag for offload feature
public final boolean mIsMdnsOffloadFeatureEnabled;
@@ -53,17 +74,76 @@
// Flag for label count limit
public final boolean mIsLabelCountLimitEnabled;
+ // Flag for known-answer suppression
+ public final boolean mIsKnownAnswerSuppressionEnabled;
+
+ // Flag to enable replying unicast to queries requesting unicast replies
+ public final boolean mIsUnicastReplyEnabled;
+
+ // Flag for aggressive query mode
+ public final boolean mIsAggressiveQueryModeEnabled;
+
+ @Nullable
+ private final FlagOverrideProvider mOverrideProvider;
+
+ /**
+ * A provider that can indicate whether a flag should be force-enabled for testing purposes.
+ */
+ public interface FlagOverrideProvider {
+ /**
+ * Indicates whether the flag should be force-enabled for testing purposes.
+ */
+ boolean isForceEnabledForTest(@NonNull String flag);
+ }
+
+ /**
+ * Indicates whether the flag should be force-enabled for testing purposes.
+ */
+ private boolean isForceEnabledForTest(@NonNull String flag) {
+ return mOverrideProvider != null && mOverrideProvider.isForceEnabledForTest(flag);
+ }
+
+ /**
+ * Indicates whether {@link #NSD_UNICAST_REPLY_ENABLED} is enabled, including for testing.
+ */
+ public boolean isUnicastReplyEnabled() {
+ return mIsUnicastReplyEnabled || isForceEnabledForTest(NSD_UNICAST_REPLY_ENABLED);
+ }
+
+ /**
+ * Indicates whether {@link #NSD_AGGRESSIVE_QUERY_MODE} is enabled, including for testing.
+ */
+ public boolean isAggressiveQueryModeEnabled() {
+ return mIsAggressiveQueryModeEnabled || isForceEnabledForTest(NSD_AGGRESSIVE_QUERY_MODE);
+ }
+
+ /**
+ * Indicates whether {@link #NSD_KNOWN_ANSWER_SUPPRESSION} is enabled, including for testing.
+ */
+ public boolean isKnownAnswerSuppressionEnabled() {
+ return mIsKnownAnswerSuppressionEnabled
+ || isForceEnabledForTest(NSD_KNOWN_ANSWER_SUPPRESSION);
+ }
+
/**
* The constructor for {@link MdnsFeatureFlags}.
*/
public MdnsFeatureFlags(boolean isOffloadFeatureEnabled,
boolean includeInetAddressRecordsInProbing,
boolean isExpiredServicesRemovalEnabled,
- boolean isLabelCountLimitEnabled) {
+ boolean isLabelCountLimitEnabled,
+ boolean isKnownAnswerSuppressionEnabled,
+ boolean isUnicastReplyEnabled,
+ boolean isAggressiveQueryModeEnabled,
+ @Nullable FlagOverrideProvider overrideProvider) {
mIsMdnsOffloadFeatureEnabled = isOffloadFeatureEnabled;
mIncludeInetAddressRecordsInProbing = includeInetAddressRecordsInProbing;
mIsExpiredServicesRemovalEnabled = isExpiredServicesRemovalEnabled;
mIsLabelCountLimitEnabled = isLabelCountLimitEnabled;
+ mIsKnownAnswerSuppressionEnabled = isKnownAnswerSuppressionEnabled;
+ mIsUnicastReplyEnabled = isUnicastReplyEnabled;
+ mIsAggressiveQueryModeEnabled = isAggressiveQueryModeEnabled;
+ mOverrideProvider = overrideProvider;
}
@@ -79,6 +159,10 @@
private boolean mIncludeInetAddressRecordsInProbing;
private boolean mIsExpiredServicesRemovalEnabled;
private boolean mIsLabelCountLimitEnabled;
+ private boolean mIsKnownAnswerSuppressionEnabled;
+ private boolean mIsUnicastReplyEnabled;
+ private boolean mIsAggressiveQueryModeEnabled;
+ private FlagOverrideProvider mOverrideProvider;
/**
* The constructor for {@link Builder}.
@@ -88,6 +172,10 @@
mIncludeInetAddressRecordsInProbing = false;
mIsExpiredServicesRemovalEnabled = false;
mIsLabelCountLimitEnabled = true; // Default enabled.
+ mIsKnownAnswerSuppressionEnabled = false;
+ mIsUnicastReplyEnabled = true;
+ mIsAggressiveQueryModeEnabled = false;
+ mOverrideProvider = null;
}
/**
@@ -132,13 +220,58 @@
}
/**
+ * Set whether the known-answer suppression is enabled.
+ *
+ * @see #NSD_KNOWN_ANSWER_SUPPRESSION
+ */
+ public Builder setIsKnownAnswerSuppressionEnabled(boolean isKnownAnswerSuppressionEnabled) {
+ mIsKnownAnswerSuppressionEnabled = isKnownAnswerSuppressionEnabled;
+ return this;
+ }
+
+ /**
+ * Set whether the unicast reply feature is enabled.
+ *
+ * @see #NSD_UNICAST_REPLY_ENABLED
+ */
+ public Builder setIsUnicastReplyEnabled(boolean isUnicastReplyEnabled) {
+ mIsUnicastReplyEnabled = isUnicastReplyEnabled;
+ return this;
+ }
+
+ /**
+ * Set a {@link FlagOverrideProvider} to be used by {@link #isForceEnabledForTest(String)}.
+ *
+ * If non-null, features that use {@link #isForceEnabledForTest(String)} will use that
+ * provider to query whether the flag should be force-enabled.
+ */
+ public Builder setOverrideProvider(@Nullable FlagOverrideProvider overrideProvider) {
+ mOverrideProvider = overrideProvider;
+ return this;
+ }
+
+ /**
+ * Set whether the aggressive query mode is enabled.
+ *
+ * @see #NSD_AGGRESSIVE_QUERY_MODE
+ */
+ public Builder setIsAggressiveQueryModeEnabled(boolean isAggressiveQueryModeEnabled) {
+ mIsAggressiveQueryModeEnabled = isAggressiveQueryModeEnabled;
+ return this;
+ }
+
+ /**
* Builds a {@link MdnsFeatureFlags} with the arguments supplied to this builder.
*/
public MdnsFeatureFlags build() {
return new MdnsFeatureFlags(mIsMdnsOffloadFeatureEnabled,
mIncludeInetAddressRecordsInProbing,
mIsExpiredServicesRemovalEnabled,
- mIsLabelCountLimitEnabled);
+ mIsLabelCountLimitEnabled,
+ mIsKnownAnswerSuppressionEnabled,
+ mIsUnicastReplyEnabled,
+ mIsAggressiveQueryModeEnabled,
+ mOverrideProvider);
}
}
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
index 463df63..c2363c0 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -22,10 +22,12 @@
import android.annotation.Nullable;
import android.annotation.RequiresApi;
import android.net.LinkAddress;
+import android.net.nsd.NsdManager;
import android.net.nsd.NsdServiceInfo;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
+import android.util.ArraySet;
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.HexDump;
@@ -37,12 +39,17 @@
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
/**
* A class that handles advertising services on a {@link MdnsInterfaceSocket} tied to an interface.
*/
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public class MdnsInterfaceAdvertiser implements MulticastPacketReader.PacketHandler {
+ public static final int CONFLICT_SERVICE = 1 << 0;
+ public static final int CONFLICT_HOST = 1 << 1;
+
private static final boolean DBG = MdnsAdvertiser.DBG;
@VisibleForTesting
public static final long EXIT_ANNOUNCEMENT_DELAY_MS = 100L;
@@ -84,10 +91,15 @@
/**
* Called by the advertiser when a conflict was found, during or after probing.
*
- * If a conflict is found during probing, the {@link #renameServiceForConflict} must be
+ * <p>If a conflict is found during probing, the {@link #renameServiceForConflict} must be
* called to restart probing and attempt registration with a different name.
+ *
+ * <p>{@code conflictType} is a bitmap telling which part of the service is conflicting. See
+ * {@link MdnsInterfaceAdvertiser#CONFLICT_SERVICE} and {@link
+ * MdnsInterfaceAdvertiser#CONFLICT_HOST}.
*/
- void onServiceConflict(@NonNull MdnsInterfaceAdvertiser advertiser, int serviceId);
+ void onServiceConflict(
+ @NonNull MdnsInterfaceAdvertiser advertiser, int serviceId, int conflictType);
/**
* Called by the advertiser when it destroyed itself.
@@ -161,10 +173,11 @@
@NonNull
public MdnsReplySender makeReplySender(@NonNull String interfaceTag, @NonNull Looper looper,
@NonNull MdnsInterfaceSocket socket, @NonNull byte[] packetCreationBuffer,
- @NonNull SharedLog sharedLog) {
+ @NonNull SharedLog sharedLog, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
return new MdnsReplySender(looper, socket, packetCreationBuffer,
sharedLog.forSubComponent(
- MdnsReplySender.class.getSimpleName() + "/" + interfaceTag), DBG);
+ MdnsReplySender.class.getSimpleName() + "/" + interfaceTag), DBG,
+ mdnsFeatureFlags);
}
/** @see MdnsAnnouncer */
@@ -207,7 +220,7 @@
mCb = cb;
mCbHandler = new Handler(looper);
mReplySender = deps.makeReplySender(sharedLog.getTag(), looper, socket,
- packetCreationBuffer, sharedLog);
+ packetCreationBuffer, sharedLog, mdnsFeatureFlags);
mPacketCreationBuffer = packetCreationBuffer;
mAnnouncer = deps.makeMdnsAnnouncer(sharedLog.getTag(), looper, mReplySender,
mAnnouncingCallback, sharedLog);
@@ -232,12 +245,12 @@
* Update an already registered service without sending exit/re-announcement packet.
*
* @param id An exiting service id
- * @param subtype A new subtype
+ * @param subtypes New subtypes
*/
- public void updateService(int id, @Nullable String subtype) {
+ public void updateService(int id, @NonNull Set<String> subtypes) {
// The current implementation is intended to be used in cases where subtypes don't get
// announced.
- mRecordRepository.updateService(id, subtype);
+ mRecordRepository.updateService(id, subtypes);
}
/**
@@ -245,9 +258,10 @@
*
* @throws NameConflictException There is already a service being advertised with that name.
*/
- public void addService(int id, NsdServiceInfo service, @Nullable String subtype)
- throws NameConflictException {
- final int replacedExitingService = mRecordRepository.addService(id, service, subtype);
+ public void addService(int id, NsdServiceInfo service,
+ @NonNull MdnsAdvertisingOptions advertisingOptions) throws NameConflictException {
+ final int replacedExitingService =
+ mRecordRepository.addService(id, service, advertisingOptions.getTtl());
// Cancel announcements for the existing service. This only happens for exiting services
// (so cancelling exiting announcements), as per RecordRepository.addService.
if (replacedExitingService >= 0) {
@@ -335,6 +349,7 @@
final MdnsProber.ProbingInfo probingInfo = mRecordRepository.setServiceProbing(serviceId);
if (probingInfo == null) return false;
+ mAnnouncer.stop(serviceId);
mProber.restartForConflict(probingInfo);
return true;
}
@@ -373,23 +388,33 @@
}
return;
}
+ // recvbuf and src are reused after this returns; ensure references to src are not kept.
+ final InetSocketAddress srcCopy = new InetSocketAddress(src.getAddress(), src.getPort());
if (DBG) {
mSharedLog.v("Parsed packet with " + packet.questions.size() + " questions, "
+ packet.answers.size() + " answers, "
+ packet.authorityRecords.size() + " authority, "
- + packet.additionalRecords.size() + " additional from " + src);
+ + packet.additionalRecords.size() + " additional from " + srcCopy);
}
- for (int conflictServiceId : mRecordRepository.getConflictingServices(packet)) {
- mCbHandler.post(() -> mCb.onServiceConflict(this, conflictServiceId));
+ Map<Integer, Integer> conflictingServices =
+ mRecordRepository.getConflictingServices(packet);
+
+ for (Map.Entry<Integer, Integer> entry : conflictingServices.entrySet()) {
+ int serviceId = entry.getKey();
+ int conflictType = entry.getValue();
+ mCbHandler.post(
+ () -> {
+ mCb.onServiceConflict(this, serviceId, conflictType);
+ });
}
// Even in case of conflict, add replies for other services. But in general conflicts would
// happen when the incoming packet has answer records (not a question), so there will be no
// answer. One exception is simultaneous probe tiebreaking (rfc6762 8.2), in which case the
// conflicting service is still probing and won't reply either.
- final MdnsReplyInfo answers = mRecordRepository.getReply(packet, src);
+ final MdnsReplyInfo answers = mRecordRepository.getReply(packet, srcCopy);
if (answers == null) return;
mReplySender.queueReply(answers);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
index e7b0eaa..869ac9b 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
@@ -96,7 +96,8 @@
@Override
public void onInterfaceDestroyed(@NonNull SocketKey socketKey,
@NonNull MdnsInterfaceSocket socket) {
- notifySocketDestroyed(socketKey);
+ mActiveSockets.remove(socketKey);
+ mSocketCreationCallback.onSocketDestroyed(socketKey);
maybeCleanupPacketHandler(socketKey);
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
index 28bd1b4..4b43989 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
@@ -176,6 +176,16 @@
}
/**
+ * For questions, returns whether a unicast reply was requested.
+ *
+ * In practice this is identical to {@link #getCacheFlush()}, as the "cache flush" flag in
+ * replies is the same as "unicast reply requested" in questions.
+ */
+ public final boolean isUnicastReplyRequested() {
+ return (cls & MdnsConstants.QCLASS_UNICAST) != 0;
+ }
+
+ /**
* Returns the record's remaining TTL.
*
* If the record was not sent yet (receipt time {@link #RECEIPT_TIME_NOT_SENT}), this is the
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
index 48ece68..ac64c3a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -19,6 +19,8 @@
import static com.android.server.connectivity.mdns.MdnsConstants.IPV4_SOCKET_ADDR;
import static com.android.server.connectivity.mdns.MdnsConstants.IPV6_SOCKET_ADDR;
import static com.android.server.connectivity.mdns.MdnsConstants.NO_PACKET;
+import static com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_HOST;
+import static com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_SERVICE;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -28,6 +30,8 @@
import android.os.Build;
import android.os.Looper;
import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.SparseArray;
@@ -41,11 +45,13 @@
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -53,6 +59,8 @@
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
/**
* A repository of records advertised through {@link MdnsInterfaceAdvertiser}.
@@ -68,14 +76,12 @@
// TTL for records with a host name as the resource record's name (e.g., A, AAAA, HINFO) or a
// host name contained within the resource record's rdata (e.g., SRV, reverse mapping PTR
// record)
- private static final long NAME_RECORDS_TTL_MILLIS = TimeUnit.SECONDS.toMillis(120);
+ private static final long DEFAULT_NAME_RECORDS_TTL_MILLIS = TimeUnit.SECONDS.toMillis(120);
// TTL for other records
- private static final long NON_NAME_RECORDS_TTL_MILLIS = TimeUnit.MINUTES.toMillis(75);
+ private static final long DEFAULT_NON_NAME_RECORDS_TTL_MILLIS = TimeUnit.MINUTES.toMillis(75);
// Top-level domain for link-local queries, as per RFC6762 3.
private static final String LOCAL_TLD = "local";
- // Subtype separator as per RFC6763 7.1 (_printer._sub._http._tcp.local)
- private static final String SUBTYPE_SEPARATOR = "_sub";
// Service type for service enumeration (RFC6763 9.)
private static final String[] DNS_SD_SERVICE_TYPE =
@@ -92,6 +98,7 @@
private final Looper mLooper;
@NonNull
private final String[] mDeviceHostname;
+ @NonNull
private final MdnsFeatureFlags mMdnsFeatureFlags;
public MdnsRecordRepository(@NonNull Looper looper, @NonNull String[] deviceHostname,
@@ -141,6 +148,9 @@
* Last time (as per SystemClock.elapsedRealtime) when sent via unicast or multicast,
* 0 if never
*/
+ // FIXME: the `lastSentTimeMs` and `lastAdvertisedTimeMs` should be maintained separately
+ // for IPv4 and IPv6, because neither IPv4 nor and IPv6 clients can receive replies in
+ // different address space.
public long lastSentTimeMs;
RecordInfo(NsdServiceInfo serviceInfo, T record, boolean sharedName) {
@@ -155,14 +165,14 @@
public final List<RecordInfo<?>> allRecords;
@NonNull
public final List<RecordInfo<MdnsPointerRecord>> ptrRecords;
- @NonNull
+ @Nullable
public final RecordInfo<MdnsServiceRecord> srvRecord;
- @NonNull
+ @Nullable
public final RecordInfo<MdnsTextRecord> txtRecord;
@NonNull
+ public final List<RecordInfo<MdnsInetAddressRecord>> addressRecords;
+ @NonNull
public final NsdServiceInfo serviceInfo;
- @Nullable
- public final String subtype;
/**
* Whether the service is sending exit announcements and will be destroyed soon.
@@ -184,91 +194,132 @@
*/
private boolean isProbing;
- /**
- * Create a ServiceRegistration with only update the subType
- */
- ServiceRegistration withSubtype(String newSubType) {
- return new ServiceRegistration(srvRecord.record.getServiceHost(), serviceInfo,
- newSubType, repliedServiceCount, sentPacketCount, exiting, isProbing);
- }
+ @Nullable
+ private Duration ttl;
+ /**
+ * Create a ServiceRegistration with only update the subType.
+ */
+ ServiceRegistration withSubtypes(@NonNull Set<String> newSubtypes) {
+ NsdServiceInfo newServiceInfo = new NsdServiceInfo(serviceInfo);
+ newServiceInfo.setSubtypes(newSubtypes);
+ return new ServiceRegistration(srvRecord.record.getServiceHost(), newServiceInfo,
+ repliedServiceCount, sentPacketCount, exiting, isProbing, ttl);
+ }
/**
* Create a ServiceRegistration for dns-sd service registration (RFC6763).
*/
ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
- @Nullable String subtype, int repliedServiceCount, int sentPacketCount,
- boolean exiting, boolean isProbing) {
+ int repliedServiceCount, int sentPacketCount, boolean exiting, boolean isProbing,
+ @Nullable Duration ttl) {
this.serviceInfo = serviceInfo;
- this.subtype = subtype;
- final String[] serviceType = splitServiceType(serviceInfo);
- final String[] serviceName = splitFullyQualifiedName(serviceInfo, serviceType);
+ final long nonNameRecordsTtlMillis;
+ final long nameRecordsTtlMillis;
- // Service PTR record
- final RecordInfo<MdnsPointerRecord> ptrRecord = new RecordInfo<>(
- serviceInfo,
- new MdnsPointerRecord(
- serviceType,
- 0L /* receiptTimeMillis */,
- false /* cacheFlush */,
- NON_NAME_RECORDS_TTL_MILLIS,
- serviceName),
- true /* sharedName */);
-
- if (subtype == null) {
- this.ptrRecords = Collections.singletonList(ptrRecord);
+ // When custom TTL is specified, all records of the service will use the custom TTL.
+ // This is typically useful for SRP (Service Registration Protocol:
+ // https://datatracker.ietf.org/doc/html/draft-ietf-dnssd-srp-24) Advertising Proxy
+ // where all records in a single SRP are required the same TTL.
+ if (ttl != null) {
+ nonNameRecordsTtlMillis = ttl.toMillis();
+ nameRecordsTtlMillis = ttl.toMillis();
} else {
- final String[] subtypeName = new String[serviceType.length + 2];
- System.arraycopy(serviceType, 0, subtypeName, 2, serviceType.length);
- subtypeName[0] = subtype;
- subtypeName[1] = SUBTYPE_SEPARATOR;
- final RecordInfo<MdnsPointerRecord> subtypeRecord = new RecordInfo<>(
- serviceInfo,
- new MdnsPointerRecord(
- subtypeName,
- 0L /* receiptTimeMillis */,
- false /* cacheFlush */,
- NON_NAME_RECORDS_TTL_MILLIS,
- serviceName),
- true /* sharedName */);
-
- this.ptrRecords = List.of(ptrRecord, subtypeRecord);
+ nonNameRecordsTtlMillis = DEFAULT_NON_NAME_RECORDS_TTL_MILLIS;
+ nameRecordsTtlMillis = DEFAULT_NAME_RECORDS_TTL_MILLIS;
}
- srvRecord = new RecordInfo<>(
- serviceInfo,
- new MdnsServiceRecord(serviceName,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- NAME_RECORDS_TTL_MILLIS, 0 /* servicePriority */, 0 /* serviceWeight */,
- serviceInfo.getPort(),
- deviceHostname),
- false /* sharedName */);
-
- txtRecord = new RecordInfo<>(
- serviceInfo,
- new MdnsTextRecord(serviceName,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */, // Service name is verified unique after probing
- NON_NAME_RECORDS_TTL_MILLIS,
- attrsToTextEntries(serviceInfo.getAttributes())),
- false /* sharedName */);
-
+ final boolean hasService = !TextUtils.isEmpty(serviceInfo.getServiceType());
+ final boolean hasCustomHost = !TextUtils.isEmpty(serviceInfo.getHostname());
+ final String[] hostname =
+ hasCustomHost
+ ? new String[] {serviceInfo.getHostname(), LOCAL_TLD}
+ : deviceHostname;
final ArrayList<RecordInfo<?>> allRecords = new ArrayList<>(5);
- allRecords.addAll(ptrRecords);
- allRecords.add(srvRecord);
- allRecords.add(txtRecord);
- // Service type enumeration record (RFC6763 9.)
- allRecords.add(new RecordInfo<>(
- serviceInfo,
- new MdnsPointerRecord(
- DNS_SD_SERVICE_TYPE,
- 0L /* receiptTimeMillis */,
- false /* cacheFlush */,
- NON_NAME_RECORDS_TTL_MILLIS,
- serviceType),
- true /* sharedName */));
+
+ if (hasService) {
+ final String[] serviceType = splitServiceType(serviceInfo);
+ final String[] serviceName = splitFullyQualifiedName(serviceInfo, serviceType);
+ // Service PTR records
+ ptrRecords = new ArrayList<>(serviceInfo.getSubtypes().size() + 1);
+ ptrRecords.add(new RecordInfo<>(
+ serviceInfo,
+ new MdnsPointerRecord(
+ serviceType,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ nonNameRecordsTtlMillis,
+ serviceName),
+ true /* sharedName */));
+ for (String subtype : serviceInfo.getSubtypes()) {
+ ptrRecords.add(new RecordInfo<>(
+ serviceInfo,
+ new MdnsPointerRecord(
+ MdnsUtils.constructFullSubtype(serviceType, subtype),
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ nonNameRecordsTtlMillis,
+ serviceName),
+ true /* sharedName */));
+ }
+
+ srvRecord = new RecordInfo<>(
+ serviceInfo,
+ new MdnsServiceRecord(serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ nameRecordsTtlMillis,
+ 0 /* servicePriority */, 0 /* serviceWeight */,
+ serviceInfo.getPort(),
+ hostname),
+ false /* sharedName */);
+
+ txtRecord = new RecordInfo<>(
+ serviceInfo,
+ new MdnsTextRecord(serviceName,
+ 0L /* receiptTimeMillis */,
+ // Service name is verified unique after probing
+ true /* cacheFlush */,
+ nonNameRecordsTtlMillis,
+ attrsToTextEntries(serviceInfo.getAttributes())),
+ false /* sharedName */);
+
+ allRecords.addAll(ptrRecords);
+ allRecords.add(srvRecord);
+ allRecords.add(txtRecord);
+ // Service type enumeration record (RFC6763 9.)
+ allRecords.add(new RecordInfo<>(
+ serviceInfo,
+ new MdnsPointerRecord(
+ DNS_SD_SERVICE_TYPE,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ nonNameRecordsTtlMillis,
+ serviceType),
+ true /* sharedName */));
+ } else {
+ ptrRecords = Collections.emptyList();
+ srvRecord = null;
+ txtRecord = null;
+ }
+
+ if (hasCustomHost) {
+ addressRecords = new ArrayList<>(serviceInfo.getHostAddresses().size());
+ for (InetAddress address : serviceInfo.getHostAddresses()) {
+ addressRecords.add(new RecordInfo<>(
+ serviceInfo,
+ new MdnsInetAddressRecord(hostname,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ nameRecordsTtlMillis,
+ address),
+ false /* sharedName */));
+ }
+ allRecords.addAll(addressRecords);
+ } else {
+ addressRecords = Collections.emptyList();
+ }
this.allRecords = Collections.unmodifiableList(allRecords);
this.repliedServiceCount = repliedServiceCount;
@@ -284,9 +335,9 @@
* @param serviceInfo Service to advertise
*/
ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
- @Nullable String subtype, int repliedServiceCount, int sentPacketCount) {
- this(deviceHostname, serviceInfo, subtype, repliedServiceCount, sentPacketCount,
- false /* exiting */, true /* isProbing */);
+ int repliedServiceCount, int sentPacketCount, @Nullable Duration ttl) {
+ this(deviceHostname, serviceInfo,repliedServiceCount, sentPacketCount,
+ false /* exiting */, true /* isProbing */, ttl);
}
void setProbing(boolean probing) {
@@ -308,7 +359,7 @@
revDnsAddr,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
- NAME_RECORDS_TTL_MILLIS,
+ DEFAULT_NAME_RECORDS_TTL_MILLIS,
mDeviceHostname),
false /* sharedName */));
@@ -318,7 +369,7 @@
mDeviceHostname,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
- NAME_RECORDS_TTL_MILLIS,
+ DEFAULT_NAME_RECORDS_TTL_MILLIS,
addr.getAddress()),
false /* sharedName */));
}
@@ -328,17 +379,16 @@
* Update a service that already registered in the repository.
*
* @param serviceId An existing service ID.
- * @param subtype A new subtype
- * @return
+ * @param subtypes New subtypes
*/
- public void updateService(int serviceId, @Nullable String subtype) {
+ public void updateService(int serviceId, @NonNull Set<String> subtypes) {
final ServiceRegistration existingRegistration = mServices.get(serviceId);
if (existingRegistration == null) {
throw new IllegalArgumentException(
"Service ID must already exist for an update request: " + serviceId);
}
- final ServiceRegistration updatedRegistration = existingRegistration.withSubtype(
- subtype);
+ final ServiceRegistration updatedRegistration = existingRegistration.withSubtypes(
+ subtypes);
mServices.put(serviceId, updatedRegistration);
}
@@ -348,26 +398,28 @@
* This may remove/replace any existing service that used the name added but is exiting.
* @param serviceId A unique service ID.
* @param serviceInfo Service info to add.
+ * @param ttl the TTL duration for all records of {@code serviceInfo} or {@code null}
* @return If the added service replaced another with a matching name (which was exiting), the
* ID of the replaced service.
* @throws NameConflictException There is already a (non-exiting) service using the name.
*/
- public int addService(int serviceId, NsdServiceInfo serviceInfo, @Nullable String subtype)
+ public int addService(int serviceId, NsdServiceInfo serviceInfo, @Nullable Duration ttl)
throws NameConflictException {
if (mServices.contains(serviceId)) {
throw new IllegalArgumentException(
"Service ID must not be reused across registrations: " + serviceId);
}
- final int existing = getServiceByName(serviceInfo.getServiceName());
+ final int existing =
+ getServiceByNameAndType(serviceInfo.getServiceName(), serviceInfo.getServiceType());
// It's OK to re-add a service that is exiting
if (existing >= 0 && !mServices.get(existing).exiting) {
throw new NameConflictException(existing);
}
final ServiceRegistration registration = new ServiceRegistration(
- mDeviceHostname, serviceInfo, subtype, NO_PACKET /* repliedServiceCount */,
- NO_PACKET /* sentPacketCount */);
+ mDeviceHostname, serviceInfo, NO_PACKET /* repliedServiceCount */,
+ NO_PACKET /* sentPacketCount */, ttl);
mServices.put(serviceId, registration);
// Remove existing exiting service
@@ -376,34 +428,41 @@
}
/**
- * @return The ID of the service identified by its name, or -1 if none.
+ * @return The ID of the service identified by its name and type, or -1 if none.
*/
- private int getServiceByName(@NonNull String serviceName) {
+ private int getServiceByNameAndType(
+ @Nullable String serviceName, @Nullable String serviceType) {
+ if (TextUtils.isEmpty(serviceName) || TextUtils.isEmpty(serviceType)) {
+ return -1;
+ }
for (int i = 0; i < mServices.size(); i++) {
- final ServiceRegistration registration = mServices.valueAt(i);
- if (MdnsUtils.equalsIgnoreDnsCase(serviceName,
- registration.serviceInfo.getServiceName())) {
+ final NsdServiceInfo info = mServices.valueAt(i).serviceInfo;
+ if (MdnsUtils.equalsIgnoreDnsCase(serviceName, info.getServiceName())
+ && MdnsUtils.equalsIgnoreDnsCase(serviceType, info.getServiceType())) {
return mServices.keyAt(i);
}
}
return -1;
}
- private MdnsProber.ProbingInfo makeProbingInfo(int serviceId,
- @NonNull MdnsServiceRecord srvRecord,
- @NonNull List<MdnsInetAddressRecord> inetAddressRecords) {
+ private MdnsProber.ProbingInfo makeProbingInfo(
+ int serviceId, ServiceRegistration registration) {
final List<MdnsRecord> probingRecords = new ArrayList<>();
// Probe with cacheFlush cleared; it is set when announcing, as it was verified unique:
// RFC6762 10.2
- probingRecords.add(new MdnsServiceRecord(srvRecord.getName(),
- 0L /* receiptTimeMillis */,
- false /* cacheFlush */,
- srvRecord.getTtl(),
- srvRecord.getServicePriority(), srvRecord.getServiceWeight(),
- srvRecord.getServicePort(),
- srvRecord.getServiceHost()));
+ if (registration.srvRecord != null) {
+ MdnsServiceRecord srvRecord = registration.srvRecord.record;
+ probingRecords.add(new MdnsServiceRecord(srvRecord.getName(),
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ srvRecord.getTtl(),
+ srvRecord.getServicePriority(), srvRecord.getServiceWeight(),
+ srvRecord.getServicePort(),
+ srvRecord.getServiceHost()));
+ }
- for (MdnsInetAddressRecord inetAddressRecord : inetAddressRecords) {
+ for (MdnsInetAddressRecord inetAddressRecord :
+ makeProbingInetAddressRecords(registration.serviceInfo)) {
probingRecords.add(new MdnsInetAddressRecord(inetAddressRecord.getName(),
0L /* receiptTimeMillis */,
false /* cacheFlush */,
@@ -500,6 +559,16 @@
return ret;
}
+ private boolean isTruncatedKnownAnswerPacket(MdnsPacket packet) {
+ if (!mMdnsFeatureFlags.isKnownAnswerSuppressionEnabled()
+ // Should ignore the response packet.
+ || (packet.flags & MdnsConstants.FLAGS_RESPONSE) != 0) {
+ return false;
+ }
+ // Check the packet contains no questions and as many more Known-Answer records as will fit.
+ return packet.questions.size() == 0 && packet.answers.size() != 0;
+ }
+
/**
* Get the reply to send to an incoming packet.
*
@@ -509,30 +578,83 @@
@Nullable
public MdnsReplyInfo getReply(MdnsPacket packet, InetSocketAddress src) {
final long now = SystemClock.elapsedRealtime();
- final boolean replyUnicast = (packet.flags & MdnsConstants.QCLASS_UNICAST) != 0;
- final ArrayList<MdnsRecord> additionalAnswerRecords = new ArrayList<>();
- final ArrayList<RecordInfo<?>> answerInfo = new ArrayList<>();
+
+ // TODO: b/322142420 - Set<RecordInfo<?>> may contain duplicate records wrapped in different
+ // RecordInfo<?>s when custom host is enabled.
+
+ // Use LinkedHashSet for preserving the insert order of the RRs, so that RRs of the same
+ // service or host are grouped together (which is more developer-friendly).
+ final Set<RecordInfo<?>> answerInfo = new LinkedHashSet<>();
+ final Set<RecordInfo<?>> additionalAnswerInfo = new LinkedHashSet<>();
+ // Reply unicast if the feature is enabled AND all replied questions request unicast
+ final boolean replyUnicastEnabled = mMdnsFeatureFlags.isUnicastReplyEnabled();
+ boolean replyUnicast = replyUnicastEnabled;
for (MdnsRecord question : packet.questions) {
// Add answers from general records
- addReplyFromService(question, mGeneralRecords, null /* servicePtrRecord */,
- null /* serviceSrvRecord */, null /* serviceTxtRecord */, replyUnicast, now,
- answerInfo, additionalAnswerRecords);
+ if (addReplyFromService(question, mGeneralRecords, null /* servicePtrRecord */,
+ null /* serviceSrvRecord */, null /* serviceTxtRecord */,
+ null /* hostname */,
+ replyUnicastEnabled, now, answerInfo, additionalAnswerInfo,
+ Collections.emptyList())) {
+ replyUnicast &= question.isUnicastReplyRequested();
+ }
// Add answers from each service
for (int i = 0; i < mServices.size(); i++) {
final ServiceRegistration registration = mServices.valueAt(i);
if (registration.exiting || registration.isProbing) continue;
if (addReplyFromService(question, registration.allRecords, registration.ptrRecords,
- registration.srvRecord, registration.txtRecord, replyUnicast, now,
- answerInfo, additionalAnswerRecords)) {
+ registration.srvRecord, registration.txtRecord,
+ registration.serviceInfo.getHostname(),
+ replyUnicastEnabled, now,
+ answerInfo, additionalAnswerInfo, packet.answers)) {
+ replyUnicast &= question.isUnicastReplyRequested();
registration.repliedServiceCount++;
registration.sentPacketCount++;
}
}
}
+ // If any record was already in the answer section, remove it from the additional answer
+ // section. This can typically happen when there are both queries for
+ // SRV / TXT / A / AAAA and PTR (which can cause SRV / TXT / A / AAAA records being added
+ // to the additional answer section).
+ additionalAnswerInfo.removeAll(answerInfo);
+
+ final List<MdnsRecord> additionalAnswerRecords =
+ new ArrayList<>(additionalAnswerInfo.size());
+ for (RecordInfo<?> info : additionalAnswerInfo) {
+ // Different RecordInfos may contain the same record.
+ // For example, when there are multiple services referring to the same custom host,
+ // there are multiple RecordInfos containing the same address record.
+ if (!additionalAnswerRecords.contains(info.record)) {
+ additionalAnswerRecords.add(info.record);
+ }
+ }
+
+ // RFC6762 6.1: negative responses
+ // "On receipt of a question for a particular name, rrtype, and rrclass, for which a
+ // responder does have one or more unique answers, the responder MAY also include an NSEC
+ // record in the Additional Record Section indicating the nonexistence of other rrtypes
+ // for that name and rrclass."
+ addNsecRecordsForUniqueNames(additionalAnswerRecords,
+ answerInfo.iterator(), additionalAnswerInfo.iterator());
+
if (answerInfo.size() == 0 && additionalAnswerRecords.size() == 0) {
- return null;
+ // RFC6762 7.2. Multipacket Known-Answer Suppression
+ // Sometimes a Multicast DNS querier will already have too many answers
+ // to fit in the Known-Answer Section of its query packets. In this
+ // case, it should issue a Multicast DNS query containing a question and
+ // as many Known-Answer records as will fit. It MUST then set the TC
+ // (Truncated) bit in the header before sending the query. It MUST
+ // immediately follow the packet with another query packet containing no
+ // questions and as many more Known-Answer records as will fit. If
+ // there are still too many records remaining to fit in the packet, it
+ // again sets the TC bit and continues until all the Known-Answer
+ // records have been sent.
+ if (!isTruncatedKnownAnswerPacket(packet)) {
+ return null;
+ }
}
// Determine the send delay
@@ -556,6 +678,12 @@
// Determine the send destination
final InetSocketAddress dest;
if (replyUnicast) {
+ // As per RFC6762 5.4, "if the responder has not multicast that record recently (within
+ // one quarter of its TTL), then the responder SHOULD instead multicast the response so
+ // as to keep all the peer caches up to date": this SHOULD is not implemented to
+ // minimize latency for queriers who have just started, so they did not receive previous
+ // multicast responses. Unicast replies are faster as they do not need to wait for the
+ // beacon interval on Wi-Fi.
dest = src;
} else if (src.getAddress() instanceof Inet4Address) {
dest = IPV4_SOCKET_ADDR;
@@ -571,10 +699,23 @@
if (!replyUnicast) {
info.lastAdvertisedTimeMs = info.lastSentTimeMs;
}
- answerRecords.add(info.record);
+ // Different RecordInfos may the contain the same record
+ if (!answerRecords.contains(info.record)) {
+ answerRecords.add(info.record);
+ }
}
- return new MdnsReplyInfo(answerRecords, additionalAnswerRecords, delayMs, dest);
+ return new MdnsReplyInfo(answerRecords, additionalAnswerRecords, delayMs, dest, src,
+ new ArrayList<>(packet.answers));
+ }
+
+ private boolean isKnownAnswer(MdnsRecord answer, @NonNull List<MdnsRecord> knownAnswerRecords) {
+ for (MdnsRecord knownAnswer : knownAnswerRecords) {
+ if (answer.equals(knownAnswer) && knownAnswer.getTtl() > (answer.getTtl() / 2)) {
+ return true;
+ }
+ }
+ return false;
}
/**
@@ -585,14 +726,16 @@
@Nullable List<RecordInfo<MdnsPointerRecord>> servicePtrRecords,
@Nullable RecordInfo<MdnsServiceRecord> serviceSrvRecord,
@Nullable RecordInfo<MdnsTextRecord> serviceTxtRecord,
- boolean replyUnicast, long now, @NonNull List<RecordInfo<?>> answerInfo,
- @NonNull List<MdnsRecord> additionalAnswerRecords) {
+ @Nullable String hostname,
+ boolean replyUnicastEnabled, long now, @NonNull Set<RecordInfo<?>> answerInfo,
+ @NonNull Set<RecordInfo<?>> additionalAnswerInfo,
+ @NonNull List<MdnsRecord> knownAnswerRecords) {
boolean hasDnsSdPtrRecordAnswer = false;
boolean hasDnsSdSrvRecordAnswer = false;
boolean hasFullyOwnedNameMatch = false;
boolean hasKnownAnswer = false;
- final int answersStartIndex = answerInfo.size();
+ final int answersStartSize = answerInfo.size();
for (RecordInfo<?> info : serviceRecords) {
/* RFC6762 6.: the record name must match the question name, the record rrtype
@@ -615,19 +758,32 @@
}
hasKnownAnswer = true;
+
+ // RFC6762 7.1. Known-Answer Suppression:
+ // A Multicast DNS responder MUST NOT answer a Multicast DNS query if
+ // the answer it would give is already included in the Answer Section
+ // with an RR TTL at least half the correct value. If the RR TTL of the
+ // answer as given in the Answer Section is less than half of the true
+ // RR TTL as known by the Multicast DNS responder, the responder MUST
+ // send an answer so as to update the querier's cache before the record
+ // becomes in danger of expiration.
+ if (mMdnsFeatureFlags.isKnownAnswerSuppressionEnabled()
+ && isKnownAnswer(info.record, knownAnswerRecords)) {
+ continue;
+ }
+
hasDnsSdPtrRecordAnswer |= (servicePtrRecords != null
&& CollectionUtils.any(servicePtrRecords, r -> info == r));
hasDnsSdSrvRecordAnswer |= (info == serviceSrvRecord);
// TODO: responses to probe queries should bypass this check and only ensure the
// reply is sent 250ms after the last sent time (RFC 6762 p.15)
- if (!replyUnicast && info.lastAdvertisedTimeMs > 0L
+ if (!(replyUnicastEnabled && question.isUnicastReplyRequested())
+ && info.lastAdvertisedTimeMs > 0L
&& now - info.lastAdvertisedTimeMs < MIN_MULTICAST_REPLY_INTERVAL_MS) {
continue;
}
- // TODO: Don't reply if in known answers of the querier (7.1) if TTL is > half
-
answerInfo.add(info);
}
@@ -636,21 +792,22 @@
// ownership, for a type for which that name has no records, the responder MUST [...]
// respond asserting the nonexistence of that record"
if (hasFullyOwnedNameMatch && !hasKnownAnswer) {
- additionalAnswerRecords.add(new MdnsNsecRecord(
+ MdnsNsecRecord nsecRecord = new MdnsNsecRecord(
question.getName(),
0L /* receiptTimeMillis */,
true /* cacheFlush */,
// TODO: RFC6762 6.1: "In general, the TTL given for an NSEC record SHOULD
// be the same as the TTL that the record would have had, had it existed."
- NAME_RECORDS_TTL_MILLIS,
+ DEFAULT_NAME_RECORDS_TTL_MILLIS,
question.getName(),
- new int[] { question.getType() }));
+ new int[] { question.getType() });
+ additionalAnswerInfo.add(
+ new RecordInfo<>(null /* serviceInfo */, nsecRecord, false /* isSharedName */));
}
// No more records to add if no answer
- if (answerInfo.size() == answersStartIndex) return false;
+ if (answerInfo.size() == answersStartSize) return false;
- final List<RecordInfo<?>> additionalAnswerInfo = new ArrayList<>();
// RFC6763 12.1: if including PTR record, include the SRV and TXT records it names
if (hasDnsSdPtrRecordAnswer) {
if (serviceTxtRecord != null) {
@@ -663,21 +820,8 @@
// RFC6763 12.1&.2: if including PTR or SRV record, include the address records it names
if (hasDnsSdPtrRecordAnswer || hasDnsSdSrvRecordAnswer) {
- for (RecordInfo<?> record : mGeneralRecords) {
- if (record.record instanceof MdnsInetAddressRecord) {
- additionalAnswerInfo.add(record);
- }
- }
+ additionalAnswerInfo.addAll(getInetAddressRecordsForHostname(hostname));
}
-
- for (RecordInfo<?> info : additionalAnswerInfo) {
- additionalAnswerRecords.add(info.record);
- }
-
- // RFC6762 6.1: negative responses
- addNsecRecordsForUniqueNames(additionalAnswerRecords,
- answerInfo.listIterator(answersStartIndex),
- additionalAnswerInfo.listIterator());
return true;
}
@@ -694,7 +838,7 @@
* answer and additionalAnswer sections)
*/
@SafeVarargs
- private static void addNsecRecordsForUniqueNames(
+ private void addNsecRecordsForUniqueNames(
List<MdnsRecord> destinationList,
Iterator<RecordInfo<?>>... answerRecords) {
// Group unique records by name. Use a TreeMap with comparator as arrays don't implement
@@ -710,6 +854,12 @@
for (String[] nsecName : namesInAddedOrder) {
final List<MdnsRecord> entryRecords = nsecByName.get(nsecName);
+
+ // Add NSEC records only when the answers include all unique records of this name
+ if (entryRecords.size() != countUniqueRecords(nsecName)) {
+ continue;
+ }
+
long minTtl = Long.MAX_VALUE;
final Set<Integer> types = new ArraySet<>(entryRecords.size());
for (MdnsRecord record : entryRecords) {
@@ -727,6 +877,27 @@
}
}
+ /** Returns the number of unique records on this device for a given {@code name}. */
+ private int countUniqueRecords(String[] name) {
+ int cnt = countUniqueRecords(mGeneralRecords, name);
+
+ for (int i = 0; i < mServices.size(); i++) {
+ final ServiceRegistration registration = mServices.valueAt(i);
+ cnt += countUniqueRecords(registration.allRecords, name);
+ }
+ return cnt;
+ }
+
+ private static int countUniqueRecords(List<RecordInfo<?>> records, String[] name) {
+ int cnt = 0;
+ for (RecordInfo<?> record : records) {
+ if (!record.isSharedName && Arrays.equals(name, record.record.getName())) {
+ cnt++;
+ }
+ }
+ return cnt;
+ }
+
/**
* Add non-shared records to a map listing them by record name, and to a list of names that
* remembers the adding order.
@@ -741,10 +912,10 @@
private static void addNonSharedRecordsToMap(
Iterator<RecordInfo<?>> records,
Map<String[], List<MdnsRecord>> dest,
- List<String[]> namesInAddedOrder) {
+ @Nullable List<String[]> namesInAddedOrder) {
while (records.hasNext()) {
final RecordInfo<?> record = records.next();
- if (record.isSharedName) continue;
+ if (record.isSharedName || record.record instanceof MdnsNsecRecord) continue;
final List<MdnsRecord> recordsForName = dest.computeIfAbsent(record.record.name,
key -> {
namesInAddedOrder.add(key);
@@ -763,29 +934,46 @@
MdnsProber.ProbingInfo probeSuccessInfo)
throws IOException {
- final ServiceRegistration registration = mServices.get(probeSuccessInfo.getServiceId());
- if (registration == null) throw new IOException(
- "Service is not registered: " + probeSuccessInfo.getServiceId());
+ int serviceId = probeSuccessInfo.getServiceId();
+ final ServiceRegistration registration = mServices.get(serviceId);
+ if (registration == null) {
+ throw new IOException("Service is not registered: " + serviceId);
+ }
registration.setProbing(false);
- final ArrayList<MdnsRecord> answers = new ArrayList<>();
+ final Set<MdnsRecord> answersSet = new LinkedHashSet<>();
final ArrayList<MdnsRecord> additionalAnswers = new ArrayList<>();
- // Interface address records in general records
- for (RecordInfo<?> record : mGeneralRecords) {
- answers.add(record.record);
+ // When using default host, add interface address records from general records
+ if (TextUtils.isEmpty(registration.serviceInfo.getHostname())) {
+ for (RecordInfo<?> record : mGeneralRecords) {
+ answersSet.add(record.record);
+ }
+ } else {
+ // TODO: b/321617573 - include PTR records for addresses
+ // The custom host may have more addresses in other registrations
+ forEachActiveServiceRegistrationWithHostname(
+ registration.serviceInfo.getHostname(),
+ (id, otherRegistration) -> {
+ if (otherRegistration.isProbing) {
+ return;
+ }
+ for (RecordInfo<?> addressRecordInfo : otherRegistration.addressRecords) {
+ answersSet.add(addressRecordInfo.record);
+ }
+ });
}
// All service records
for (RecordInfo<?> info : registration.allRecords) {
- answers.add(info.record);
+ answersSet.add(info.record);
}
addNsecRecordsForUniqueNames(additionalAnswers,
mGeneralRecords.iterator(), registration.allRecords.iterator());
- return new MdnsAnnouncer.AnnouncementInfo(probeSuccessInfo.getServiceId(),
- answers, additionalAnswers);
+ return new MdnsAnnouncer.AnnouncementInfo(
+ probeSuccessInfo.getServiceId(), new ArrayList<>(answersSet), additionalAnswers);
}
/**
@@ -804,8 +992,13 @@
for (RecordInfo<MdnsPointerRecord> ptrRecord : registration.ptrRecords) {
answers.add(ptrRecord.record);
}
- answers.add(registration.srvRecord.record);
- answers.add(registration.txtRecord.record);
+ if (registration.srvRecord != null) {
+ answers.add(registration.srvRecord.record);
+ }
+ if (registration.txtRecord != null) {
+ answers.add(registration.txtRecord.record);
+ }
+ // TODO: Support custom host. It currently only supports default host.
for (RecordInfo<?> record : mGeneralRecords) {
if (record.record instanceof MdnsInetAddressRecord) {
answers.add(record.record);
@@ -820,70 +1013,181 @@
Collections.emptyList() /* additionalRecords */);
}
+ /** Check if the record is in any service registration */
+ private boolean hasInetAddressRecord(@NonNull MdnsInetAddressRecord record) {
+ for (int i = 0; i < mServices.size(); i++) {
+ final ServiceRegistration registration = mServices.valueAt(i);
+ if (registration.exiting) continue;
+
+ for (RecordInfo<MdnsInetAddressRecord> localRecord : registration.addressRecords) {
+ if (Objects.equals(localRecord.record, record)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
/**
* Get the service IDs of services conflicting with a received packet.
+ *
+ * <p>It returns a Map of service ID => conflict type. Conflict type is a bitmap telling which
+ * part of the service is conflicting. See {@link MdnsInterfaceAdvertiser#CONFLICT_SERVICE} and
+ * {@link MdnsInterfaceAdvertiser#CONFLICT_HOST}.
*/
- public Set<Integer> getConflictingServices(MdnsPacket packet) {
+ public Map<Integer, Integer> getConflictingServices(MdnsPacket packet) {
// Avoid allocating a new set for each incoming packet: use an empty set by default.
- Set<Integer> conflicting = Collections.emptySet();
+ Map<Integer, Integer> conflicting = Collections.emptyMap();
for (MdnsRecord record : packet.answers) {
for (int i = 0; i < mServices.size(); i++) {
final ServiceRegistration registration = mServices.valueAt(i);
if (registration.exiting) continue;
- // Only look for conflicts in service name, as a different service name can be used
- // if there is a conflict, but there is nothing actionable if any other conflict
- // happens. In fact probing is only done for the service name in the SRV record.
- // This means only SRV and TXT records need to be checked.
- final RecordInfo<MdnsServiceRecord> srvRecord = registration.srvRecord;
- if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(record.getName(),
- srvRecord.record.getName())) {
- continue;
+ int conflictType = 0;
+
+ if (conflictForService(record, registration)) {
+ conflictType |= CONFLICT_SERVICE;
}
- // As per RFC6762 9., it's fine if the "conflict" is an identical record with same
- // data.
- if (record instanceof MdnsServiceRecord) {
- final MdnsServiceRecord local = srvRecord.record;
- final MdnsServiceRecord other = (MdnsServiceRecord) record;
- // Note "equals" does not consider TTL or receipt time, as intended here
- if (Objects.equals(local, other)) {
- continue;
+ if (conflictForHost(record, registration)) {
+ conflictType |= CONFLICT_HOST;
+ }
+
+ if (conflictType != 0) {
+ if (conflicting.isEmpty()) {
+ // Conflict was found: use a mutable set
+ conflicting = new ArrayMap<>();
}
+ final int serviceId = mServices.keyAt(i);
+ conflicting.put(serviceId, conflictType);
}
-
- if (record instanceof MdnsTextRecord) {
- final MdnsTextRecord local = registration.txtRecord.record;
- final MdnsTextRecord other = (MdnsTextRecord) record;
- if (Objects.equals(local, other)) {
- continue;
- }
- }
-
- if (conflicting.size() == 0) {
- // Conflict was found: use a mutable set
- conflicting = new ArraySet<>();
- }
- final int serviceId = mServices.keyAt(i);
- conflicting.add(serviceId);
}
}
return conflicting;
}
- private List<MdnsInetAddressRecord> makeProbingInetAddressRecords() {
- final List<MdnsInetAddressRecord> records = new ArrayList<>();
- if (mMdnsFeatureFlags.mIncludeInetAddressRecordsInProbing) {
- for (RecordInfo<?> record : mGeneralRecords) {
- if (record.record instanceof MdnsInetAddressRecord) {
- records.add((MdnsInetAddressRecord) record.record);
- }
+
+ private static boolean conflictForService(
+ @NonNull MdnsRecord record, @NonNull ServiceRegistration registration) {
+ if (registration.srvRecord == null) {
+ return false;
+ }
+
+ final RecordInfo<MdnsServiceRecord> srvRecord = registration.srvRecord;
+ if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(record.getName(), srvRecord.record.getName())) {
+ return false;
+ }
+
+ // As per RFC6762 9., it's fine if the "conflict" is an identical record with same
+ // data.
+ if (record instanceof MdnsServiceRecord) {
+ final MdnsServiceRecord local = srvRecord.record;
+ final MdnsServiceRecord other = (MdnsServiceRecord) record;
+ // Note "equals" does not consider TTL or receipt time, as intended here
+ if (Objects.equals(local, other)) {
+ return false;
}
}
+
+ if (record instanceof MdnsTextRecord) {
+ final MdnsTextRecord local = registration.txtRecord.record;
+ final MdnsTextRecord other = (MdnsTextRecord) record;
+ if (Objects.equals(local, other)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean conflictForHost(
+ @NonNull MdnsRecord record, @NonNull ServiceRegistration registration) {
+ // Only custom hosts are checked. When using the default host, the hostname is derived from
+ // a UUID and it's supposed to be unique.
+ if (registration.serviceInfo.getHostname() == null) {
+ return false;
+ }
+
+ // The record's name cannot be registered by NsdManager so it's not a conflict.
+ if (record.getName().length != 2 || !record.getName()[1].equals(LOCAL_TLD)) {
+ return false;
+ }
+
+ // Different names. There won't be a conflict.
+ if (!MdnsUtils.equalsIgnoreDnsCase(
+ record.getName()[0], registration.serviceInfo.getHostname())) {
+ return false;
+ }
+
+ // If this registration has any address record and there's no identical record in the
+ // repository, it's a conflict. There will be no conflict if no registration has addresses
+ // for that hostname.
+ if (record instanceof MdnsInetAddressRecord) {
+ if (!registration.addressRecords.isEmpty()) {
+ return !hasInetAddressRecord((MdnsInetAddressRecord) record);
+ }
+ }
+
+ return false;
+ }
+
+ private List<RecordInfo<MdnsInetAddressRecord>> getInetAddressRecordsForHostname(
+ @Nullable String hostname) {
+ List<RecordInfo<MdnsInetAddressRecord>> records = new ArrayList<>();
+ if (TextUtils.isEmpty(hostname)) {
+ forEachAddressRecord(mGeneralRecords, records::add);
+ } else {
+ forEachActiveServiceRegistrationWithHostname(
+ hostname,
+ (id, service) -> {
+ if (service.isProbing) return;
+ records.addAll(service.addressRecords);
+ });
+ }
return records;
}
+ private List<MdnsInetAddressRecord> makeProbingInetAddressRecords(
+ @NonNull NsdServiceInfo serviceInfo) {
+ final List<MdnsInetAddressRecord> records = new ArrayList<>();
+ if (TextUtils.isEmpty(serviceInfo.getHostname())) {
+ if (mMdnsFeatureFlags.mIncludeInetAddressRecordsInProbing) {
+ forEachAddressRecord(mGeneralRecords, r -> records.add(r.record));
+ }
+ } else {
+ forEachActiveServiceRegistrationWithHostname(
+ serviceInfo.getHostname(),
+ (id, service) -> {
+ for (RecordInfo<MdnsInetAddressRecord> recordInfo :
+ service.addressRecords) {
+ records.add(recordInfo.record);
+ }
+ });
+ }
+ return records;
+ }
+
+ private static void forEachAddressRecord(
+ List<RecordInfo<?>> records, Consumer<RecordInfo<MdnsInetAddressRecord>> consumer) {
+ for (RecordInfo<?> record : records) {
+ if (record.record instanceof MdnsInetAddressRecord) {
+ consumer.accept((RecordInfo<MdnsInetAddressRecord>) record);
+ }
+ }
+ }
+
+ private void forEachActiveServiceRegistrationWithHostname(
+ @NonNull String hostname, BiConsumer<Integer, ServiceRegistration> consumer) {
+ for (int i = 0; i < mServices.size(); ++i) {
+ int id = mServices.keyAt(i);
+ ServiceRegistration service = mServices.valueAt(i);
+ if (service.exiting) continue;
+ if (MdnsUtils.equalsIgnoreDnsCase(service.serviceInfo.getHostname(), hostname)) {
+ consumer.accept(id, service);
+ }
+ }
+ }
+
/**
* (Re)set a service to the probing state.
* @return The {@link MdnsProber.ProbingInfo} to send for probing.
@@ -894,8 +1198,8 @@
if (registration == null) return null;
registration.setProbing(true);
- return makeProbingInfo(
- serviceId, registration.srvRecord.record, makeProbingInetAddressRecords());
+
+ return makeProbingInfo(serviceId, registration);
}
/**
@@ -929,10 +1233,9 @@
if (existing == null) return null;
final ServiceRegistration newService = new ServiceRegistration(mDeviceHostname, newInfo,
- existing.subtype, existing.repliedServiceCount, existing.sentPacketCount);
+ existing.repliedServiceCount, existing.sentPacketCount, existing.ttl);
mServices.put(serviceId, newService);
- return makeProbingInfo(
- serviceId, newService.srvRecord.record, makeProbingInetAddressRecords());
+ return makeProbingInfo(serviceId, newService);
}
/**
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsReplyInfo.java b/service-t/src/com/android/server/connectivity/mdns/MdnsReplyInfo.java
index ce61b54..8747f67 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsReplyInfo.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsReplyInfo.java
@@ -32,22 +32,32 @@
public final long sendDelayMs;
@NonNull
public final InetSocketAddress destination;
+ @NonNull
+ public final InetSocketAddress source;
+ @NonNull
+ public final List<MdnsRecord> knownAnswers;
public MdnsReplyInfo(
@NonNull List<MdnsRecord> answers,
@NonNull List<MdnsRecord> additionalAnswers,
long sendDelayMs,
- @NonNull InetSocketAddress destination) {
+ @NonNull InetSocketAddress destination,
+ @NonNull InetSocketAddress source,
+ @NonNull List<MdnsRecord> knownAnswers) {
this.answers = answers;
this.additionalAnswers = additionalAnswers;
this.sendDelayMs = sendDelayMs;
this.destination = destination;
+ this.source = source;
+ this.knownAnswers = knownAnswers;
}
@Override
public String toString() {
- return "{MdnsReplyInfo to " + destination + ", answers: " + answers.size()
+ return "{MdnsReplyInfo: " + source + " to " + destination
+ + ", answers: " + answers.size()
+ ", additionalAnswers: " + additionalAnswers.size()
+ + ", knownAnswers: " + knownAnswers.size()
+ ", sendDelayMs " + sendDelayMs + "}";
}
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
index ea3af5e..db3845a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
@@ -16,6 +16,8 @@
package com.android.server.connectivity.mdns;
+import static com.android.server.connectivity.mdns.MdnsConstants.IPV4_SOCKET_ADDR;
+import static com.android.server.connectivity.mdns.MdnsConstants.IPV6_SOCKET_ADDR;
import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
import android.annotation.NonNull;
@@ -24,7 +26,10 @@
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.mdns.util.MdnsUtils;
@@ -34,7 +39,10 @@
import java.net.Inet6Address;
import java.net.InetSocketAddress;
import java.net.MulticastSocket;
+import java.util.ArrayList;
import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
/**
* A class that handles sending mDNS replies to a {@link MulticastSocket}, possibly queueing them
@@ -57,15 +65,78 @@
@NonNull
private final SharedLog mSharedLog;
private final boolean mEnableDebugLog;
+ @NonNull
+ private final Dependencies mDependencies;
+ // RFC6762 15.2. Multipacket Known-Answer lists
+ // Multicast DNS responders associate the initial truncated query with its
+ // continuation packets by examining the source IP address in each packet.
+ private final Map<InetSocketAddress, MdnsReplyInfo> mSrcReplies = new ArrayMap<>();
+ @NonNull
+ private final MdnsFeatureFlags mMdnsFeatureFlags;
+
+ /**
+ * Dependencies of MdnsReplySender, for injection in tests.
+ */
+ @VisibleForTesting
+ public static class Dependencies {
+ /**
+ * @see Handler#sendMessageDelayed(Message, long)
+ */
+ public void sendMessageDelayed(@NonNull Handler handler, @NonNull Message message,
+ long delayMillis) {
+ handler.sendMessageDelayed(message, delayMillis);
+ }
+
+ /**
+ * @see Handler#removeMessages(int)
+ */
+ public void removeMessages(@NonNull Handler handler, int what) {
+ handler.removeMessages(what);
+ }
+
+ /**
+ * @see Handler#removeMessages(int)
+ */
+ public void removeMessages(@NonNull Handler handler, int what, @NonNull Object object) {
+ handler.removeMessages(what, object);
+ }
+ }
public MdnsReplySender(@NonNull Looper looper, @NonNull MdnsInterfaceSocket socket,
@NonNull byte[] packetCreationBuffer, @NonNull SharedLog sharedLog,
- boolean enableDebugLog) {
+ boolean enableDebugLog, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
+ this(looper, socket, packetCreationBuffer, sharedLog, enableDebugLog, new Dependencies(),
+ mdnsFeatureFlags);
+ }
+
+ @VisibleForTesting
+ public MdnsReplySender(@NonNull Looper looper, @NonNull MdnsInterfaceSocket socket,
+ @NonNull byte[] packetCreationBuffer, @NonNull SharedLog sharedLog,
+ boolean enableDebugLog, @NonNull Dependencies dependencies,
+ @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
mHandler = new SendHandler(looper);
mSocket = socket;
mPacketCreationBuffer = packetCreationBuffer;
mSharedLog = sharedLog;
mEnableDebugLog = enableDebugLog;
+ mDependencies = dependencies;
+ mMdnsFeatureFlags = mdnsFeatureFlags;
+ }
+
+ static InetSocketAddress getReplyDestination(@NonNull InetSocketAddress queuingDest,
+ @NonNull InetSocketAddress incomingDest) {
+ // The queuing reply is multicast, just use the current destination.
+ if (queuingDest.equals(IPV4_SOCKET_ADDR) || queuingDest.equals(IPV6_SOCKET_ADDR)) {
+ return queuingDest;
+ }
+
+ // The incoming reply is multicast, change the reply from unicast to multicast since
+ // replying unicast when the query requests unicast reply is optional.
+ if (incomingDest.equals(IPV4_SOCKET_ADDR) || incomingDest.equals(IPV6_SOCKET_ADDR)) {
+ return incomingDest;
+ }
+
+ return queuingDest;
}
/**
@@ -73,8 +144,53 @@
*/
public void queueReply(@NonNull MdnsReplyInfo reply) {
ensureRunningOnHandlerThread(mHandler);
- // TODO: implement response aggregation (RFC 6762 6.4)
- mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SEND, reply), reply.sendDelayMs);
+
+ if (mMdnsFeatureFlags.isKnownAnswerSuppressionEnabled()) {
+ mDependencies.removeMessages(mHandler, MSG_SEND, reply.source);
+
+ final MdnsReplyInfo queuingReply = mSrcReplies.remove(reply.source);
+ final ArraySet<MdnsRecord> answers = new ArraySet<>();
+ final Set<MdnsRecord> additionalAnswers = new ArraySet<>();
+ final Set<MdnsRecord> knownAnswers = new ArraySet<>();
+ if (queuingReply != null) {
+ answers.addAll(queuingReply.answers);
+ additionalAnswers.addAll(queuingReply.additionalAnswers);
+ knownAnswers.addAll(queuingReply.knownAnswers);
+ }
+ answers.addAll(reply.answers);
+ additionalAnswers.addAll(reply.additionalAnswers);
+ knownAnswers.addAll(reply.knownAnswers);
+ // RFC6762 7.2. Multipacket Known-Answer Suppression
+ // If the responder sees any of its answers listed in the Known-Answer
+ // lists of subsequent packets from the querying host, it MUST delete
+ // that answer from the list of answers it is planning to give.
+ for (MdnsRecord knownAnswer : knownAnswers) {
+ final int idx = answers.indexOf(knownAnswer);
+ if (idx >= 0 && knownAnswer.getTtl() > answers.valueAt(idx).getTtl() / 2) {
+ answers.removeAt(idx);
+ }
+ }
+
+ if (answers.size() == 0) {
+ return;
+ }
+
+ final MdnsReplyInfo newReply = new MdnsReplyInfo(
+ new ArrayList<>(answers),
+ new ArrayList<>(additionalAnswers),
+ reply.sendDelayMs,
+ queuingReply == null ? reply.destination
+ : getReplyDestination(queuingReply.destination, reply.destination),
+ reply.source,
+ new ArrayList<>(knownAnswers));
+
+ mSrcReplies.put(newReply.source, newReply);
+ mDependencies.sendMessageDelayed(mHandler,
+ mHandler.obtainMessage(MSG_SEND, newReply.source), newReply.sendDelayMs);
+ } else {
+ mDependencies.sendMessageDelayed(
+ mHandler, mHandler.obtainMessage(MSG_SEND, reply), reply.sendDelayMs);
+ }
if (mEnableDebugLog) {
mSharedLog.v("Scheduling " + reply);
@@ -104,7 +220,7 @@
*/
public void cancelAll() {
ensureRunningOnHandlerThread(mHandler);
- mHandler.removeMessages(MSG_SEND);
+ mDependencies.removeMessages(mHandler, MSG_SEND);
}
private class SendHandler extends Handler {
@@ -114,7 +230,21 @@
@Override
public void handleMessage(@NonNull Message msg) {
- final MdnsReplyInfo replyInfo = (MdnsReplyInfo) msg.obj;
+ final MdnsReplyInfo replyInfo;
+ if (mMdnsFeatureFlags.isKnownAnswerSuppressionEnabled()) {
+ // Retrieve the MdnsReplyInfo from the map via a source address, as the reply info
+ // will be combined or updated.
+ final InetSocketAddress source = (InetSocketAddress) msg.obj;
+ replyInfo = mSrcReplies.remove(source);
+ } else {
+ replyInfo = (MdnsReplyInfo) msg.obj;
+ }
+
+ if (replyInfo == null) {
+ mSharedLog.wtf("Unknown reply info.");
+ return;
+ }
+
if (mEnableDebugLog) mSharedLog.v("Sending " + replyInfo);
final int flags = 0x8400; // Response, authoritative (rfc6762 18.4)
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java
index 63835d9..086094b 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java
@@ -39,6 +39,15 @@
* @hide
*/
public class MdnsSearchOptions implements Parcelable {
+ // Passive query mode scans less frequently in order to conserve battery and produce less
+ // network traffic.
+ public static final int PASSIVE_QUERY_MODE = 0;
+ // Active query mode scans frequently.
+ public static final int ACTIVE_QUERY_MODE = 1;
+ // Aggressive query mode scans more frequently than the active mode at first, and sends both
+ // unicast and multicast queries simultaneously, but in long sessions it eventually sends as
+ // many queries as the PASSIVE mode.
+ public static final int AGGRESSIVE_QUERY_MODE = 2;
/** @hide */
public static final Parcelable.Creator<MdnsSearchOptions> CREATOR =
@@ -47,7 +56,7 @@
public MdnsSearchOptions createFromParcel(Parcel source) {
return new MdnsSearchOptions(
source.createStringArrayList(),
- source.readInt() == 1,
+ source.readInt(),
source.readInt() == 1,
source.readParcelable(null),
source.readString(),
@@ -64,7 +73,7 @@
private final List<String> subtypes;
@Nullable
private final String resolveInstanceName;
- private final boolean isPassiveMode;
+ private final int queryMode;
private final boolean onlyUseIpv6OnIpv6OnlyNetworks;
private final int numOfQueriesBeforeBackoff;
private final boolean removeExpiredService;
@@ -74,7 +83,7 @@
/** Parcelable constructs for a {@link MdnsSearchOptions}. */
MdnsSearchOptions(
List<String> subtypes,
- boolean isPassiveMode,
+ int queryMode,
boolean removeExpiredService,
@Nullable Network network,
@Nullable String resolveInstanceName,
@@ -84,7 +93,7 @@
if (subtypes != null) {
this.subtypes.addAll(subtypes);
}
- this.isPassiveMode = isPassiveMode;
+ this.queryMode = queryMode;
this.onlyUseIpv6OnIpv6OnlyNetworks = onlyUseIpv6OnIpv6OnlyNetworks;
this.numOfQueriesBeforeBackoff = numOfQueriesBeforeBackoff;
this.removeExpiredService = removeExpiredService;
@@ -111,11 +120,10 @@
}
/**
- * @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.
+ * @return the current query mode.
*/
- public boolean isPassiveMode() {
- return isPassiveMode;
+ public int getQueryMode() {
+ return queryMode;
}
/**
@@ -166,7 +174,7 @@
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeStringList(subtypes);
- out.writeInt(isPassiveMode ? 1 : 0);
+ out.writeInt(queryMode);
out.writeInt(removeExpiredService ? 1 : 0);
out.writeParcelable(mNetwork, 0);
out.writeString(resolveInstanceName);
@@ -177,7 +185,7 @@
/** A builder to create {@link MdnsSearchOptions}. */
public static final class Builder {
private final Set<String> subtypes;
- private boolean isPassiveMode = true;
+ private int queryMode = PASSIVE_QUERY_MODE;
private boolean onlyUseIpv6OnIpv6OnlyNetworks = false;
private int numOfQueriesBeforeBackoff = 3;
private boolean removeExpiredService;
@@ -212,14 +220,12 @@
}
/**
- * 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.
+ * Sets which query mode should be used.
*
- * @param isPassiveMode If set to {@code true}, passive mode will be used. If set to {@code
- * false}, active mode will be used.
+ * @param queryMode the query mode should be used.
*/
- public Builder setIsPassiveMode(boolean isPassiveMode) {
- this.isPassiveMode = isPassiveMode;
+ public Builder setQueryMode(int queryMode) {
+ this.queryMode = queryMode;
return this;
}
@@ -276,7 +282,7 @@
public MdnsSearchOptions build() {
return new MdnsSearchOptions(
new ArrayList<>(subtypes),
- isPassiveMode,
+ queryMode,
removeExpiredService,
mNetwork,
resolveInstanceName,
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
index 78df6df..f60a95e 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
@@ -28,6 +28,7 @@
import com.android.net.module.util.ByteUtils;
import java.nio.charset.Charset;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -62,7 +63,8 @@
source.createStringArrayList(),
source.createTypedArrayList(TextEntry.CREATOR),
source.readInt(),
- source.readParcelable(null));
+ source.readParcelable(Network.class.getClassLoader()),
+ Instant.ofEpochSecond(source.readLong()));
}
@Override
@@ -89,6 +91,9 @@
@Nullable
private final Network network;
+ @NonNull
+ private final Instant expirationTime;
+
/** Constructs a {@link MdnsServiceInfo} object with default values. */
public MdnsServiceInfo(
String serviceInstanceName,
@@ -110,7 +115,8 @@
textStrings,
/* textEntries= */ null,
/* interfaceIndex= */ INTERFACE_INDEX_UNSPECIFIED,
- /* network= */ null);
+ /* network= */ null,
+ /* expirationTime= */ Instant.MAX);
}
/** Constructs a {@link MdnsServiceInfo} object with default values. */
@@ -135,7 +141,8 @@
textStrings,
textEntries,
/* interfaceIndex= */ INTERFACE_INDEX_UNSPECIFIED,
- /* network= */ null);
+ /* network= */ null,
+ /* expirationTime= */ Instant.MAX);
}
/**
@@ -165,7 +172,8 @@
textStrings,
textEntries,
interfaceIndex,
- /* network= */ null);
+ /* network= */ null,
+ /* expirationTime= */ Instant.MAX);
}
/**
@@ -184,7 +192,8 @@
@Nullable List<String> textStrings,
@Nullable List<TextEntry> textEntries,
int interfaceIndex,
- @Nullable Network network) {
+ @Nullable Network network,
+ @NonNull Instant expirationTime) {
this.serviceInstanceName = serviceInstanceName;
this.serviceType = serviceType;
this.subtypes = new ArrayList<>();
@@ -217,6 +226,7 @@
this.attributes = Collections.unmodifiableMap(attributes);
this.interfaceIndex = interfaceIndex;
this.network = network;
+ this.expirationTime = Instant.ofEpochSecond(expirationTime.getEpochSecond());
}
private static List<TextEntry> parseTextStrings(List<String> textStrings) {
@@ -314,6 +324,17 @@
}
/**
+ * Returns the timestamp after when this service is expired or {@code null} if the expiration
+ * time is unknown.
+ *
+ * A service is considered expired if any of its DNS record is expired.
+ */
+ @NonNull
+ public Instant getExpirationTime() {
+ return expirationTime;
+ }
+
+ /**
* Returns attribute value for {@code key} as a UTF-8 string. It's the caller who must make sure
* that the value of {@code key} is indeed a UTF-8 string. {@code null} will be returned if no
* attribute value exists for {@code key}.
@@ -364,6 +385,7 @@
out.writeTypedList(textEntries);
out.writeInt(interfaceIndex);
out.writeParcelable(network, 0);
+ out.writeLong(expirationTime.getEpochSecond());
}
@Override
@@ -377,7 +399,8 @@
+ ", interfaceIndex: " + interfaceIndex
+ ", network: " + network
+ ", textStrings: " + textStrings
- + ", textEntries: " + textEntries;
+ + ", textEntries: " + textEntries
+ + ", expirationTime: " + expirationTime;
}
@@ -496,4 +519,4 @@
out.writeByteArray(value);
}
}
-}
\ No newline at end of file
+}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
index 32f604e..8f41b94 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -35,10 +35,13 @@
import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.mdns.util.MdnsUtils;
+import java.io.PrintWriter;
import java.net.Inet4Address;
import java.net.Inet6Address;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
@@ -81,7 +84,7 @@
notifyRemovedServiceToListeners(previousResponse, "Service record expired");
}
};
- private final ArrayMap<MdnsServiceBrowserListener, MdnsSearchOptions> listeners =
+ private final ArrayMap<MdnsServiceBrowserListener, ListenerInfo> listeners =
new ArrayMap<>();
private final boolean removeServiceAfterTtlExpires =
MdnsConfigs.removeServiceAfterTtlExpires();
@@ -95,6 +98,32 @@
private long currentSessionId = 0;
private long lastSentTime;
+ private static class ListenerInfo {
+ @NonNull
+ final MdnsSearchOptions searchOptions;
+ final Set<String> discoveredServiceNames;
+
+ ListenerInfo(@NonNull MdnsSearchOptions searchOptions,
+ @Nullable ListenerInfo previousInfo) {
+ this.searchOptions = searchOptions;
+ this.discoveredServiceNames = previousInfo == null
+ ? MdnsUtils.newSet() : previousInfo.discoveredServiceNames;
+ }
+
+ /**
+ * Set the given service name as discovered.
+ *
+ * @return true if the service name was not discovered before.
+ */
+ boolean setServiceDiscovered(@NonNull String serviceName) {
+ return discoveredServiceNames.add(MdnsUtils.toDnsLowerCase(serviceName));
+ }
+
+ void unsetServiceDiscovered(@NonNull String serviceName) {
+ discoveredServiceNames.remove(MdnsUtils.toDnsLowerCase(serviceName));
+ }
+ }
+
private class QueryTaskHandler extends Handler {
QueryTaskHandler(Looper looper) {
super(looper);
@@ -113,7 +142,7 @@
// before sending the query, it needs to be called just before sending it.
final List<MdnsResponse> servicesToResolve = makeResponsesForResolve(socketKey);
final QueryTask queryTask = new QueryTask(taskArgs, servicesToResolve,
- servicesToResolve.size() < listeners.size() /* sendDiscoveryQueries */);
+ getAllDiscoverySubtypes(), needSendDiscoveryQueries(listeners));
executor.submit(queryTask);
break;
}
@@ -281,6 +310,7 @@
textStrings = response.getTextRecord().getStrings();
textEntries = response.getTextRecord().getEntries();
}
+ Instant now = Instant.now();
// TODO: Throw an error message if response doesn't have Inet6 or Inet4 address.
return new MdnsServiceInfo(
serviceInstanceName,
@@ -293,7 +323,8 @@
textStrings,
textEntries,
response.getInterfaceIndex(),
- response.getNetwork());
+ response.getNetwork(),
+ now.plusMillis(response.getMinRemainingTtl(now.toEpochMilli())));
}
/**
@@ -311,12 +342,16 @@
ensureRunningOnHandlerThread(handler);
this.searchOptions = searchOptions;
boolean hadReply = false;
- if (listeners.put(listener, searchOptions) == null) {
+ final ListenerInfo existingInfo = listeners.get(listener);
+ final ListenerInfo listenerInfo = new ListenerInfo(searchOptions, existingInfo);
+ listeners.put(listener, listenerInfo);
+ if (existingInfo == null) {
for (MdnsResponse existingResponse : serviceCache.getCachedServices(cacheKey)) {
if (!responseMatchesOptions(existingResponse, searchOptions)) continue;
final MdnsServiceInfo info =
buildMdnsServiceInfoFromResponse(existingResponse, serviceTypeLabels);
listener.onServiceNameDiscovered(info, true /* isServiceFromCache */);
+ listenerInfo.setServiceDiscovered(info.getServiceInstanceName());
if (existingResponse.isComplete()) {
listener.onServiceFound(info, true /* isServiceFromCache */);
hadReply = true;
@@ -329,8 +364,7 @@
// Keep tracking the ScheduledFuture for the task so we can cancel it if caller is not
// interested anymore.
final QueryTaskConfig taskConfig = new QueryTaskConfig(
- searchOptions.getSubtypes(),
- searchOptions.isPassiveMode(),
+ searchOptions.getQueryMode(),
searchOptions.onlyUseIpv6OnIpv6OnlyNetworks(),
searchOptions.numOfQueriesBeforeBackoff(),
socketKey);
@@ -357,13 +391,22 @@
final QueryTask queryTask = new QueryTask(
mdnsQueryScheduler.scheduleFirstRun(taskConfig, now,
minRemainingTtl, currentSessionId), servicesToResolve,
- servicesToResolve.size() < listeners.size() /* sendDiscoveryQueries */);
+ getAllDiscoverySubtypes(), needSendDiscoveryQueries(listeners));
executor.submit(queryTask);
}
serviceCache.registerServiceExpiredCallback(cacheKey, serviceExpiredCallback);
}
+ private Set<String> getAllDiscoverySubtypes() {
+ final Set<String> subtypes = MdnsUtils.newSet();
+ for (int i = 0; i < listeners.size(); i++) {
+ final MdnsSearchOptions listenerOptions = listeners.valueAt(i).searchOptions;
+ subtypes.addAll(listenerOptions.getSubtypes());
+ }
+ return subtypes;
+ }
+
/**
* Get the executor service.
*/
@@ -480,9 +523,10 @@
private void notifyRemovedServiceToListeners(@NonNull MdnsResponse response,
@NonNull String message) {
for (int i = 0; i < listeners.size(); i++) {
- if (!responseMatchesOptions(response, listeners.valueAt(i))) continue;
+ if (!responseMatchesOptions(response, listeners.valueAt(i).searchOptions)) continue;
final MdnsServiceBrowserListener listener = listeners.keyAt(i);
if (response.getServiceInstanceName() != null) {
+ listeners.valueAt(i).unsetServiceDiscovered(response.getServiceInstanceName());
final MdnsServiceInfo serviceInfo = buildMdnsServiceInfoFromResponse(
response, serviceTypeLabels);
if (response.isComplete()) {
@@ -511,10 +555,9 @@
final MdnsResponse currentResponse =
serviceCache.getCachedService(serviceInstanceName, cacheKey);
- boolean newServiceFound = false;
+ final boolean newInCache = currentResponse == null;
boolean serviceBecomesComplete = false;
- if (currentResponse == null) {
- newServiceFound = true;
+ if (newInCache) {
if (serviceInstanceName != null) {
serviceCache.addOrUpdateService(cacheKey, response);
}
@@ -525,16 +568,22 @@
serviceBecomesComplete = !before && after;
}
sharedLog.i(String.format(
- "Handling response from service: %s, newServiceFound: %b, serviceBecomesComplete:"
+ "Handling response from service: %s, newInCache: %b, serviceBecomesComplete:"
+ " %b, responseIsComplete: %b",
- serviceInstanceName, newServiceFound, serviceBecomesComplete,
+ serviceInstanceName, newInCache, serviceBecomesComplete,
response.isComplete()));
MdnsServiceInfo serviceInfo =
buildMdnsServiceInfoFromResponse(response, serviceTypeLabels);
for (int i = 0; i < listeners.size(); i++) {
- if (!responseMatchesOptions(response, listeners.valueAt(i))) continue;
+ // If a service stops matching the options (currently can only happen if it loses a
+ // subtype), service lost callbacks should also be sent; this is not done today as
+ // only expiration of SRV records is used, not PTR records used for subtypes, so
+ // services never lose PTR record subtypes.
+ if (!responseMatchesOptions(response, listeners.valueAt(i).searchOptions)) continue;
final MdnsServiceBrowserListener listener = listeners.keyAt(i);
+ final ListenerInfo listenerInfo = listeners.valueAt(i);
+ final boolean newServiceFound = listenerInfo.setServiceDiscovered(serviceInstanceName);
if (newServiceFound) {
sharedLog.log("onServiceNameDiscovered: " + serviceInfo);
listener.onServiceNameDiscovered(serviceInfo, false /* isServiceFromCache */);
@@ -576,10 +625,14 @@
private List<MdnsResponse> makeResponsesForResolve(@NonNull SocketKey socketKey) {
final List<MdnsResponse> resolveResponses = new ArrayList<>();
for (int i = 0; i < listeners.size(); i++) {
- final String resolveName = listeners.valueAt(i).getResolveInstanceName();
+ final String resolveName = listeners.valueAt(i).searchOptions.getResolveInstanceName();
if (resolveName == null) {
continue;
}
+ if (CollectionUtils.any(resolveResponses,
+ r -> MdnsUtils.equalsIgnoreDnsCase(resolveName, r.getServiceInstanceName()))) {
+ continue;
+ }
MdnsResponse knownResponse =
serviceCache.getCachedService(resolveName, cacheKey);
if (knownResponse == null) {
@@ -596,6 +649,17 @@
return resolveResponses;
}
+ private static boolean needSendDiscoveryQueries(
+ @NonNull ArrayMap<MdnsServiceBrowserListener, ListenerInfo> listeners) {
+ // Note iterators are discouraged on ArrayMap as per its documentation
+ for (int i = 0; i < listeners.size(); i++) {
+ if (listeners.valueAt(i).searchOptions.getResolveInstanceName() == null) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private void tryRemoveServiceAfterTtlExpires() {
if (!shouldRemoveServiceAfterTtlExpires()) return;
@@ -628,11 +692,15 @@
private class QueryTask implements Runnable {
private final MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs;
private final List<MdnsResponse> servicesToResolve = new ArrayList<>();
+ private final List<String> subtypes = new ArrayList<>();
private final boolean sendDiscoveryQueries;
QueryTask(@NonNull MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs,
- @NonNull List<MdnsResponse> servicesToResolve, boolean sendDiscoveryQueries) {
+ @NonNull Collection<MdnsResponse> servicesToResolve,
+ @NonNull Collection<String> subtypes,
+ boolean sendDiscoveryQueries) {
this.taskArgs = taskArgs;
this.servicesToResolve.addAll(servicesToResolve);
+ this.subtypes.addAll(subtypes);
this.sendDiscoveryQueries = sendDiscoveryQueries;
}
@@ -645,7 +713,7 @@
socketClient,
createMdnsPacketWriter(),
serviceType,
- taskArgs.config.subtypes,
+ subtypes,
taskArgs.config.expectUnicastResponse,
taskArgs.config.transactionId,
taskArgs.config.socketKey,
@@ -657,7 +725,7 @@
.call();
} catch (RuntimeException e) {
sharedLog.e(String.format("Failed to run EnqueueMdnsQueryCallable for subtype: %s",
- TextUtils.join(",", taskArgs.config.subtypes)), e);
+ TextUtils.join(",", subtypes)), e);
result = Pair.create(INVALID_TRANSACTION_ID, new ArrayList<>());
}
dependencies.sendMessage(
@@ -692,4 +760,13 @@
args.sessionId, timeToNextTasksWithBackoffInMs));
return timeToNextTasksWithBackoffInMs;
}
+
+ /**
+ * Dump ServiceTypeClient state.
+ */
+ public void dump(PrintWriter pw) {
+ ensureRunningOnHandlerThread(handler);
+ pw.println("ServiceTypeClient: Type{" + serviceType + "} " + socketKey + " with "
+ + listeners.size() + " listeners.");
+ }
}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java b/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java
index 3cd77a4..70451f3 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java
@@ -42,6 +42,12 @@
private final Set<PacketHandler> mPacketHandlers = MdnsUtils.newSet();
interface PacketHandler {
+ /**
+ * Handle an incoming packet.
+ *
+ * The recvbuf and src <b>will be reused and modified</b> after this method returns, so
+ * implementers must ensure that they are not accessed after handlePacket returns.
+ */
void handlePacket(byte[] recvbuf, int length, InetSocketAddress src);
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
index 19282b0..0894166 100644
--- a/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
+++ b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
@@ -16,15 +16,14 @@
package com.android.server.connectivity.mdns;
+import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE;
+import static com.android.server.connectivity.mdns.MdnsSearchOptions.PASSIVE_QUERY_MODE;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-
/**
* 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.
@@ -33,19 +32,26 @@
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 MAX_TIME_BETWEEN_ACTIVE_PASSIVE_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;
+ // RFC 6762 5.2: The interval between the first two queries MUST be at least one second.
+ static final int INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS = 1000;
+ @VisibleForTesting
+ // Basically this tries to send one query per typical DTIM interval 100ms, to maximize the
+ // chances that a query will be received if devices are using a DTIM multiplier (in which case
+ // they only listen once every [multiplier] DTIM intervals).
+ static final int TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS = 100;
+ static final int MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS = 60000;
private final boolean alwaysAskForUnicastResponse =
MdnsConfigs.alwaysAskForUnicastResponseInEachBurst();
- private final boolean usePassiveMode;
+ private final int queryMode;
final boolean onlyUseIpv6OnIpv6OnlyNetworks;
private final int numOfQueriesBeforeBackoff;
@VisibleForTesting
@@ -65,8 +71,7 @@
boolean expectUnicastResponse, boolean isFirstBurst, int burstCounter,
int queriesPerBurst, int timeBetweenBurstsInMs,
long delayUntilNextTaskWithoutBackoffMs) {
- this.subtypes = new ArrayList<>(other.subtypes);
- this.usePassiveMode = other.usePassiveMode;
+ this.queryMode = other.queryMode;
this.onlyUseIpv6OnIpv6OnlyNetworks = other.onlyUseIpv6OnIpv6OnlyNetworks;
this.numOfQueriesBeforeBackoff = other.numOfQueriesBeforeBackoff;
this.transactionId = transactionId;
@@ -79,36 +84,72 @@
this.queryCount = queryCount;
this.socketKey = other.socketKey;
}
- QueryTaskConfig(@NonNull Collection<String> subtypes,
- boolean usePassiveMode,
+
+ QueryTaskConfig(int queryMode,
boolean onlyUseIpv6OnIpv6OnlyNetworks,
int numOfQueriesBeforeBackoff,
@Nullable SocketKey socketKey) {
- this.usePassiveMode = usePassiveMode;
+ this.queryMode = queryMode;
this.onlyUseIpv6OnIpv6OnlyNetworks = onlyUseIpv6OnIpv6OnlyNetworks;
this.numOfQueriesBeforeBackoff = numOfQueriesBeforeBackoff;
- this.subtypes = new ArrayList<>(subtypes);
this.queriesPerBurst = QUERIES_PER_BURST;
this.burstCounter = 0;
this.transactionId = 1;
this.expectUnicastResponse = true;
this.isFirstBurst = true;
// Config the scan frequency based on the scan mode.
- if (this.usePassiveMode) {
+ if (this.queryMode == AGGRESSIVE_QUERY_MODE) {
+ this.timeBetweenBurstsInMs = INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS;
+ this.delayUntilNextTaskWithoutBackoffMs =
+ TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS;
+ } else if (this.queryMode == PASSIVE_QUERY_MODE) {
// 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;
+ this.timeBetweenBurstsInMs = MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS;
+ this.delayUntilNextTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_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;
+ this.delayUntilNextTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
}
this.socketKey = socketKey;
this.queryCount = 0;
- this.delayUntilNextTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
+ }
+
+ long getDelayUntilNextTaskWithoutBackoff(boolean isFirstQueryInBurst,
+ boolean isLastQueryInBurst) {
+ if (isFirstQueryInBurst && queryMode == AGGRESSIVE_QUERY_MODE) {
+ return 0;
+ }
+ if (isLastQueryInBurst) {
+ return timeBetweenBurstsInMs;
+ }
+ return queryMode == AGGRESSIVE_QUERY_MODE
+ ? TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS
+ : TIME_BETWEEN_QUERIES_IN_BURST_MS;
+ }
+
+ boolean getNextExpectUnicastResponse(boolean isLastQueryInBurst) {
+ if (!isLastQueryInBurst) {
+ return false;
+ }
+ if (queryMode == AGGRESSIVE_QUERY_MODE) {
+ return true;
+ }
+ return alwaysAskForUnicastResponse;
+ }
+
+ int getNextTimeBetweenBurstsMs(boolean isLastQueryInBurst) {
+ if (!isLastQueryInBurst) {
+ return timeBetweenBurstsInMs;
+ }
+ final int maxTimeBetweenBursts = queryMode == AGGRESSIVE_QUERY_MODE
+ ? MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS : MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS;
+ return Math.min(timeBetweenBurstsInMs * 2, maxTimeBetweenBursts);
}
/**
@@ -120,43 +161,26 @@
if (newTransactionId > UNSIGNED_SHORT_MAX_VALUE) {
newTransactionId = 1;
}
- boolean newExpectUnicastResponse = false;
- boolean newIsFirstBurst = isFirstBurst;
+
int newQueriesPerBurst = queriesPerBurst;
int newBurstCounter = burstCounter + 1;
- long newDelayUntilNextTaskWithoutBackoffMs = delayUntilNextTaskWithoutBackoffMs;
- int newTimeBetweenBurstsInMs = timeBetweenBurstsInMs;
- // Only the first query expects uni-cast response.
- if (newBurstCounter == queriesPerBurst) {
+ final boolean isFirstQueryInBurst = newBurstCounter == 1;
+ final boolean isLastQueryInBurst = newBurstCounter == queriesPerBurst;
+ boolean newIsFirstBurst = isFirstBurst && !isLastQueryInBurst;
+ if (isLastQueryInBurst) {
newBurstCounter = 0;
-
- if (alwaysAskForUnicastResponse) {
- newExpectUnicastResponse = 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) {
- newIsFirstBurst = false;
- if (usePassiveMode) {
- newQueriesPerBurst = QUERIES_PER_BURST_PASSIVE_MODE;
- }
+ if (isFirstBurst && queryMode == PASSIVE_QUERY_MODE) {
+ newQueriesPerBurst = 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.
- newDelayUntilNextTaskWithoutBackoffMs = timeBetweenBurstsInMs;
- if (timeBetweenBurstsInMs < TIME_BETWEEN_BURSTS_MS) {
- newTimeBetweenBurstsInMs = Math.min(timeBetweenBurstsInMs * 2,
- TIME_BETWEEN_BURSTS_MS);
- }
- } else {
- newDelayUntilNextTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
}
+
return new QueryTaskConfig(this, newQueryCount, newTransactionId,
- newExpectUnicastResponse, newIsFirstBurst, newBurstCounter, newQueriesPerBurst,
- newTimeBetweenBurstsInMs, newDelayUntilNextTaskWithoutBackoffMs);
+ getNextExpectUnicastResponse(isLastQueryInBurst), newIsFirstBurst, newBurstCounter,
+ newQueriesPerBurst, getNextTimeBetweenBurstsMs(isLastQueryInBurst),
+ getDelayUntilNextTaskWithoutBackoff(isFirstQueryInBurst, isLastQueryInBurst));
}
/**
diff --git a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
index 1482ebb..d553210 100644
--- a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
+++ b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
@@ -86,7 +86,10 @@
/**
* Compare two strings by DNS case-insensitive lowercase.
*/
- public static boolean equalsIgnoreDnsCase(@NonNull String a, @NonNull String b) {
+ public static boolean equalsIgnoreDnsCase(@Nullable String a, @Nullable String b) {
+ if (a == null || b == null) {
+ return a == null && b == null;
+ }
if (a.length() != b.length()) return false;
for (int i = 0; i < a.length(); i++) {
if (toDnsLowerCase(a.charAt(i)) != toDnsLowerCase(b.charAt(i))) {
@@ -233,6 +236,20 @@
&& mdnsRecord.getRemainingTTL(now) <= mdnsRecord.getTtl() / 2;
}
+ /**
+ * Creates a new full subtype name with given service type and subtype labels.
+ *
+ * For example, given ["_http", "_tcp"] and "_printer", this method returns a new String array
+ * of ["_printer", "_sub", "_http", "_tcp"].
+ */
+ public static String[] constructFullSubtype(String[] serviceType, String subtype) {
+ String[] fullSubtype = new String[serviceType.length + 2];
+ fullSubtype[0] = subtype;
+ fullSubtype[1] = MdnsConstants.SUBTYPE_LABEL;
+ System.arraycopy(serviceType, 0, fullSubtype, 2, serviceType.length);
+ return fullSubtype;
+ }
+
/** A wrapper class of {@link SystemClock} to be mocked in unit tests. */
public static class Clock {
/**
diff --git a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
index e7af569..b8689d6 100644
--- a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
+++ b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
@@ -19,6 +19,7 @@
import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+import android.annotation.CheckResult;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
@@ -72,8 +73,9 @@
methodName + " is only available on automotive devices.");
}
- private boolean checkUseRestrictedNetworksPermission() {
- return PermissionUtils.checkAnyPermissionOf(mContext,
+ @CheckResult
+ private boolean hasUseRestrictedNetworksPermission() {
+ return PermissionUtils.hasAnyPermissionOf(mContext,
android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS);
}
@@ -92,7 +94,7 @@
@Override
public String[] getAvailableInterfaces() throws RemoteException {
PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG);
- return mTracker.getClientModeInterfaces(checkUseRestrictedNetworksPermission());
+ return mTracker.getClientModeInterfaces(hasUseRestrictedNetworksPermission());
}
/**
@@ -146,7 +148,7 @@
public void addListener(IEthernetServiceListener listener) throws RemoteException {
Objects.requireNonNull(listener, "listener must not be null");
PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG);
- mTracker.addListener(listener, checkUseRestrictedNetworksPermission());
+ mTracker.addListener(listener, hasUseRestrictedNetworksPermission());
}
/**
@@ -187,7 +189,7 @@
@Override
protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ");
- if (!PermissionUtils.checkDumpPermission(mContext, TAG, pw)) return;
+ if (!PermissionUtils.hasDumpPermission(mContext, TAG, pw)) return;
pw.println("Current Ethernet state: ");
pw.increaseIndent();
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 48e86d8..458d64f 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -48,6 +48,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
+import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.NetdUtils;
import com.android.net.module.util.PermissionUtils;
import com.android.net.module.util.SharedLog;
@@ -237,7 +238,18 @@
mDeps = deps;
// Interface match regex.
- mIfaceMatch = mDeps.getInterfaceRegexFromResource(mContext);
+ String ifaceMatchRegex = mDeps.getInterfaceRegexFromResource(mContext);
+ // "*" is a magic string to indicate "pick the default".
+ if (ifaceMatchRegex.equals("*")) {
+ if (SdkLevel.isAtLeastV()) {
+ // On V+, include both usb%d and eth%d interfaces.
+ ifaceMatchRegex = "(usb|eth)\\d+";
+ } else {
+ // On T and U, include only eth%d interfaces.
+ ifaceMatchRegex = "eth\\d+";
+ }
+ }
+ mIfaceMatch = ifaceMatchRegex;
// Read default Ethernet interface configuration from resources
final String[] interfaceConfigs = mDeps.getInterfaceConfigFromResource(context);
diff --git a/service-t/src/com/android/server/net/BpfInterfaceMapHelper.java b/service-t/src/com/android/server/net/BpfInterfaceMapHelper.java
new file mode 100644
index 0000000..3c95b8e
--- /dev/null
+++ b/service-t/src/com/android/server/net/BpfInterfaceMapHelper.java
@@ -0,0 +1,101 @@
+/*
+ * 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 com.android.server.net;
+
+import android.os.Build;
+import android.system.ErrnoException;
+import android.util.IndentingPrintWriter;
+import android.util.Log;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.BpfDump;
+import com.android.net.module.util.BpfMap;
+import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.Struct.S32;
+
+/**
+ * Monitor interface added (without removed) and right interface name and its index to bpf map.
+ */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+public class BpfInterfaceMapHelper {
+ private static final String TAG = BpfInterfaceMapHelper.class.getSimpleName();
+ // This is current path but may be changed soon.
+ private static final String IFACE_INDEX_NAME_MAP_PATH =
+ "/sys/fs/bpf/netd_shared/map_netd_iface_index_name_map";
+ private final IBpfMap<S32, InterfaceMapValue> mIndexToIfaceBpfMap;
+
+ public BpfInterfaceMapHelper() {
+ this(new Dependencies());
+ }
+
+ @VisibleForTesting
+ public BpfInterfaceMapHelper(Dependencies deps) {
+ mIndexToIfaceBpfMap = deps.getInterfaceMap();
+ }
+
+ /**
+ * Dependencies of BpfInerfaceMapUpdater, for injection in tests.
+ */
+ @VisibleForTesting
+ public static class Dependencies {
+ /** Create BpfMap for updating interface and index mapping. */
+ public IBpfMap<S32, InterfaceMapValue> getInterfaceMap() {
+ try {
+ return new BpfMap<>(IFACE_INDEX_NAME_MAP_PATH,
+ S32.class, InterfaceMapValue.class);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Cannot create interface map: " + e);
+ return null;
+ }
+ }
+ }
+
+ /** get interface name by interface index from bpf map */
+ public String getIfNameByIndex(final int index) {
+ try {
+ final InterfaceMapValue value = mIndexToIfaceBpfMap.getValue(new S32(index));
+ if (value == null) {
+ Log.e(TAG, "No if name entry for index " + index);
+ return null;
+ }
+ return value.getInterfaceNameString();
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to get entry for index " + index + ": " + e);
+ return null;
+ }
+ }
+
+ /**
+ * Dump BPF map
+ *
+ * @param pw print writer
+ */
+ public void dump(final IndentingPrintWriter pw) {
+ pw.println("BPF map status:");
+ pw.increaseIndent();
+ BpfDump.dumpMapStatus(mIndexToIfaceBpfMap, pw, "IfaceIndexNameMap",
+ IFACE_INDEX_NAME_MAP_PATH);
+ pw.decreaseIndent();
+ pw.println("BPF map content:");
+ pw.increaseIndent();
+ BpfDump.dumpMap(mIndexToIfaceBpfMap, pw, "IfaceIndexNameMap",
+ (key, value) -> "ifaceIndex=" + key.val
+ + " ifaceName=" + value.getInterfaceNameString());
+ pw.decreaseIndent();
+ }
+}
diff --git a/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java b/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java
deleted file mode 100644
index 27c0f9f..0000000
--- a/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java
+++ /dev/null
@@ -1,175 +0,0 @@
-/*
- * 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 com.android.server.net;
-
-import android.content.Context;
-import android.net.INetd;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.os.ServiceSpecificException;
-import android.system.ErrnoException;
-import android.util.IndentingPrintWriter;
-import android.util.Log;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
-import com.android.net.module.util.BpfDump;
-import com.android.net.module.util.BpfMap;
-import com.android.net.module.util.IBpfMap;
-import com.android.net.module.util.InterfaceParams;
-import com.android.net.module.util.Struct.S32;
-
-/**
- * Monitor interface added (without removed) and right interface name and its index to bpf map.
- */
-public class BpfInterfaceMapUpdater {
- private static final String TAG = BpfInterfaceMapUpdater.class.getSimpleName();
- // This is current path but may be changed soon.
- private static final String IFACE_INDEX_NAME_MAP_PATH =
- "/sys/fs/bpf/netd_shared/map_netd_iface_index_name_map";
- private final IBpfMap<S32, InterfaceMapValue> mIndexToIfaceBpfMap;
- private final INetd mNetd;
- private final Handler mHandler;
- private final Dependencies mDeps;
-
- public BpfInterfaceMapUpdater(Context ctx, Handler handler) {
- this(ctx, handler, new Dependencies());
- }
-
- @VisibleForTesting
- public BpfInterfaceMapUpdater(Context ctx, Handler handler, Dependencies deps) {
- mDeps = deps;
- mIndexToIfaceBpfMap = deps.getInterfaceMap();
- mNetd = deps.getINetd(ctx);
- mHandler = handler;
- }
-
- /**
- * Dependencies of BpfInerfaceMapUpdater, for injection in tests.
- */
- @VisibleForTesting
- public static class Dependencies {
- /** Create BpfMap for updating interface and index mapping. */
- public IBpfMap<S32, InterfaceMapValue> getInterfaceMap() {
- try {
- return new BpfMap<>(IFACE_INDEX_NAME_MAP_PATH, BpfMap.BPF_F_RDWR,
- S32.class, InterfaceMapValue.class);
- } catch (ErrnoException e) {
- Log.e(TAG, "Cannot create interface map: " + e);
- return null;
- }
- }
-
- /** Get InterfaceParams for giving interface name. */
- public InterfaceParams getInterfaceParams(String ifaceName) {
- return InterfaceParams.getByName(ifaceName);
- }
-
- /** Get INetd binder object. */
- public INetd getINetd(Context ctx) {
- return INetd.Stub.asInterface((IBinder) ctx.getSystemService(Context.NETD_SERVICE));
- }
- }
-
- /**
- * Start listening interface update event.
- * Query current interface names before listening.
- */
- public void start() {
- mHandler.post(() -> {
- if (mIndexToIfaceBpfMap == null) {
- Log.wtf(TAG, "Fail to start: Null bpf map");
- return;
- }
-
- try {
- // TODO: use a NetlinkMonitor and listen for RTM_NEWLINK messages instead.
- mNetd.registerUnsolicitedEventListener(new InterfaceChangeObserver());
- } catch (RemoteException e) {
- Log.wtf(TAG, "Unable to register netd UnsolicitedEventListener, " + e);
- }
-
- final String[] ifaces;
- try {
- // TODO: use a netlink dump to get the current interface list.
- ifaces = mNetd.interfaceGetList();
- } catch (RemoteException | ServiceSpecificException e) {
- Log.wtf(TAG, "Unable to query interface names by netd, " + e);
- return;
- }
-
- for (String ifaceName : ifaces) {
- addInterface(ifaceName);
- }
- });
- }
-
- private void addInterface(String ifaceName) {
- final InterfaceParams iface = mDeps.getInterfaceParams(ifaceName);
- if (iface == null) {
- Log.e(TAG, "Unable to get InterfaceParams for " + ifaceName);
- return;
- }
-
- try {
- mIndexToIfaceBpfMap.updateEntry(new S32(iface.index), new InterfaceMapValue(ifaceName));
- } catch (ErrnoException e) {
- Log.e(TAG, "Unable to update entry for " + ifaceName + ", " + e);
- }
- }
-
- private class InterfaceChangeObserver extends BaseNetdUnsolicitedEventListener {
- @Override
- public void onInterfaceAdded(String ifName) {
- mHandler.post(() -> addInterface(ifName));
- }
- }
-
- /** get interface name by interface index from bpf map */
- public String getIfNameByIndex(final int index) {
- try {
- final InterfaceMapValue value = mIndexToIfaceBpfMap.getValue(new S32(index));
- if (value == null) {
- Log.e(TAG, "No if name entry for index " + index);
- return null;
- }
- return value.getInterfaceNameString();
- } catch (ErrnoException e) {
- Log.e(TAG, "Failed to get entry for index " + index + ": " + e);
- return null;
- }
- }
-
- /**
- * Dump BPF map
- *
- * @param pw print writer
- */
- public void dump(final IndentingPrintWriter pw) {
- pw.println("BPF map status:");
- pw.increaseIndent();
- BpfDump.dumpMapStatus(mIndexToIfaceBpfMap, pw, "IfaceIndexNameMap",
- IFACE_INDEX_NAME_MAP_PATH);
- pw.decreaseIndent();
- pw.println("BPF map content:");
- pw.increaseIndent();
- BpfDump.dumpMap(mIndexToIfaceBpfMap, pw, "IfaceIndexNameMap",
- (key, value) -> "ifaceIndex=" + key.val
- + " ifaceName=" + value.getInterfaceNameString());
- pw.decreaseIndent();
- }
-}
diff --git a/service-t/src/com/android/server/net/NetworkStatsRecorder.java b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
index 3da1585..8ee8591 100644
--- a/service-t/src/com/android/server/net/NetworkStatsRecorder.java
+++ b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
@@ -22,6 +22,7 @@
import static android.text.format.DateUtils.YEAR_IN_MILLIS;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.net.NetworkIdentitySet;
import android.net.NetworkStats;
import android.net.NetworkStats.NonMonotonicObserver;
@@ -32,17 +33,20 @@
import android.net.TrafficStats;
import android.os.Binder;
import android.os.DropBoxManager;
+import android.os.SystemClock;
import android.service.NetworkStatsRecorderProto;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.proto.ProtoOutputStream;
import com.android.internal.util.FileRotator;
+import com.android.metrics.NetworkStatsMetricsLogger;
import com.android.net.module.util.NetworkStatsUtils;
import libcore.io.IoUtils;
import java.io.ByteArrayOutputStream;
+import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -79,6 +83,7 @@
private final long mBucketDuration;
private final boolean mOnlyTags;
private final boolean mWipeOnError;
+ private final boolean mUseFastDataInput;
private long mPersistThresholdBytes = 2 * MB_IN_BYTES;
private NetworkStats mLastSnapshot;
@@ -89,6 +94,9 @@
private final CombiningRewriter mPendingRewriter;
private WeakReference<NetworkStatsCollection> mComplete;
+ private final NetworkStatsMetricsLogger mMetricsLogger = new NetworkStatsMetricsLogger();
+ @Nullable
+ private final File mStatsDir;
/**
* Non-persisted recorder, with only one bucket. Used by {@link NetworkStatsObservers}.
@@ -104,11 +112,13 @@
mBucketDuration = YEAR_IN_MILLIS;
mOnlyTags = false;
mWipeOnError = true;
+ mUseFastDataInput = false;
mPending = null;
mSinceBoot = new NetworkStatsCollection(mBucketDuration);
mPendingRewriter = null;
+ mStatsDir = null;
}
/**
@@ -116,7 +126,7 @@
*/
public NetworkStatsRecorder(FileRotator rotator, NonMonotonicObserver<String> observer,
DropBoxManager dropBox, String cookie, long bucketDuration, boolean onlyTags,
- boolean wipeOnError) {
+ boolean wipeOnError, boolean useFastDataInput, @Nullable File statsDir) {
mRotator = Objects.requireNonNull(rotator, "missing FileRotator");
mObserver = Objects.requireNonNull(observer, "missing NonMonotonicObserver");
mDropBox = Objects.requireNonNull(dropBox, "missing DropBoxManager");
@@ -125,11 +135,13 @@
mBucketDuration = bucketDuration;
mOnlyTags = onlyTags;
mWipeOnError = wipeOnError;
+ mUseFastDataInput = useFastDataInput;
mPending = new NetworkStatsCollection(bucketDuration);
mSinceBoot = new NetworkStatsCollection(bucketDuration);
mPendingRewriter = new CombiningRewriter(mPending);
+ mStatsDir = statsDir;
}
public void setPersistThreshold(long thresholdBytes) {
@@ -179,8 +191,16 @@
Objects.requireNonNull(mRotator, "missing FileRotator");
NetworkStatsCollection res = mComplete != null ? mComplete.get() : null;
if (res == null) {
+ final long readStart = SystemClock.elapsedRealtime();
res = loadLocked(Long.MIN_VALUE, Long.MAX_VALUE);
mComplete = new WeakReference<NetworkStatsCollection>(res);
+ final long readEnd = SystemClock.elapsedRealtime();
+ // For legacy recorders which are used for data integrity check, which
+ // have wipeOnError flag unset, skip reporting metrics.
+ if (mWipeOnError) {
+ mMetricsLogger.logRecorderFileReading(mCookie, (int) (readEnd - readStart),
+ mStatsDir, res, mUseFastDataInput);
+ }
}
return res;
}
@@ -195,8 +215,12 @@
}
private NetworkStatsCollection loadLocked(long start, long end) {
- if (LOGD) Log.d(TAG, "loadLocked() reading from disk for " + mCookie);
- final NetworkStatsCollection res = new NetworkStatsCollection(mBucketDuration);
+ if (LOGD) {
+ Log.d(TAG, "loadLocked() reading from disk for " + mCookie
+ + " useFastDataInput: " + mUseFastDataInput);
+ }
+ final NetworkStatsCollection res =
+ new NetworkStatsCollection(mBucketDuration, mUseFastDataInput);
try {
mRotator.readMatching(res, start, end);
res.recordCollection(mPending);
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 2c9f30c..f7f133c 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -44,7 +44,6 @@
import static android.net.NetworkStats.TAG_ALL;
import static android.net.NetworkStats.TAG_NONE;
import static android.net.NetworkStats.UID_ALL;
-import static android.net.NetworkStatsCollection.compareStats;
import static android.net.NetworkStatsHistory.FIELD_ALL;
import static android.net.NetworkTemplate.MATCH_MOBILE;
import static android.net.NetworkTemplate.MATCH_TEST;
@@ -295,6 +294,11 @@
static final String CONFIG_ENABLE_NETWORK_STATS_EVENT_LOGGER =
"enable_network_stats_event_logger";
+ static final String NETSTATS_FASTDATAINPUT_TARGET_ATTEMPTS =
+ "netstats_fastdatainput_target_attempts";
+ static final String NETSTATS_FASTDATAINPUT_SUCCESSES_COUNTER_NAME = "fastdatainput.successes";
+ static final String NETSTATS_FASTDATAINPUT_FALLBACKS_COUNTER_NAME = "fastdatainput.fallbacks";
+
private final Context mContext;
private final NetworkStatsFactory mStatsFactory;
private final AlarmManager mAlarmManager;
@@ -318,6 +322,8 @@
private PersistentInt mImportLegacyAttemptsCounter = null;
private PersistentInt mImportLegacySuccessesCounter = null;
private PersistentInt mImportLegacyFallbacksCounter = null;
+ private PersistentInt mFastDataInputSuccessesCounter = null;
+ private PersistentInt mFastDataInputFallbacksCounter = null;
@VisibleForTesting
public static final String ACTION_NETWORK_STATS_POLL =
@@ -470,11 +476,13 @@
private final LocationPermissionChecker mLocationPermissionChecker;
@NonNull
- private final BpfInterfaceMapUpdater mInterfaceMapUpdater;
+ private final BpfInterfaceMapHelper mInterfaceMapHelper;
@Nullable
private final SkDestroyListener mSkDestroyListener;
+ private static final int MAX_SOCKET_DESTROY_LISTENER_LOGS = 20;
+
private static @NonNull Clock getDefaultClock() {
return new BestClock(ZoneOffset.UTC, SystemClock.currentNetworkTimeClock(),
Clock.systemUTC());
@@ -486,9 +494,10 @@
*/
private static class OpenSessionKey {
public final int uid;
+ @Nullable
public final String packageName;
- OpenSessionKey(int uid, @NonNull String packageName) {
+ OpenSessionKey(int uid, @Nullable String packageName) {
this.uid = uid;
this.packageName = packageName;
}
@@ -619,8 +628,7 @@
mContentObserver = mDeps.makeContentObserver(mHandler, mSettings,
mNetworkStatsSubscriptionsMonitor);
mLocationPermissionChecker = mDeps.makeLocationPermissionChecker(mContext);
- mInterfaceMapUpdater = mDeps.makeBpfInterfaceMapUpdater(mContext, mHandler);
- mInterfaceMapUpdater.start();
+ mInterfaceMapHelper = mDeps.makeBpfInterfaceMapHelper();
mUidCounterSetMap = mDeps.getUidCounterSetMap();
mCookieTagMap = mDeps.getCookieTagMap();
mStatsMapA = mDeps.getStatsMapA();
@@ -695,6 +703,24 @@
}
/**
+ * Get the count of using FastDataInput target attempts.
+ */
+ public int getUseFastDataInputTargetAttempts() {
+ return DeviceConfigUtils.getDeviceConfigPropertyInt(
+ DeviceConfig.NAMESPACE_TETHERING,
+ NETSTATS_FASTDATAINPUT_TARGET_ATTEMPTS, 0);
+ }
+
+ /**
+ * Compare two {@link NetworkStatsCollection} instances and returning a human-readable
+ * string description of difference for debugging purpose.
+ */
+ public String compareStats(@NonNull NetworkStatsCollection a,
+ @NonNull NetworkStatsCollection b, boolean allowKeyChange) {
+ return NetworkStatsCollection.compareStats(a, b, allowKeyChange);
+ }
+
+ /**
* Create a persistent counter for given directory and name.
*/
public PersistentInt createPersistentCounter(@NonNull Path dir, @NonNull String name)
@@ -771,18 +797,16 @@
return new LocationPermissionChecker(context);
}
- /** Create BpfInterfaceMapUpdater to update bpf interface map. */
+ /** Create BpfInterfaceMapHelper to update bpf interface map. */
@NonNull
- public BpfInterfaceMapUpdater makeBpfInterfaceMapUpdater(
- @NonNull Context ctx, @NonNull Handler handler) {
- return new BpfInterfaceMapUpdater(ctx, handler);
+ public BpfInterfaceMapHelper makeBpfInterfaceMapHelper() {
+ return new BpfInterfaceMapHelper();
}
/** Get counter sets map for each UID. */
public IBpfMap<S32, U8> getUidCounterSetMap() {
try {
- return new BpfMap<S32, U8>(UID_COUNTERSET_MAP_PATH, BpfMap.BPF_F_RDWR,
- S32.class, U8.class);
+ return new BpfMap<>(UID_COUNTERSET_MAP_PATH, S32.class, U8.class);
} catch (ErrnoException e) {
Log.wtf(TAG, "Cannot open uid counter set map: " + e);
return null;
@@ -792,8 +816,8 @@
/** Gets the cookie tag map */
public IBpfMap<CookieTagMapKey, CookieTagMapValue> getCookieTagMap() {
try {
- return new BpfMap<CookieTagMapKey, CookieTagMapValue>(COOKIE_TAG_MAP_PATH,
- BpfMap.BPF_F_RDWR, CookieTagMapKey.class, CookieTagMapValue.class);
+ return new BpfMap<>(COOKIE_TAG_MAP_PATH,
+ CookieTagMapKey.class, CookieTagMapValue.class);
} catch (ErrnoException e) {
Log.wtf(TAG, "Cannot open cookie tag map: " + e);
return null;
@@ -803,8 +827,7 @@
/** Gets stats map A */
public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapA() {
try {
- return new BpfMap<StatsMapKey, StatsMapValue>(STATS_MAP_A_PATH,
- BpfMap.BPF_F_RDWR, StatsMapKey.class, StatsMapValue.class);
+ return new BpfMap<>(STATS_MAP_A_PATH, StatsMapKey.class, StatsMapValue.class);
} catch (ErrnoException e) {
Log.wtf(TAG, "Cannot open stats map A: " + e);
return null;
@@ -814,8 +837,7 @@
/** Gets stats map B */
public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapB() {
try {
- return new BpfMap<StatsMapKey, StatsMapValue>(STATS_MAP_B_PATH,
- BpfMap.BPF_F_RDWR, StatsMapKey.class, StatsMapValue.class);
+ return new BpfMap<>(STATS_MAP_B_PATH, StatsMapKey.class, StatsMapValue.class);
} catch (ErrnoException e) {
Log.wtf(TAG, "Cannot open stats map B: " + e);
return null;
@@ -825,8 +847,8 @@
/** Gets the uid stats map */
public IBpfMap<UidStatsMapKey, StatsMapValue> getAppUidStatsMap() {
try {
- return new BpfMap<UidStatsMapKey, StatsMapValue>(APP_UID_STATS_MAP_PATH,
- BpfMap.BPF_F_RDWR, UidStatsMapKey.class, StatsMapValue.class);
+ return new BpfMap<>(APP_UID_STATS_MAP_PATH,
+ UidStatsMapKey.class, StatsMapValue.class);
} catch (ErrnoException e) {
Log.wtf(TAG, "Cannot open app uid stats map: " + e);
return null;
@@ -836,8 +858,7 @@
/** Gets interface stats map */
public IBpfMap<S32, StatsMapValue> getIfaceStatsMap() {
try {
- return new BpfMap<S32, StatsMapValue>(IFACE_STATS_MAP_PATH,
- BpfMap.BPF_F_RDWR, S32.class, StatsMapValue.class);
+ return new BpfMap<>(IFACE_STATS_MAP_PATH, S32.class, StatsMapValue.class);
} catch (ErrnoException e) {
throw new IllegalStateException("Failed to open interface stats map", e);
}
@@ -856,7 +877,8 @@
/** Create a new SkDestroyListener. */
public SkDestroyListener makeSkDestroyListener(
IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap, Handler handler) {
- return new SkDestroyListener(cookieTagMap, handler, new SharedLog(TAG));
+ return new SkDestroyListener(
+ cookieTagMap, handler, new SharedLog(MAX_SOCKET_DESTROY_LISTENER_LOGS, TAG));
}
/**
@@ -892,13 +914,7 @@
synchronized (mStatsLock) {
mSystemReady = true;
- // create data recorders along with historical rotators
- 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, true /* wipeOnError */);
+ makeRecordersLocked();
updatePersistThresholdsLocked();
@@ -963,13 +979,106 @@
private NetworkStatsRecorder buildRecorder(
String prefix, NetworkStatsSettings.Config config, boolean includeTags,
- File baseDir, boolean wipeOnError) {
+ File baseDir, boolean wipeOnError, boolean useFastDataInput) {
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,
- wipeOnError);
+ wipeOnError, useFastDataInput, baseDir);
+ }
+
+ @GuardedBy("mStatsLock")
+ private void makeRecordersLocked() {
+ boolean useFastDataInput = true;
+ try {
+ mFastDataInputSuccessesCounter = mDeps.createPersistentCounter(mStatsDir.toPath(),
+ NETSTATS_FASTDATAINPUT_SUCCESSES_COUNTER_NAME);
+ mFastDataInputFallbacksCounter = mDeps.createPersistentCounter(mStatsDir.toPath(),
+ NETSTATS_FASTDATAINPUT_FALLBACKS_COUNTER_NAME);
+ } catch (IOException e) {
+ Log.wtf(TAG, "Failed to create persistent counters, skip.", e);
+ useFastDataInput = false;
+ }
+
+ final int targetAttempts = mDeps.getUseFastDataInputTargetAttempts();
+ int successes = 0;
+ int fallbacks = 0;
+ try {
+ successes = mFastDataInputSuccessesCounter.get();
+ // Fallbacks counter would be set to non-zero value to indicate the reading was
+ // not successful.
+ fallbacks = mFastDataInputFallbacksCounter.get();
+ } catch (IOException e) {
+ Log.wtf(TAG, "Failed to read counters, skip.", e);
+ useFastDataInput = false;
+ }
+
+ final boolean doComparison;
+ if (useFastDataInput) {
+ // Use FastDataInput if it needs to be evaluated or at least one success.
+ doComparison = targetAttempts > successes + fallbacks;
+ // Set target attempt to -1 as the kill switch to disable the feature.
+ useFastDataInput = targetAttempts >= 0 && (doComparison || successes > 0);
+ } else {
+ // useFastDataInput is false due to previous failures.
+ doComparison = false;
+ }
+
+ // create data recorders along with historical rotators.
+ // Don't wipe on error if comparison is needed.
+ mXtRecorder = buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, mStatsDir,
+ !doComparison /* wipeOnError */, useFastDataInput);
+ mUidRecorder = buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, mStatsDir,
+ !doComparison /* wipeOnError */, useFastDataInput);
+ mUidTagRecorder = buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true,
+ mStatsDir, !doComparison /* wipeOnError */, useFastDataInput);
+
+ if (!doComparison) return;
+
+ final MigrationInfo[] migrations = new MigrationInfo[]{
+ new MigrationInfo(mXtRecorder),
+ new MigrationInfo(mUidRecorder),
+ new MigrationInfo(mUidTagRecorder)
+ };
+ // Set wipeOnError flag false so the recorder won't damage persistent data if reads
+ // failed and calling deleteAll.
+ final NetworkStatsRecorder[] legacyRecorders = new NetworkStatsRecorder[]{
+ buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, mStatsDir,
+ false /* wipeOnError */, false /* useFastDataInput */),
+ buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, mStatsDir,
+ false /* wipeOnError */, false /* useFastDataInput */),
+ buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true, mStatsDir,
+ false /* wipeOnError */, false /* useFastDataInput */)};
+ boolean success = true;
+ for (int i = 0; i < migrations.length; i++) {
+ try {
+ migrations[i].collection = migrations[i].recorder.getOrLoadCompleteLocked();
+ } catch (Throwable t) {
+ Log.wtf(TAG, "Failed to load collection, skip.", t);
+ success = false;
+ break;
+ }
+ if (!compareImportedToLegacyStats(migrations[i], legacyRecorders[i],
+ false /* allowKeyChange */)) {
+ success = false;
+ break;
+ }
+ }
+
+ try {
+ if (success) {
+ mFastDataInputSuccessesCounter.set(successes + 1);
+ } else {
+ // Fallback.
+ mXtRecorder = legacyRecorders[0];
+ mUidRecorder = legacyRecorders[1];
+ mUidTagRecorder = legacyRecorders[2];
+ mFastDataInputFallbacksCounter.set(fallbacks + 1);
+ }
+ } catch (IOException e) {
+ Log.wtf(TAG, "Failed to update counters. success = " + success, e);
+ }
}
@GuardedBy("mStatsLock")
@@ -1068,7 +1177,7 @@
new NetworkStatsSettings.Config(HOUR_IN_MILLIS,
15 * DAY_IN_MILLIS, 90 * DAY_IN_MILLIS);
final NetworkStatsRecorder devRecorder = buildRecorder(PREFIX_DEV, devConfig,
- false, mStatsDir, true /* wipeOnError */);
+ false, mStatsDir, true /* wipeOnError */, false /* useFastDataInput */);
final MigrationInfo[] migrations = new MigrationInfo[]{
new MigrationInfo(devRecorder), new MigrationInfo(mXtRecorder),
new MigrationInfo(mUidRecorder), new MigrationInfo(mUidTagRecorder)
@@ -1085,11 +1194,11 @@
legacyRecorders = new NetworkStatsRecorder[]{
null /* dev Recorder */,
buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, legacyBaseDir,
- false /* wipeOnError */),
+ false /* wipeOnError */, false /* useFastDataInput */),
buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, legacyBaseDir,
- false /* wipeOnError */),
+ false /* wipeOnError */, false /* useFastDataInput */),
buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true, legacyBaseDir,
- false /* wipeOnError */)};
+ false /* wipeOnError */, false /* useFastDataInput */)};
} else {
legacyRecorders = null;
}
@@ -1120,7 +1229,8 @@
if (runComparison) {
final boolean success =
- compareImportedToLegacyStats(migration, legacyRecorders[i]);
+ compareImportedToLegacyStats(migration, legacyRecorders[i],
+ true /* allowKeyChange */);
if (!success && !dryRunImportOnly) {
tryIncrementLegacyFallbacksCounter();
}
@@ -1243,7 +1353,7 @@
* does not match or throw with exceptions.
*/
private boolean compareImportedToLegacyStats(@NonNull MigrationInfo migration,
- @Nullable NetworkStatsRecorder legacyRecorder) {
+ @Nullable NetworkStatsRecorder legacyRecorder, boolean allowKeyChange) {
final NetworkStatsCollection legacyStats;
// Skip the recorder that doesn't need to be compared.
if (legacyRecorder == null) return true;
@@ -1258,7 +1368,8 @@
// The result of comparison is only for logging.
try {
- final String error = compareStats(migration.collection, legacyStats);
+ final String error = mDeps.compareStats(migration.collection, legacyStats,
+ allowKeyChange);
if (error != null) {
Log.wtf(TAG, "Unexpected comparison result for recorder "
+ legacyRecorder.getCookie() + ": " + error);
@@ -1319,7 +1430,11 @@
}
@Override
- public INetworkStatsSession openSessionForUsageStats(int flags, String callingPackage) {
+ public INetworkStatsSession openSessionForUsageStats(
+ int flags, @NonNull String callingPackage) {
+ Objects.requireNonNull(callingPackage);
+ PermissionUtils.enforcePackageNameMatchesUid(
+ mContext, Binder.getCallingUid(), callingPackage);
return openSessionInternal(flags, callingPackage);
}
@@ -1348,9 +1463,9 @@
return now - lastCallTime < POLL_RATE_LIMIT_MS;
}
- private int restrictFlagsForCaller(int flags, @NonNull String callingPackage) {
+ private int restrictFlagsForCaller(int flags, @Nullable String callingPackage) {
// All non-privileged callers are not allowed to turn off POLL_ON_OPEN.
- final boolean isPrivileged = PermissionUtils.checkAnyPermissionOf(mContext,
+ final boolean isPrivileged = PermissionUtils.hasAnyPermissionOf(mContext,
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
android.Manifest.permission.NETWORK_STACK);
if (!isPrivileged) {
@@ -1365,7 +1480,8 @@
return flags;
}
- private INetworkStatsSession openSessionInternal(final int flags, final String callingPackage) {
+ private INetworkStatsSession openSessionInternal(
+ final int flags, @Nullable final String callingPackage) {
final int restrictedFlags = restrictFlagsForCaller(flags, callingPackage);
if ((restrictedFlags & (NetworkStatsManager.FLAG_POLL_ON_OPEN
| NetworkStatsManager.FLAG_POLL_FORCE)) != 0) {
@@ -1382,6 +1498,7 @@
return new INetworkStatsSession.Stub() {
private final int mCallingUid = Binder.getCallingUid();
+ @Nullable
private final String mCallingPackage = callingPackage;
private final @NetworkStatsAccess.Level int mAccessLevel = checkAccessLevel(
callingPackage);
@@ -1520,7 +1637,7 @@
}
private void enforceTemplatePermissions(@NonNull NetworkTemplate template,
- @NonNull String callingPackage) {
+ @Nullable String callingPackage) {
// For a template with wifi network keys, it is possible for a malicious
// client to track the user locations via querying data usage. Thus, enforce
// fine location permission check.
@@ -1541,7 +1658,7 @@
}
}
- private @NetworkStatsAccess.Level int checkAccessLevel(String callingPackage) {
+ private @NetworkStatsAccess.Level int checkAccessLevel(@Nullable String callingPackage) {
return NetworkStatsAccess.checkAccessLevel(
mContext, Binder.getCallingPid(), Binder.getCallingUid(), callingPackage);
}
@@ -1832,6 +1949,7 @@
final int callingPid = Binder.getCallingPid();
final int callingUid = Binder.getCallingUid();
+ PermissionUtils.enforcePackageNameMatchesUid(mContext, callingUid, callingPackage);
@NetworkStatsAccess.Level int accessLevel = checkAccessLevel(callingPackage);
DataUsageRequest normalizedRequest;
final long token = Binder.clearCallingIdentity();
@@ -1868,36 +1986,56 @@
if (callingUid != android.os.Process.SYSTEM_UID && callingUid != uid) {
return UNSUPPORTED;
}
- return nativeGetUidStat(uid, type);
+ return getEntryValueForType(nativeGetUidStat(uid), type);
}
@Override
public long getIfaceStats(@NonNull String iface, int type) {
Objects.requireNonNull(iface);
- long nativeIfaceStats = nativeGetIfaceStat(iface, type);
- if (nativeIfaceStats == -1) {
- return nativeIfaceStats;
+ final NetworkStats.Entry entry = nativeGetIfaceStat(iface);
+ final long value = getEntryValueForType(entry, type);
+ if (value == UNSUPPORTED) {
+ return UNSUPPORTED;
} else {
// When tethering offload is in use, nativeIfaceStats does not contain usage from
// offload, add it back here. Note that the included statistics might be stale
// since polling newest stats from hardware might impact system health and not
// suitable for TrafficStats API use cases.
- return nativeIfaceStats + getProviderIfaceStats(iface, type);
+ entry.add(getProviderIfaceStats(iface));
+ return getEntryValueForType(entry, type);
+ }
+ }
+
+ private long getEntryValueForType(@Nullable NetworkStats.Entry entry, int type) {
+ if (entry == null) return UNSUPPORTED;
+ switch (type) {
+ case TrafficStats.TYPE_RX_BYTES:
+ return entry.rxBytes;
+ case TrafficStats.TYPE_TX_BYTES:
+ return entry.txBytes;
+ case TrafficStats.TYPE_RX_PACKETS:
+ return entry.rxPackets;
+ case TrafficStats.TYPE_TX_PACKETS:
+ return entry.txPackets;
+ default:
+ return UNSUPPORTED;
}
}
@Override
public long getTotalStats(int type) {
- long nativeTotalStats = nativeGetTotalStat(type);
- if (nativeTotalStats == -1) {
- return nativeTotalStats;
+ final NetworkStats.Entry entry = nativeGetTotalStat();
+ final long value = getEntryValueForType(entry, type);
+ if (value == UNSUPPORTED) {
+ return UNSUPPORTED;
} else {
// Refer to comment in getIfaceStats
- return nativeTotalStats + getProviderIfaceStats(IFACE_ALL, type);
+ entry.add(getProviderIfaceStats(IFACE_ALL));
+ return getEntryValueForType(entry, type);
}
}
- private long getProviderIfaceStats(@Nullable String iface, int type) {
+ private NetworkStats.Entry getProviderIfaceStats(@Nullable String iface) {
final NetworkStats providerSnapshot = getNetworkStatsFromProviders(STATS_PER_IFACE);
final HashSet<String> limitIfaces;
if (iface == IFACE_ALL) {
@@ -1906,19 +2044,7 @@
limitIfaces = new HashSet<>();
limitIfaces.add(iface);
}
- final NetworkStats.Entry entry = providerSnapshot.getTotal(null, limitIfaces);
- switch (type) {
- case TrafficStats.TYPE_RX_BYTES:
- return entry.rxBytes;
- case TrafficStats.TYPE_RX_PACKETS:
- return entry.rxPackets;
- case TrafficStats.TYPE_TX_BYTES:
- return entry.txBytes;
- case TrafficStats.TYPE_TX_PACKETS:
- return entry.txPackets;
- default:
- return 0;
- }
+ return providerSnapshot.getTotal(null, limitIfaces);
}
/**
@@ -2088,6 +2214,7 @@
// both total usage and UID details.
final String baseIface = snapshot.getLinkProperties().getInterfaceName();
if (baseIface != null) {
+ nativeRegisterIface(baseIface);
findOrCreateNetworkIdentitySet(mActiveIfaces, baseIface).add(ident);
findOrCreateNetworkIdentitySet(mActiveUidIfaces, baseIface).add(ident);
@@ -2109,7 +2236,7 @@
.setDefaultNetwork(true)
.setOemManaged(ident.getOemManaged())
.setSubId(ident.getSubId()).build();
- final String ifaceVt = IFACE_VT + getSubIdForMobile(snapshot);
+ final String ifaceVt = IFACE_VT + getSubIdForCellularOrSatellite(snapshot);
findOrCreateNetworkIdentitySet(mActiveIfaces, ifaceVt).add(vtIdent);
findOrCreateNetworkIdentitySet(mActiveUidIfaces, ifaceVt).add(vtIdent);
}
@@ -2159,6 +2286,7 @@
// baseIface has been handled, so ignore it.
if (TextUtils.equals(baseIface, iface)) continue;
if (iface != null) {
+ nativeRegisterIface(iface);
findOrCreateNetworkIdentitySet(mActiveIfaces, iface).add(ident);
findOrCreateNetworkIdentitySet(mActiveUidIfaces, iface).add(ident);
if (isMobile) {
@@ -2177,9 +2305,15 @@
mMobileIfaces = mobileIfaces.toArray(new String[0]);
}
- private static int getSubIdForMobile(@NonNull NetworkStateSnapshot state) {
- if (!state.getNetworkCapabilities().hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
- throw new IllegalArgumentException("Mobile state need capability TRANSPORT_CELLULAR");
+ private static int getSubIdForCellularOrSatellite(@NonNull NetworkStateSnapshot state) {
+ if (!state.getNetworkCapabilities().hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
+ // Both cellular and satellite are 2 different network transport at Mobile using
+ // same telephony network specifier. So adding satellite transport to consider
+ // for, when satellite network is active at mobile.
+ && !state.getNetworkCapabilities().hasTransport(
+ NetworkCapabilities.TRANSPORT_SATELLITE)) {
+ throw new IllegalArgumentException(
+ "Mobile state need capability TRANSPORT_CELLULAR or TRANSPORT_SATELLITE");
}
final NetworkSpecifier spec = state.getNetworkCapabilities().getNetworkSpecifier();
@@ -2544,7 +2678,7 @@
@Override
protected void dump(FileDescriptor fd, PrintWriter rawWriter, String[] args) {
- if (!PermissionUtils.checkDumpPermission(mContext, TAG, rawWriter)) return;
+ if (!PermissionUtils.hasDumpPermission(mContext, TAG, rawWriter)) return;
long duration = DateUtils.DAY_IN_MILLIS;
final HashSet<String> argSet = new HashSet<String>();
@@ -2639,6 +2773,17 @@
}
}
pw.println(CONFIG_ENABLE_NETWORK_STATS_EVENT_LOGGER + ": " + mSupportEventLogger);
+ pw.print(NETSTATS_FASTDATAINPUT_TARGET_ATTEMPTS,
+ mDeps.getUseFastDataInputTargetAttempts());
+ pw.println();
+ try {
+ pw.print("FastDataInput successes", mFastDataInputSuccessesCounter.get());
+ pw.println();
+ pw.print("FastDataInput fallbacks", mFastDataInputFallbacksCounter.get());
+ pw.println();
+ } catch (IOException e) {
+ pw.println("(failed to dump FastDataInput counters)");
+ }
pw.decreaseIndent();
@@ -2753,9 +2898,9 @@
}
pw.println();
- pw.println("InterfaceMapUpdater:");
+ pw.println("InterfaceMapHelper:");
pw.increaseIndent();
- mInterfaceMapUpdater.dump(pw);
+ mInterfaceMapHelper.dump(pw);
pw.decreaseIndent();
pw.println();
@@ -2777,6 +2922,12 @@
dumpStatsMapLocked(mStatsMapB, pw, "mStatsMapB");
dumpIfaceStatsMapLocked(pw);
pw.decreaseIndent();
+
+ pw.println();
+ pw.println("SkDestroyListener logs:");
+ pw.increaseIndent();
+ mSkDestroyListener.dump(pw);
+ pw.decreaseIndent();
}
}
@@ -2896,7 +3047,7 @@
BpfDump.dumpMap(statsMap, pw, mapName,
"ifaceIndex ifaceName tag_hex uid_int cnt_set rxBytes rxPackets txBytes txPackets",
(key, value) -> {
- final String ifName = mInterfaceMapUpdater.getIfNameByIndex(key.ifaceIndex);
+ final String ifName = mInterfaceMapHelper.getIfNameByIndex(key.ifaceIndex);
return key.ifaceIndex + " "
+ (ifName != null ? ifName : "unknown") + " "
+ "0x" + Long.toHexString(key.tag) + " "
@@ -2914,7 +3065,7 @@
BpfDump.dumpMap(mIfaceStatsMap, pw, "mIfaceStatsMap",
"ifaceIndex ifaceName rxBytes rxPackets txBytes txPackets",
(key, value) -> {
- final String ifName = mInterfaceMapUpdater.getIfNameByIndex(key.val);
+ final String ifName = mInterfaceMapHelper.getIfNameByIndex(key.val);
return key.val + " "
+ (ifName != null ? ifName : "unknown") + " "
+ value.rxBytes + " "
@@ -3274,10 +3425,14 @@
}
}
- private static native long nativeGetTotalStat(int type);
- private static native long nativeGetIfaceStat(String iface, int type);
- private static native long nativeGetIfIndexStat(int ifindex, int type);
- private static native long nativeGetUidStat(int uid, int type);
+ // TODO: Read stats by using BpfNetMapsReader.
+ private static native void nativeRegisterIface(String iface);
+ @Nullable
+ private static native NetworkStats.Entry nativeGetTotalStat();
+ @Nullable
+ private static native NetworkStats.Entry nativeGetIfaceStat(String iface);
+ @Nullable
+ private static native NetworkStats.Entry nativeGetUidStat(int uid);
/** Initializes and registers the Perfetto Network Trace data source */
public static native void nativeInitNetworkTracing();
diff --git a/service-t/src/com/android/server/net/SkDestroyListener.java b/service-t/src/com/android/server/net/SkDestroyListener.java
index 7b68f89..a6cc2b5 100644
--- a/service-t/src/com/android/server/net/SkDestroyListener.java
+++ b/service-t/src/com/android/server/net/SkDestroyListener.java
@@ -30,6 +30,8 @@
import com.android.net.module.util.netlink.NetlinkMessage;
import com.android.net.module.util.netlink.StructInetDiagSockId;
+import java.io.PrintWriter;
+
/**
* Monitor socket destroy and delete entry from cookie tag bpf map.
*/
@@ -72,4 +74,11 @@
mLog.e("Failed to delete CookieTagMap entry for " + sockId.cookie + ": " + e);
}
}
+
+ /**
+ * Dump the contents of SkDestroyListener log.
+ */
+ public void dump(PrintWriter pw) {
+ mLog.reverseDump(pw);
+ }
}
diff --git a/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
new file mode 100644
index 0000000..8598ac4
--- /dev/null
+++ b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.NetworkStats;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.time.Clock;
+import java.util.HashMap;
+import java.util.Objects;
+
+/**
+ * A thread-safe cache for storing and retrieving {@link NetworkStats.Entry} objects,
+ * with an adjustable expiry duration to manage data freshness.
+ */
+class TrafficStatsRateLimitCache {
+ private final Clock mClock;
+ private final long mExpiryDurationMs;
+
+ /**
+ * Constructs a new {@link TrafficStatsRateLimitCache} with the specified expiry duration.
+ *
+ * @param clock The {@link Clock} to use for determining timestamps.
+ * @param expiryDurationMs The expiry duration in milliseconds.
+ */
+ TrafficStatsRateLimitCache(@NonNull Clock clock, long expiryDurationMs) {
+ mClock = clock;
+ mExpiryDurationMs = expiryDurationMs;
+ }
+
+ private static class TrafficStatsCacheKey {
+ @Nullable
+ public final String iface;
+ public final int uid;
+
+ TrafficStatsCacheKey(@Nullable String iface, int uid) {
+ this.iface = iface;
+ this.uid = uid;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof TrafficStatsCacheKey)) return false;
+ TrafficStatsCacheKey that = (TrafficStatsCacheKey) o;
+ return uid == that.uid && Objects.equals(iface, that.iface);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(iface, uid);
+ }
+ }
+
+ private static class TrafficStatsCacheValue {
+ public final long timestamp;
+ @NonNull
+ public final NetworkStats.Entry entry;
+
+ TrafficStatsCacheValue(long timestamp, NetworkStats.Entry entry) {
+ this.timestamp = timestamp;
+ this.entry = entry;
+ }
+ }
+
+ @GuardedBy("mMap")
+ private final HashMap<TrafficStatsCacheKey, TrafficStatsCacheValue> mMap = new HashMap<>();
+
+ /**
+ * Retrieves a {@link NetworkStats.Entry} from the cache, associated with the given key.
+ *
+ * @param iface The interface name to include in the cache key. Null if not applicable.
+ * @param uid The UID to include in the cache key. {@code UID_ALL} if not applicable.
+ * @return The cached {@link NetworkStats.Entry}, or null if not found or expired.
+ */
+ @Nullable
+ NetworkStats.Entry get(String iface, int uid) {
+ final TrafficStatsCacheKey key = new TrafficStatsCacheKey(iface, uid);
+ synchronized (mMap) { // Synchronize for thread-safety
+ final TrafficStatsCacheValue value = mMap.get(key);
+ if (value != null && !isExpired(value.timestamp)) {
+ return value.entry;
+ } else {
+ mMap.remove(key); // Remove expired entries
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Stores a {@link NetworkStats.Entry} in the cache, associated with the given key.
+ *
+ * @param iface The interface name to include in the cache key. Null if not applicable.
+ * @param uid The UID to include in the cache key. {@code UID_ALL} if not applicable.
+ * @param entry The {@link NetworkStats.Entry} to store in the cache.
+ */
+ void put(String iface, int uid, @NonNull final NetworkStats.Entry entry) {
+ Objects.requireNonNull(entry);
+ final TrafficStatsCacheKey key = new TrafficStatsCacheKey(iface, uid);
+ synchronized (mMap) { // Synchronize for thread-safety
+ mMap.put(key, new TrafficStatsCacheValue(mClock.millis(), entry));
+ }
+ }
+
+ /**
+ * Clear the cache.
+ */
+ void clear() {
+ synchronized (mMap) {
+ mMap.clear();
+ }
+ }
+
+ private boolean isExpired(long timestamp) {
+ return mClock.millis() > timestamp + mExpiryDurationMs;
+ }
+}
diff --git a/service/Android.bp b/service/Android.bp
index e2dab9e..c35c4f8 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -178,6 +179,8 @@
"unsupportedappusage",
"ServiceConnectivityResources",
"framework-statsd",
+ "framework-permission",
+ "framework-permission-s",
],
static_libs: [
// Do not add libs here if they are already included
@@ -185,7 +188,7 @@
"androidx.annotation_annotation",
"connectivity-net-module-utils-bpf",
"connectivity_native_aidl_interface-lateststable-java",
- "dnsresolver_aidl_interface-V13-java",
+ "dnsresolver_aidl_interface-V14-java",
"modules-utils-shell-command-handler",
"net-utils-device-common",
"net-utils-device-common-ip",
@@ -196,11 +199,15 @@
"PlatformProperties",
"service-connectivity-protos",
"service-connectivity-stats-protos",
+ "net-utils-multicast-forwarding-structs",
],
apex_available: [
"com.android.tethering",
],
- lint: { strict_updatability_linting: true },
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+
+ },
visibility: [
"//packages/modules/Connectivity/service-t",
"//packages/modules/Connectivity/tests:__subpackages__",
@@ -225,6 +232,7 @@
],
lint: {
strict_updatability_linting: true,
+
},
}
@@ -260,6 +268,8 @@
"framework-tethering.impl",
"framework-wifi",
"libprotobuf-java-nano",
+ "framework-permission",
+ "framework-permission-s",
],
jarjar_rules: ":connectivity-jarjar-rules",
apex_available: [
@@ -268,9 +278,6 @@
optimize: {
proguard_flags_files: ["proguard.flags"],
},
- lint: {
- strict_updatability_linting: true,
- },
}
// A special library created strictly for use by the tests as they need the
diff --git a/service/ServiceConnectivityResources/Android.bp b/service/ServiceConnectivityResources/Android.bp
index 2260596..2621256 100644
--- a/service/ServiceConnectivityResources/Android.bp
+++ b/service/ServiceConnectivityResources/Android.bp
@@ -16,6 +16,7 @@
// APK to hold all the wifi overlayable resources.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/service/ServiceConnectivityResources/OWNERS b/service/ServiceConnectivityResources/OWNERS
new file mode 100644
index 0000000..df41ff2
--- /dev/null
+++ b/service/ServiceConnectivityResources/OWNERS
@@ -0,0 +1,2 @@
+per-file res/values/config_thread.xml = file:platform/packages/modules/Connectivity:main:/thread/OWNERS
+per-file res/values/overlayable.xml = file:platform/packages/modules/Connectivity:main:/thread/OWNERS
diff --git a/service/ServiceConnectivityResources/res/values-de/strings.xml b/service/ServiceConnectivityResources/res/values-de/strings.xml
index 536ebda..f58efb0 100644
--- a/service/ServiceConnectivityResources/res/values-de/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-de/strings.xml
@@ -29,7 +29,7 @@
<string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Für Optionen tippen"</string>
<string name="mobile_no_internet" msgid="4087718456753201450">"Mobiles Netzwerk hat keinen Internetzugriff"</string>
<string name="other_networks_no_internet" msgid="5693932964749676542">"Netzwerk hat keinen Internetzugriff"</string>
- <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Auf den privaten DNS-Server kann nicht zugegriffen werden"</string>
+ <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Auf den Server des privaten DNS kann nicht zugegriffen werden"</string>
<string name="network_partial_connectivity" msgid="5549503845834993258">"Schlechte Verbindung mit <xliff:g id="NETWORK_SSID">%1$s</xliff:g>"</string>
<string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Tippen, um die Verbindung trotzdem herzustellen"</string>
<string name="network_switch_metered" msgid="5016937523571166319">"Zu <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> gewechselt"</string>
diff --git a/service/ServiceConnectivityResources/res/values/config.xml b/service/ServiceConnectivityResources/res/values/config.xml
index f30abc6..2d3647a 100644
--- a/service/ServiceConnectivityResources/res/values/config.xml
+++ b/service/ServiceConnectivityResources/res/values/config.xml
@@ -128,6 +128,13 @@
<string-array translatable="false" name="config_networkNotifySwitches">
</string-array>
+ <!-- An array of priorities of service types for services to be offloaded via
+ NsdManager#registerOffloadEngine.
+ Format is [priority int]:[service type], for example: "0:_testservice._tcp"
+ -->
+ <string-array translatable="false" name="config_nsdOffloadServicesPriority">
+ </string-array>
+
<!-- Whether to use an ongoing notification for signing in to captive portals, instead of a
notification that can be dismissed. -->
<bool name="config_ongoingSignInNotification">false</bool>
@@ -194,8 +201,11 @@
-->
</string-array>
- <!-- Regex of wired ethernet ifaces -->
- <string translatable="false" name="config_ethernet_iface_regex">eth\\d</string>
+ <!-- Regex of wired ethernet ifaces. Network interfaces that match this regex will be tracked
+ by ethernet service.
+ If set to "*", ethernet service uses "(eth|usb)\\d+" on Android V+ and eth\\d+ on
+ Android T and U. -->
+ <string translatable="false" name="config_ethernet_iface_regex">*</string>
<!-- Ignores Wi-Fi validation failures after roam.
If validation fails on a Wi-Fi network after a roam to a new BSSID,
diff --git a/service/ServiceConnectivityResources/res/values/config_thread.xml b/service/ServiceConnectivityResources/res/values/config_thread.xml
new file mode 100644
index 0000000..f7e47f5
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values/config_thread.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- These resources are around just to allow their values to be customized
+ for different hardware and product builds for Thread Network. All
+ configuration names should use the "config_thread" prefix.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Sets to {@code true} to enable Thread on the device by default. Note this is the default
+ value, the actual Thread enabled state can be changed by the {@link
+ ThreadNetworkController#setEnabled} API.
+ -->
+ <bool name="config_thread_default_enabled">true</bool>
+
+ <!-- Whether to use location APIs in the algorithm to determine country code or not.
+ If disabled, will use other sources (telephony, wifi, etc) to determine device location for
+ Thread Network regulatory purposes.
+ -->
+ <bool name="config_thread_location_use_for_country_code_enabled">true</bool>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index 4c85e8c..d9af5a3 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -31,6 +31,7 @@
<item type="integer" name="config_networkWakeupPacketMask"/>
<item type="integer" name="config_networkNotifySwitchType"/>
<item type="array" name="config_networkNotifySwitches"/>
+ <item type="array" name="config_nsdOffloadServicesPriority"/>
<item type="bool" name="config_ongoingSignInNotification"/>
<item type="bool" name="config_autoCancelNetworkNotifications"/>
<item type="bool" name="config_notifyNoInternetAsDialogWhenHighPriority"/>
@@ -43,6 +44,10 @@
<item type="string" name="config_ethernet_iface_regex"/>
<item type="integer" name="config_validationFailureAfterRoamIgnoreTimeMillis" />
<item type="integer" name="config_netstats_validate_import" />
+
+ <!-- Configuration values for ThreadNetworkService -->
+ <item type="bool" name="config_thread_default_enabled" />
+ <item type="bool" name="config_thread_location_use_for_country_code_enabled" />
</policy>
</overlayable>
</resources>
diff --git a/service/libconnectivity/Android.bp b/service/libconnectivity/Android.bp
index e063af7..3a72134 100644
--- a/service/libconnectivity/Android.bp
+++ b/service/libconnectivity/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/service/lint-baseline.xml b/service/lint-baseline.xml
index 5149e6d..3e11d52 100644
--- a/service/lint-baseline.xml
+++ b/service/lint-baseline.xml
@@ -1,5 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `getUidRule`"
+ errorLine1=" return BpfNetMapsReader.getUidRule(sUidOwnerMap, childChain, uid);"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/BpfNetMaps.java"
+ line="643"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `BpfBitmap`"
+ errorLine1=" return new BpfBitmap(BLOCKED_PORTS_MAP_PATH);"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ConnectivityNativeService.java"
+ line="61"
+ column="24"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `set`"
+ errorLine1=" mBpfBlockedPortsMap.set(port);"
+ errorLine2=" ~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ConnectivityNativeService.java"
+ line="96"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `unset`"
+ errorLine1=" mBpfBlockedPortsMap.unset(port);"
+ errorLine2=" ~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ConnectivityNativeService.java"
+ line="107"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `clear`"
+ errorLine1=" mBpfBlockedPortsMap.clear();"
+ errorLine2=" ~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ConnectivityNativeService.java"
+ line="118"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `get`"
+ errorLine1=" if (mBpfBlockedPortsMap.get(i)) portMap.add(i);"
+ errorLine2=" ~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ConnectivityNativeService.java"
+ line="131"
+ column="41"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportNetworkInterfaceForTransports`"
+ errorLine1=" batteryStats.reportNetworkInterfaceForTransports(iface, transportTypes);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="1447"
+ column="26"/>
+ </issue>
<issue
id="NewApi"
@@ -8,23 +85,562 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~">
<location
file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="1358"
+ line="1458"
column="22"/>
</issue>
<issue
id="NewApi"
+ message="Call requires API level 33 (current min is 30): `getProgramId`"
+ errorLine1=" return BpfUtils.getProgramId(attachType);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="1572"
+ column="29"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager`"
+ errorLine1=" mPolicyManager = mContext.getSystemService(NetworkPolicyManager.class);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="1740"
+ column="52"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#registerNetworkPolicyCallback`"
+ errorLine1=" mPolicyManager.registerNetworkPolicyCallback(null, mPolicyCallback);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="1753"
+ column="24"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Cast to `UidFrozenStateChangedCallback` requires API level 34 (current min is 30)"
+ errorLine1=" new UidFrozenStateChangedCallback() {"
+ errorLine2=" ^">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="1888"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 34 (current min is 30): `android.app.ActivityManager.UidFrozenStateChangedCallback`"
+ errorLine1=" new UidFrozenStateChangedCallback() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="1888"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 34 (current min is 30): `android.app.ActivityManager#registerUidFrozenStateChangedCallback`"
+ errorLine1=" activityManager.registerUidFrozenStateChangedCallback("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="1907"
+ column="29"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#isUidNetworkingBlocked`"
+ errorLine1=" return mPolicyManager.isUidNetworkingBlocked(uid, metered);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="2162"
+ column="35"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#getRestrictBackgroundStatus`"
+ errorLine1=" return mPolicyManager.getRestrictBackgroundStatus(callerUid);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="2947"
+ column="35"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetwork`"
+ errorLine1=" final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(snapshot.getNetwork());"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="2963"
+ column="81"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getLinkProperties`"
+ errorLine1=" snapshot.getLinkProperties(), snapshot.getNetworkCapabilities(),"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="2966"
+ column="34"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetworkCapabilities`"
+ errorLine1=" snapshot.getLinkProperties(), snapshot.getNetworkCapabilities(),"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="2966"
+ column="64"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetwork`"
+ errorLine1=" snapshot.getNetwork(), snapshot.getSubscriberId()));"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="2967"
+ column="34"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getSubscriberId`"
+ errorLine1=" snapshot.getNetwork(), snapshot.getSubscriberId()));"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="2967"
+ column="57"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager.NetworkPolicyCallback`"
+ errorLine1=" private final NetworkPolicyCallback mPolicyCallback = new NetworkPolicyCallback() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="3210"
+ column="63"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `dump`"
+ errorLine1=" mBpfNetMaps.dump(pw, fd, verbose);"
+ errorLine2=" ~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="4155"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.Build#isDebuggable`"
+ errorLine1=" if (!Build.isDebuggable()) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="5721"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager`"
+ errorLine1=" mContext.getSystemService(NetworkPolicyManager.class);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="6174"
+ column="44"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#getMultipathPreference`"
+ errorLine1=" networkPreference = netPolicyManager.getMultipathPreference(network);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="6179"
+ column="50"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `new android.net.UnderlyingNetworkInfo`"
+ errorLine1=" return new UnderlyingNetworkInfo(nai.networkCapabilities.getOwnerUid(),"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="6819"
+ column="16"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#isUidRestrictedOnMeteredNetworks`"
+ errorLine1=" if (mPolicyManager.isUidRestrictedOnMeteredNetworks(uid)) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="7822"
+ column="32"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.Build#isDebuggable`"
+ errorLine1=" if (Build.isDebuggable()) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="9943"
+ column="23"/>
+ </issue>
+
+ <issue
+ id="NewApi"
message="Call requires API level 31 (current min is 30): `android.app.usage.NetworkStatsManager#notifyNetworkStatus`"
errorLine1=" mStatsManager.notifyNetworkStatus(getDefaultNetworks(),"
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
<location
file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="9938"
+ line="10909"
column="27"/>
</issue>
<issue
id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(pfd);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="10962"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(pfd);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="10979"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.net.NetworkWatchlistManager`"
+ errorLine1=" NetworkWatchlistManager nwm = mContext.getSystemService(NetworkWatchlistManager.class);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="11035"
+ column="65"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkWatchlistManager#getWatchlistConfigHash`"
+ errorLine1=" return nwm.getWatchlistConfigHash();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="11041"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `getProgramId`"
+ errorLine1=" final int ret = BpfUtils.getProgramId(type);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="11180"
+ column="50"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportMobileRadioPowerState`"
+ errorLine1=" bs.reportMobileRadioPowerState(isActive, uid);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="12254"
+ column="24"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportWifiRadioPowerState`"
+ errorLine1=" bs.reportWifiRadioPowerState(isActive, uid);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="12257"
+ column="24"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `addNiceApp`"
+ errorLine1=" mBpfNetMaps.addNiceApp(uid);"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="13079"
+ column="29"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `removeNiceApp`"
+ errorLine1=" mBpfNetMaps.removeNiceApp(uid);"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="13081"
+ column="29"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `addNaughtyApp`"
+ errorLine1=" mBpfNetMaps.addNaughtyApp(uid);"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="13094"
+ column="29"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `removeNaughtyApp`"
+ errorLine1=" mBpfNetMaps.removeNaughtyApp(uid);"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="13096"
+ column="29"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.UserHandle#getUid`"
+ errorLine1=" final int uid = uh.getUid(appId);"
+ errorLine2=" ~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="13112"
+ column="32"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `setUidRule`"
+ errorLine1=" mBpfNetMaps.setUidRule(chain, uid, firewallRule);"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="13130"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `setChildChain`"
+ errorLine1=" mBpfNetMaps.setChildChain(chain, enable);"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="13195"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `isChainEnabled`"
+ errorLine1=" return mBpfNetMaps.isChainEnabled(chain);"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="13213"
+ column="28"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `replaceUidChain`"
+ errorLine1=" mBpfNetMaps.replaceUidChain(chain, uids);"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="13220"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `BpfMap`"
+ errorLine1=" mBpfDscpIpv4Policies = new BpfMap<Struct.S32, DscpPolicyValue>(IPV4_POLICY_MAP_PATH,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyTracker.java"
+ line="88"
+ column="32"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `BpfMap`"
+ errorLine1=" mBpfDscpIpv6Policies = new BpfMap<Struct.S32, DscpPolicyValue>(IPV6_POLICY_MAP_PATH,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyTracker.java"
+ line="90"
+ column="32"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `insertOrReplaceEntry`"
+ errorLine1=" mBpfDscpIpv4Policies.insertOrReplaceEntry("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyTracker.java"
+ line="183"
+ column="38"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `insertOrReplaceEntry`"
+ errorLine1=" mBpfDscpIpv6Policies.insertOrReplaceEntry("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyTracker.java"
+ line="194"
+ column="38"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `replaceEntry`"
+ errorLine1=" mBpfDscpIpv4Policies.replaceEntry(new Struct.S32(index), DscpPolicyValue.NONE);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyTracker.java"
+ line="261"
+ column="34"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `replaceEntry`"
+ errorLine1=" mBpfDscpIpv6Policies.replaceEntry(new Struct.S32(index), DscpPolicyValue.NONE);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyTracker.java"
+ line="262"
+ column="34"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
+ errorLine1=' InetAddress.parseNumericAddress("::").getAddress();'
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyValue.java"
+ line="99"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `new android.net.NetworkStateSnapshot`"
+ errorLine1=" return new NetworkStateSnapshot(network, new NetworkCapabilities(networkCapabilities),"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/NetworkAgentInfo.java"
+ line="1353"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(mFileDescriptor);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/NetworkDiagnostics.java"
+ line="570"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 30): `android.os.Build.VERSION#DEVICE_INITIAL_SDK_INT`"
+ errorLine1=" return Build.VERSION.DEVICE_INITIAL_SDK_INT;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
+ line="212"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.SystemConfigManager#getSystemPermissionUids`"
+ errorLine1=" for (final int uid : mSystemConfigManager.getSystemPermissionUids(INTERNET)) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
+ line="396"
+ column="51"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.SystemConfigManager#getSystemPermissionUids`"
+ errorLine1=" for (final int uid : mSystemConfigManager.getSystemPermissionUids(UPDATE_DEVICE_STATS)) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
+ line="404"
+ column="51"/>
+ </issue>
+
+ <issue
+ id="NewApi"
message="Call requires API level 31 (current min is 30): `android.content.pm.ApplicationInfo#isOem`"
errorLine1=" return appInfo.isVendor() || appInfo.isOem() || appInfo.isProduct();"
errorLine2=" ~~~~~">
@@ -58,441 +674,34 @@
<issue
id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#getMultipathPreference`"
- errorLine1=" networkPreference = netPolicyManager.getMultipathPreference(network);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="5498"
- column="50"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#getRestrictBackgroundStatus`"
- errorLine1=" return mPolicyManager.getRestrictBackgroundStatus(callerUid);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="2565"
- column="35"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#isUidNetworkingBlocked`"
- errorLine1=" return mPolicyManager.isUidNetworkingBlocked(uid, metered);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="1914"
- column="35"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#isUidRestrictedOnMeteredNetworks`"
- errorLine1=" if (mPolicyManager.isUidRestrictedOnMeteredNetworks(uid)) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="7094"
- column="32"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#registerNetworkPolicyCallback`"
- errorLine1=" mPolicyManager.registerNetworkPolicyCallback(null, mPolicyCallback);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="1567"
- column="24"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getLinkProperties`"
- errorLine1=" snapshot.getLinkProperties(), snapshot.getNetworkCapabilities(),"
- errorLine2=" ~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="2584"
- column="34"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetworkCapabilities`"
- errorLine1=" snapshot.getLinkProperties(), snapshot.getNetworkCapabilities(),"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="2584"
- column="64"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetwork`"
- errorLine1=" snapshot.getNetwork(), snapshot.getSubscriberId()));"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="2585"
- column="34"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetwork`"
- errorLine1=" final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(snapshot.getNetwork());"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="2581"
- column="81"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getSubscriberId`"
- errorLine1=" snapshot.getNetwork(), snapshot.getSubscriberId()));"
- errorLine2=" ~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="2585"
- column="57"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.NetworkWatchlistManager#getWatchlistConfigHash`"
- errorLine1=" return nwm.getWatchlistConfigHash();"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="10060"
- column="20"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#addPacProxyInstalledListener`"
- errorLine1=" mPacProxyManager.addPacProxyInstalledListener("
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
- line="111"
- column="26"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#setCurrentProxyScriptUrl`"
- errorLine1=" () -> mPacProxyManager.setCurrentProxyScriptUrl(proxyProperties));"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
- line="208"
- column="48"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#setCurrentProxyScriptUrl`"
- errorLine1=" mPacProxyManager.setCurrentProxyScriptUrl(proxyInfo);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
- line="252"
- column="26"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportMobileRadioPowerState`"
- errorLine1=" bs.reportMobileRadioPowerState(isActive, NO_UID);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="11006"
- column="24"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportNetworkInterfaceForTransports`"
- errorLine1=" batteryStats.reportNetworkInterfaceForTransports(iface, transportTypes);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="1347"
- column="26"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportWifiRadioPowerState`"
- errorLine1=" bs.reportWifiRadioPowerState(isActive, NO_UID);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="11009"
- column="24"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.os.Build#isDebuggable`"
- errorLine1=" if (Build.isDebuggable()) {"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="9074"
- column="23"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.os.Build#isDebuggable`"
- errorLine1=" if (!Build.isDebuggable()) {"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="5039"
- column="20"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.os.SystemConfigManager#getSystemPermissionUids`"
- errorLine1=" for (final int uid : mSystemConfigManager.getSystemPermissionUids(INTERNET)) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
- line="396"
- column="51"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.os.SystemConfigManager#getSystemPermissionUids`"
- errorLine1=" for (final int uid : mSystemConfigManager.getSystemPermissionUids(UPDATE_DEVICE_STATS)) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
- line="404"
- column="51"/>
- </issue>
-
- <issue
- id="NewApi"
message="Call requires API level 31 (current min is 30): `android.os.UserHandle#getUid`"
errorLine1=" final int uid = handle.getUid(appId);"
errorLine2=" ~~~~~~">
<location
file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
- line="1069"
+ line="1070"
column="44"/>
</issue>
<issue
id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
- errorLine1=" tcpDetails.tos = Os.getsockoptInt(fd, IPPROTO_IP, IP_TOS);"
- errorLine2=" ~~~~~~~~~~~~~">
+ message="Call requires API level 33 (current min is 30): `updateUidLockdownRule`"
+ errorLine1=" mBpfNetMaps.updateUidLockdownRule(uid, add);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
<location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
- line="285"
- column="37"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
- errorLine1=" tcpDetails.ttl = Os.getsockoptInt(fd, IPPROTO_IP, IP_TTL);"
- errorLine2=" ~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
- line="287"
- column="37"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
- errorLine1=" tcpDetails.ack = Os.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ);"
- errorLine2=" ~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
- line="265"
- column="33"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
- errorLine1=" tcpDetails.seq = Os.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ);"
- errorLine2=" ~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
- line="262"
- column="33"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.system.Os#ioctlInt`"
- errorLine1=" final int result = Os.ioctlInt(fd, SIOCINQ);"
- errorLine2=" ~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
- line="392"
- column="31"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.system.Os#ioctlInt`"
- errorLine1=" final int result = Os.ioctlInt(fd, SIOCOUTQ);"
- errorLine2=" ~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
- line="402"
- column="31"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
- errorLine1=' InetAddress.parseNumericAddress("::").getAddress();'
- errorLine2=" ~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyValue.java"
- line="99"
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
+ line="1123"
column="25"/>
</issue>
<issue
id="NewApi"
- message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
- errorLine1=' private static final InetAddress GOOGLE_DNS_4 = InetAddress.parseNumericAddress("8.8.8.8");'
- errorLine2=" ~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ClatCoordinator.java"
- line="89"
- column="65"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
- errorLine1=" IoUtils.closeQuietly(pfd);"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="9991"
- column="25"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
- errorLine1=" IoUtils.closeQuietly(pfd);"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="10008"
- column="25"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
- errorLine1=" IoUtils.closeQuietly(mFileDescriptor);"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/NetworkDiagnostics.java"
- line="481"
- column="21"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `new android.net.NetworkStateSnapshot`"
- errorLine1=" return new NetworkStateSnapshot(network, new NetworkCapabilities(networkCapabilities),"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/NetworkAgentInfo.java"
- line="1269"
- column="20"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `new android.net.UnderlyingNetworkInfo`"
- errorLine1=" return new UnderlyingNetworkInfo(nai.networkCapabilities.getOwnerUid(),"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="6123"
- column="16"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager.NetworkPolicyCallback`"
- errorLine1=" private final NetworkPolicyCallback mPolicyCallback = new NetworkPolicyCallback() {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="2827"
- column="63"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager`"
- errorLine1=" mContext.getSystemService(NetworkPolicyManager.class);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="5493"
- column="44"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager`"
- errorLine1=" mPolicyManager = mContext.getSystemService(NetworkPolicyManager.class);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="1554"
- column="52"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Class requires API level 31 (current min is 30): `android.net.NetworkWatchlistManager`"
- errorLine1=" NetworkWatchlistManager nwm = mContext.getSystemService(NetworkWatchlistManager.class);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="10054"
- column="65"/>
- </issue>
-
- <issue
- id="NewApi"
message="Class requires API level 31 (current min is 30): `android.net.PacProxyManager.PacProxyInstalledListener`"
errorLine1=" private class PacProxyInstalledListener implements PacProxyManager.PacProxyInstalledListener {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
- line="90"
+ line="92"
column="56"/>
</issue>
@@ -503,8 +712,107 @@
errorLine2=" ~~~~~~~~~~~~~~~">
<location
file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
- line="108"
+ line="111"
column="53"/>
</issue>
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#addPacProxyInstalledListener`"
+ errorLine1=" mPacProxyManager.addPacProxyInstalledListener("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
+ line="115"
+ column="30"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#setCurrentProxyScriptUrl`"
+ errorLine1=" () -> mPacProxyManager.setCurrentProxyScriptUrl(proxyProperties));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
+ line="213"
+ column="48"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#setCurrentProxyScriptUrl`"
+ errorLine1=" mPacProxyManager.setCurrentProxyScriptUrl(proxyInfo);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
+ line="259"
+ column="30"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
+ errorLine1=" tcpDetails.seq = Os.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ);"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+ line="269"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
+ errorLine1=" tcpDetails.ack = Os.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ);"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+ line="272"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
+ errorLine1=" tcpDetails.tos = Os.getsockoptInt(fd, IPPROTO_IP, IP_TOS);"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+ line="292"
+ column="37"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
+ errorLine1=" tcpDetails.ttl = Os.getsockoptInt(fd, IPPROTO_IP, IP_TTL);"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+ line="294"
+ column="37"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.system.Os#ioctlInt`"
+ errorLine1=" final int result = Os.ioctlInt(fd, SIOCINQ);"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+ line="401"
+ column="31"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.system.Os#ioctlInt`"
+ errorLine1=" final int result = Os.ioctlInt(fd, SIOCOUTQ);"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+ line="411"
+ column="31"/>
+ </issue>
+
</issues>
\ No newline at end of file
diff --git a/service/native/libs/libclat/Android.bp b/service/native/libs/libclat/Android.bp
index 5c6b123..6c1c2c4 100644
--- a/service/native/libs/libclat/Android.bp
+++ b/service/native/libs/libclat/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -38,7 +39,10 @@
cc_test {
name: "libclat_test",
defaults: ["netd_defaults"],
- test_suites: ["general-tests", "mts-tethering"],
+ test_suites: [
+ "general-tests",
+ "mts-tethering",
+ ],
test_config_template: ":net_native_test_config_template",
srcs: [
"clatutils_test.cpp",
diff --git a/service/proguard.flags b/service/proguard.flags
index cf25f05..ed9a65f 100644
--- a/service/proguard.flags
+++ b/service/proguard.flags
@@ -15,3 +15,7 @@
static final % EVENT_*;
}
+# b/313539492 Keep the onLocalNetworkInfoChanged method in classes extending Connectivity.NetworkCallback.
+-keepclassmembers class * extends **android.net.ConnectivityManager$NetworkCallback {
+ public void onLocalNetworkInfoChanged(**android.net.Network, **android.net.LocalNetworkInfo);
+}
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index ad9cfbe..a7fddd0 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -31,7 +31,6 @@
import static android.net.BpfNetMapsConstants.UID_OWNER_MAP_PATH;
import static android.net.BpfNetMapsConstants.UID_PERMISSION_MAP_PATH;
import static android.net.BpfNetMapsConstants.UID_RULES_CONFIGURATION_KEY;
-import static android.net.BpfNetMapsUtils.PRE_T;
import static android.net.BpfNetMapsUtils.getMatchByFirewallChain;
import static android.net.BpfNetMapsUtils.isFirewallAllowList;
import static android.net.BpfNetMapsUtils.matchToString;
@@ -68,6 +67,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.BackgroundThread;
+import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.BpfDump;
import com.android.net.module.util.BpfMap;
import com.android.net.module.util.IBpfMap;
@@ -95,7 +95,7 @@
*/
public class BpfNetMaps {
static {
- if (!PRE_T) {
+ if (SdkLevel.isAtLeastT()) {
System.loadLibrary("service-connectivity");
}
}
@@ -184,60 +184,67 @@
sIngressDiscardMap = ingressDiscardMap;
}
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
private static IBpfMap<S32, U32> getConfigurationMap() {
try {
return new BpfMap<>(
- CONFIGURATION_MAP_PATH, BpfMap.BPF_F_RDWR, S32.class, U32.class);
+ CONFIGURATION_MAP_PATH, S32.class, U32.class);
} catch (ErrnoException e) {
throw new IllegalStateException("Cannot open netd configuration map", e);
}
}
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
private static IBpfMap<S32, UidOwnerValue> getUidOwnerMap() {
try {
return new BpfMap<>(
- UID_OWNER_MAP_PATH, BpfMap.BPF_F_RDWR, S32.class, UidOwnerValue.class);
+ UID_OWNER_MAP_PATH, S32.class, UidOwnerValue.class);
} catch (ErrnoException e) {
throw new IllegalStateException("Cannot open uid owner map", e);
}
}
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
private static IBpfMap<S32, U8> getUidPermissionMap() {
try {
return new BpfMap<>(
- UID_PERMISSION_MAP_PATH, BpfMap.BPF_F_RDWR, S32.class, U8.class);
+ UID_PERMISSION_MAP_PATH, S32.class, U8.class);
} catch (ErrnoException e) {
throw new IllegalStateException("Cannot open uid permission map", e);
}
}
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
private static IBpfMap<CookieTagMapKey, CookieTagMapValue> getCookieTagMap() {
try {
- return new BpfMap<>(COOKIE_TAG_MAP_PATH, BpfMap.BPF_F_RDWR,
+ return new BpfMap<>(COOKIE_TAG_MAP_PATH,
CookieTagMapKey.class, CookieTagMapValue.class);
} catch (ErrnoException e) {
throw new IllegalStateException("Cannot open cookie tag map", e);
}
}
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
private static IBpfMap<S32, U8> getDataSaverEnabledMap() {
try {
return new BpfMap<>(
- DATA_SAVER_ENABLED_MAP_PATH, BpfMap.BPF_F_RDWR, S32.class, U8.class);
+ DATA_SAVER_ENABLED_MAP_PATH, S32.class, U8.class);
} catch (ErrnoException e) {
throw new IllegalStateException("Cannot open data saver enabled map", e);
}
}
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
private static IBpfMap<IngressDiscardKey, IngressDiscardValue> getIngressDiscardMap() {
try {
- return new BpfMap<>(INGRESS_DISCARD_MAP_PATH, BpfMap.BPF_F_RDWR,
+ return new BpfMap<>(INGRESS_DISCARD_MAP_PATH,
IngressDiscardKey.class, IngressDiscardValue.class);
} catch (ErrnoException e) {
throw new IllegalStateException("Cannot open ingress discard map", e);
}
}
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
private static void initBpfMaps() {
if (sConfigurationMap == null) {
sConfigurationMap = getConfigurationMap();
@@ -295,6 +302,7 @@
* 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.
*/
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
private static synchronized void ensureInitialized(final Context context) {
if (sInitialized) return;
initBpfMaps();
@@ -348,7 +356,7 @@
public BpfNetMaps(final Context context) {
this(context, null);
- if (PRE_T) throw new IllegalArgumentException("BpfNetMaps need to use netd before T");
+ if (!SdkLevel.isAtLeastT()) throw new IllegalArgumentException("BpfNetMaps need to use netd before T");
}
public BpfNetMaps(final Context context, final INetd netd) {
@@ -357,7 +365,7 @@
@VisibleForTesting
public BpfNetMaps(final Context context, final INetd netd, final Dependencies deps) {
- if (!PRE_T) {
+ if (SdkLevel.isAtLeastT()) {
ensureInitialized(context);
}
mNetd = netd;
@@ -371,7 +379,7 @@
}
private void throwIfPreT(final String msg) {
- if (PRE_T) {
+ if (!SdkLevel.isAtLeastT()) {
throw new UnsupportedOperationException(msg);
}
}
@@ -712,7 +720,7 @@
* cause of the failure.
*/
public void addUidInterfaceRules(final String ifName, final int[] uids) throws RemoteException {
- if (PRE_T) {
+ if (!SdkLevel.isAtLeastT()) {
mNetd.firewallAddUidInterfaceRules(ifName, uids);
return;
}
@@ -750,7 +758,7 @@
* cause of the failure.
*/
public void removeUidInterfaceRules(final int[] uids) throws RemoteException {
- if (PRE_T) {
+ if (!SdkLevel.isAtLeastT()) {
mNetd.firewallRemoveUidInterfaceRules(uids);
return;
}
@@ -829,7 +837,7 @@
* @throws RemoteException when netd has crashed.
*/
public void setNetPermForUids(final int permissions, final int[] uids) throws RemoteException {
- if (PRE_T) {
+ if (!SdkLevel.isAtLeastT()) {
mNetd.trafficSetNetPermForUids(permissions, uids);
return;
}
@@ -1019,7 +1027,7 @@
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public void dump(final IndentingPrintWriter pw, final FileDescriptor fd, boolean verbose)
throws IOException, ServiceSpecificException {
- if (PRE_T) {
+ if (!SdkLevel.isAtLeastT()) {
throw new ServiceSpecificException(
EOPNOTSUPP, "dumpsys connectivity trafficcontroller dump not available on pre-T"
+ " devices, use dumpsys netd trafficcontroller instead.");
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 0ec0f13..6839c22 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -38,6 +38,7 @@
import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
import static android.net.ConnectivityManager.CALLBACK_IP_CHANGED;
import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND;
import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
import static android.net.ConnectivityManager.FIREWALL_RULE_DEFAULT;
import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
@@ -65,6 +66,7 @@
import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_PARTIAL;
import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_SKIPPED;
import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_VALID;
+import static android.net.MulticastRoutingConfig.FORWARD_NONE;
import static android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL;
import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE;
import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
@@ -107,15 +109,15 @@
import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET_INGRESS;
import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET_SOCK_CREATE;
import static com.android.net.module.util.NetworkMonitorUtils.isPrivateDnsValidationRequired;
-import static com.android.net.module.util.PermissionUtils.checkAnyPermissionOf;
import static com.android.net.module.util.PermissionUtils.enforceAnyPermissionOf;
import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermission;
import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermissionOr;
+import static com.android.net.module.util.PermissionUtils.hasAnyPermissionOf;
import static com.android.server.ConnectivityStatsLog.CONNECTIVITY_STATE_SAMPLE;
-
-import static java.util.Map.Entry;
+import static com.android.server.connectivity.ConnectivityFlags.REQUEST_RESTRICTED_WIFI;
import android.Manifest;
+import android.annotation.CheckResult;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
@@ -171,6 +173,7 @@
import android.net.LocalNetworkConfig;
import android.net.LocalNetworkInfo;
import android.net.MatchAllNetworkSpecifier;
+import android.net.MulticastRoutingConfig;
import android.net.NativeNetworkConfig;
import android.net.NativeNetworkType;
import android.net.NattSocketKeepalive;
@@ -253,6 +256,7 @@
import android.stats.connectivity.ValidatedState;
import android.sysprop.NetworkProperties;
import android.system.ErrnoException;
+import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.ArrayMap;
@@ -290,6 +294,7 @@
import com.android.net.module.util.BpfUtils;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.HandlerUtils;
import com.android.net.module.util.InterfaceParams;
import com.android.net.module.util.LinkPropertiesUtils.CompareOrUpdateResult;
import com.android.net.module.util.LinkPropertiesUtils.CompareResult;
@@ -314,12 +319,12 @@
import com.android.server.connectivity.DnsManager.PrivateDnsValidationUpdate;
import com.android.server.connectivity.DscpPolicyTracker;
import com.android.server.connectivity.FullScore;
-import com.android.server.connectivity.HandlerUtils;
import com.android.server.connectivity.InvalidTagException;
import com.android.server.connectivity.KeepaliveResourceUtil;
import com.android.server.connectivity.KeepaliveTracker;
import com.android.server.connectivity.LingerMonitor;
import com.android.server.connectivity.MockableSystemProperties;
+import com.android.server.connectivity.MulticastRoutingCoordinatorService;
import com.android.server.connectivity.MultinetworkPolicyTracker;
import com.android.server.connectivity.NetworkAgentInfo;
import com.android.server.connectivity.NetworkDiagnostics;
@@ -328,11 +333,13 @@
import com.android.server.connectivity.NetworkOffer;
import com.android.server.connectivity.NetworkPreferenceList;
import com.android.server.connectivity.NetworkRanker;
+import com.android.server.connectivity.NetworkRequestStateStatsMetrics;
import com.android.server.connectivity.PermissionMonitor;
import com.android.server.connectivity.ProfileNetworkPreferenceInfo;
import com.android.server.connectivity.ProxyTracker;
import com.android.server.connectivity.QosCallbackTracker;
import com.android.server.connectivity.RoutingCoordinatorService;
+import com.android.server.connectivity.SatelliteAccessController;
import com.android.server.connectivity.UidRangeUtils;
import com.android.server.connectivity.VpnNetworkPreferenceInfo;
import com.android.server.connectivity.wear.CompanionDeviceManagerProxyService;
@@ -361,6 +368,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
@@ -369,6 +377,8 @@
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
/**
* @hide
@@ -463,6 +473,8 @@
private volatile boolean mLockdownEnabled;
+ private final boolean mRequestRestrictedWifiEnabled;
+
/**
* Stale copy of uid blocked reasons provided by NPMS. As long as they are accessed only in
* internal handler thread, they don't need a lock.
@@ -496,6 +508,7 @@
@GuardedBy("mTNSLock")
private TestNetworkService mTNS;
private final CompanionDeviceManagerProxyService mCdmps;
+ private final MulticastRoutingCoordinatorService mMulticastRoutingCoordinatorService;
private final RoutingCoordinatorService mRoutingCoordinatorService;
private final Object mTNSLock = new Object();
@@ -557,6 +570,10 @@
// See {@link ConnectivitySettingsManager#setMobileDataPreferredUids}
@VisibleForTesting
static final int PREFERENCE_ORDER_MOBILE_DATA_PREFERERRED = 30;
+ // Order of setting satellite network preference fallback when default message application
+ // with role_sms role and android.permission.SATELLITE_COMMUNICATION permission detected
+ @VisibleForTesting
+ static final int PREFERENCE_ORDER_SATELLITE_FALLBACK = 40;
// Preference order that signifies the network shouldn't be set as a default network for
// the UIDs, only give them access to it. TODO : replace this with a boolean
// in NativeUidRangeConfig
@@ -827,6 +844,11 @@
private static final int EVENT_UID_FROZEN_STATE_CHANGED = 61;
/**
+ * Event to inform the ConnectivityService handler when a uid has lost carrier privileges.
+ */
+ private static final int EVENT_UID_CARRIER_PRIVILEGES_LOST = 62;
+
+ /**
* Argument for {@link #EVENT_PROVISIONING_NOTIFICATION} to indicate that the notification
* should be shown.
*/
@@ -915,6 +937,7 @@
private final QosCallbackTracker mQosCallbackTracker;
private final NetworkNotificationManager mNotifier;
private final LingerMonitor mLingerMonitor;
+ private final SatelliteAccessController mSatelliteAccessController;
// sequence number of NetworkRequests
private int mNextNetworkRequestId = NetworkRequest.FIRST_REQUEST_ID;
@@ -941,6 +964,8 @@
private final IpConnectivityLog mMetricsLog;
+ @Nullable private final NetworkRequestStateStatsMetrics mNetworkRequestStateStatsMetrics;
+
@GuardedBy("mBandwidthRequests")
private final SparseArray<Integer> mBandwidthRequests = new SparseArray<>(10);
@@ -1263,6 +1288,14 @@
}
private final LegacyTypeTracker mLegacyTypeTracker = new LegacyTypeTracker(this);
+ @VisibleForTesting
+ void onCarrierPrivilegesLost(Integer uid, Integer subId) {
+ if (mRequestRestrictedWifiEnabled) {
+ mHandler.sendMessage(mHandler.obtainMessage(
+ EVENT_UID_CARRIER_PRIVILEGES_LOST, uid, subId));
+ }
+ }
+
final LocalPriorityDump mPriorityDumper = new LocalPriorityDump();
/**
* Helper class which parses out priority arguments and dumps sections according to their
@@ -1277,7 +1310,7 @@
LocalPriorityDump() {}
private void dumpHigh(FileDescriptor fd, PrintWriter pw) {
- if (!HandlerUtils.runWithScissors(mHandler, () -> {
+ if (!HandlerUtils.runWithScissorsForDump(mHandler, () -> {
doDump(fd, pw, new String[]{DIAG_ARG});
doDump(fd, pw, new String[]{SHORT_ARG});
}, DUMPSYS_DEFAULT_TIMEOUT_MS)) {
@@ -1286,7 +1319,7 @@
}
private void dumpNormal(FileDescriptor fd, PrintWriter pw, String[] args) {
- if (!HandlerUtils.runWithScissors(mHandler, () -> doDump(fd, pw, args),
+ if (!HandlerUtils.runWithScissorsForDump(mHandler, () -> doDump(fd, pw, args),
DUMPSYS_DEFAULT_TIMEOUT_MS)) {
pw.println("dumpNormal timeout");
}
@@ -1421,6 +1454,30 @@
return new AutomaticOnOffKeepaliveTracker(c, h);
}
+ public MulticastRoutingCoordinatorService makeMulticastRoutingCoordinatorService(
+ @NonNull Handler h) {
+ try {
+ return new MulticastRoutingCoordinatorService(h);
+ } catch (UnsupportedOperationException e) {
+ // Multicast routing is not supported by the kernel
+ Log.i(TAG, "Skipping unsupported MulticastRoutingCoordinatorService");
+ return null;
+ }
+ }
+
+ /**
+ * @see NetworkRequestStateStatsMetrics
+ */
+ public NetworkRequestStateStatsMetrics makeNetworkRequestStateStatsMetrics(
+ Context context) {
+ // We currently have network requests metric for Watch devices only
+ if (context.getPackageManager().hasSystemFeature(FEATURE_WATCH)) {
+ return new NetworkRequestStateStatsMetrics();
+ } else {
+ return null;
+ }
+ }
+
/**
* @see BatteryStatsManager
*/
@@ -1457,15 +1514,31 @@
*/
@Nullable
public CarrierPrivilegeAuthenticator makeCarrierPrivilegeAuthenticator(
- @NonNull final Context context, @NonNull final TelephonyManager tm) {
+ @NonNull final Context context,
+ @NonNull final TelephonyManager tm,
+ boolean requestRestrictedWifiEnabled,
+ @NonNull BiConsumer<Integer, Integer> listener) {
if (isAtLeastT()) {
- return new CarrierPrivilegeAuthenticator(context, tm);
+ return new CarrierPrivilegeAuthenticator(
+ context, tm, requestRestrictedWifiEnabled, listener);
} else {
return null;
}
}
/**
+ * @see SatelliteAccessController
+ */
+ @Nullable
+ public SatelliteAccessController makeSatelliteAccessController(
+ @NonNull final Context context,
+ Consumer<Set<Integer>> updateSatelliteNetworkFallbackUidCallback,
+ @NonNull final Handler connectivityServiceInternalHandler) {
+ return new SatelliteAccessController(context, updateSatelliteNetworkFallbackUidCallback,
+ connectivityServiceInternalHandler);
+ }
+
+ /**
* @see DeviceConfigUtils#isTetheringFeatureEnabled
*/
public boolean isFeatureEnabled(Context context, String name) {
@@ -1654,6 +1727,7 @@
new RequestInfoPerUidCounter(MAX_NETWORK_REQUESTS_PER_SYSTEM_UID - 1);
mMetricsLog = logger;
+ mNetworkRequestStateStatsMetrics = mDeps.makeNetworkRequestStateStatsMetrics(mContext);
final NetworkRequest defaultInternetRequest = createDefaultRequest();
mDefaultRequest = new NetworkRequestInfo(
Process.myUid(), defaultInternetRequest, null,
@@ -1727,8 +1801,20 @@
mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
mLocationPermissionChecker = mDeps.makeLocationPermissionChecker(mContext);
- mCarrierPrivilegeAuthenticator =
- mDeps.makeCarrierPrivilegeAuthenticator(mContext, mTelephonyManager);
+ mRequestRestrictedWifiEnabled = mDeps.isAtLeastU()
+ && mDeps.isFeatureEnabled(context, REQUEST_RESTRICTED_WIFI);
+ mCarrierPrivilegeAuthenticator = mDeps.makeCarrierPrivilegeAuthenticator(
+ mContext, mTelephonyManager, mRequestRestrictedWifiEnabled,
+ this::onCarrierPrivilegesLost);
+
+ if (mDeps.isAtLeastU()
+ && mDeps
+ .isFeatureNotChickenedOut(mContext, ALLOW_SATALLITE_NETWORK_FALLBACK)) {
+ mSatelliteAccessController = mDeps.makeSatelliteAccessController(
+ mContext, this::updateSatelliteNetworkPreferenceUids, mHandler);
+ } else {
+ mSatelliteAccessController = null;
+ }
// To ensure uid state is synchronized with Network Policy, register for
// NetworkPolicyManagerService events must happen prior to NetworkPolicyManagerService
@@ -1859,9 +1945,11 @@
}
mRoutingCoordinatorService = new RoutingCoordinatorService(netd);
+ mMulticastRoutingCoordinatorService =
+ mDeps.makeMulticastRoutingCoordinatorService(mHandler);
- mDestroyFrozenSockets = mDeps.isAtLeastU()
- && mDeps.isFeatureEnabled(context, KEY_DESTROY_FROZEN_SOCKETS_VERSION);
+ mDestroyFrozenSockets = mDeps.isAtLeastV() || (mDeps.isAtLeastU()
+ && mDeps.isFeatureEnabled(context, KEY_DESTROY_FROZEN_SOCKETS_VERSION));
mDelayDestroyFrozenSockets = mDeps.isAtLeastU()
&& mDeps.isFeatureEnabled(context, DELAY_DESTROY_FROZEN_SOCKETS_VERSION);
mAllowSysUiConnectivityReports = mDeps.isFeatureNotChickenedOut(
@@ -1996,6 +2084,18 @@
new Pair<>(network, proxyInfo)).sendToTarget();
}
+ /**
+ * Called when satellite network fallback uids at {@link SatelliteAccessController}
+ * cache was updated based on {@link
+ * android.app.role.OnRoleHoldersChangedListener#onRoleHoldersChanged(String, UserHandle)},
+ * to create multilayer request with preference order
+ * {@link #PREFERENCE_ORDER_SATELLITE_FALLBACK} there on.
+ *
+ */
+ private void updateSatelliteNetworkPreferenceUids(Set<Integer> satelliteNetworkFallbackUids) {
+ handleSetSatelliteNetworkPreference(satelliteNetworkFallbackUids);
+ }
+
private void handleAlwaysOnNetworkRequest(
NetworkRequest networkRequest, String settingName, boolean defaultValue) {
final boolean enable = toBool(Settings.Global.getInt(
@@ -2615,7 +2715,7 @@
Objects.requireNonNull(packageName);
Objects.requireNonNull(lp);
enforceNetworkStackOrSettingsPermission();
- if (!checkAccessPermission(-1 /* pid */, uid)) {
+ if (!hasAccessPermission(-1 /* pid */, uid)) {
return null;
}
return linkPropertiesRestrictedForCallerPermissions(lp, -1 /* callerPid */, uid);
@@ -2651,7 +2751,7 @@
Objects.requireNonNull(nc);
Objects.requireNonNull(packageName);
enforceNetworkStackOrSettingsPermission();
- if (!checkAccessPermission(-1 /* pid */, uid)) {
+ if (!hasAccessPermission(-1 /* pid */, uid)) {
return null;
}
return createWithLocationInfoSanitizedIfNecessaryWhenParceled(
@@ -2662,14 +2762,14 @@
private void redactUnderlyingNetworksForCapabilities(NetworkCapabilities nc, int pid, int uid) {
if (nc.getUnderlyingNetworks() != null
- && !checkNetworkFactoryOrSettingsPermission(pid, uid)) {
+ && !hasNetworkFactoryOrSettingsPermission(pid, uid)) {
nc.setUnderlyingNetworks(null);
}
}
private boolean canSeeAllowedUids(final int pid, final int uid, final int netOwnerUid) {
return Process.SYSTEM_UID == uid
- || checkAnyPermissionOf(mContext, pid, uid,
+ || hasAnyPermissionOf(mContext, pid, uid,
android.Manifest.permission.NETWORK_FACTORY);
}
@@ -2682,14 +2782,14 @@
// it happens for some reason (e.g. the package is uninstalled while CS is trying to
// send the callback) it would crash the system server with NPE.
final NetworkCapabilities newNc = new NetworkCapabilities(nc);
- if (!checkSettingsPermission(callerPid, callerUid)) {
+ if (!hasSettingsPermission(callerPid, callerUid)) {
newNc.setUids(null);
newNc.setSSID(null);
}
if (newNc.getNetworkSpecifier() != null) {
newNc.setNetworkSpecifier(newNc.getNetworkSpecifier().redact());
}
- if (!checkAnyPermissionOf(mContext, callerPid, callerUid,
+ if (!hasAnyPermissionOf(mContext, callerPid, callerUid,
android.Manifest.permission.NETWORK_STACK,
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)) {
newNc.setAdministratorUids(new int[0]);
@@ -2757,11 +2857,12 @@
* Returns whether the app holds local mac address permission or not (might return cached
* result if the permission was already checked before).
*/
+ @CheckResult
public boolean hasLocalMacAddressPermission() {
if (mHasLocalMacAddressPermission == null) {
// If there is no cached result, perform the check now.
- mHasLocalMacAddressPermission =
- checkLocalMacAddressPermission(mCallingPid, mCallingUid);
+ mHasLocalMacAddressPermission = ConnectivityService.this
+ .hasLocalMacAddressPermission(mCallingPid, mCallingUid);
}
return mHasLocalMacAddressPermission;
}
@@ -2770,10 +2871,12 @@
* Returns whether the app holds settings permission or not (might return cached
* result if the permission was already checked before).
*/
+ @CheckResult
public boolean hasSettingsPermission() {
if (mHasSettingsPermission == null) {
// If there is no cached result, perform the check now.
- mHasSettingsPermission = checkSettingsPermission(mCallingPid, mCallingUid);
+ mHasSettingsPermission =
+ ConnectivityService.this.hasSettingsPermission(mCallingPid, mCallingUid);
}
return mHasSettingsPermission;
}
@@ -2877,7 +2980,7 @@
return new LinkProperties(lp);
}
- if (checkSettingsPermission(callerPid, callerUid)) {
+ if (hasSettingsPermission(callerPid, callerUid)) {
return new LinkProperties(lp, true /* parcelSensitiveFields */);
}
@@ -2893,7 +2996,7 @@
int callerUid, String callerPackageName) {
// There is no need to track the effective UID of the request here. If the caller
// lacks the settings permission, the effective UID is the same as the calling ID.
- if (!checkSettingsPermission()) {
+ if (!hasSettingsPermission()) {
// Unprivileged apps can only pass in null or their own UID.
if (nc.getUids() == null) {
// If the caller passes in null, the callback will also match networks that do not
@@ -3004,26 +3107,6 @@
return false;
}
- private int getAppUid(final String app, final UserHandle user) {
- final PackageManager pm =
- mContext.createContextAsUser(user, 0 /* flags */).getPackageManager();
- final long token = Binder.clearCallingIdentity();
- try {
- return pm.getPackageUid(app, 0 /* flags */);
- } catch (PackageManager.NameNotFoundException e) {
- return -1;
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- private void verifyCallingUidAndPackage(String packageName, int callingUid) {
- final UserHandle user = UserHandle.getUserHandleForUid(callingUid);
- if (getAppUid(packageName, user) != callingUid) {
- throw new SecurityException(packageName + " does not belong to uid " + callingUid);
- }
- }
-
/**
* Ensure that a network route exists to deliver traffic to the specified
* host via the specified network interface.
@@ -3039,7 +3122,8 @@
if (disallowedBecauseSystemCaller()) {
return false;
}
- verifyCallingUidAndPackage(callingPackageName, mDeps.getCallingUid());
+ PermissionUtils.enforcePackageNameMatchesUid(
+ mContext, mDeps.getCallingUid(), callingPackageName);
enforceChangePermission(callingPackageName, callingAttributionTag);
if (mProtectedNetworks.contains(networkType)) {
enforceConnectivityRestrictedNetworksPermission(true /* checkUidsAllowedList */);
@@ -3354,6 +3438,9 @@
public static final String LOG_BPF_RC = "log_bpf_rc_force_disable";
+ public static final String ALLOW_SATALLITE_NETWORK_FALLBACK =
+ "allow_satallite_network_fallback";
+
private void enforceInternetPermission() {
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.INTERNET,
@@ -3366,7 +3453,8 @@
"ConnectivityService");
}
- private boolean checkAccessPermission(int pid, int uid) {
+ @CheckResult
+ private boolean hasAccessPermission(int pid, int uid) {
return mContext.checkPermission(android.Manifest.permission.ACCESS_NETWORK_STATE, pid, uid)
== PERMISSION_GRANTED;
}
@@ -3452,7 +3540,8 @@
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
}
- private boolean checkNetworkFactoryOrSettingsPermission(int pid, int uid) {
+ @CheckResult
+ private boolean hasNetworkFactoryOrSettingsPermission(int pid, int uid) {
return PERMISSION_GRANTED == mContext.checkPermission(
android.Manifest.permission.NETWORK_FACTORY, pid, uid)
|| PERMISSION_GRANTED == mContext.checkPermission(
@@ -3462,13 +3551,14 @@
|| UserHandle.getAppId(uid) == Process.BLUETOOTH_UID;
}
- private boolean checkSettingsPermission() {
- return PermissionUtils.checkAnyPermissionOf(mContext,
- android.Manifest.permission.NETWORK_SETTINGS,
+ @CheckResult
+ private boolean hasSettingsPermission() {
+ return hasAnyPermissionOf(mContext, android.Manifest.permission.NETWORK_SETTINGS,
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
}
- private boolean checkSettingsPermission(int pid, int uid) {
+ @CheckResult
+ private boolean hasSettingsPermission(int pid, int uid) {
return PERMISSION_GRANTED == mContext.checkPermission(
android.Manifest.permission.NETWORK_SETTINGS, pid, uid)
|| PERMISSION_GRANTED == mContext.checkPermission(
@@ -3505,33 +3595,36 @@
"ConnectivityService");
}
- private boolean checkNetworkStackPermission() {
- return PermissionUtils.checkAnyPermissionOf(mContext,
- android.Manifest.permission.NETWORK_STACK,
+ @CheckResult
+ private boolean hasNetworkStackPermission() {
+ return hasAnyPermissionOf(mContext, android.Manifest.permission.NETWORK_STACK,
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
}
- private boolean checkNetworkStackPermission(int pid, int uid) {
- return checkAnyPermissionOf(mContext, pid, uid,
- android.Manifest.permission.NETWORK_STACK,
+ @CheckResult
+ private boolean hasNetworkStackPermission(int pid, int uid) {
+ return hasAnyPermissionOf(mContext, pid, uid, android.Manifest.permission.NETWORK_STACK,
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
}
- private boolean checkSystemBarServicePermission(int pid, int uid) {
- return checkAnyPermissionOf(mContext, pid, uid,
+ @CheckResult
+ private boolean hasSystemBarServicePermission(int pid, int uid) {
+ return hasAnyPermissionOf(mContext, pid, uid,
android.Manifest.permission.STATUS_BAR_SERVICE);
}
- private boolean checkNetworkSignalStrengthWakeupPermission(int pid, int uid) {
- return checkAnyPermissionOf(mContext, pid, uid,
+ @CheckResult
+ private boolean hasNetworkSignalStrengthWakeupPermission(int pid, int uid) {
+ return hasAnyPermissionOf(mContext, pid, uid,
android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP,
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
android.Manifest.permission.NETWORK_SETTINGS);
}
- private boolean checkConnectivityRestrictedNetworksPermission(int callingUid,
+ @CheckResult
+ private boolean hasConnectivityRestrictedNetworksPermission(int callingUid,
boolean checkUidsAllowedList) {
- if (PermissionUtils.checkAnyPermissionOf(mContext,
+ if (hasAnyPermissionOf(mContext,
android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS)) {
return true;
}
@@ -3539,8 +3632,7 @@
// fallback to ConnectivityInternalPermission
// TODO: Remove this fallback check after all apps have declared
// CONNECTIVITY_USE_RESTRICTED_NETWORKS.
- if (PermissionUtils.checkAnyPermissionOf(mContext,
- android.Manifest.permission.CONNECTIVITY_INTERNAL)) {
+ if (hasAnyPermissionOf(mContext, android.Manifest.permission.CONNECTIVITY_INTERNAL)) {
return true;
}
@@ -3554,7 +3646,7 @@
private void enforceConnectivityRestrictedNetworksPermission(boolean checkUidsAllowedList) {
final int callingUid = mDeps.getCallingUid();
- if (!checkConnectivityRestrictedNetworksPermission(callingUid, checkUidsAllowedList)) {
+ if (!hasConnectivityRestrictedNetworksPermission(callingUid, checkUidsAllowedList)) {
throw new SecurityException("ConnectivityService: user " + callingUid
+ " has no permission to access restricted network.");
}
@@ -3564,7 +3656,8 @@
mContext.enforceCallingOrSelfPermission(KeepaliveTracker.PERMISSION, "ConnectivityService");
}
- private boolean checkLocalMacAddressPermission(int pid, int uid) {
+ @CheckResult
+ private boolean hasLocalMacAddressPermission(int pid, int uid) {
return PERMISSION_GRANTED == mContext.checkPermission(
Manifest.permission.LOCAL_MAC_ADDRESS, pid, uid);
}
@@ -3714,6 +3807,10 @@
updateMobileDataPreferredUids();
}
+ if (mSatelliteAccessController != null) {
+ mSatelliteAccessController.start();
+ }
+
// On T+ devices, register callback for statsd to pull NETWORK_BPF_MAP_INFO atom
if (mDeps.isAtLeastT()) {
mBpfNetMaps.setPullAtomCallback(mContext);
@@ -3858,12 +3955,13 @@
@Override
protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer,
@Nullable String[] args) {
- if (!checkDumpPermission(mContext, TAG, writer)) return;
+ if (!hasDumpPermission(mContext, TAG, writer)) return;
mPriorityDumper.dump(fd, writer, args);
}
- private boolean checkDumpPermission(Context context, String tag, PrintWriter pw) {
+ @CheckResult
+ private boolean hasDumpPermission(Context context, String tag, PrintWriter pw) {
if (context.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
!= PackageManager.PERMISSION_GRANTED) {
pw.println("Permission Denial: can't dump " + tag + " from from pid="
@@ -4034,6 +4132,10 @@
pw.increaseIndent();
mNetworkActivityTracker.dump(pw);
pw.decreaseIndent();
+
+ pw.println();
+ pw.println("Multicast routing supported: " +
+ (mMulticastRoutingCoordinatorService != null));
}
private void dumpNetworks(IndentingPrintWriter pw) {
@@ -5175,9 +5277,12 @@
private void removeLocalNetworkUpstream(@NonNull final NetworkAgentInfo localAgent,
@NonNull final NetworkAgentInfo upstream) {
try {
+ final String localNetworkInterfaceName = localAgent.linkProperties.getInterfaceName();
+ final String upstreamNetworkInterfaceName = upstream.linkProperties.getInterfaceName();
mRoutingCoordinatorService.removeInterfaceForward(
- localAgent.linkProperties.getInterfaceName(),
- upstream.linkProperties.getInterfaceName());
+ localNetworkInterfaceName,
+ upstreamNetworkInterfaceName);
+ disableMulticastRouting(localNetworkInterfaceName, upstreamNetworkInterfaceName);
} catch (RemoteException e) {
loge("Couldn't remove interface forward for "
+ localAgent.linkProperties.getInterfaceName() + " to "
@@ -5288,6 +5393,13 @@
return false;
}
+ private int getSubscriptionIdFromNetworkCaps(@NonNull final NetworkCapabilities caps) {
+ if (mCarrierPrivilegeAuthenticator != null) {
+ return mCarrierPrivilegeAuthenticator.getSubIdFromNetworkCapabilities(caps);
+ }
+ return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+ }
+
private void handleRegisterNetworkRequestWithIntent(@NonNull final Message msg) {
final NetworkRequestInfo nri = (NetworkRequestInfo) (msg.obj);
// handleRegisterNetworkRequestWithIntent() doesn't apply to multilayer requests.
@@ -5324,6 +5436,8 @@
updateSignalStrengthThresholds(network, "REGISTER", req);
}
}
+ } else if (req.isRequest() && mNetworkRequestStateStatsMetrics != null) {
+ mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(req);
}
}
@@ -5541,6 +5655,8 @@
}
if (req.isListen()) {
removeListenRequestFromNetworks(req);
+ } else if (req.isRequest() && mNetworkRequestStateStatsMetrics != null) {
+ mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(req);
}
}
nri.unlinkDeathRecipient();
@@ -5669,7 +5785,7 @@
}
private RequestInfoPerUidCounter getRequestCounter(NetworkRequestInfo nri) {
- return checkAnyPermissionOf(mContext,
+ return hasAnyPermissionOf(mContext,
nri.mPid, nri.mUid, NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
? mSystemNetworkRequestCounter : mNetworkRequestCounter;
}
@@ -5893,7 +6009,7 @@
if (nm == null) return;
if (request == CaptivePortal.APP_REQUEST_REEVALUATION_REQUIRED) {
- checkNetworkStackPermission();
+ enforceNetworkStackPermission(mContext);
nm.forceReevaluation(mDeps.getCallingUid());
}
}
@@ -5923,7 +6039,7 @@
* @see MultinetworkPolicyTracker#getAvoidBadWifi()
*/
public boolean shouldAvoidBadWifi() {
- if (!checkNetworkStackPermission()) {
+ if (!hasNetworkStackPermission()) {
throw new SecurityException("avoidBadWifi requires NETWORK_STACK permission");
}
return avoidBadWifi();
@@ -6292,10 +6408,8 @@
if (!networkFound) return;
if (underpinnedNetworkFound) {
- final NetworkCapabilities underpinnedNc =
- getNetworkCapabilitiesInternal(underpinnedNetwork);
mKeepaliveTracker.handleMonitorAutomaticKeepalive(ki,
- underpinnedNetwork.netId, underpinnedNc.getUids());
+ underpinnedNetwork.netId);
} else {
// If no underpinned network, then make sure the keepalive is running.
mKeepaliveTracker.handleMaybeResumeKeepalive(ki);
@@ -6376,6 +6490,9 @@
UidFrozenStateChangedArgs args = (UidFrozenStateChangedArgs) msg.obj;
handleFrozenUids(args.mUids, args.mFrozenStates);
break;
+ case EVENT_UID_CARRIER_PRIVILEGES_LOST:
+ handleUidCarrierPrivilegesLost(msg.arg1, msg.arg2);
+ break;
}
}
}
@@ -7445,20 +7562,25 @@
// specific SSID/SignalStrength, or the calling app has permission to do so.
private void ensureSufficientPermissionsForRequest(NetworkCapabilities nc,
int callerPid, int callerUid, String callerPackageName) {
- if (null != nc.getSsid() && !checkSettingsPermission(callerPid, callerUid)) {
+ if (null != nc.getSsid() && !hasSettingsPermission(callerPid, callerUid)) {
throw new SecurityException("Insufficient permissions to request a specific SSID");
}
if (nc.hasSignalStrength()
- && !checkNetworkSignalStrengthWakeupPermission(callerPid, callerUid)) {
+ && !hasNetworkSignalStrengthWakeupPermission(callerPid, callerUid)) {
throw new SecurityException(
"Insufficient permissions to request a specific signal strength");
}
mAppOpsManager.checkPackage(callerUid, callerPackageName);
- if (!nc.getSubscriptionIds().isEmpty()) {
- enforceNetworkFactoryPermission();
+ if (nc.getSubscriptionIds().isEmpty()) {
+ return;
}
+ if (mRequestRestrictedWifiEnabled
+ && canRequestRestrictedNetworkDueToCarrierPrivileges(nc, callerUid)) {
+ return;
+ }
+ enforceNetworkFactoryPermission();
}
private int[] getSignalStrengthThresholds(@NonNull final NetworkAgentInfo nai) {
@@ -7548,7 +7670,7 @@
int reqTypeInt, Messenger messenger, int timeoutMs, final IBinder binder,
int legacyType, int callbackFlags, @NonNull String callingPackageName,
@Nullable String callingAttributionTag) {
- if (legacyType != TYPE_NONE && !checkNetworkStackPermission()) {
+ if (legacyType != TYPE_NONE && !hasNetworkStackPermission()) {
if (isTargetSdkAtleast(Build.VERSION_CODES.M, mDeps.getCallingUid(),
callingPackageName)) {
throw new SecurityException("Insufficient permissions to specify legacy type");
@@ -7738,6 +7860,22 @@
applicationNetworkCapabilities.enforceSelfCertifiedNetworkCapabilitiesDeclared(
networkCapabilities);
}
+
+ private boolean canRequestRestrictedNetworkDueToCarrierPrivileges(
+ NetworkCapabilities networkCapabilities, int callingUid) {
+ if (mRequestRestrictedWifiEnabled) {
+ // For U+ devices, callers with carrier privilege could request restricted networks
+ // with CBS capabilities, or any restricted WiFi networks.
+ return ((networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)
+ || networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI))
+ && hasCarrierPrivilegeForNetworkCaps(callingUid, networkCapabilities));
+ } else {
+ // For T+ devices, callers with carrier privilege could request with CBS
+ // capabilities.
+ return (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)
+ && hasCarrierPrivilegeForNetworkCaps(callingUid, networkCapabilities));
+ }
+ }
private void enforceNetworkRequestPermissions(NetworkCapabilities networkCapabilities,
String callingPackageName, String callingAttributionTag, final int callingUid) {
if (shouldCheckCapabilitiesDeclaration(networkCapabilities, callingUid,
@@ -7745,13 +7883,11 @@
enforceRequestCapabilitiesDeclaration(callingPackageName, networkCapabilities,
callingUid);
}
- if (networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED) == false) {
- // For T+ devices, callers with carrier privilege could request with CBS capabilities.
- if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)
- && hasCarrierPrivilegeForNetworkCaps(callingUid, networkCapabilities)) {
- return;
+ if (!networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) {
+ if (!canRequestRestrictedNetworkDueToCarrierPrivileges(
+ networkCapabilities, callingUid)) {
+ enforceConnectivityRestrictedNetworksPermission(true /* checkUidsAllowedList */);
}
- enforceConnectivityRestrictedNetworksPermission(true /* checkUidsAllowedList */);
} else {
enforceChangePermission(callingPackageName, callingAttributionTag);
}
@@ -9018,6 +9154,40 @@
}
}
+ private void handleUidCarrierPrivilegesLost(int uid, int subId) {
+ ensureRunningOnConnectivityServiceThread();
+ // A NetworkRequest needs to be revoked when all the conditions are met
+ // 1. It requests restricted network
+ // 2. The requestor uid matches the uid with the callback
+ // 3. The app doesn't have Carrier Privileges
+ // 4. The app doesn't have permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS
+ for (final NetworkRequest nr : mNetworkRequests.keySet()) {
+ if ((nr.isRequest() || nr.isListen())
+ && !nr.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)
+ && nr.getRequestorUid() == uid
+ && getSubscriptionIdFromNetworkCaps(nr.networkCapabilities) == subId
+ && !hasConnectivityRestrictedNetworksPermission(uid, true)) {
+ declareNetworkRequestUnfulfillable(nr);
+ }
+ }
+
+ // A NetworkAgent's allowedUids may need to be updated if the app has lost
+ // carrier config
+ for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
+ if (nai.networkCapabilities.getAllowedUidsNoCopy().contains(uid)
+ && getSubscriptionIdFromNetworkCaps(nai.networkCapabilities) == subId) {
+ final NetworkCapabilities nc = new NetworkCapabilities(nai.networkCapabilities);
+ NetworkAgentInfo.restrictCapabilitiesFromNetworkAgent(
+ nc,
+ uid,
+ false /* hasAutomotiveFeature (irrelevant) */,
+ mDeps,
+ mCarrierPrivilegeAuthenticator);
+ updateCapabilities(nai.getScore(), nai, nc);
+ }
+ }
+ }
+
/**
* Update the NetworkCapabilities for {@code nai} to {@code nc}. Specifically:
*
@@ -9095,6 +9265,71 @@
updateCapabilities(nai.getScore(), nai, nai.networkCapabilities);
}
+ private void maybeApplyMulticastRoutingConfig(@NonNull final NetworkAgentInfo nai,
+ final LocalNetworkConfig oldConfig,
+ final LocalNetworkConfig newConfig) {
+ final MulticastRoutingConfig oldUpstreamConfig =
+ oldConfig == null ? MulticastRoutingConfig.CONFIG_FORWARD_NONE :
+ oldConfig.getUpstreamMulticastRoutingConfig();
+ final MulticastRoutingConfig oldDownstreamConfig =
+ oldConfig == null ? MulticastRoutingConfig.CONFIG_FORWARD_NONE :
+ oldConfig.getDownstreamMulticastRoutingConfig();
+ final MulticastRoutingConfig newUpstreamConfig =
+ newConfig == null ? MulticastRoutingConfig.CONFIG_FORWARD_NONE :
+ newConfig.getUpstreamMulticastRoutingConfig();
+ final MulticastRoutingConfig newDownstreamConfig =
+ newConfig == null ? MulticastRoutingConfig.CONFIG_FORWARD_NONE :
+ newConfig.getDownstreamMulticastRoutingConfig();
+
+ if (oldUpstreamConfig.equals(newUpstreamConfig) &&
+ oldDownstreamConfig.equals(newDownstreamConfig)) {
+ return;
+ }
+
+ final String downstreamNetworkName = nai.linkProperties.getInterfaceName();
+ final LocalNetworkInfo lni = localNetworkInfoForNai(nai);
+ final Network upstreamNetwork = lni.getUpstreamNetwork();
+
+ if (upstreamNetwork != null) {
+ final String upstreamNetworkName =
+ getLinkProperties(upstreamNetwork).getInterfaceName();
+ applyMulticastRoutingConfig(downstreamNetworkName, upstreamNetworkName, newConfig);
+ }
+ }
+
+ private void applyMulticastRoutingConfig(@NonNull String localNetworkInterfaceName,
+ @NonNull String upstreamNetworkInterfaceName,
+ @NonNull final LocalNetworkConfig config) {
+ if (mMulticastRoutingCoordinatorService == null) {
+ if (config.getDownstreamMulticastRoutingConfig().getForwardingMode() != FORWARD_NONE ||
+ config.getUpstreamMulticastRoutingConfig().getForwardingMode() != FORWARD_NONE) {
+ loge("Multicast routing is not supported, failed to configure " + config
+ + " for " + localNetworkInterfaceName + " to "
+ + upstreamNetworkInterfaceName);
+ }
+ return;
+ }
+
+ mMulticastRoutingCoordinatorService.applyMulticastRoutingConfig(localNetworkInterfaceName,
+ upstreamNetworkInterfaceName, config.getUpstreamMulticastRoutingConfig());
+ mMulticastRoutingCoordinatorService.applyMulticastRoutingConfig
+ (upstreamNetworkInterfaceName, localNetworkInterfaceName,
+ config.getDownstreamMulticastRoutingConfig());
+ }
+
+ private void disableMulticastRouting(@NonNull String localNetworkInterfaceName,
+ @NonNull String upstreamNetworkInterfaceName) {
+ if (mMulticastRoutingCoordinatorService == null) {
+ return;
+ }
+
+ mMulticastRoutingCoordinatorService.applyMulticastRoutingConfig(localNetworkInterfaceName,
+ upstreamNetworkInterfaceName, MulticastRoutingConfig.CONFIG_FORWARD_NONE);
+ mMulticastRoutingCoordinatorService.applyMulticastRoutingConfig
+ (upstreamNetworkInterfaceName, localNetworkInterfaceName,
+ MulticastRoutingConfig.CONFIG_FORWARD_NONE);
+ }
+
// oldConfig is null iff this is the original registration of the local network config
private void handleUpdateLocalNetworkConfig(@NonNull final NetworkAgentInfo nai,
@Nullable final LocalNetworkConfig oldConfig,
@@ -9108,7 +9343,6 @@
Log.v(TAG, "Update local network config " + nai.network.netId + " : " + newConfig);
}
final LocalNetworkConfig.Builder configBuilder = new LocalNetworkConfig.Builder();
- // TODO : apply the diff for multicast routing.
configBuilder.setUpstreamMulticastRoutingConfig(
newConfig.getUpstreamMulticastRoutingConfig());
configBuilder.setDownstreamMulticastRoutingConfig(
@@ -9167,6 +9401,7 @@
configBuilder.setUpstreamSelector(oldRequest);
nai.localNetworkConfig = configBuilder.build();
}
+ maybeApplyMulticastRoutingConfig(nai, oldConfig, newConfig);
}
/**
@@ -9400,7 +9635,6 @@
final ArraySet<Integer> toAdd = new ArraySet<>(newUids);
toRemove.removeAll(newUids);
toAdd.removeAll(prevUids);
-
try {
if (!toAdd.isEmpty()) {
mNetd.networkAddUidRangesParcel(new NativeUidRangeConfig(
@@ -10166,6 +10400,8 @@
if (null != change.mOldNetwork) {
mRoutingCoordinatorService.removeInterfaceForward(fromIface,
change.mOldNetwork.linkProperties.getInterfaceName());
+ disableMulticastRouting(fromIface,
+ change.mOldNetwork.linkProperties.getInterfaceName());
}
// If the new upstream is already destroyed, there is no point in setting up
// a forward (in fact, it might forward to the interface for some new network !)
@@ -10174,6 +10410,9 @@
if (null != change.mNewNetwork && !change.mNewNetwork.isDestroyed()) {
mRoutingCoordinatorService.addInterfaceForward(fromIface,
change.mNewNetwork.linkProperties.getInterfaceName());
+ applyMulticastRoutingConfig(fromIface,
+ change.mNewNetwork.linkProperties.getInterfaceName(),
+ nai.localNetworkConfig);
}
} catch (final RemoteException e) {
loge("Can't update forwarding rules", e);
@@ -11041,17 +11280,28 @@
err.getFileDescriptor(), args);
}
- private Boolean parseBooleanArgument(final String arg) {
- if ("true".equals(arg)) {
- return true;
- } else if ("false".equals(arg)) {
- return false;
- } else {
- return null;
- }
- }
-
private class ShellCmd extends BasicShellCommandHandler {
+
+ private Boolean parseBooleanArgument(final String arg) {
+ if ("true".equals(arg)) {
+ return true;
+ } else if ("false".equals(arg)) {
+ return false;
+ } else {
+ getOutPrintWriter().println("Invalid boolean argument: " + arg);
+ return null;
+ }
+ }
+
+ private Integer parseIntegerArgument(final String arg) {
+ try {
+ return Integer.valueOf(arg);
+ } catch (NumberFormatException ne) {
+ getOutPrintWriter().println("Invalid integer argument: " + arg);
+ return null;
+ }
+ }
+
@Override
public int onCommand(String cmd) {
if (cmd == null) {
@@ -11128,6 +11378,38 @@
}
return 0;
}
+ case "set-background-networking-enabled-for-uid": {
+ final Integer uid = parseIntegerArgument(getNextArg());
+ final Boolean enabled = parseBooleanArgument(getNextArg());
+ if (null == enabled || null == uid) {
+ onHelp();
+ return -1;
+ }
+ final int rule = enabled ? FIREWALL_RULE_ALLOW : FIREWALL_RULE_DEFAULT;
+ setUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, uid, rule);
+ final String msg = (enabled ? "Enabled" : "Disabled")
+ + " background networking for uid " + uid;
+ Log.i(TAG, msg);
+ pw.println(msg);
+ return 0;
+ }
+ case "get-background-networking-enabled-for-uid": {
+ final Integer uid = parseIntegerArgument(getNextArg());
+ if (null == uid) {
+ onHelp();
+ return -1;
+ }
+ final int rule = getUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, uid);
+ if (FIREWALL_RULE_ALLOW == rule) {
+ pw.println(uid + ": allow");
+ } else if (FIREWALL_RULE_DENY == rule || FIREWALL_RULE_DEFAULT == rule) {
+ pw.println(uid + ": deny");
+ } else {
+ throw new IllegalStateException(
+ "Unknown rule " + rule + " for uid " + uid);
+ }
+ return 0;
+ }
case "reevaluate":
// Usage : adb shell cmd connectivity reevaluate <netId>
// If netId is omitted, then reevaluate the default network
@@ -11188,6 +11470,10 @@
+ " no effect if the chain is disabled.");
pw.println(" get-package-networking-enabled [package name]");
pw.println(" Get the deny bit in FIREWALL_CHAIN_OEM_DENY_3 for package.");
+ pw.println(" set-background-networking-enabled-for-uid [uid] [true|false]");
+ pw.println(" Set the allow bit in FIREWALL_CHAIN_BACKGROUND for the given uid.");
+ pw.println(" get-background-networking-enabled-for-uid [uid]");
+ pw.println(" Get the allow bit in FIREWALL_CHAIN_BACKGROUND for the given uid.");
}
}
@@ -11228,7 +11514,7 @@
// Connection owner UIDs are visible only to the network stack and to the VpnService-based
// VPN, if any, that applies to the UID that owns the connection.
- if (checkNetworkStackPermission()) return uid;
+ if (hasNetworkStackPermission()) return uid;
final NetworkAgentInfo vpn = getVpnForUid(uid);
if (vpn == null || getVpnType(vpn) != VpnManager.TYPE_VPN_SERVICE
@@ -11488,7 +11774,7 @@
if (report == null) {
continue;
}
- if (!checkConnectivityDiagnosticsPermissions(
+ if (!hasConnectivityDiagnosticsPermissions(
nri.mPid, nri.mUid, nai, cbInfo.mCallingPackageName)) {
continue;
}
@@ -11651,7 +11937,7 @@
continue;
}
- if (!checkConnectivityDiagnosticsPermissions(
+ if (!hasConnectivityDiagnosticsPermissions(
nri.mPid, nri.mUid, nai, cbInfo.mCallingPackageName)) {
continue;
}
@@ -11695,14 +11981,15 @@
return false;
}
+ @CheckResult
@VisibleForTesting
- boolean checkConnectivityDiagnosticsPermissions(
+ boolean hasConnectivityDiagnosticsPermissions(
int callbackPid, int callbackUid, NetworkAgentInfo nai, String callbackPackageName) {
- if (checkNetworkStackPermission(callbackPid, callbackUid)) {
+ if (hasNetworkStackPermission(callbackPid, callbackUid)) {
return true;
}
if (mAllowSysUiConnectivityReports
- && checkSystemBarServicePermission(callbackPid, callbackUid)) {
+ && hasSystemBarServicePermission(callbackPid, callbackUid)) {
return true;
}
@@ -12568,16 +12855,27 @@
@VisibleForTesting
@NonNull
- ArraySet<NetworkRequestInfo> createNrisFromMobileDataPreferredUids(
- @NonNull final Set<Integer> uids) {
+ ArraySet<NetworkRequestInfo> createNrisForPreferenceOrder(@NonNull final Set<Integer> uids,
+ @NonNull final List<NetworkRequest> requests,
+ final int preferenceOrder) {
final ArraySet<NetworkRequestInfo> nris = new ArraySet<>();
if (uids.size() == 0) {
// Should not create NetworkRequestInfo if no preferences. Without uid range in
// NetworkRequestInfo, makeDefaultForApps() would treat it as a illegal NRI.
- if (DBG) log("Don't create NetworkRequestInfo because no preferences");
return nris;
}
+ final Set<UidRange> ranges = new ArraySet<>();
+ for (final int uid : uids) {
+ ranges.add(new UidRange(uid, uid));
+ }
+ setNetworkRequestUids(requests, ranges);
+ nris.add(new NetworkRequestInfo(Process.myUid(), requests, preferenceOrder));
+ return nris;
+ }
+
+ ArraySet<NetworkRequestInfo> createNrisFromMobileDataPreferredUids(
+ @NonNull final Set<Integer> uids) {
final List<NetworkRequest> requests = new ArrayList<>();
// The NRI should be comprised of two layers:
// - The request for the mobile network preferred.
@@ -12586,14 +12884,28 @@
TRANSPORT_CELLULAR, NetworkRequest.Type.REQUEST));
requests.add(createDefaultInternetRequestForTransport(
TYPE_NONE, NetworkRequest.Type.TRACK_DEFAULT));
- final Set<UidRange> ranges = new ArraySet<>();
- for (final int uid : uids) {
- ranges.add(new UidRange(uid, uid));
- }
- setNetworkRequestUids(requests, ranges);
- nris.add(new NetworkRequestInfo(Process.myUid(), requests,
- PREFERENCE_ORDER_MOBILE_DATA_PREFERERRED));
- return nris;
+ return createNrisForPreferenceOrder(uids, requests, PREFERENCE_ORDER_MOBILE_DATA_PREFERERRED
+ );
+ }
+
+ ArraySet<NetworkRequestInfo> createMultiLayerNrisFromSatelliteNetworkFallbackUids(
+ @NonNull final Set<Integer> uids) {
+ final List<NetworkRequest> requests = new ArrayList<>();
+
+ // request: track default(unrestricted internet network)
+ requests.add(createDefaultInternetRequestForTransport(
+ TYPE_NONE, NetworkRequest.Type.TRACK_DEFAULT));
+
+ // request: restricted Satellite internet
+ final NetworkCapabilities cap = new NetworkCapabilities.Builder()
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+ .addTransportType(NetworkCapabilities.TRANSPORT_SATELLITE)
+ .build();
+ requests.add(createNetworkRequest(NetworkRequest.Type.REQUEST, cap));
+
+ return createNrisForPreferenceOrder(uids, requests, PREFERENCE_ORDER_SATELLITE_FALLBACK);
}
private void handleMobileDataPreferredUidsChanged() {
@@ -12605,6 +12917,16 @@
rematchAllNetworksAndRequests();
}
+ private void handleSetSatelliteNetworkPreference(
+ @NonNull final Set<Integer> satelliteNetworkPreferredUids) {
+ removeDefaultNetworkRequestsForPreference(PREFERENCE_ORDER_SATELLITE_FALLBACK);
+ addPerAppDefaultNetworkRequests(
+ createMultiLayerNrisFromSatelliteNetworkFallbackUids(satelliteNetworkPreferredUids)
+ );
+ // Finally, rematch.
+ rematchAllNetworksAndRequests();
+ }
+
private void handleIngressRateLimitChanged() {
final long oldIngressRateLimit = mIngressRateLimit;
mIngressRateLimit = ConnectivitySettingsManager.getIngressRateLimitInBytesPerSecond(
diff --git a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
index 8036ae9..31108fc 100644
--- a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
+++ b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
@@ -25,9 +25,6 @@
import static android.system.OsConstants.SOL_SOCKET;
import static android.system.OsConstants.SO_SNDTIMEO;
-import static com.android.net.module.util.netlink.NetlinkConstants.NLMSG_DONE;
-import static com.android.net.module.util.netlink.NetlinkConstants.SOCKDIAG_MSG_HEADER_SIZE;
-import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DIAG_BY_FAMILY;
import static com.android.net.module.util.netlink.NetlinkUtils.IO_TIMEOUT_MS;
import android.annotation.IntDef;
@@ -53,7 +50,6 @@
import android.util.LocalLog;
import android.util.Log;
import android.util.Pair;
-import android.util.Range;
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
@@ -78,7 +74,6 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
-import java.util.Set;
/**
* Manages automatic on/off socket keepalive requests.
@@ -90,6 +85,7 @@
*/
public class AutomaticOnOffKeepaliveTracker {
private static final String TAG = "AutomaticOnOffKeepaliveTracker";
+ private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
private static final int[] ADDRESS_FAMILIES = new int[] {AF_INET6, AF_INET};
private static final long LOW_TCP_POLLING_INTERVAL_MS = 1_000L;
private static final int ADJUST_TCP_POLLING_DELAY_MS = 2000;
@@ -375,27 +371,26 @@
* Determine if any state transition is needed for the specific automatic keepalive.
*/
public void handleMonitorAutomaticKeepalive(@NonNull final AutomaticOnOffKeepalive ki,
- final int vpnNetId, @NonNull Set<Range<Integer>> vpnUidRanges) {
+ final int vpnNetId) {
// Might happen if the automatic keepalive was removed by the app just as the alarm fires.
if (!mAutomaticOnOffKeepalives.contains(ki)) return;
if (STATE_ALWAYS_ON == ki.mAutomaticOnOffState) {
throw new IllegalStateException("Should not monitor non-auto keepalive");
}
- handleMonitorTcpConnections(ki, vpnNetId, vpnUidRanges);
+ handleMonitorTcpConnections(ki, vpnNetId);
}
/**
* Determine if disable or re-enable keepalive is needed or not based on TCP sockets status.
*/
- private void handleMonitorTcpConnections(@NonNull AutomaticOnOffKeepalive ki, int vpnNetId,
- @NonNull Set<Range<Integer>> vpnUidRanges) {
+ private void handleMonitorTcpConnections(@NonNull AutomaticOnOffKeepalive ki, int vpnNetId) {
// Might happen if the automatic keepalive was removed by the app just as the alarm fires.
if (!mAutomaticOnOffKeepalives.contains(ki)) return;
if (STATE_ALWAYS_ON == ki.mAutomaticOnOffState) {
throw new IllegalStateException("Should not monitor non-auto keepalive");
}
- if (!isAnyTcpSocketConnected(vpnNetId, vpnUidRanges)) {
+ if (!isAnyTcpSocketConnected(vpnNetId)) {
// No TCP socket exists. Stop keepalive if ENABLED, and remain SUSPENDED if currently
// SUSPENDED.
if (ki.mAutomaticOnOffState == STATE_ENABLED) {
@@ -747,7 +742,7 @@
}
@VisibleForTesting
- boolean isAnyTcpSocketConnected(int netId, @NonNull Set<Range<Integer>> vpnUidRanges) {
+ boolean isAnyTcpSocketConnected(int netId) {
FileDescriptor fd = null;
try {
@@ -760,8 +755,7 @@
// Send request for each IP family
for (final int family : ADDRESS_FAMILIES) {
- if (isAnyTcpSocketConnectedForFamily(
- fd, family, networkMark, networkMask, vpnUidRanges)) {
+ if (isAnyTcpSocketConnectedForFamily(fd, family, networkMark, networkMask)) {
return true;
}
}
@@ -775,7 +769,7 @@
}
private boolean isAnyTcpSocketConnectedForFamily(FileDescriptor fd, int family, int networkMark,
- int networkMask, @NonNull Set<Range<Integer>> vpnUidRanges)
+ int networkMask)
throws ErrnoException, InterruptedIOException {
ensureRunningOnHandlerThread();
// Build SocketDiag messages and cache it.
@@ -794,22 +788,18 @@
try {
while (NetlinkUtils.enoughBytesRemainForValidNlMsg(bytes)) {
- final int startPos = bytes.position();
+ // NetlinkMessage.parse() will move the byte buffer position.
+ // TODO: Parse dst address information to filter socket.
+ final NetlinkMessage nlMsg = NetlinkMessage.parse(
+ bytes, OsConstants.NETLINK_INET_DIAG);
+ if (!(nlMsg instanceof InetDiagMessage)) {
+ if (DBG) Log.e(TAG, "Not a SOCK_DIAG_BY_FAMILY msg");
+ return false;
+ }
- final int nlmsgLen = bytes.getInt();
- final int nlmsgType = bytes.getShort();
- if (isEndOfMessageOrError(nlmsgType)) return false;
- // TODO: Parse InetDiagMessage to get uid and dst address information to filter
- // socket via NetlinkMessage.parse.
-
- // Skip the header to move to data part.
- bytes.position(startPos + SOCKDIAG_MSG_HEADER_SIZE);
-
- if (isTargetTcpSocket(bytes, nlmsgLen, networkMark, networkMask)) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- bytes.position(startPos);
- final InetDiagMessage diagMsg = (InetDiagMessage) NetlinkMessage.parse(
- bytes, OsConstants.NETLINK_INET_DIAG);
+ final InetDiagMessage diagMsg = (InetDiagMessage) nlMsg;
+ if (isTargetTcpSocket(diagMsg, networkMark, networkMask)) {
+ if (DBG) {
Log.d(TAG, String.format("Found open TCP connection by uid %d to %s"
+ " cookie %d",
diagMsg.inetDiagMsg.idiag_uid,
@@ -834,26 +824,20 @@
return false;
}
- private boolean isEndOfMessageOrError(int nlmsgType) {
- return nlmsgType == NLMSG_DONE || nlmsgType != SOCK_DIAG_BY_FAMILY;
- }
-
- private boolean isTargetTcpSocket(@NonNull ByteBuffer bytes, int nlmsgLen, int networkMark,
- int networkMask) {
- final int mark = readSocketDataAndReturnMark(bytes, nlmsgLen);
+ private boolean isTargetTcpSocket(@NonNull InetDiagMessage diagMsg,
+ int networkMark, int networkMask) {
+ final int mark = readSocketDataAndReturnMark(diagMsg);
return (mark & networkMask) == networkMark;
}
- private int readSocketDataAndReturnMark(@NonNull ByteBuffer bytes, int nlmsgLen) {
- final int nextMsgOffset = bytes.position() + nlmsgLen - SOCKDIAG_MSG_HEADER_SIZE;
+ private int readSocketDataAndReturnMark(@NonNull InetDiagMessage diagMsg) {
int mark = NetlinkUtils.INIT_MARK_VALUE;
// Get socket mark
- // TODO: Add a parsing method in NetlinkMessage.parse to support this to skip the remaining
- // data.
- while (bytes.position() < nextMsgOffset) {
- final StructNlAttr nlattr = StructNlAttr.parse(bytes);
- if (nlattr != null && nlattr.nla_type == NetlinkUtils.INET_DIAG_MARK) {
- mark = nlattr.getValueAsInteger();
+ for (StructNlAttr attr : diagMsg.nlAttrs) {
+ if (attr.nla_type == NetlinkUtils.INET_DIAG_MARK) {
+ // The netlink attributes should contain only one INET_DIAG_MARK for each socket.
+ mark = attr.getValueAsInteger();
+ break;
}
}
return mark;
diff --git a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
index 5705ebe..04d0fc1 100644
--- a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
+++ b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
@@ -40,12 +40,13 @@
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.util.Log;
-import android.util.SparseIntArray;
+import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.modules.utils.HandlerExecutor;
+import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.DeviceConfigUtils;
import com.android.networkstack.apishim.TelephonyManagerShimImpl;
import com.android.networkstack.apishim.common.TelephonyManagerShim;
@@ -55,6 +56,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
+import java.util.function.BiConsumer;
/**
* Tracks the uid of the carrier privileged app that provides the carrier config.
@@ -71,7 +73,8 @@
private final TelephonyManagerShim mTelephonyManagerShim;
private final TelephonyManager mTelephonyManager;
@GuardedBy("mLock")
- private final SparseIntArray mCarrierServiceUid = new SparseIntArray(2 /* initialCapacity */);
+ private final SparseArray<CarrierServiceUidWithSubId> mCarrierServiceUidWithSubId =
+ new SparseArray<>(2 /* initialCapacity */);
@GuardedBy("mLock")
private int mModemCount = 0;
private final Object mLock = new Object();
@@ -79,11 +82,16 @@
@NonNull
private final List<PrivilegeListener> mCarrierPrivilegesChangedListeners = new ArrayList<>();
private final boolean mUseCallbacksForServiceChanged;
+ private final boolean mRequestRestrictedWifiEnabled;
+ @NonNull
+ private final BiConsumer<Integer, Integer> mListener;
public CarrierPrivilegeAuthenticator(@NonNull final Context c,
@NonNull final Dependencies deps,
@NonNull final TelephonyManager t,
- @NonNull final TelephonyManagerShim telephonyManagerShim) {
+ @NonNull final TelephonyManagerShim telephonyManagerShim,
+ final boolean requestRestrictedWifiEnabled,
+ @NonNull BiConsumer<Integer, Integer> listener) {
mContext = c;
mTelephonyManager = t;
mTelephonyManagerShim = telephonyManagerShim;
@@ -92,6 +100,8 @@
mHandler = new Handler(thread.getLooper());
mUseCallbacksForServiceChanged = deps.isFeatureEnabled(
c, CARRIER_SERVICE_CHANGED_USE_CALLBACK);
+ mRequestRestrictedWifiEnabled = requestRestrictedWifiEnabled;
+ mListener = listener;
final IntentFilter filter = new IntentFilter();
filter.addAction(TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED);
synchronized (mLock) {
@@ -113,8 +123,10 @@
}
public CarrierPrivilegeAuthenticator(@NonNull final Context c,
- @NonNull final TelephonyManager t) {
- this(c, new Dependencies(), t, TelephonyManagerShimImpl.newInstance(t));
+ @NonNull final TelephonyManager t, final boolean requestRestrictedWifiEnabled,
+ @NonNull BiConsumer<Integer, Integer> listener) {
+ this(c, new Dependencies(), t, TelephonyManagerShimImpl.newInstance(t),
+ requestRestrictedWifiEnabled, listener);
}
public static class Dependencies {
@@ -142,6 +154,29 @@
}
}
+ private static class CarrierServiceUidWithSubId {
+ final int mUid;
+ final int mSubId;
+
+ CarrierServiceUidWithSubId(int uid, int subId) {
+ mUid = uid;
+ mSubId = subId;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof CarrierServiceUidWithSubId)) {
+ return false;
+ }
+ CarrierServiceUidWithSubId compare = (CarrierServiceUidWithSubId) obj;
+ return (mUid == compare.mUid && mSubId == compare.mSubId);
+ }
+
+ @Override
+ public int hashCode() {
+ return mUid * 31 + mSubId;
+ }
+ }
private class PrivilegeListener implements CarrierPrivilegesListenerShim {
public final int mLogicalSlot;
@@ -171,7 +206,18 @@
return;
}
synchronized (mLock) {
- mCarrierServiceUid.put(mLogicalSlot, carrierServiceUid);
+ CarrierServiceUidWithSubId oldPair =
+ mCarrierServiceUidWithSubId.get(mLogicalSlot);
+ int subId = getSubId(mLogicalSlot);
+ mCarrierServiceUidWithSubId.put(
+ mLogicalSlot,
+ new CarrierServiceUidWithSubId(carrierServiceUid, subId));
+ if (oldPair != null
+ && oldPair.mUid != Process.INVALID_UID
+ && oldPair.mSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID
+ && !oldPair.equals(mCarrierServiceUidWithSubId.get(mLogicalSlot))) {
+ mListener.accept(oldPair.mUid, oldPair.mSubId);
+ }
}
}
}
@@ -193,7 +239,14 @@
private void unregisterCarrierPrivilegesListeners() {
for (PrivilegeListener carrierPrivilegesListener : mCarrierPrivilegesChangedListeners) {
removeCarrierPrivilegesListener(carrierPrivilegesListener);
- mCarrierServiceUid.delete(carrierPrivilegesListener.mLogicalSlot);
+ CarrierServiceUidWithSubId oldPair =
+ mCarrierServiceUidWithSubId.get(carrierPrivilegesListener.mLogicalSlot);
+ mCarrierServiceUidWithSubId.remove(carrierPrivilegesListener.mLogicalSlot);
+ if (oldPair != null
+ && oldPair.mUid != Process.INVALID_UID
+ && oldPair.mSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+ mListener.accept(oldPair.mUid, oldPair.mSubId);
+ }
}
mCarrierPrivilegesChangedListeners.clear();
}
@@ -230,8 +283,24 @@
*/
public boolean isCarrierServiceUidForNetworkCapabilities(int callingUid,
@NonNull NetworkCapabilities networkCapabilities) {
- if (callingUid == Process.INVALID_UID) return false;
- final int subId;
+ if (callingUid == Process.INVALID_UID) {
+ return false;
+ }
+ int subId = getSubIdFromNetworkCapabilities(networkCapabilities);
+ if (SubscriptionManager.INVALID_SUBSCRIPTION_ID == subId) {
+ return false;
+ }
+ return callingUid == getCarrierServiceUidForSubId(subId);
+ }
+
+ /**
+ * Extract the SubscriptionId from the NetworkCapabilities.
+ *
+ * @param networkCapabilities the network capabilities which may contains the SubscriptionId.
+ * @return the SubscriptionId.
+ */
+ public int getSubIdFromNetworkCapabilities(@NonNull NetworkCapabilities networkCapabilities) {
+ int subId;
if (networkCapabilities.hasSingleTransportBesidesTest(TRANSPORT_CELLULAR)) {
subId = getSubIdFromTelephonySpecifier(networkCapabilities.getNetworkSpecifier());
} else if (networkCapabilities.hasSingleTransportBesidesTest(TRANSPORT_WIFI)) {
@@ -239,6 +308,12 @@
} else {
subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
}
+ if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID
+ && mRequestRestrictedWifiEnabled
+ && networkCapabilities.getSubscriptionIds().size() == 1) {
+ subId = networkCapabilities.getSubscriptionIds().toArray(new Integer[0])[0];
+ }
+
if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID
&& !networkCapabilities.getSubscriptionIds().contains(subId)) {
// Ideally, the code above should just use networkCapabilities.getSubscriptionIds()
@@ -250,34 +325,60 @@
Log.wtf(TAG, "NetworkCapabilities subIds are inconsistent between "
+ "specifier/transportInfo and mSubIds : " + networkCapabilities);
}
- if (SubscriptionManager.INVALID_SUBSCRIPTION_ID == subId) return false;
- return callingUid == getCarrierServiceUidForSubId(subId);
+ return subId;
+ }
+
+ @VisibleForTesting
+ protected int getSubId(int slotIndex) {
+ if (SdkLevel.isAtLeastU()) {
+ return SubscriptionManager.getSubscriptionId(slotIndex);
+ } else {
+ SubscriptionManager sm = mContext.getSystemService(SubscriptionManager.class);
+ int[] subIds = sm.getSubscriptionIds(slotIndex);
+ if (subIds != null && subIds.length > 0) {
+ return subIds[0];
+ }
+ return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+ }
}
@VisibleForTesting
void updateCarrierServiceUid() {
synchronized (mLock) {
- mCarrierServiceUid.clear();
+ SparseArray<CarrierServiceUidWithSubId> copy = mCarrierServiceUidWithSubId.clone();
+ mCarrierServiceUidWithSubId.clear();
for (int i = 0; i < mModemCount; i++) {
- mCarrierServiceUid.put(i, getCarrierServicePackageUidForSlot(i));
+ int subId = getSubId(i);
+ mCarrierServiceUidWithSubId.put(
+ i,
+ new CarrierServiceUidWithSubId(
+ getCarrierServicePackageUidForSlot(i), subId));
+ }
+ for (int i = 0; i < copy.size(); ++i) {
+ CarrierServiceUidWithSubId oldPair = copy.valueAt(i);
+ CarrierServiceUidWithSubId newPair = mCarrierServiceUidWithSubId.get(copy.keyAt(i));
+ if (oldPair.mUid != Process.INVALID_UID
+ && oldPair.mSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID
+ && !oldPair.equals(newPair)) {
+ mListener.accept(oldPair.mUid, oldPair.mSubId);
+ }
}
}
}
@VisibleForTesting
int getCarrierServiceUidForSubId(int subId) {
- final int slotId = getSlotIndex(subId);
synchronized (mLock) {
- return mCarrierServiceUid.get(slotId, Process.INVALID_UID);
+ for (int i = 0; i < mCarrierServiceUidWithSubId.size(); ++i) {
+ if (mCarrierServiceUidWithSubId.valueAt(i).mSubId == subId) {
+ return mCarrierServiceUidWithSubId.valueAt(i).mUid;
+ }
+ }
+ return Process.INVALID_UID;
}
}
@VisibleForTesting
- protected int getSlotIndex(int subId) {
- return SubscriptionManager.getSlotIndex(subId);
- }
-
- @VisibleForTesting
int getUidForPackage(String pkgName) {
if (pkgName == null) {
return Process.INVALID_UID;
@@ -340,12 +441,14 @@
public void dump(IndentingPrintWriter pw) {
pw.println("CarrierPrivilegeAuthenticator:");
+ pw.println("mRequestRestrictedWifiEnabled = " + mRequestRestrictedWifiEnabled);
synchronized (mLock) {
- final int size = mCarrierServiceUid.size();
- for (int i = 0; i < size; ++i) {
- final int logicalSlot = mCarrierServiceUid.keyAt(i);
- final int serviceUid = mCarrierServiceUid.valueAt(i);
- pw.println("Logical slot = " + logicalSlot + " : uid = " + serviceUid);
+ for (int i = 0; i < mCarrierServiceUidWithSubId.size(); ++i) {
+ final int logicalSlot = mCarrierServiceUidWithSubId.keyAt(i);
+ final int serviceUid = mCarrierServiceUidWithSubId.valueAt(i).mUid;
+ final int subId = mCarrierServiceUidWithSubId.valueAt(i).mSubId;
+ pw.println("Logical slot = " + logicalSlot + " : uid = " + serviceUid
+ + " : subId = " + subId);
}
}
}
diff --git a/service/src/com/android/server/connectivity/ClatCoordinator.java b/service/src/com/android/server/connectivity/ClatCoordinator.java
index 17de146..daaf91d 100644
--- a/service/src/com/android/server/connectivity/ClatCoordinator.java
+++ b/service/src/com/android/server/connectivity/ClatCoordinator.java
@@ -256,7 +256,7 @@
public IBpfMap<ClatIngress6Key, ClatIngress6Value> getBpfIngress6Map() {
try {
return new BpfMap<>(CLAT_INGRESS6_MAP_PATH,
- BpfMap.BPF_F_RDWR, ClatIngress6Key.class, ClatIngress6Value.class);
+ ClatIngress6Key.class, ClatIngress6Value.class);
} catch (ErrnoException e) {
Log.e(TAG, "Cannot create ingress6 map: " + e);
return null;
@@ -268,7 +268,7 @@
public IBpfMap<ClatEgress4Key, ClatEgress4Value> getBpfEgress4Map() {
try {
return new BpfMap<>(CLAT_EGRESS4_MAP_PATH,
- BpfMap.BPF_F_RDWR, ClatEgress4Key.class, ClatEgress4Value.class);
+ ClatEgress4Key.class, ClatEgress4Value.class);
} catch (ErrnoException e) {
Log.e(TAG, "Cannot create egress4 map: " + e);
return null;
@@ -280,7 +280,7 @@
public IBpfMap<CookieTagMapKey, CookieTagMapValue> getBpfCookieTagMap() {
try {
return new BpfMap<>(COOKIE_TAG_MAP_PATH,
- BpfMap.BPF_F_RDWR, CookieTagMapKey.class, CookieTagMapValue.class);
+ CookieTagMapKey.class, CookieTagMapValue.class);
} catch (ErrnoException e) {
Log.wtf(TAG, "Cannot open cookie tag map: " + e);
return null;
diff --git a/service/src/com/android/server/connectivity/ConnectivityFlags.java b/service/src/com/android/server/connectivity/ConnectivityFlags.java
index f8f76ef..bf09160 100644
--- a/service/src/com/android/server/connectivity/ConnectivityFlags.java
+++ b/service/src/com/android/server/connectivity/ConnectivityFlags.java
@@ -36,6 +36,8 @@
public static final String CARRIER_SERVICE_CHANGED_USE_CALLBACK =
"carrier_service_changed_use_callback_version";
+ public static final String REQUEST_RESTRICTED_WIFI =
+ "request_restricted_wifi";
private boolean mNoRematchAllRequestsOnRegister;
/**
diff --git a/service/src/com/android/server/connectivity/DscpPolicyTracker.java b/service/src/com/android/server/connectivity/DscpPolicyTracker.java
index 8d566b6..15d6adb 100644
--- a/service/src/com/android/server/connectivity/DscpPolicyTracker.java
+++ b/service/src/com/android/server/connectivity/DscpPolicyTracker.java
@@ -85,10 +85,10 @@
public DscpPolicyTracker() throws ErrnoException {
mAttachedIfaces = new HashSet<String>();
mIfaceIndexToPolicyIdBpfMapIndex = new HashMap<Integer, SparseIntArray>();
- mBpfDscpIpv4Policies = new BpfMap<Struct.S32, DscpPolicyValue>(IPV4_POLICY_MAP_PATH,
- BpfMap.BPF_F_RDWR, Struct.S32.class, DscpPolicyValue.class);
- mBpfDscpIpv6Policies = new BpfMap<Struct.S32, DscpPolicyValue>(IPV6_POLICY_MAP_PATH,
- BpfMap.BPF_F_RDWR, Struct.S32.class, DscpPolicyValue.class);
+ mBpfDscpIpv4Policies = new BpfMap<>(IPV4_POLICY_MAP_PATH,
+ Struct.S32.class, DscpPolicyValue.class);
+ mBpfDscpIpv6Policies = new BpfMap<>(IPV6_POLICY_MAP_PATH,
+ Struct.S32.class, DscpPolicyValue.class);
}
private boolean isUnusedIndex(int index) {
diff --git a/service/src/com/android/server/connectivity/HandlerUtils.java b/service/src/com/android/server/connectivity/HandlerUtils.java
deleted file mode 100644
index 997ecbf..0000000
--- a/service/src/com/android/server/connectivity/HandlerUtils.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.connectivity;
-
-import android.annotation.NonNull;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.SystemClock;
-
-/**
- * Helper class for Handler related utilities.
- *
- * @hide
- */
-public class HandlerUtils {
- // Note: @hide methods copied from android.os.Handler
- /**
- * Runs the specified task synchronously.
- * <p>
- * If the current thread is the same as the handler thread, then the runnable
- * runs immediately without being enqueued. Otherwise, posts the runnable
- * to the handler and waits for it to complete before returning.
- * </p><p>
- * This method is dangerous! Improper use can result in deadlocks.
- * Never call this method while any locks are held or use it in a
- * possibly re-entrant manner.
- * </p><p>
- * This method is occasionally useful in situations where a background thread
- * must synchronously await completion of a task that must run on the
- * handler's thread. However, this problem is often a symptom of bad design.
- * Consider improving the design (if possible) before resorting to this method.
- * </p><p>
- * One example of where you might want to use this method is when you just
- * set up a Handler thread and need to perform some initialization steps on
- * it before continuing execution.
- * </p><p>
- * If timeout occurs then this method returns <code>false</code> but the runnable
- * will remain posted on the handler and may already be in progress or
- * complete at a later time.
- * </p><p>
- * When using this method, be sure to use {@link Looper#quitSafely} when
- * quitting the looper. Otherwise {@link #runWithScissors} may hang indefinitely.
- * (TODO: We should fix this by making MessageQueue aware of blocking runnables.)
- * </p>
- *
- * @param h The target handler.
- * @param r The Runnable that will be executed synchronously.
- * @param timeout The timeout in milliseconds, or 0 to wait indefinitely.
- *
- * @return Returns true if the Runnable was successfully executed.
- * Returns false on failure, usually because the
- * looper processing the message queue is exiting.
- *
- * @hide This method is prone to abuse and should probably not be in the API.
- * If we ever do make it part of the API, we might want to rename it to something
- * less funny like runUnsafe().
- */
- public static boolean runWithScissors(@NonNull Handler h, @NonNull Runnable r, long timeout) {
- if (r == null) {
- throw new IllegalArgumentException("runnable must not be null");
- }
- if (timeout < 0) {
- throw new IllegalArgumentException("timeout must be non-negative");
- }
-
- if (Looper.myLooper() == h.getLooper()) {
- r.run();
- return true;
- }
-
- BlockingRunnable br = new BlockingRunnable(r);
- return br.postAndWait(h, timeout);
- }
-
- private static final class BlockingRunnable implements Runnable {
- private final Runnable mTask;
- private boolean mDone;
-
- BlockingRunnable(Runnable task) {
- mTask = task;
- }
-
- @Override
- public void run() {
- try {
- mTask.run();
- } finally {
- synchronized (this) {
- mDone = true;
- notifyAll();
- }
- }
- }
-
- public boolean postAndWait(Handler handler, long timeout) {
- if (!handler.post(this)) {
- return false;
- }
-
- synchronized (this) {
- if (timeout > 0) {
- final long expirationTime = SystemClock.uptimeMillis() + timeout;
- while (!mDone) {
- long delay = expirationTime - SystemClock.uptimeMillis();
- if (delay <= 0) {
- return false; // timeout
- }
- try {
- wait(delay);
- } catch (InterruptedException ex) {
- }
- }
- } else {
- while (!mDone) {
- try {
- wait();
- } catch (InterruptedException ex) {
- }
- }
- }
- }
- return true;
- }
- }
-}
diff --git a/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java b/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
index 7a8b41b..48af9fa 100644
--- a/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
+++ b/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
@@ -74,9 +74,10 @@
public class KeepaliveStatsTracker {
private static final String TAG = KeepaliveStatsTracker.class.getSimpleName();
private static final int INVALID_KEEPALIVE_ID = -1;
- // 1 hour acceptable deviation in metrics collection duration time.
+ // 2 hour acceptable deviation in metrics collection duration time to account for the 1 hour
+ // window of AlarmManager.
private static final long MAX_EXPECTED_DURATION_MS =
- AutomaticOnOffKeepaliveTracker.METRICS_COLLECTION_DURATION_MS + 1 * 60 * 60 * 1_000L;
+ AutomaticOnOffKeepaliveTracker.METRICS_COLLECTION_DURATION_MS + 2 * 60 * 60 * 1_000L;
@NonNull private final Handler mConnectivityServiceHandler;
@NonNull private final Dependencies mDependencies;
diff --git a/service/src/com/android/server/connectivity/MulticastRoutingCoordinatorService.java b/service/src/com/android/server/connectivity/MulticastRoutingCoordinatorService.java
new file mode 100644
index 0000000..4d5001b
--- /dev/null
+++ b/service/src/com/android/server/connectivity/MulticastRoutingCoordinatorService.java
@@ -0,0 +1,820 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import static android.net.MulticastRoutingConfig.FORWARD_NONE;
+import static android.net.MulticastRoutingConfig.FORWARD_SELECTED;
+import static android.net.MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE;
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.EADDRINUSE;
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.system.OsConstants.IPPROTO_IPV6;
+import static android.system.OsConstants.SOCK_CLOEXEC;
+import static android.system.OsConstants.SOCK_NONBLOCK;
+import static android.system.OsConstants.SOCK_RAW;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.MulticastRoutingConfig;
+import android.net.NetworkUtils;
+import android.os.Handler;
+import android.os.Looper;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.LinkPropertiesUtils.CompareResult;
+import com.android.net.module.util.PacketReader;
+import com.android.net.module.util.SocketUtils;
+import com.android.net.module.util.netlink.NetlinkUtils;
+import com.android.net.module.util.netlink.RtNetlinkRouteMessage;
+import com.android.net.module.util.structs.StructMf6cctl;
+import com.android.net.module.util.structs.StructMif6ctl;
+import com.android.net.module.util.structs.StructMrt6Msg;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.Inet6Address;
+import java.net.InetSocketAddress;
+import java.net.MulticastSocket;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Class to coordinate multicast routing between network interfaces.
+ *
+ * <p>Supports IPv6 multicast routing.
+ *
+ * <p>Note that usage of this class is not thread-safe. All public methods must be called from the
+ * same thread that the handler from {@code dependencies.getHandler} is associated.
+ */
+public class MulticastRoutingCoordinatorService {
+ private static final String TAG = MulticastRoutingCoordinatorService.class.getSimpleName();
+ private static final int ICMP6_FILTER = 1;
+ private static final int MRT6_INIT = 200;
+ private static final int MRT6_ADD_MIF = 202;
+ private static final int MRT6_DEL_MIF = 203;
+ private static final int MRT6_ADD_MFC = 204;
+ private static final int MRT6_DEL_MFC = 205;
+ private static final int ONE = 1;
+
+ private final Dependencies mDependencies;
+
+ private final Handler mHandler;
+ private final MulticastNocacheUpcallListener mMulticastNoCacheUpcallListener;
+ @NonNull private final FileDescriptor mMulticastRoutingFd; // For multicast routing config
+ @NonNull private final MulticastSocket mMulticastSocket; // For join group and leave group
+
+ @VisibleForTesting public static final int MFC_INACTIVE_CHECK_INTERVAL_MS = 60_000;
+ @VisibleForTesting public static final int MFC_INACTIVE_TIMEOUT_MS = 300_000;
+ @VisibleForTesting public static final int MFC_MAX_NUMBER_OF_ENTRIES = 1_000;
+
+ // The kernel supports max 32 virtual interfaces per multicast routing table.
+ private static final int MAX_NUM_OF_MULTICAST_VIRTUAL_INTERFACES = 32;
+
+ /** Tracks if checking for inactive MFC has been scheduled */
+ private boolean mMfcPollingScheduled = false;
+
+ /** Mapping from multicast virtual interface index to interface name */
+ private SparseArray<String> mVirtualInterfaces =
+ new SparseArray<>(MAX_NUM_OF_MULTICAST_VIRTUAL_INTERFACES);
+ /** Mapping from physical interface index to interface name */
+ private SparseArray<String> mInterfaces =
+ new SparseArray<>(MAX_NUM_OF_MULTICAST_VIRTUAL_INTERFACES);
+
+ /** Mapping of iif to PerInterfaceMulticastRoutingConfig */
+ private Map<String, PerInterfaceMulticastRoutingConfig> mMulticastRoutingConfigs =
+ new HashMap<String, PerInterfaceMulticastRoutingConfig>();
+
+ private static final class PerInterfaceMulticastRoutingConfig {
+ // mapping of oif name to MulticastRoutingConfig
+ public Map<String, MulticastRoutingConfig> oifConfigs =
+ new HashMap<String, MulticastRoutingConfig>();
+ }
+
+ /** Tracks the MFCs added to kernel. Using LinkedHashMap to keep the added order, so
+ // when the number of MFCs reaches the max limit then the earliest added one is removed. */
+ private LinkedHashMap<MfcKey, MfcValue> mMfcs = new LinkedHashMap<>();
+
+ public MulticastRoutingCoordinatorService(Handler h) {
+ this(h, new Dependencies());
+ }
+
+ @VisibleForTesting
+ /* @throws UnsupportedOperationException if multicast routing is not supported */
+ public MulticastRoutingCoordinatorService(Handler h, Dependencies dependencies) {
+ mDependencies = dependencies;
+ mMulticastRoutingFd = mDependencies.createMulticastRoutingSocket();
+ mMulticastSocket = mDependencies.createMulticastSocket();
+ mHandler = h;
+ mMulticastNoCacheUpcallListener =
+ new MulticastNocacheUpcallListener(mHandler, mMulticastRoutingFd);
+ mHandler.post(() -> mMulticastNoCacheUpcallListener.start());
+ }
+
+ private void checkOnHandlerThread() {
+ if (Looper.myLooper() != mHandler.getLooper()) {
+ throw new IllegalStateException(
+ "Not running on ConnectivityService thread (" + mHandler.getLooper() + ") : "
+ + Looper.myLooper());
+ }
+ }
+
+ private Integer getInterfaceIndex(String ifName) {
+ int mapIndex = mInterfaces.indexOfValue(ifName);
+ if (mapIndex < 0) return null;
+ return mInterfaces.keyAt(mapIndex);
+ }
+
+ /**
+ * Apply multicast routing configuration
+ *
+ * @param iifName name of the incoming interface
+ * @param oifName name of the outgoing interface
+ * @param newConfig the multicast routing configuration to be applied from iif to oif
+ * @throws MulticastRoutingException when failed to apply the config
+ */
+ public void applyMulticastRoutingConfig(
+ final String iifName, final String oifName, final MulticastRoutingConfig newConfig) {
+ checkOnHandlerThread();
+
+ if (newConfig.getForwardingMode() != FORWARD_NONE) {
+ // Make sure iif and oif are added as multicast forwarding interfaces
+ try {
+ maybeAddAndTrackInterface(iifName);
+ maybeAddAndTrackInterface(oifName);
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Failed to apply multicast routing config, ", e);
+ return;
+ }
+ }
+
+ final MulticastRoutingConfig oldConfig = getMulticastRoutingConfig(iifName, oifName);
+
+ if (oldConfig.equals(newConfig)) return;
+
+ int oldMode = oldConfig.getForwardingMode();
+ int newMode = newConfig.getForwardingMode();
+ Integer iifIndex = getInterfaceIndex(iifName);
+ if (iifIndex == null) {
+ // This cannot happen unless the new config has FORWARD_NONE but is not the same
+ // as the old config. This is not possible in current code.
+ Log.wtf(TAG, "Adding multicast configuration on null interface?");
+ return;
+ }
+
+ // When new addresses are added to FORWARD_SELECTED mode, join these multicast groups
+ // on their upstream interface, so upstream multicast routers know about the subscription.
+ // When addresses are removed from FORWARD_SELECTED mode, leave the multicast groups.
+ final Set<Inet6Address> oldListeningAddresses =
+ (oldMode == FORWARD_SELECTED)
+ ? oldConfig.getListeningAddresses()
+ : new ArraySet<>();
+ final Set<Inet6Address> newListeningAddresses =
+ (newMode == FORWARD_SELECTED)
+ ? newConfig.getListeningAddresses()
+ : new ArraySet<>();
+ final CompareResult<Inet6Address> addressDiff =
+ new CompareResult<>(oldListeningAddresses, newListeningAddresses);
+ joinGroups(iifIndex, addressDiff.added);
+ leaveGroups(iifIndex, addressDiff.removed);
+
+ setMulticastRoutingConfig(iifName, oifName, newConfig);
+ Log.d(
+ TAG,
+ "Applied multicast routing config for iif "
+ + iifName
+ + " to oif "
+ + oifName
+ + " with Config "
+ + newConfig);
+
+ // Update existing MFCs to make sure they align with the updated configuration
+ updateMfcs();
+
+ if (newConfig.getForwardingMode() == FORWARD_NONE) {
+ if (!hasActiveMulticastConfig(iifName)) {
+ removeInterfaceFromMulticastRouting(iifName);
+ }
+ if (!hasActiveMulticastConfig(oifName)) {
+ removeInterfaceFromMulticastRouting(oifName);
+ }
+ }
+ }
+
+ /**
+ * Removes an network interface from multicast routing.
+ *
+ * <p>Remove the network interface from multicast configs and remove it from the list of
+ * multicast routing interfaces in the kernel
+ *
+ * @param ifName name of the interface that should be removed
+ */
+ @VisibleForTesting
+ public void removeInterfaceFromMulticastRouting(final String ifName) {
+ checkOnHandlerThread();
+ final Integer virtualIndex = getVirtualInterfaceIndex(ifName);
+ if (virtualIndex == null) return;
+
+ updateMfcs();
+ mInterfaces.removeAt(mInterfaces.indexOfValue(ifName));
+ mVirtualInterfaces.remove(virtualIndex);
+ try {
+ mDependencies.setsockoptMrt6DelMif(mMulticastRoutingFd, virtualIndex);
+ Log.d(TAG, "Removed mifi " + virtualIndex + " from MIF");
+ } catch (ErrnoException e) {
+ Log.e(TAG, "failed to remove multicast virtual interface" + virtualIndex, e);
+ }
+ }
+
+ private int getNextAvailableVirtualIndex() {
+ if (mVirtualInterfaces.size() >= MAX_NUM_OF_MULTICAST_VIRTUAL_INTERFACES) {
+ throw new IllegalStateException("Can't allocate new multicast virtual interface");
+ }
+ for (int i = 0; i < mVirtualInterfaces.size(); i++) {
+ if (!mVirtualInterfaces.contains(i)) {
+ return i;
+ }
+ }
+ return mVirtualInterfaces.size();
+ }
+
+ @VisibleForTesting
+ public Integer getVirtualInterfaceIndex(String ifName) {
+ int mapIndex = mVirtualInterfaces.indexOfValue(ifName);
+ if (mapIndex < 0) return null;
+ return mVirtualInterfaces.keyAt(mapIndex);
+ }
+
+ private Integer getVirtualInterfaceIndex(int physicalIndex) {
+ String ifName = mInterfaces.get(physicalIndex);
+ if (ifName == null) {
+ // This is only used to match MFCs from kernel to MFCs we know about.
+ // Unknown MFCs should be ignored.
+ return null;
+ }
+ return getVirtualInterfaceIndex(ifName);
+ }
+
+ private String getInterfaceName(int virtualIndex) {
+ return mVirtualInterfaces.get(virtualIndex);
+ }
+
+ private void maybeAddAndTrackInterface(String ifName) {
+ checkOnHandlerThread();
+ if (mVirtualInterfaces.indexOfValue(ifName) >= 0) return;
+
+ int nextVirtualIndex = getNextAvailableVirtualIndex();
+ int ifIndex = mDependencies.getInterfaceIndex(ifName);
+ final StructMif6ctl mif6ctl =
+ new StructMif6ctl(
+ nextVirtualIndex,
+ (short) 0 /* mif6c_flags */,
+ (short) 1 /* vifc_threshold */,
+ ifIndex,
+ 0 /* vifc_rate_limit */);
+ try {
+ mDependencies.setsockoptMrt6AddMif(mMulticastRoutingFd, mif6ctl);
+ Log.d(TAG, "Added mifi " + nextVirtualIndex + " to MIF");
+ } catch (ErrnoException e) {
+ Log.e(TAG, "failed to add multicast virtual interface", e);
+ return;
+ }
+ mVirtualInterfaces.put(nextVirtualIndex, ifName);
+ mInterfaces.put(ifIndex, ifName);
+ }
+
+ @VisibleForTesting
+ public MulticastRoutingConfig getMulticastRoutingConfig(String iifName, String oifName) {
+ PerInterfaceMulticastRoutingConfig configs = mMulticastRoutingConfigs.get(iifName);
+ final MulticastRoutingConfig defaultConfig = MulticastRoutingConfig.CONFIG_FORWARD_NONE;
+ if (configs == null) {
+ return defaultConfig;
+ } else {
+ return configs.oifConfigs.getOrDefault(oifName, defaultConfig);
+ }
+ }
+
+ private void setMulticastRoutingConfig(
+ final String iifName, final String oifName, final MulticastRoutingConfig config) {
+ checkOnHandlerThread();
+ PerInterfaceMulticastRoutingConfig iifConfig = mMulticastRoutingConfigs.get(iifName);
+
+ if (config.getForwardingMode() == FORWARD_NONE) {
+ if (iifConfig != null) {
+ iifConfig.oifConfigs.remove(oifName);
+ }
+ if (iifConfig.oifConfigs.isEmpty()) {
+ mMulticastRoutingConfigs.remove(iifName);
+ }
+ return;
+ }
+
+ if (iifConfig == null) {
+ iifConfig = new PerInterfaceMulticastRoutingConfig();
+ mMulticastRoutingConfigs.put(iifName, iifConfig);
+ }
+ iifConfig.oifConfigs.put(oifName, config);
+ }
+
+ /** Returns whether an interface has multicast routing config */
+ private boolean hasActiveMulticastConfig(final String ifName) {
+ // FORWARD_NONE configs are not saved in the config tables, so
+ // any existing config is an active multicast routing config
+ if (mMulticastRoutingConfigs.containsKey(ifName)) return true;
+ for (var pic : mMulticastRoutingConfigs.values()) {
+ if (pic.oifConfigs.containsKey(ifName)) return true;
+ }
+ return false;
+ }
+
+ /**
+ * A multicast forwarding cache (MFC) entry holds a multicast forwarding route where packet from
+ * incoming interface(iif) with source address(S) to group address (G) are forwarded to outgoing
+ * interfaces(oifs).
+ *
+ * <p>iif, S and G identifies an MFC entry. For example an MFC1 is added: [iif1, S1, G1, oifs1]
+ * Adding another MFC2 of [iif1, S1, G1, oifs2] to the kernel overwrites MFC1.
+ */
+ private static final class MfcKey {
+ public final int mIifVirtualIdx;
+ public final Inet6Address mSrcAddr;
+ public final Inet6Address mDstAddr;
+
+ public MfcKey(int iif, Inet6Address src, Inet6Address dst) {
+ mIifVirtualIdx = iif;
+ mSrcAddr = src;
+ mDstAddr = dst;
+ }
+
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ } else if (!(other instanceof MfcKey)) {
+ return false;
+ } else {
+ MfcKey otherKey = (MfcKey) other;
+ return mIifVirtualIdx == otherKey.mIifVirtualIdx
+ && mSrcAddr.equals(otherKey.mSrcAddr)
+ && mDstAddr.equals(otherKey.mDstAddr);
+ }
+ }
+
+ public int hashCode() {
+ return Objects.hash(mIifVirtualIdx, mSrcAddr, mDstAddr);
+ }
+
+ public String toString() {
+ return "{iifVirtualIndex: "
+ + Integer.toString(mIifVirtualIdx)
+ + ", sourceAddress: "
+ + mSrcAddr.toString()
+ + ", destinationAddress: "
+ + mDstAddr.toString()
+ + "}";
+ }
+ }
+
+ private static final class MfcValue {
+ private Set<Integer> mOifVirtualIndices;
+ // timestamp of when the mfc was last used in the kernel
+ // (e.g. created, or used to forward a packet)
+ private Instant mLastUsedAt;
+
+ public MfcValue(Set<Integer> oifs, Instant timestamp) {
+ mOifVirtualIndices = oifs;
+ mLastUsedAt = timestamp;
+ }
+
+ public boolean hasSameOifsAs(MfcValue other) {
+ return this.mOifVirtualIndices.equals(other.mOifVirtualIndices);
+ }
+
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ } else if (!(other instanceof MfcValue)) {
+ return false;
+ } else {
+ MfcValue otherValue = (MfcValue) other;
+ return mOifVirtualIndices.equals(otherValue.mOifVirtualIndices)
+ && mLastUsedAt.equals(otherValue.mLastUsedAt);
+ }
+ }
+
+ public int hashCode() {
+ return Objects.hash(mOifVirtualIndices, mLastUsedAt);
+ }
+
+ public Set<Integer> getOifIndices() {
+ return mOifVirtualIndices;
+ }
+
+ public void setLastUsedAt(Instant timestamp) {
+ mLastUsedAt = timestamp;
+ }
+
+ public Instant getLastUsedAt() {
+ return mLastUsedAt;
+ }
+
+ public String toString() {
+ return "{oifVirtualIdxes: "
+ + mOifVirtualIndices.toString()
+ + ", lastUsedAt: "
+ + mLastUsedAt.toString()
+ + "}";
+ }
+ }
+
+ /**
+ * Returns the MFC value for the given MFC key according to current multicast routing config. If
+ * the MFC should be removed return null.
+ */
+ private MfcValue computeMfcValue(int iif, Inet6Address dst) {
+ final int dstScope = getGroupAddressScope(dst);
+ Set<Integer> forwardingOifs = new ArraySet<>();
+
+ PerInterfaceMulticastRoutingConfig iifConfig =
+ mMulticastRoutingConfigs.get(getInterfaceName(iif));
+
+ if (iifConfig == null) {
+ // An iif may have been removed from multicast routing, in this
+ // case remove the MFC directly
+ return null;
+ }
+
+ for (var config : iifConfig.oifConfigs.entrySet()) {
+ if ((config.getValue().getForwardingMode() == FORWARD_WITH_MIN_SCOPE
+ && config.getValue().getMinimumScope() <= dstScope)
+ || (config.getValue().getForwardingMode() == FORWARD_SELECTED
+ && config.getValue().getListeningAddresses().contains(dst))) {
+ forwardingOifs.add(getVirtualInterfaceIndex(config.getKey()));
+ }
+ }
+
+ return new MfcValue(forwardingOifs, Instant.now(mDependencies.getClock()));
+ }
+
+ /**
+ * Given the iif, source address and group destination address, add an MFC entry or update the
+ * existing MFC according to the multicast routing config. If such an MFC should not exist,
+ * return null for caller of the function to remove it.
+ *
+ * <p>Note that if a packet has no matching MFC entry in the kernel, kernel creates an
+ * unresolved route and notifies multicast socket with a NOCACHE upcall message. The unresolved
+ * route is kept for no less than 10s. If packets with the same source and destination arrives
+ * before the 10s timeout, they will not be notified. Thus we need to add a 'blocking' MFC which
+ * is an MFC with an empty oif list. When the multicast configs changes, the 'blocking' MFC
+ * will be updated to a 'forwarding' MFC so that corresponding multicast traffic can be
+ * forwarded instantly.
+ *
+ * @return {@code true} if the MFC is updated and no operation is needed from caller.
+ * {@code false} if the MFC should not be added, caller of the function should remove
+ * the MFC if needed.
+ */
+ private boolean addOrUpdateMfc(int vif, Inet6Address src, Inet6Address dst) {
+ checkOnHandlerThread();
+ final MfcKey key = new MfcKey(vif, src, dst);
+ final MfcValue value = mMfcs.get(key);
+ final MfcValue updatedValue = computeMfcValue(vif, dst);
+
+ if (updatedValue == null) {
+ return false;
+ }
+
+ if (value != null && value.hasSameOifsAs(updatedValue)) {
+ // no updates to make
+ return true;
+ }
+
+ final StructMf6cctl mf6cctl =
+ new StructMf6cctl(src, dst, vif, updatedValue.getOifIndices());
+ try {
+ mDependencies.setsockoptMrt6AddMfc(mMulticastRoutingFd, mf6cctl);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "failed to add MFC: " + e);
+ return false;
+ }
+ mMfcs.put(key, updatedValue);
+ String operation = (value == null ? "Added" : "Updated");
+ Log.d(TAG, operation + " MFC key: " + key + " value: " + updatedValue);
+ return true;
+ }
+
+ private void checkMfcsExpiration() {
+ checkOnHandlerThread();
+ // Check if there are inactive MFCs that can be removed
+ refreshMfcInactiveDuration();
+ maybeExpireMfcs();
+ if (mMfcs.size() > 0) {
+ mHandler.postDelayed(() -> checkMfcsExpiration(), MFC_INACTIVE_CHECK_INTERVAL_MS);
+ mMfcPollingScheduled = true;
+ } else {
+ mMfcPollingScheduled = false;
+ }
+ }
+
+ private void checkMfcEntriesLimit() {
+ checkOnHandlerThread();
+ // If the max number of MFC entries is reached, remove the first MFC entry. This can be
+ // any entry, as if this entry is needed again there will be a NOCACHE upcall to add it
+ // back.
+ if (mMfcs.size() == MFC_MAX_NUMBER_OF_ENTRIES) {
+ Log.w(TAG, "Reached max number of MFC entries " + MFC_MAX_NUMBER_OF_ENTRIES);
+ var iter = mMfcs.entrySet().iterator();
+ MfcKey firstMfcKey = iter.next().getKey();
+ removeMfcFromKernel(firstMfcKey);
+ iter.remove();
+ }
+ }
+
+ /**
+ * Reads multicast routes information from the kernel, and update the last used timestamp for
+ * each multicast route save in this class.
+ */
+ private void refreshMfcInactiveDuration() {
+ checkOnHandlerThread();
+ final List<RtNetlinkRouteMessage> multicastRoutes = NetlinkUtils.getIpv6MulticastRoutes();
+
+ for (var route : multicastRoutes) {
+ if (!route.isResolved()) {
+ continue; // Don't handle unresolved mfc, the kernel will recycle in 10s
+ }
+ Integer iif = getVirtualInterfaceIndex(route.getIifIndex());
+ if (iif == null) {
+ Log.e(TAG, "Can't find kernel returned IIF " + route.getIifIndex());
+ return;
+ }
+ final MfcKey key =
+ new MfcKey(
+ iif,
+ (Inet6Address) route.getSource().getAddress(),
+ (Inet6Address) route.getDestination().getAddress());
+ MfcValue value = mMfcs.get(key);
+ if (value == null) {
+ Log.e(TAG, "Can't find kernel returned MFC " + key);
+ continue;
+ }
+ value.setLastUsedAt(
+ Instant.now(mDependencies.getClock())
+ .minusMillis(route.getSinceLastUseMillis()));
+ }
+ }
+
+ /** Remove MFC entry from mMfcs map and the kernel if exists. */
+ private void removeMfcFromKernel(MfcKey key) {
+ checkOnHandlerThread();
+
+ final MfcValue value = mMfcs.get(key);
+ final Set<Integer> oifs = new ArraySet<>();
+ final StructMf6cctl mf6cctl =
+ new StructMf6cctl(key.mSrcAddr, key.mDstAddr, key.mIifVirtualIdx, oifs);
+ try {
+ mDependencies.setsockoptMrt6DelMfc(mMulticastRoutingFd, mf6cctl);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "failed to remove MFC: " + e);
+ return;
+ }
+ Log.d(TAG, "Removed MFC key: " + key + " value: " + value);
+ }
+
+ /**
+ * This is called every MFC_INACTIVE_CHECK_INTERVAL_MS milliseconds to remove any MFC that is
+ * inactive for more than MFC_INACTIVE_TIMEOUT_MS milliseconds.
+ */
+ private void maybeExpireMfcs() {
+ checkOnHandlerThread();
+
+ for (var it = mMfcs.entrySet().iterator(); it.hasNext(); ) {
+ var entry = it.next();
+ if (entry.getValue()
+ .getLastUsedAt()
+ .plusMillis(MFC_INACTIVE_TIMEOUT_MS)
+ .isBefore(Instant.now(mDependencies.getClock()))) {
+ removeMfcFromKernel(entry.getKey());
+ it.remove();
+ }
+ }
+ }
+
+ private void updateMfcs() {
+ checkOnHandlerThread();
+
+ for (Iterator<Map.Entry<MfcKey, MfcValue>> it = mMfcs.entrySet().iterator();
+ it.hasNext(); ) {
+ MfcKey key = it.next().getKey();
+ if (!addOrUpdateMfc(key.mIifVirtualIdx, key.mSrcAddr, key.mDstAddr)) {
+ removeMfcFromKernel(key);
+ it.remove();
+ }
+ }
+
+ refreshMfcInactiveDuration();
+ }
+
+ private void joinGroups(int ifIndex, List<Inet6Address> addresses) {
+ for (Inet6Address address : addresses) {
+ InetSocketAddress socketAddress = new InetSocketAddress(address, 0);
+ try {
+ mMulticastSocket.joinGroup(
+ socketAddress, mDependencies.getNetworkInterface(ifIndex));
+ } catch (IOException e) {
+ if (e.getCause() instanceof ErrnoException) {
+ ErrnoException ee = (ErrnoException) e.getCause();
+ if (ee.errno == EADDRINUSE) {
+ // The list of added address are calculated from address changes,
+ // repeated join group is unexpected
+ Log.e(TAG, "Already joined group" + e);
+ continue;
+ }
+ }
+ Log.e(TAG, "failed to join group: " + e);
+ }
+ }
+ }
+
+ private void leaveGroups(int ifIndex, List<Inet6Address> addresses) {
+ for (Inet6Address address : addresses) {
+ InetSocketAddress socketAddress = new InetSocketAddress(address, 0);
+ try {
+ mMulticastSocket.leaveGroup(
+ socketAddress, mDependencies.getNetworkInterface(ifIndex));
+ } catch (IOException e) {
+ Log.e(TAG, "failed to leave group: " + e);
+ }
+ }
+ }
+
+ private int getGroupAddressScope(Inet6Address address) {
+ return address.getAddress()[1] & 0xf;
+ }
+
+ /**
+ * Handles a NoCache upcall that indicates a multicast packet is received and requires
+ * a multicast forwarding cache to be added.
+ *
+ * A forwarding or blocking MFC is added according to the multicast config.
+ *
+ * The number of MFCs is checked to make sure it doesn't exceed the
+ * {@code MFC_MAX_NUMBER_OF_ENTRIES} limit.
+ */
+ @VisibleForTesting
+ public void handleMulticastNocacheUpcall(final StructMrt6Msg mrt6Msg) {
+ final int iifVid = mrt6Msg.mif;
+
+ // add MFC to forward the packet or add blocking MFC to not forward the packet
+ // If the packet comes from an interface the service doesn't care about, the
+ // addOrUpdateMfc function will return null and not MFC will be added.
+ if (!addOrUpdateMfc(iifVid, mrt6Msg.src, mrt6Msg.dst)) return;
+ // If the list of MFCs is not empty and there is no MFC check scheduled,
+ // schedule one now
+ if (!mMfcPollingScheduled) {
+ mHandler.postDelayed(() -> checkMfcsExpiration(), MFC_INACTIVE_CHECK_INTERVAL_MS);
+ mMfcPollingScheduled = true;
+ }
+
+ checkMfcEntriesLimit();
+ }
+
+ /**
+ * A packet reader that handles the packets sent to the multicast routing socket
+ */
+ private final class MulticastNocacheUpcallListener extends PacketReader {
+ private final FileDescriptor mFd;
+
+ public MulticastNocacheUpcallListener(Handler h, FileDescriptor fd) {
+ super(h);
+ mFd = fd;
+ }
+
+ @Override
+ protected FileDescriptor createFd() {
+ return mFd;
+ }
+
+ @Override
+ protected void handlePacket(byte[] recvbuf, int length) {
+ final ByteBuffer buf = ByteBuffer.wrap(recvbuf);
+ final StructMrt6Msg mrt6Msg = StructMrt6Msg.parse(buf);
+ if (mrt6Msg.msgType != StructMrt6Msg.MRT6MSG_NOCACHE) {
+ return;
+ }
+ handleMulticastNocacheUpcall(mrt6Msg);
+ }
+ }
+
+ /** Dependencies of RoutingCoordinatorService, for test injections. */
+ @VisibleForTesting
+ public static class Dependencies {
+ private final Clock mClock = Clock.system(ZoneId.systemDefault());
+
+ /**
+ * Creates a socket to configure multicast routing in the kernel.
+ *
+ * <p>If the kernel doesn't support multicast routing, then the {@code setsockoptInt} with
+ * {@code MRT6_INIT} method would fail.
+ *
+ * @return the multicast routing socket, or null if it fails to be created/configured.
+ */
+ public FileDescriptor createMulticastRoutingSocket() {
+ FileDescriptor sock = null;
+ byte[] filter = new byte[32]; // filter all ICMPv6 messages
+ try {
+ sock = Os.socket(AF_INET6, SOCK_RAW | SOCK_CLOEXEC | SOCK_NONBLOCK, IPPROTO_ICMPV6);
+ Os.setsockoptInt(sock, IPPROTO_IPV6, MRT6_INIT, ONE);
+ NetworkUtils.setsockoptBytes(sock, IPPROTO_ICMPV6, ICMP6_FILTER, filter);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "failed to create multicast socket: " + e);
+ if (sock != null) {
+ SocketUtils.closeSocketQuietly(sock);
+ }
+ throw new UnsupportedOperationException("Multicast routing is not supported ", e);
+ }
+ Log.i(TAG, "socket created for multicast routing: " + sock);
+ return sock;
+ }
+
+ public MulticastSocket createMulticastSocket() {
+ try {
+ return new MulticastSocket();
+ } catch (IOException e) {
+ Log.wtf(TAG, "Failed to create multicast socket " + e);
+ throw new IllegalStateException(e);
+ }
+ }
+
+ public void setsockoptMrt6AddMif(FileDescriptor fd, StructMif6ctl mif6ctl)
+ throws ErrnoException {
+ final byte[] bytes = mif6ctl.writeToBytes();
+ NetworkUtils.setsockoptBytes(fd, IPPROTO_IPV6, MRT6_ADD_MIF, bytes);
+ }
+
+ public void setsockoptMrt6DelMif(FileDescriptor fd, int virtualIfIndex)
+ throws ErrnoException {
+ Os.setsockoptInt(fd, IPPROTO_IPV6, MRT6_DEL_MIF, virtualIfIndex);
+ }
+
+ public void setsockoptMrt6AddMfc(FileDescriptor fd, StructMf6cctl mf6cctl)
+ throws ErrnoException {
+ final byte[] bytes = mf6cctl.writeToBytes();
+ NetworkUtils.setsockoptBytes(fd, IPPROTO_IPV6, MRT6_ADD_MFC, bytes);
+ }
+
+ public void setsockoptMrt6DelMfc(FileDescriptor fd, StructMf6cctl mf6cctl)
+ throws ErrnoException {
+ final byte[] bytes = mf6cctl.writeToBytes();
+ NetworkUtils.setsockoptBytes(fd, IPPROTO_IPV6, MRT6_DEL_MFC, bytes);
+ }
+
+ public Integer getInterfaceIndex(String ifName) {
+ try {
+ NetworkInterface ni = NetworkInterface.getByName(ifName);
+ return ni.getIndex();
+ } catch (NullPointerException | SocketException e) {
+ return null;
+ }
+ }
+
+ public NetworkInterface getNetworkInterface(int physicalIndex) {
+ try {
+ return NetworkInterface.getByIndex(physicalIndex);
+ } catch (SocketException e) {
+ return null;
+ }
+ }
+
+ public Clock getClock() {
+ return mClock;
+ }
+ }
+}
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index 50cad45..76993a6 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -1551,7 +1551,7 @@
* @param hasAutomotiveFeature true if this device has the automotive feature, false otherwise
* @param authenticator the carrier privilege authenticator to check for telephony constraints
*/
- public void restrictCapabilitiesFromNetworkAgent(@NonNull final NetworkCapabilities nc,
+ public static void restrictCapabilitiesFromNetworkAgent(@NonNull final NetworkCapabilities nc,
final int creatorUid, final boolean hasAutomotiveFeature,
@NonNull final ConnectivityService.Dependencies deps,
@Nullable final CarrierPrivilegeAuthenticator authenticator) {
@@ -1564,7 +1564,7 @@
}
}
- private boolean areAllowedUidsAcceptableFromNetworkAgent(
+ private static boolean areAllowedUidsAcceptableFromNetworkAgent(
@NonNull final NetworkCapabilities nc, final boolean hasAutomotiveFeature,
@NonNull final ConnectivityService.Dependencies deps,
@Nullable final CarrierPrivilegeAuthenticator carrierPrivilegeAuthenticator) {
diff --git a/service/src/com/android/server/connectivity/NetworkRequestStateInfo.java b/service/src/com/android/server/connectivity/NetworkRequestStateInfo.java
new file mode 100644
index 0000000..ab3d315
--- /dev/null
+++ b/service/src/com/android/server/connectivity/NetworkRequestStateInfo.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_UNKNOWN;
+
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.SystemClock;
+
+import com.android.net.module.util.BitUtils;
+
+
+class NetworkRequestStateInfo {
+ private final NetworkRequest mNetworkRequest;
+ private final long mNetworkRequestReceivedTime;
+
+ private enum NetworkRequestState {
+ RECEIVED,
+ REMOVED
+ }
+ private NetworkRequestState mNetworkRequestState;
+ private int mNetworkRequestDurationMillis;
+ private final Dependencies mDependencies;
+
+ NetworkRequestStateInfo(NetworkRequest networkRequest,
+ Dependencies deps) {
+ mDependencies = deps;
+ mNetworkRequest = networkRequest;
+ mNetworkRequestReceivedTime = mDependencies.getElapsedRealtime();
+ mNetworkRequestDurationMillis = 0;
+ mNetworkRequestState = NetworkRequestState.RECEIVED;
+ }
+
+ NetworkRequestStateInfo(NetworkRequestStateInfo anotherNetworkRequestStateInfo) {
+ mDependencies = anotherNetworkRequestStateInfo.mDependencies;
+ mNetworkRequest = new NetworkRequest(anotherNetworkRequestStateInfo.mNetworkRequest);
+ mNetworkRequestReceivedTime = anotherNetworkRequestStateInfo.mNetworkRequestReceivedTime;
+ mNetworkRequestDurationMillis =
+ anotherNetworkRequestStateInfo.mNetworkRequestDurationMillis;
+ mNetworkRequestState = anotherNetworkRequestStateInfo.mNetworkRequestState;
+ }
+
+ public void setNetworkRequestRemoved() {
+ mNetworkRequestState = NetworkRequestState.REMOVED;
+ mNetworkRequestDurationMillis = (int) (
+ mDependencies.getElapsedRealtime() - mNetworkRequestReceivedTime);
+ }
+
+ public int getNetworkRequestStateStatsType() {
+ if (mNetworkRequestState == NetworkRequestState.RECEIVED) {
+ return NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED;
+ } else if (mNetworkRequestState == NetworkRequestState.REMOVED) {
+ return NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED;
+ } else {
+ return NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_UNKNOWN;
+ }
+ }
+
+ public int getRequestId() {
+ return mNetworkRequest.requestId;
+ }
+
+ public int getPackageUid() {
+ return mNetworkRequest.networkCapabilities.getRequestorUid();
+ }
+
+ public int getTransportTypes() {
+ return (int) BitUtils.packBits(mNetworkRequest.networkCapabilities.getTransportTypes());
+ }
+
+ public boolean getNetCapabilityNotMetered() {
+ return mNetworkRequest.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+ }
+
+ public boolean getNetCapabilityInternet() {
+ return mNetworkRequest.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+ }
+
+ public int getNetworkRequestDurationMillis() {
+ return mNetworkRequestDurationMillis;
+ }
+
+ /** Dependency class */
+ public static class Dependencies {
+ // Returns a timestamp with the time base of SystemClock.elapsedRealtime to keep durations
+ // relative to start time and avoid timezone change, including time spent in deep sleep.
+ public long getElapsedRealtime() {
+ return SystemClock.elapsedRealtime();
+ }
+ }
+}
diff --git a/service/src/com/android/server/connectivity/NetworkRequestStateStatsMetrics.java b/service/src/com/android/server/connectivity/NetworkRequestStateStatsMetrics.java
new file mode 100644
index 0000000..1bc654a
--- /dev/null
+++ b/service/src/com/android/server/connectivity/NetworkRequestStateStatsMetrics.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED;
+
+import android.annotation.NonNull;
+import android.net.NetworkRequest;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.ConnectivityStatsLog;
+
+import java.util.ArrayDeque;
+
+/**
+ * A Connectivity Service helper class to push atoms capturing network requests have been received
+ * and removed and its metadata.
+ *
+ * Atom events are logged in the ConnectivityStatsLog. Network request id: network request metadata
+ * hashmap is stored to calculate network request duration when it is removed.
+ *
+ * Note that this class is not thread-safe. The instance of the class needs to be
+ * synchronized in the callers when being used in multiple threads.
+ */
+public class NetworkRequestStateStatsMetrics {
+
+ private static final String TAG = "NetworkRequestStateStatsMetrics";
+ private static final int CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC = 0;
+ private static final int CMD_SEND_MAYBE_ENQUEUE_NETWORK_REQUEST_STATE_METRIC = 1;
+
+ @VisibleForTesting
+ static final int MAX_QUEUED_REQUESTS = 20;
+
+ // Stats logging frequency is limited to 10 ms at least, 500ms are taken as a safely margin
+ // for cases of longer periods of frequent network requests.
+ private static final int ATOM_INTERVAL_MS = 500;
+ private final StatsLoggingHandler mStatsLoggingHandler;
+
+ private final Dependencies mDependencies;
+
+ private final NetworkRequestStateInfo.Dependencies mNRStateInfoDeps;
+ private final SparseArray<NetworkRequestStateInfo> mNetworkRequestsActive;
+
+ public NetworkRequestStateStatsMetrics() {
+ this(new Dependencies(), new NetworkRequestStateInfo.Dependencies());
+ }
+
+ @VisibleForTesting
+ NetworkRequestStateStatsMetrics(Dependencies deps,
+ NetworkRequestStateInfo.Dependencies nrStateInfoDeps) {
+ mNetworkRequestsActive = new SparseArray<>();
+ mDependencies = deps;
+ mNRStateInfoDeps = nrStateInfoDeps;
+ HandlerThread handlerThread = mDependencies.makeHandlerThread(TAG);
+ handlerThread.start();
+ mStatsLoggingHandler = new StatsLoggingHandler(handlerThread.getLooper());
+ }
+
+ /**
+ * Register network request receive event, push RECEIVE atom
+ *
+ * @param networkRequest network request received
+ */
+ public void onNetworkRequestReceived(NetworkRequest networkRequest) {
+ if (mNetworkRequestsActive.contains(networkRequest.requestId)) {
+ Log.w(TAG, "Received already registered network request, id = "
+ + networkRequest.requestId);
+ } else {
+ Log.d(TAG, "Registered nr with ID = " + networkRequest.requestId
+ + ", package_uid = " + networkRequest.networkCapabilities.getRequestorUid());
+ NetworkRequestStateInfo networkRequestStateInfo = new NetworkRequestStateInfo(
+ networkRequest, mNRStateInfoDeps);
+ mNetworkRequestsActive.put(networkRequest.requestId, networkRequestStateInfo);
+ mStatsLoggingHandler.sendMessage(Message.obtain(
+ mStatsLoggingHandler,
+ CMD_SEND_MAYBE_ENQUEUE_NETWORK_REQUEST_STATE_METRIC,
+ networkRequestStateInfo));
+ }
+ }
+
+ /**
+ * Register network request remove event, push REMOVE atom
+ *
+ * @param networkRequest network request removed
+ */
+ public void onNetworkRequestRemoved(NetworkRequest networkRequest) {
+ NetworkRequestStateInfo networkRequestStateInfo = mNetworkRequestsActive.get(
+ networkRequest.requestId);
+ if (networkRequestStateInfo == null) {
+ Log.w(TAG, "This NR hasn't been registered. NR id = " + networkRequest.requestId);
+ } else {
+ Log.d(TAG, "Removed nr with ID = " + networkRequest.requestId);
+ mNetworkRequestsActive.remove(networkRequest.requestId);
+ networkRequestStateInfo = new NetworkRequestStateInfo(networkRequestStateInfo);
+ networkRequestStateInfo.setNetworkRequestRemoved();
+ mStatsLoggingHandler.sendMessage(Message.obtain(
+ mStatsLoggingHandler,
+ CMD_SEND_MAYBE_ENQUEUE_NETWORK_REQUEST_STATE_METRIC,
+ networkRequestStateInfo));
+ }
+ }
+
+ /** Dependency class */
+ public static class Dependencies {
+ /**
+ * Creates a thread with provided tag.
+ *
+ * @param tag for the thread.
+ */
+ public HandlerThread makeHandlerThread(@NonNull final String tag) {
+ return new HandlerThread(tag);
+ }
+
+ /**
+ * @see Handler#sendMessageDelayed(Message, long)
+ */
+ public void sendMessageDelayed(@NonNull Handler handler, int what, long delayMillis) {
+ handler.sendMessageDelayed(Message.obtain(handler, what), delayMillis);
+ }
+
+ /**
+ * Gets number of millis since event.
+ *
+ * @param eventTimeMillis long timestamp in millis when the event occurred.
+ */
+ public long getMillisSinceEvent(long eventTimeMillis) {
+ return SystemClock.elapsedRealtime() - eventTimeMillis;
+ }
+
+ /**
+ * Writes a NETWORK_REQUEST_STATE_CHANGED event to ConnectivityStatsLog.
+ *
+ * @param networkRequestStateInfo NetworkRequestStateInfo containing network request info.
+ */
+ public void writeStats(NetworkRequestStateInfo networkRequestStateInfo) {
+ ConnectivityStatsLog.write(
+ NETWORK_REQUEST_STATE_CHANGED,
+ networkRequestStateInfo.getPackageUid(),
+ networkRequestStateInfo.getTransportTypes(),
+ networkRequestStateInfo.getNetCapabilityNotMetered(),
+ networkRequestStateInfo.getNetCapabilityInternet(),
+ networkRequestStateInfo.getNetworkRequestStateStatsType(),
+ networkRequestStateInfo.getNetworkRequestDurationMillis());
+ }
+ }
+
+ private class StatsLoggingHandler extends Handler {
+ private static final String TAG = "NetworkRequestsStateStatsLoggingHandler";
+
+ private final ArrayDeque<NetworkRequestStateInfo> mPendingState = new ArrayDeque<>();
+
+ private long mLastLogTime = 0;
+
+ StatsLoggingHandler(Looper looper) {
+ super(looper);
+ }
+
+ private void maybeEnqueueStatsMessage(NetworkRequestStateInfo networkRequestStateInfo) {
+ if (mPendingState.size() < MAX_QUEUED_REQUESTS) {
+ mPendingState.add(networkRequestStateInfo);
+ } else {
+ Log.w(TAG, "Too many network requests received within last " + ATOM_INTERVAL_MS
+ + " ms, dropping the last network request (id = "
+ + networkRequestStateInfo.getRequestId() + ") event");
+ return;
+ }
+ if (hasMessages(CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC)) {
+ return;
+ }
+ long millisSinceLastLog = mDependencies.getMillisSinceEvent(mLastLogTime);
+
+ if (millisSinceLastLog >= ATOM_INTERVAL_MS) {
+ sendMessage(
+ Message.obtain(this, CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC));
+ } else {
+ mDependencies.sendMessageDelayed(
+ this,
+ CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC,
+ ATOM_INTERVAL_MS - millisSinceLastLog);
+ }
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ NetworkRequestStateInfo loggingInfo;
+ switch (msg.what) {
+ case CMD_SEND_MAYBE_ENQUEUE_NETWORK_REQUEST_STATE_METRIC:
+ maybeEnqueueStatsMessage((NetworkRequestStateInfo) msg.obj);
+ break;
+ case CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC:
+ mLastLogTime = SystemClock.elapsedRealtime();
+ if (!mPendingState.isEmpty()) {
+ loggingInfo = mPendingState.remove();
+ mDependencies.writeStats(loggingInfo);
+ if (!mPendingState.isEmpty()) {
+ mDependencies.sendMessageDelayed(
+ this,
+ CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC,
+ ATOM_INTERVAL_MS);
+ }
+ }
+ break;
+ default: // fall out
+ }
+ }
+ }
+}
diff --git a/service/src/com/android/server/connectivity/RoutingCoordinatorService.java b/service/src/com/android/server/connectivity/RoutingCoordinatorService.java
index 3350d2d..742a2cc 100644
--- a/service/src/com/android/server/connectivity/RoutingCoordinatorService.java
+++ b/service/src/com/android/server/connectivity/RoutingCoordinatorService.java
@@ -171,7 +171,8 @@
}
final ForwardingPair fwp = new ForwardingPair(fromIface, toIface);
if (mForwardedInterfaces.contains(fwp)) {
- throw new IllegalStateException("Forward already exists between ifaces "
+ // TODO: remove if no reports are observed from the below log
+ Log.wtf(TAG, "Forward already exists between ifaces "
+ fromIface + " → " + toIface);
}
mForwardedInterfaces.add(fwp);
diff --git a/service/src/com/android/server/connectivity/SatelliteAccessController.java b/service/src/com/android/server/connectivity/SatelliteAccessController.java
new file mode 100644
index 0000000..b53abce
--- /dev/null
+++ b/service/src/com/android/server/connectivity/SatelliteAccessController.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.app.role.OnRoleHoldersChangedListener;
+import android.app.role.RoleManager;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Handler;
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * Tracks the uid of all the default messaging application which are role_sms role and
+ * satellite_communication permission complaint and requests ConnectivityService to create multi
+ * layer request with satellite internet access support for the default message application.
+ * @hide
+ */
+public class SatelliteAccessController {
+ private static final String TAG = SatelliteAccessController.class.getSimpleName();
+ private final Context mContext;
+ private final Dependencies mDeps;
+ private final DefaultMessageRoleListener mDefaultMessageRoleListener;
+ private final UserManager mUserManager;
+ private final Consumer<Set<Integer>> mCallback;
+ private final Handler mConnectivityServiceHandler;
+
+ // At this sparseArray, Key is userId and values are uids of SMS apps that are allowed
+ // to use satellite network as fallback.
+ private final SparseArray<Set<Integer>> mAllUsersSatelliteNetworkFallbackUidCache =
+ new SparseArray<>();
+
+ /**
+ * Monitor {@link android.app.role.OnRoleHoldersChangedListener#onRoleHoldersChanged(String,
+ * UserHandle)},
+ *
+ */
+ private final class DefaultMessageRoleListener
+ implements OnRoleHoldersChangedListener {
+ @Override
+ public void onRoleHoldersChanged(String role, UserHandle userHandle) {
+ if (RoleManager.ROLE_SMS.equals(role)) {
+ Log.i(TAG, "ROLE_SMS Change detected ");
+ onRoleSmsChanged(userHandle);
+ }
+ }
+
+ public void register() {
+ try {
+ mDeps.addOnRoleHoldersChangedListenerAsUser(
+ mConnectivityServiceHandler::post, this, UserHandle.ALL);
+ } catch (RuntimeException e) {
+ Log.wtf(TAG, "Could not register satellite controller listener due to " + e);
+ }
+ }
+ }
+
+ public SatelliteAccessController(@NonNull final Context c,
+ Consumer<Set<Integer>> callback,
+ @NonNull final Handler connectivityServiceInternalHandler) {
+ this(c, new Dependencies(c), callback, connectivityServiceInternalHandler);
+ }
+
+ public static class Dependencies {
+ private final RoleManager mRoleManager;
+
+ private Dependencies(Context context) {
+ mRoleManager = context.getSystemService(RoleManager.class);
+ }
+
+ /** See {@link RoleManager#getRoleHoldersAsUser(String, UserHandle)} */
+ public List<String> getRoleHoldersAsUser(String roleName, UserHandle userHandle) {
+ return mRoleManager.getRoleHoldersAsUser(roleName, userHandle);
+ }
+
+ /** See {@link RoleManager#addOnRoleHoldersChangedListenerAsUser} */
+ public void addOnRoleHoldersChangedListenerAsUser(@NonNull Executor executor,
+ @NonNull OnRoleHoldersChangedListener listener, UserHandle user) {
+ mRoleManager.addOnRoleHoldersChangedListenerAsUser(executor, listener, user);
+ }
+ }
+
+ @VisibleForTesting
+ SatelliteAccessController(@NonNull final Context c, @NonNull final Dependencies deps,
+ Consumer<Set<Integer>> callback,
+ @NonNull final Handler connectivityServiceInternalHandler) {
+ mContext = c;
+ mDeps = deps;
+ mUserManager = mContext.getSystemService(UserManager.class);
+ mDefaultMessageRoleListener = new DefaultMessageRoleListener();
+ mCallback = callback;
+ mConnectivityServiceHandler = connectivityServiceInternalHandler;
+ }
+
+ private Set<Integer> updateSatelliteNetworkFallbackUidListCache(List<String> packageNames,
+ @NonNull UserHandle userHandle) {
+ Set<Integer> fallbackUids = new ArraySet<>();
+ PackageManager pm =
+ mContext.createContextAsUser(userHandle, 0).getPackageManager();
+ if (pm != null) {
+ for (String packageName : packageNames) {
+ // Check if SATELLITE_COMMUNICATION permission is enabled for default sms
+ // application package before adding it part of satellite network fallback uid
+ // cache list.
+ if (isSatellitePermissionEnabled(pm, packageName)) {
+ int uid = getUidForPackage(pm, packageName);
+ if (uid != Process.INVALID_UID) {
+ fallbackUids.add(uid);
+ }
+ }
+ }
+ } else {
+ Log.wtf(TAG, "package manager found null");
+ }
+ return fallbackUids;
+ }
+
+ //Check if satellite communication is enabled for the package
+ private boolean isSatellitePermissionEnabled(PackageManager packageManager,
+ String packageName) {
+ return packageManager.checkPermission(
+ Manifest.permission.SATELLITE_COMMUNICATION, packageName)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ private int getUidForPackage(PackageManager packageManager, String pkgName) {
+ if (pkgName == null) {
+ return Process.INVALID_UID;
+ }
+ try {
+ ApplicationInfo applicationInfo = packageManager.getApplicationInfo(pkgName, 0);
+ return applicationInfo.uid;
+ } catch (PackageManager.NameNotFoundException exception) {
+ Log.e(TAG, "Unable to find uid for package: " + pkgName);
+ }
+ return Process.INVALID_UID;
+ }
+
+ // on Role sms change triggered by OnRoleHoldersChangedListener()
+ // TODO(b/326373613): using UserLifecycleListener, callback to be received when user removed for
+ // user delete scenario. This to be used to update uid list and ML Layer request can also be
+ // updated.
+ private void onRoleSmsChanged(@NonNull UserHandle userHandle) {
+ int userId = userHandle.getIdentifier();
+ if (userId == Process.INVALID_UID) {
+ Log.wtf(TAG, "Invalid User Id");
+ return;
+ }
+
+ //Returns empty list if no package exists
+ final List<String> packageNames =
+ mDeps.getRoleHoldersAsUser(RoleManager.ROLE_SMS, userHandle);
+
+ // Store previous satellite fallback uid available
+ final Set<Integer> prevUidsForUser =
+ mAllUsersSatelliteNetworkFallbackUidCache.get(userId, new ArraySet<>());
+
+ Log.i(TAG, "currentUser : role_sms_packages: " + userId + " : " + packageNames);
+ final Set<Integer> newUidsForUser = !packageNames.isEmpty()
+ ? updateSatelliteNetworkFallbackUidListCache(packageNames, userHandle)
+ : new ArraySet<>();
+ Log.i(TAG, "satellite_fallback_uid: " + newUidsForUser);
+
+ // on Role change, update the multilayer request at ConnectivityService with updated
+ // satellite network fallback uid cache list of multiple users as applicable
+ if (newUidsForUser.equals(prevUidsForUser)) {
+ return;
+ }
+
+ mAllUsersSatelliteNetworkFallbackUidCache.put(userId, newUidsForUser);
+
+ // Merge all uids of multiple users available
+ Set<Integer> mergedSatelliteNetworkFallbackUidCache = new ArraySet<>();
+ for (int i = 0; i < mAllUsersSatelliteNetworkFallbackUidCache.size(); i++) {
+ mergedSatelliteNetworkFallbackUidCache.addAll(
+ mAllUsersSatelliteNetworkFallbackUidCache.valueAt(i));
+ }
+ Log.i(TAG, "merged uid list for multi layer request : "
+ + mergedSatelliteNetworkFallbackUidCache);
+
+ // trigger multiple layer request for satellite network fallback of multi user uids
+ mCallback.accept(mergedSatelliteNetworkFallbackUidCache);
+ }
+
+ private List<String> getRoleSmsChangedPackageName(UserHandle userHandle) {
+ try {
+ return mDeps.getRoleHoldersAsUser(RoleManager.ROLE_SMS, userHandle);
+ } catch (RuntimeException e) {
+ Log.wtf(TAG, "Could not get package name at role sms change update due to: " + e);
+ return null;
+ }
+ }
+
+ /** Register OnRoleHoldersChangedListener */
+ public void start() {
+ mConnectivityServiceHandler.post(this::updateAllUserRoleSmsUids);
+ mDefaultMessageRoleListener.register();
+ }
+
+ private void updateAllUserRoleSmsUids() {
+ List<UserHandle> existingUsers = mUserManager.getUserHandles(true /* excludeDying */);
+ // Iterate through the user handles and obtain their uids with role sms and satellite
+ // communication permission
+ for (UserHandle userHandle : existingUsers) {
+ onRoleSmsChanged(userHandle);
+ }
+ }
+}
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index 9f1debc..f7b42a6 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -28,33 +28,35 @@
// though they are not in the current.txt files.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
java_library {
- name: "net-utils-device-common",
- srcs: [
- "device/com/android/net/module/util/arp/ArpPacket.java",
- "device/com/android/net/module/util/DeviceConfigUtils.java",
- "device/com/android/net/module/util/DomainUtils.java",
- "device/com/android/net/module/util/FdEventsReader.java",
- "device/com/android/net/module/util/NetworkMonitorUtils.java",
- "device/com/android/net/module/util/PacketReader.java",
- "device/com/android/net/module/util/SharedLog.java",
- "device/com/android/net/module/util/SocketUtils.java",
- "device/com/android/net/module/util/FeatureVersions.java",
- // This library is used by system modules, for which the system health impact of Kotlin
- // has not yet been evaluated. Annotations may need jarjar'ing.
- // "src_devicecommon/**/*.kt",
- ],
- sdk_version: "module_current",
- min_sdk_version: "30",
- target_sdk_version: "30",
- apex_available: [
- "//apex_available:anyapex",
- "//apex_available:platform",
- ],
- visibility: [
+ name: "net-utils-device-common",
+ srcs: [
+ "device/com/android/net/module/util/arp/ArpPacket.java",
+ "device/com/android/net/module/util/DeviceConfigUtils.java",
+ "device/com/android/net/module/util/DomainUtils.java",
+ "device/com/android/net/module/util/FdEventsReader.java",
+ "device/com/android/net/module/util/NetworkMonitorUtils.java",
+ "device/com/android/net/module/util/PacketReader.java",
+ "device/com/android/net/module/util/SharedLog.java",
+ "device/com/android/net/module/util/SocketUtils.java",
+ "device/com/android/net/module/util/FeatureVersions.java",
+ "device/com/android/net/module/util/HandlerUtils.java",
+ // This library is used by system modules, for which the system health impact of Kotlin
+ // has not yet been evaluated. Annotations may need jarjar'ing.
+ // "src_devicecommon/**/*.kt",
+ ],
+ sdk_version: "module_current",
+ min_sdk_version: "30",
+ target_sdk_version: "30",
+ apex_available: [
+ "//apex_available:anyapex",
+ "//apex_available:platform",
+ ],
+ visibility: [
"//frameworks/base/packages/Tethering",
"//packages/modules/Connectivity:__subpackages__",
"//packages/modules/Connectivity/framework:__subpackages__",
@@ -64,26 +66,26 @@
"//frameworks/opt/net/telephony",
"//packages/modules/NetworkStack:__subpackages__",
"//packages/modules/CaptivePortalLogin",
- ],
- static_libs: [
- "net-utils-framework-common",
- ],
- libs: [
- "androidx.annotation_annotation",
- "framework-annotations-lib",
- "framework-configinfrastructure",
- "framework-connectivity.stubs.module_lib",
- ],
- lint: {
- strict_updatability_linting: true,
- error_checks: ["NewApi"],
- },
+ ],
+ static_libs: [
+ "net-utils-framework-common",
+ ],
+ libs: [
+ "androidx.annotation_annotation",
+ "framework-annotations-lib",
+ "framework-configinfrastructure",
+ "framework-connectivity.stubs.module_lib",
+ ],
+ lint: {
+ strict_updatability_linting: true,
+ error_checks: ["NewApi"],
+ },
}
java_defaults {
name: "lib_mockito_extended",
static_libs: [
- "mockito-target-extended-minus-junit4"
+ "mockito-target-extended-minus-junit4",
],
jni_libs: [
"libdexmakerjvmtiagent",
@@ -94,12 +96,12 @@
java_library {
name: "net-utils-dnspacket-common",
srcs: [
- "framework/**/DnsPacket.java",
- "framework/**/DnsPacketUtils.java",
- "framework/**/DnsSvcbPacket.java",
- "framework/**/DnsSvcbRecord.java",
- "framework/**/HexDump.java",
- "framework/**/NetworkStackConstants.java",
+ "framework/**/DnsPacket.java",
+ "framework/**/DnsPacketUtils.java",
+ "framework/**/DnsSvcbPacket.java",
+ "framework/**/DnsSvcbRecord.java",
+ "framework/**/HexDump.java",
+ "framework/**/NetworkStackConstants.java",
],
sdk_version: "module_current",
visibility: [
@@ -186,6 +188,33 @@
},
}
+// The net-utils-multicast-forwarding-structs library requires the callers to
+// contain net-utils-device-common-bpf.
+java_library {
+ name: "net-utils-multicast-forwarding-structs",
+ srcs: [
+ "device/com/android/net/module/util/structs/StructMf6cctl.java",
+ "device/com/android/net/module/util/structs/StructMif6ctl.java",
+ "device/com/android/net/module/util/structs/StructMrt6Msg.java",
+ ],
+ sdk_version: "module_current",
+ min_sdk_version: "30",
+ visibility: [
+ "//packages/modules/Connectivity:__subpackages__",
+ ],
+ libs: [
+ // Only Struct.java is needed from "net-utils-device-common-bpf"
+ "net-utils-device-common-bpf",
+ ],
+ apex_available: [
+ "com.android.tethering",
+ ],
+ lint: {
+ strict_updatability_linting: true,
+ error_checks: ["NewApi"],
+ },
+}
+
// The net-utils-device-common-netlink library requires the callers to contain
// net-utils-device-common-struct.
java_library {
@@ -246,7 +275,7 @@
"//apex_available:platform",
],
lint: {
- strict_updatability_linting: true,
+ baseline_filename: "lint-baseline.xml",
error_checks: ["NewApi"],
},
}
@@ -270,13 +299,11 @@
"//cts/tests/tests/net",
"//cts/tests/tests/wifi",
"//packages/modules/Connectivity/tests/cts/net",
- "//frameworks/base/packages/Tethering",
"//packages/modules/Connectivity/Tethering",
"//frameworks/base/tests:__subpackages__",
"//frameworks/opt/net/ike",
"//frameworks/opt/telephony",
"//frameworks/base/wifi:__subpackages__",
- "//frameworks/base/packages/Connectivity:__subpackages__",
"//packages/modules/Connectivity:__subpackages__",
"//packages/modules/NetworkStack:__subpackages__",
"//packages/modules/CaptivePortalLogin",
@@ -295,6 +322,10 @@
"-Xep:NullablePrimitive:ERROR",
],
},
+ apex_available: [
+ "//apex_available:platform",
+ "com.android.tethering",
+ ],
}
java_library {
@@ -459,10 +490,10 @@
filegroup {
name: "net-utils-wifi-service-common-srcs",
srcs: [
- "device/android/net/NetworkFactory.java",
- "device/android/net/NetworkFactoryImpl.java",
- "device/android/net/NetworkFactoryLegacyImpl.java",
- "device/android/net/NetworkFactoryShim.java",
+ "device/android/net/NetworkFactory.java",
+ "device/android/net/NetworkFactoryImpl.java",
+ "device/android/net/NetworkFactoryLegacyImpl.java",
+ "device/android/net/NetworkFactoryShim.java",
],
visibility: [
"//frameworks/opt/net/wifi/service",
diff --git a/staticlibs/client-libs/Android.bp b/staticlibs/client-libs/Android.bp
index c938dd6..f665584 100644
--- a/staticlibs/client-libs/Android.bp
+++ b/staticlibs/client-libs/Android.bp
@@ -1,4 +1,5 @@
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -10,17 +11,17 @@
apex_available: [
"//apex_available:platform",
"com.android.tethering",
- "com.android.wifi"
+ "com.android.wifi",
],
visibility: [
"//packages/modules/Connectivity:__subpackages__",
"//frameworks/base/services:__subpackages__",
"//frameworks/base/packages:__subpackages__",
- "//packages/modules/Wifi/service:__subpackages__"
+ "//packages/modules/Wifi/service:__subpackages__",
],
libs: ["androidx.annotation_annotation"],
static_libs: [
"netd_aidl_interface-lateststable-java",
- "netd_event_listener_interface-lateststable-java"
- ]
+ "netd_event_listener_interface-lateststable-java",
+ ],
}
diff --git a/staticlibs/client-libs/tests/unit/Android.bp b/staticlibs/client-libs/tests/unit/Android.bp
index 03e3e70..7aafd69 100644
--- a/staticlibs/client-libs/tests/unit/Android.bp
+++ b/staticlibs/client-libs/tests/unit/Android.bp
@@ -1,4 +1,5 @@
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -26,7 +27,7 @@
"//packages/modules/Connectivity/tests:__subpackages__",
"//packages/modules/Connectivity/Tethering/tests:__subpackages__",
"//packages/modules/NetworkStack/tests/integration",
- ]
+ ],
}
android_test {
diff --git a/staticlibs/device/com/android/net/module/util/BpfBitmap.java b/staticlibs/device/com/android/net/module/util/BpfBitmap.java
index acb3ca5..b62a430 100644
--- a/staticlibs/device/com/android/net/module/util/BpfBitmap.java
+++ b/staticlibs/device/com/android/net/module/util/BpfBitmap.java
@@ -38,8 +38,7 @@
* @param path The path of the BPF map.
*/
public BpfBitmap(@NonNull String path) throws ErrnoException {
- mBpfMap = new BpfMap<Struct.S32, Struct.S64>(path, BpfMap.BPF_F_RDWR,
- Struct.S32.class, Struct.S64.class);
+ mBpfMap = new BpfMap<>(path, Struct.S32.class, Struct.S64.class);
}
/**
diff --git a/staticlibs/device/com/android/net/module/util/BpfMap.java b/staticlibs/device/com/android/net/module/util/BpfMap.java
index e3ef0f0..da77ae8 100644
--- a/staticlibs/device/com/android/net/module/util/BpfMap.java
+++ b/staticlibs/device/com/android/net/module/util/BpfMap.java
@@ -27,7 +27,6 @@
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
-import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.NoSuchElementException;
@@ -110,6 +109,17 @@
}
/**
+ * Create a R/W BpfMap map wrapper with "path" of filesystem.
+ *
+ * @throws ErrnoException if the BPF map associated with {@code path} cannot be retrieved.
+ * @throws NullPointerException if {@code path} is null.
+ */
+ public BpfMap(@NonNull final String path, final Class<K> key,
+ final Class<V> value) throws ErrnoException, NullPointerException {
+ this(path, BPF_F_RDWR, key, value);
+ }
+
+ /**
* Update an existing or create a new key -> value entry in an eBbpf map.
* (use insertOrReplaceEntry() if you need to know whether insert or replace happened)
*/
diff --git a/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
index 42f26f4..5b7cbb8 100644
--- a/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
+++ b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
@@ -64,9 +64,6 @@
@VisibleForTesting
public static final long DEFAULT_PACKAGE_VERSION = 1000;
- private static final String CORE_NETWORKING_TRUNK_STABLE_NAMESPACE = "android_core_networking";
- private static final String CORE_NETWORKING_TRUNK_STABLE_FLAG_PACKAGE = "com.android.net.flags";
-
@VisibleForTesting
public static void resetPackageVersionCacheForTest() {
sPackageVersion = -1;
@@ -409,31 +406,4 @@
return pkgs.get(0).activityInfo.applicationInfo.packageName;
}
-
- /**
- * Check whether one specific trunk stable flag in android_core_networking namespace is enabled.
- * This method reads trunk stable feature flag value from DeviceConfig directly since
- * java_aconfig_library soong module is not available in the mainline branch.
- * After the mainline branch support the aconfig soong module, this function must be removed and
- * java_aconfig_library must be used instead to check if the feature is enabled.
- *
- * @param flagName The name of the trunk stable flag
- * @return true if this feature is enabled, or false if disabled.
- */
- public static boolean isTrunkStableFeatureEnabled(final String flagName) {
- return isTrunkStableFeatureEnabled(
- CORE_NETWORKING_TRUNK_STABLE_NAMESPACE,
- CORE_NETWORKING_TRUNK_STABLE_FLAG_PACKAGE,
- flagName
- );
- }
-
- private static boolean isTrunkStableFeatureEnabled(final String namespace,
- final String packageName, final String flagName) {
- return DeviceConfig.getBoolean(
- namespace,
- packageName + "." + flagName,
- false /* defaultValue */
- );
- }
}
diff --git a/staticlibs/device/com/android/net/module/util/HandlerUtils.java b/staticlibs/device/com/android/net/module/util/HandlerUtils.java
new file mode 100644
index 0000000..c620368
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/HandlerUtils.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import android.annotation.NonNull;
+import android.os.Handler;
+import android.os.Looper;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Helper class for Handler related utilities.
+ *
+ * @hide
+ */
+public class HandlerUtils {
+ /**
+ * Runs the specified task synchronously for dump method.
+ * <p>
+ * If the current thread is the same as the handler thread, then the runnable
+ * runs immediately without being enqueued. Otherwise, posts the runnable
+ * to the handler and waits for it to complete before returning.
+ * </p><p>
+ * This method is dangerous! Improper use can result in deadlocks.
+ * Never call this method while any locks are held or use it in a
+ * possibly re-entrant manner.
+ * </p><p>
+ * This method is made to let dump method access members on the handler thread to
+ * avoid concurrent access problems or races.
+ * </p><p>
+ * If timeout occurs then this method returns <code>false</code> but the runnable
+ * will remain posted on the handler and may already be in progress or
+ * complete at a later time.
+ * </p><p>
+ * When using this method, be sure to use {@link Looper#quitSafely} when
+ * quitting the looper. Otherwise {@link #runWithScissorsForDump} may hang indefinitely.
+ * (TODO: We should fix this by making MessageQueue aware of blocking runnables.)
+ * </p>
+ *
+ * @param h The target handler.
+ * @param r The Runnable that will be executed synchronously.
+ * @param timeout The timeout in milliseconds, or 0 to not wait at all.
+ *
+ * @return Returns true if the Runnable was successfully executed.
+ * Returns false on failure, usually because the
+ * looper processing the message queue is exiting.
+ *
+ * @hide
+ */
+ public static boolean runWithScissorsForDump(@NonNull Handler h, @NonNull Runnable r,
+ long timeout) {
+ if (r == null) {
+ throw new IllegalArgumentException("runnable must not be null");
+ }
+ if (timeout < 0) {
+ throw new IllegalArgumentException("timeout must be non-negative");
+ }
+ if (Looper.myLooper() == h.getLooper()) {
+ r.run();
+ return true;
+ }
+
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ // Don't crash in the handler if something in the runnable throws an exception,
+ // but try to propagate the exception to the caller.
+ AtomicReference<RuntimeException> exceptionRef = new AtomicReference<>();
+ h.post(() -> {
+ try {
+ r.run();
+ } catch (RuntimeException e) {
+ exceptionRef.set(e);
+ }
+ latch.countDown();
+ });
+
+ try {
+ if (!latch.await(timeout, TimeUnit.MILLISECONDS)) {
+ return false;
+ }
+ } catch (InterruptedException e) {
+ exceptionRef.compareAndSet(null, new IllegalStateException("Thread interrupted", e));
+ }
+
+ final RuntimeException e = exceptionRef.get();
+ if (e != null) throw e;
+ return true;
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/Ipv6Utils.java b/staticlibs/device/com/android/net/module/util/Ipv6Utils.java
index d538221..497b8cb 100644
--- a/staticlibs/device/com/android/net/module/util/Ipv6Utils.java
+++ b/staticlibs/device/com/android/net/module/util/Ipv6Utils.java
@@ -166,6 +166,24 @@
}
/**
+ * Build an ICMPv6 Router Solicitation packet from the required specified parameters without
+ * ethernet header.
+ */
+ public static ByteBuffer buildRsPacket(
+ final Inet6Address srcIp, final Inet6Address dstIp, final ByteBuffer... options) {
+ final RsHeader rsHeader = new RsHeader((int) 0 /* reserved */);
+ final ByteBuffer[] payload =
+ buildIcmpv6Payload(
+ ByteBuffer.wrap(rsHeader.writeToBytes(ByteOrder.BIG_ENDIAN)), options);
+ return buildIcmpv6Packet(
+ srcIp,
+ dstIp,
+ (byte) ICMPV6_ROUTER_SOLICITATION /* type */,
+ (byte) 0 /* code */,
+ payload);
+ }
+
+ /**
* Build an ICMPv6 Echo Request packet from the required specified parameters.
*/
public static ByteBuffer buildEchoRequestPacket(final MacAddress srcMac,
@@ -176,11 +194,21 @@
}
/**
- * Build an ICMPv6 Echo Reply packet without ethernet header.
+ * Build an ICMPv6 Echo Request packet from the required specified parameters without ethernet
+ * header.
*/
- public static ByteBuffer buildEchoReplyPacket(final Inet6Address srcIp,
+ public static ByteBuffer buildEchoRequestPacket(final Inet6Address srcIp,
final Inet6Address dstIp) {
final ByteBuffer payload = ByteBuffer.allocate(4); // ID and Sequence number may be zero.
+ return buildIcmpv6Packet(srcIp, dstIp, (byte) ICMPV6_ECHO_REQUEST_TYPE /* type */,
+ (byte) 0 /* code */,
+ payload);
+ }
+
+ /** Build an ICMPv6 Echo Reply packet without ethernet header. */
+ public static ByteBuffer buildEchoReplyPacket(
+ final Inet6Address srcIp, final Inet6Address dstIp) {
+ final ByteBuffer payload = ByteBuffer.allocate(4); // ID and Sequence number may be zero.
return buildIcmpv6Packet(srcIp, dstIp, (byte) ICMPV6_ECHO_REPLY_TYPE /* type */,
(byte) 0 /* code */, payload);
}
diff --git a/staticlibs/device/com/android/net/module/util/arp/ArpPacket.java b/staticlibs/device/com/android/net/module/util/arp/ArpPacket.java
index dab9694..bf447d3 100644
--- a/staticlibs/device/com/android/net/module/util/arp/ArpPacket.java
+++ b/staticlibs/device/com/android/net/module/util/arp/ArpPacket.java
@@ -45,14 +45,18 @@
public class ArpPacket {
private static final String TAG = "ArpPacket";
+ public final MacAddress destination;
+ public final MacAddress source;
public final short opCode;
public final Inet4Address senderIp;
public final Inet4Address targetIp;
public final MacAddress senderHwAddress;
public final MacAddress targetHwAddress;
- ArpPacket(short opCode, MacAddress senderHwAddress, Inet4Address senderIp,
- MacAddress targetHwAddress, Inet4Address targetIp) {
+ ArpPacket(MacAddress destination, MacAddress source, short opCode, MacAddress senderHwAddress,
+ Inet4Address senderIp, MacAddress targetHwAddress, Inet4Address targetIp) {
+ this.destination = destination;
+ this.source = source;
this.opCode = opCode;
this.senderHwAddress = senderHwAddress;
this.senderIp = senderIp;
@@ -145,7 +149,9 @@
buffer.get(targetHwAddress);
buffer.get(targetIp);
- return new ArpPacket(opCode, MacAddress.fromBytes(senderHwAddress),
+ return new ArpPacket(MacAddress.fromBytes(l2dst),
+ MacAddress.fromBytes(l2src), opCode,
+ MacAddress.fromBytes(senderHwAddress),
(Inet4Address) InetAddress.getByAddress(senderIp),
MacAddress.fromBytes(targetHwAddress),
(Inet4Address) InetAddress.getByAddress(targetIp));
diff --git a/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java b/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
index 4f76577..fecaa09 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
@@ -24,14 +24,14 @@
import static android.system.OsConstants.IPPROTO_UDP;
import static android.system.OsConstants.NETLINK_INET_DIAG;
-import static com.android.net.module.util.netlink.NetlinkConstants.NLMSG_DONE;
import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DESTROY;
import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DIAG_BY_FAMILY;
-import static com.android.net.module.util.netlink.NetlinkConstants.hexify;
+import static com.android.net.module.util.netlink.NetlinkConstants.SOCKDIAG_MSG_HEADER_SIZE;
import static com.android.net.module.util.netlink.NetlinkConstants.stringForAddressFamily;
import static com.android.net.module.util.netlink.NetlinkConstants.stringForProtocol;
import static com.android.net.module.util.netlink.NetlinkUtils.DEFAULT_RECV_BUFSIZE;
import static com.android.net.module.util.netlink.NetlinkUtils.IO_TIMEOUT_MS;
+import static com.android.net.module.util.netlink.NetlinkUtils.SOCKET_RECV_BUFSIZE;
import static com.android.net.module.util.netlink.NetlinkUtils.TCP_ALIVE_STATE_FILTER;
import static com.android.net.module.util.netlink.NetlinkUtils.connectToKernel;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
@@ -59,8 +59,11 @@
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import java.util.ArrayList;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
import java.util.function.Predicate;
/**
@@ -154,7 +157,8 @@
}
public StructInetDiagMsg inetDiagMsg;
-
+ // The netlink attributes.
+ public List<StructNlAttr> nlAttrs = new ArrayList<>();
@VisibleForTesting
public InetDiagMessage(@NonNull StructNlMsgHdr header) {
super(header);
@@ -172,6 +176,19 @@
if (msg.inetDiagMsg == null) {
return null;
}
+ final int payloadLength = header.nlmsg_len - SOCKDIAG_MSG_HEADER_SIZE;
+ final ByteBuffer payload = byteBuffer.slice();
+ while (payload.position() < payloadLength) {
+ final StructNlAttr attr = StructNlAttr.parse(payload);
+ // Stop parsing for truncated or malformed attribute
+ if (attr == null) {
+ Log.wtf(TAG, "Got truncated or malformed attribute");
+ return null;
+ }
+
+ msg.nlAttrs.add(attr);
+ }
+
return msg;
}
@@ -265,7 +282,7 @@
int uid = INVALID_UID;
FileDescriptor fd = null;
try {
- fd = NetlinkUtils.netlinkSocketForProto(NETLINK_INET_DIAG);
+ fd = NetlinkUtils.netlinkSocketForProto(NETLINK_INET_DIAG, SOCKET_RECV_BUFSIZE);
connectToKernel(fd);
uid = lookupUid(protocol, local, remote, fd);
} catch (ErrnoException | SocketException | IllegalArgumentException
@@ -307,9 +324,8 @@
NetlinkUtils.receiveNetlinkAck(fd);
}
- private static void sendNetlinkDumpRequest(FileDescriptor fd, int proto, int states, int family)
- throws InterruptedIOException, ErrnoException {
- final byte[] dumpMsg = InetDiagMessage.inetDiagReqV2(
+ private static byte [] makeNetlinkDumpRequest(int proto, int states, int family) {
+ return InetDiagMessage.inetDiagReqV2(
proto,
null /* id */,
family,
@@ -318,51 +334,29 @@
0 /* pad */,
0 /* idiagExt */,
states);
- NetlinkUtils.sendMessage(fd, dumpMsg, 0, dumpMsg.length, IO_TIMEOUT_MS);
}
- private static int processNetlinkDumpAndDestroySockets(FileDescriptor dumpFd,
+ private static int processNetlinkDumpAndDestroySockets(byte[] dumpReq,
FileDescriptor destroyFd, int proto, Predicate<InetDiagMessage> filter)
- throws InterruptedIOException, ErrnoException {
- int destroyedSockets = 0;
-
- while (true) {
- final ByteBuffer buf = NetlinkUtils.recvMessage(
- dumpFd, DEFAULT_RECV_BUFSIZE, IO_TIMEOUT_MS);
-
- while (buf.remaining() > 0) {
- final int position = buf.position();
- final NetlinkMessage nlMsg = NetlinkMessage.parse(buf, NETLINK_INET_DIAG);
- if (nlMsg == null) {
- // Move to the position where parse started for error log.
- buf.position(position);
- Log.e(TAG, "Failed to parse netlink message: " + hexify(buf));
- break;
- }
-
- if (nlMsg.getHeader().nlmsg_type == NLMSG_DONE) {
- return destroyedSockets;
- }
-
- if (!(nlMsg instanceof InetDiagMessage)) {
- Log.wtf(TAG, "Received unexpected netlink message: " + nlMsg);
- continue;
- }
-
- final InetDiagMessage diagMsg = (InetDiagMessage) nlMsg;
- if (filter.test(diagMsg)) {
- try {
- sendNetlinkDestroyRequest(destroyFd, proto, diagMsg);
- destroyedSockets++;
- } catch (InterruptedIOException | ErrnoException e) {
- if (!(e instanceof ErrnoException
- && ((ErrnoException) e).errno == ENOENT)) {
- Log.e(TAG, "Failed to destroy socket: diagMsg=" + diagMsg + ", " + e);
- }
+ throws SocketException, InterruptedIOException, ErrnoException {
+ AtomicInteger destroyedSockets = new AtomicInteger(0);
+ Consumer<InetDiagMessage> handleNlDumpMsg = (diagMsg) -> {
+ if (filter.test(diagMsg)) {
+ try {
+ sendNetlinkDestroyRequest(destroyFd, proto, diagMsg);
+ destroyedSockets.getAndIncrement();
+ } catch (InterruptedIOException | ErrnoException e) {
+ if (!(e instanceof ErrnoException
+ && ((ErrnoException) e).errno == ENOENT)) {
+ Log.e(TAG, "Failed to destroy socket: diagMsg=" + diagMsg + ", " + e);
}
}
}
- }
+ };
+
+ NetlinkUtils.<InetDiagMessage>getAndProcessNetlinkDumpMessages(dumpReq,
+ NETLINK_INET_DIAG, InetDiagMessage.class, handleNlDumpMsg);
+ return destroyedSockets.get();
}
/**
@@ -420,31 +414,28 @@
private static void destroySockets(int proto, int states, Predicate<InetDiagMessage> filter)
throws ErrnoException, SocketException, InterruptedIOException {
- FileDescriptor dumpFd = null;
FileDescriptor destroyFd = null;
try {
- dumpFd = NetlinkUtils.createNetLinkInetDiagSocket();
destroyFd = NetlinkUtils.createNetLinkInetDiagSocket();
- connectToKernel(dumpFd);
connectToKernel(destroyFd);
for (int family : List.of(AF_INET, AF_INET6)) {
+ byte[] req = makeNetlinkDumpRequest(proto, states, family);
+
try {
- sendNetlinkDumpRequest(dumpFd, proto, states, family);
- } catch (InterruptedIOException | ErrnoException e) {
- Log.e(TAG, "Failed to send netlink dump request: " + e);
- continue;
- }
- final int destroyedSockets = processNetlinkDumpAndDestroySockets(
- dumpFd, destroyFd, proto, filter);
- Log.d(TAG, "Destroyed " + destroyedSockets + " sockets"
+ final int destroyedSockets = processNetlinkDumpAndDestroySockets(
+ req, destroyFd, proto, filter);
+ Log.d(TAG, "Destroyed " + destroyedSockets + " sockets"
+ ", proto=" + stringForProtocol(proto)
+ ", family=" + stringForAddressFamily(family)
+ ", states=" + states);
+ } catch (SocketException | InterruptedIOException | ErrnoException e) {
+ Log.e(TAG, "Failed to send netlink dump request or receive messages: " + e);
+ continue;
+ }
}
} finally {
- closeSocketQuietly(dumpFd);
closeSocketQuietly(destroyFd);
}
}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NduseroptMessage.java b/staticlibs/device/com/android/net/module/util/netlink/NduseroptMessage.java
index bdf574d..2e9a99b 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NduseroptMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NduseroptMessage.java
@@ -20,6 +20,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import java.net.Inet6Address;
import java.net.InetAddress;
@@ -63,6 +64,20 @@
/** The IP address that sent the packet containing the option. */
public final InetAddress srcaddr;
+ @VisibleForTesting
+ public NduseroptMessage(@NonNull final StructNlMsgHdr header, byte family, int optslen,
+ int ifindex, byte icmptype, byte icmpcode, @NonNull final NdOption option,
+ final InetAddress srcaddr) {
+ super(header);
+ this.family = family;
+ this.opts_len = optslen;
+ this.ifindex = ifindex;
+ this.icmp_type = icmptype;
+ this.icmp_code = icmpcode;
+ this.option = option;
+ this.srcaddr = srcaddr;
+ }
+
NduseroptMessage(@NonNull StructNlMsgHdr header, @NonNull ByteBuffer buf)
throws UnknownHostException {
super(header);
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
index 44c51d8..ad7a4d7 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
@@ -151,6 +151,9 @@
public static final int RTNLGRP_ND_USEROPT = 20;
public static final int RTMGRP_ND_USEROPT = 1 << (RTNLGRP_ND_USEROPT - 1);
+ // Netlink family
+ public static final short RTNL_FAMILY_IP6MR = 129;
+
// Device flags.
public static final int IFF_UP = 1 << 0;
public static final int IFF_LOWER_UP = 1 << 16;
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
index f1f30d3..541a375 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
@@ -30,6 +30,12 @@
import static android.system.OsConstants.SO_RCVTIMEO;
import static android.system.OsConstants.SO_SNDTIMEO;
+import static com.android.net.module.util.netlink.NetlinkConstants.hexify;
+import static com.android.net.module.util.netlink.NetlinkConstants.NLMSG_DONE;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTNL_FAMILY_IP6MR;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
+
import android.net.util.SocketUtils;
import android.system.ErrnoException;
import android.system.Os;
@@ -44,10 +50,14 @@
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.Inet6Address;
+import java.net.InetAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Objects;
+import java.util.function.Consumer;
/**
* Utilities for netlink related class that may not be able to fit into a specific class.
@@ -76,6 +86,7 @@
public static final int DEFAULT_RECV_BUFSIZE = 8 * 1024;
public static final int SOCKET_RECV_BUFSIZE = 64 * 1024;
+ public static final int SOCKET_DUMP_RECV_BUFSIZE = 128 * 1024;
/**
* Return whether the input ByteBuffer contains enough remaining bytes for
@@ -150,7 +161,7 @@
*/
public static void sendOneShotKernelMessage(int nlProto, byte[] msg) throws ErrnoException {
final String errPrefix = "Error in NetlinkSocket.sendOneShotKernelMessage";
- final FileDescriptor fd = netlinkSocketForProto(nlProto);
+ final FileDescriptor fd = netlinkSocketForProto(nlProto, SOCKET_RECV_BUFSIZE);
try {
connectToKernel(fd);
@@ -163,28 +174,24 @@
Log.e(TAG, errPrefix, e);
throw new ErrnoException(errPrefix, EIO, e);
} finally {
- try {
- SocketUtils.closeSocket(fd);
- } catch (IOException e) {
- // Nothing we can do here
- }
+ closeSocketQuietly(fd);
}
}
/**
- * Send an RTM_NEWADDR message to kernel to add or update an IPv6 address.
+ * Send an RTM_NEWADDR message to kernel to add or update an IP address.
*
* @param ifIndex interface index.
- * @param ip IPv6 address to be added.
- * @param prefixlen IPv6 address prefix length.
- * @param flags IPv6 address flags.
- * @param scope IPv6 address scope.
- * @param preferred The preferred lifetime of IPv6 address.
- * @param valid The valid lifetime of IPv6 address.
+ * @param ip IP address to be added.
+ * @param prefixlen IP address prefix length.
+ * @param flags IP address flags.
+ * @param scope IP address scope.
+ * @param preferred The preferred lifetime of IP address.
+ * @param valid The valid lifetime of IP address.
*/
- public static boolean sendRtmNewAddressRequest(int ifIndex, @NonNull final Inet6Address ip,
+ public static boolean sendRtmNewAddressRequest(int ifIndex, @NonNull final InetAddress ip,
short prefixlen, int flags, byte scope, long preferred, long valid) {
- Objects.requireNonNull(ip, "IPv6 address to be added should not be null.");
+ Objects.requireNonNull(ip, "IP address to be added should not be null.");
final byte[] msg = RtNetlinkAddressMessage.newRtmNewAddressMessage(1 /* seqNo*/, ip,
prefixlen, flags, scope, ifIndex, preferred, valid);
try {
@@ -218,22 +225,41 @@
}
/**
- * Create netlink socket with the given netlink protocol type.
+ * Create netlink socket with the given netlink protocol type and buffersize.
+ *
+ * @param nlProto the netlink protocol
+ * @param bufferSize the receive buffer size to set when the value is not 0
*
* @return fd the fileDescriptor of the socket.
* @throws ErrnoException if the FileDescriptor not connect to be created successfully
*/
- public static FileDescriptor netlinkSocketForProto(int nlProto) throws ErrnoException {
- final FileDescriptor fd = Os.socket(AF_NETLINK, SOCK_DGRAM, nlProto);
- Os.setsockoptInt(fd, SOL_SOCKET, SO_RCVBUF, SOCKET_RECV_BUFSIZE);
+ public static FileDescriptor netlinkSocketForProto(int nlProto, int bufferSize)
+ throws ErrnoException {
+ final FileDescriptor fd = Os.socket(AF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC, nlProto);
+ if (bufferSize > 0) {
+ Os.setsockoptInt(fd, SOL_SOCKET, SO_RCVBUF, bufferSize);
+ }
return fd;
}
/**
+ * Create netlink socket with the given netlink protocol type. Receive buffer size is not set.
+ *
+ * @param nlProto the netlink protocol
+ *
+ * @return fd the fileDescriptor of the socket.
+ * @throws ErrnoException if the FileDescriptor not connect to be created successfully
+ */
+ public static FileDescriptor netlinkSocketForProto(int nlProto)
+ throws ErrnoException {
+ return netlinkSocketForProto(nlProto, 0);
+ }
+
+ /**
* Construct a netlink inet_diag socket.
*/
public static FileDescriptor createNetLinkInetDiagSocket() throws ErrnoException {
- return Os.socket(AF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC, NETLINK_INET_DIAG);
+ return netlinkSocketForProto(NETLINK_INET_DIAG);
}
/**
@@ -308,4 +334,139 @@
}
private NetlinkUtils() {}
+
+ private static <T extends NetlinkMessage> void getAndProcessNetlinkDumpMessagesWithFd(
+ FileDescriptor fd, byte[] dumpRequestMessage, int nlFamily, Class<T> msgClass,
+ Consumer<T> func)
+ throws SocketException, InterruptedIOException, ErrnoException {
+ // connecToKernel throws ErrnoException and SocketException, should be handled by caller
+ connectToKernel(fd);
+
+ // sendMessage throws InterruptedIOException and ErrnoException,
+ // should be handled by caller
+ sendMessage(fd, dumpRequestMessage, 0, dumpRequestMessage.length, IO_TIMEOUT_MS);
+
+ while (true) {
+ // recvMessage throws ErrnoException, InterruptedIOException
+ // should be handled by caller
+ final ByteBuffer buf = recvMessage(
+ fd, NetlinkUtils.DEFAULT_RECV_BUFSIZE, IO_TIMEOUT_MS);
+
+ while (buf.remaining() > 0) {
+ final int position = buf.position();
+ final NetlinkMessage nlMsg = NetlinkMessage.parse(buf, nlFamily);
+ if (nlMsg == null) {
+ // Move to the position where parse started for error log.
+ buf.position(position);
+ Log.e(TAG, "Failed to parse netlink message: " + hexify(buf));
+ break;
+ }
+
+ if (nlMsg.getHeader().nlmsg_type == NLMSG_DONE) {
+ return;
+ }
+
+ if (!msgClass.isInstance(nlMsg)) {
+ Log.wtf(TAG, "Received unexpected netlink message: " + nlMsg);
+ continue;
+ }
+
+ final T msg = (T) nlMsg;
+ func.accept(msg);
+ }
+ }
+ }
+ /**
+ * Sends a netlink dump request and processes the returned dump messages
+ *
+ * @param <T> extends NetlinkMessage
+ * @param dumpRequestMessage netlink dump request message to be sent
+ * @param nlFamily netlink family
+ * @param msgClass expected class of the netlink message
+ * @param func function defined by caller to handle the dump messages
+ * @throws SocketException when fails to connect socket to kernel
+ * @throws InterruptedIOException when fails to read the dumpFd
+ * @throws ErrnoException when fails to create dump fd, send dump request
+ * or receive messages
+ */
+ public static <T extends NetlinkMessage> void getAndProcessNetlinkDumpMessages(
+ byte[] dumpRequestMessage, int nlFamily, Class<T> msgClass,
+ Consumer<T> func)
+ throws SocketException, InterruptedIOException, ErrnoException {
+ // Create socket
+ final FileDescriptor fd = netlinkSocketForProto(nlFamily, SOCKET_DUMP_RECV_BUFSIZE);
+ try {
+ getAndProcessNetlinkDumpMessagesWithFd(fd, dumpRequestMessage, nlFamily,
+ msgClass, func);
+ } finally {
+ closeSocketQuietly(fd);
+ }
+ }
+
+ /**
+ * Construct a RTM_GETROUTE message for dumping multicast IPv6 routes from kernel.
+ */
+ private static byte[] newIpv6MulticastRouteDumpRequest() {
+ final StructNlMsgHdr nlmsghdr = new StructNlMsgHdr();
+ nlmsghdr.nlmsg_type = NetlinkConstants.RTM_GETROUTE;
+ nlmsghdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
+ final short shortZero = 0;
+
+ // family must be RTNL_FAMILY_IP6MR to dump IPv6 multicast routes.
+ // dstLen, srcLen, tos and scope must be zero in FIB dump request.
+ // protocol, flags must be 0, and type must be RTN_MULTICAST (if not 0) for multicast
+ // dump request.
+ // table or RTA_TABLE attributes can be used to dump a specific routing table.
+ // RTA_OIF attribute can be used to dump only routes containing given oif.
+ // Here no attributes are set so the kernel can return all multicast routes.
+ final StructRtMsg rtMsg =
+ new StructRtMsg(RTNL_FAMILY_IP6MR /* family */, shortZero /* dstLen */,
+ shortZero /* srcLen */, shortZero /* tos */, shortZero /* table */,
+ shortZero /* protocol */, shortZero /* scope */, shortZero /* type */,
+ 0L /* flags */);
+ final RtNetlinkRouteMessage msg =
+ new RtNetlinkRouteMessage(nlmsghdr, rtMsg);
+
+ final int spaceRequired = StructNlMsgHdr.STRUCT_SIZE + StructRtMsg.STRUCT_SIZE;
+ nlmsghdr.nlmsg_len = spaceRequired;
+ final byte[] bytes = new byte[NetlinkConstants.alignedLengthOf(spaceRequired)];
+ final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
+ byteBuffer.order(ByteOrder.nativeOrder());
+ msg.pack(byteBuffer);
+ return bytes;
+ }
+
+ /**
+ * Get the list of IPv6 multicast route messages from kernel.
+ */
+ public static List<RtNetlinkRouteMessage> getIpv6MulticastRoutes() {
+ final byte[] dumpMsg = newIpv6MulticastRouteDumpRequest();
+ List<RtNetlinkRouteMessage> routes = new ArrayList<>();
+ Consumer<RtNetlinkRouteMessage> handleNlDumpMsg = (msg) -> {
+ if (msg.getRtmFamily() == RTNL_FAMILY_IP6MR) {
+ // Sent rtmFamily RTNL_FAMILY_IP6MR in dump request to make sure ipv6
+ // multicast routes are included in netlink reply messages, the kernel
+ // may also reply with other kind of routes, so we filter them out here.
+ routes.add(msg);
+ }
+ };
+ try {
+ NetlinkUtils.<RtNetlinkRouteMessage>getAndProcessNetlinkDumpMessages(
+ dumpMsg, NETLINK_ROUTE, RtNetlinkRouteMessage.class,
+ handleNlDumpMsg);
+ } catch (SocketException | InterruptedIOException | ErrnoException e) {
+ Log.e(TAG, "Failed to dump multicast routes");
+ return routes;
+ }
+
+ return routes;
+ }
+
+ private static void closeSocketQuietly(final FileDescriptor fd) {
+ try {
+ SocketUtils.closeSocket(fd);
+ } catch (IOException e) {
+ // Nothing we can do here
+ }
+ }
}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkAddressMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkAddressMessage.java
index cbe0ab0..4846df7 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkAddressMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkAddressMessage.java
@@ -16,6 +16,7 @@
package com.android.net.module.util.netlink;
+import static com.android.net.module.util.Inet4AddressUtils.getBroadcastAddress;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_ACK;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REPLACE;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
@@ -28,6 +29,7 @@
import com.android.net.module.util.HexDump;
+import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.nio.ByteBuffer;
@@ -48,6 +50,8 @@
*/
public class RtNetlinkAddressMessage extends NetlinkMessage {
public static final short IFA_ADDRESS = 1;
+ public static final short IFA_LOCAL = 2;
+ public static final short IFA_BROADCAST = 4;
public static final short IFA_CACHEINFO = 6;
public static final short IFA_FLAGS = 8;
@@ -71,6 +75,7 @@
mIfacacheInfo = structIfacacheInfo;
mFlags = flags;
}
+
private RtNetlinkAddressMessage(@NonNull StructNlMsgHdr header) {
this(header, null, null, null, 0);
}
@@ -158,6 +163,24 @@
// still be packed to ByteBuffer even if the flag is 0.
final StructNlAttr flags = new StructNlAttr(IFA_FLAGS, mFlags);
flags.pack(byteBuffer);
+
+ // Add the required IFA_LOCAL and IFA_BROADCAST attributes for IPv4 addresses. The IFA_LOCAL
+ // attribute represents the local address, which is equivalent to IFA_ADDRESS on a normally
+ // configured broadcast interface, however, for PPP interfaces, IFA_ADDRESS indicates the
+ // destination address and the local address is provided in the IFA_LOCAL attribute. If the
+ // IFA_LOCAL attribute is not present in the RTM_NEWADDR message, the kernel replies with an
+ // error netlink message with invalid parameters. IFA_BROADCAST is also required, otherwise
+ // the broadcast on the interface is 0.0.0.0. See include/uapi/linux/if_addr.h for details.
+ // For IPv6 addresses, the IFA_ADDRESS attribute applies and introduces no ambiguity.
+ if (mIpAddress instanceof Inet4Address) {
+ final StructNlAttr localAddress = new StructNlAttr(IFA_LOCAL, mIpAddress);
+ localAddress.pack(byteBuffer);
+
+ final Inet4Address broadcast =
+ getBroadcastAddress((Inet4Address) mIpAddress, mIfaddrmsg.prefixLen);
+ final StructNlAttr broadcastAddress = new StructNlAttr(IFA_BROADCAST, broadcast);
+ broadcastAddress.pack(byteBuffer);
+ }
}
/**
@@ -184,7 +207,7 @@
0 /* tstamp */);
msg.mFlags = flags;
- final byte[] bytes = new byte[msg.getRequiredSpace()];
+ final byte[] bytes = new byte[msg.getRequiredSpace(family)];
nlmsghdr.nlmsg_len = bytes.length;
final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
byteBuffer.order(ByteOrder.nativeOrder());
@@ -237,7 +260,7 @@
// RtNetlinkAddressMessage, e.g. RTM_DELADDR sent from user space to kernel to delete an
// IP address only requires IFA_ADDRESS attribute. The caller should check if these attributes
// are necessary to carry when constructing a RtNetlinkAddressMessage.
- private int getRequiredSpace() {
+ private int getRequiredSpace(int family) {
int spaceRequired = StructNlMsgHdr.STRUCT_SIZE + StructIfaddrMsg.STRUCT_SIZE;
// IFA_ADDRESS attr
spaceRequired += NetlinkConstants.alignedLengthOf(
@@ -247,6 +270,14 @@
StructNlAttr.NLA_HEADERLEN + StructIfacacheInfo.STRUCT_SIZE);
// IFA_FLAGS "u32" attr
spaceRequired += StructNlAttr.NLA_HEADERLEN + 4;
+ if (family == OsConstants.AF_INET) {
+ // IFA_LOCAL attr
+ spaceRequired += NetlinkConstants.alignedLengthOf(
+ StructNlAttr.NLA_HEADERLEN + mIpAddress.getAddress().length);
+ // IFA_BROADCAST attr
+ spaceRequired += NetlinkConstants.alignedLengthOf(
+ StructNlAttr.NLA_HEADERLEN + mIpAddress.getAddress().length);
+ }
return spaceRequired;
}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java
index 9acac69..545afea 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java
@@ -21,9 +21,11 @@
import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY;
import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ANY;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTNL_FAMILY_IP6MR;
import android.annotation.SuppressLint;
import android.net.IpPrefix;
+import android.net.RouteInfo;
import android.system.OsConstants;
import androidx.annotation.NonNull;
@@ -49,31 +51,78 @@
*/
public class RtNetlinkRouteMessage extends NetlinkMessage {
public static final short RTA_DST = 1;
+ public static final short RTA_SRC = 2;
+ public static final short RTA_IIF = 3;
public static final short RTA_OIF = 4;
public static final short RTA_GATEWAY = 5;
public static final short RTA_CACHEINFO = 12;
+ public static final short RTA_EXPIRES = 23;
- private int mIfindex;
+ public static final short RTNH_F_UNRESOLVED = 32; // The multicast route is unresolved
+
+ public static final String TAG = "NetlinkRouteMessage";
+
+ // For multicast routes, whether the route is resolved or unresolved
+ private boolean mIsResolved;
+ // The interface index for incoming interface, this is set for multicast
+ // routes, see common/net/ipv4/ipmr_base.c mr_fill_mroute
+ private int mIifIndex; // Incoming interface of a route, for resolved multicast routes
+ private int mOifIndex;
@NonNull
private StructRtMsg mRtmsg;
- @NonNull
- private IpPrefix mDestination;
+ @Nullable
+ private IpPrefix mSource; // Source address of a route, for all multicast routes
+ @Nullable
+ private IpPrefix mDestination; // Destination of a route, can be null for RTM_GETROUTE
@Nullable
private InetAddress mGateway;
@Nullable
private StructRtaCacheInfo mRtaCacheInfo;
+ private long mSinceLastUseMillis; // Milliseconds since the route was used,
+ // for resolved multicast routes
- private RtNetlinkRouteMessage(StructNlMsgHdr header) {
+
+ @VisibleForTesting
+ public RtNetlinkRouteMessage(final StructNlMsgHdr header, final StructRtMsg rtMsg,
+ final IpPrefix source, final IpPrefix destination, final InetAddress gateway,
+ int iif, int oif, final StructRtaCacheInfo cacheInfo) {
super(header);
- mRtmsg = null;
- mDestination = null;
- mGateway = null;
- mIfindex = 0;
- mRtaCacheInfo = null;
+ mRtmsg = rtMsg;
+ mSource = source;
+ mDestination = destination;
+ mGateway = gateway;
+ mIifIndex = iif;
+ mOifIndex = oif;
+ mRtaCacheInfo = cacheInfo;
+ mSinceLastUseMillis = -1;
+ }
+
+ public RtNetlinkRouteMessage(StructNlMsgHdr header, StructRtMsg rtMsg) {
+ this(header, rtMsg, null /* source */, null /* destination */, null /* gateway */,
+ 0 /* iif */, 0 /* oif */, null /* cacheInfo */);
+ }
+
+ /**
+ * Returns the rtnetlink family.
+ */
+ public short getRtmFamily() {
+ return mRtmsg.family;
+ }
+
+ /**
+ * Returns if the route is resolved. This is always true for unicast,
+ * and may be false only for multicast routes.
+ */
+ public boolean isResolved() {
+ return mIsResolved;
+ }
+
+ public int getIifIndex() {
+ return mIifIndex;
}
public int getInterfaceIndex() {
- return mIfindex;
+ return mOifIndex;
}
@NonNull
@@ -86,6 +135,14 @@
return mDestination;
}
+ /**
+ * Get source address of a route. This is for multicast routes.
+ */
+ @NonNull
+ public IpPrefix getSource() {
+ return mSource;
+ }
+
@Nullable
public InetAddress getGateway() {
return mGateway;
@@ -97,6 +154,18 @@
}
/**
+ * RTA_EXPIRES attribute returned by kernel to indicate the clock ticks
+ * from the route was last used to now, converted to milliseconds.
+ * This is set for multicast routes.
+ *
+ * Note that this value is not updated with the passage of time. It always
+ * returns the value that was read when the netlink message was parsed.
+ */
+ public long getSinceLastUseMillis() {
+ return mSinceLastUseMillis;
+ }
+
+ /**
* Check whether the address families of destination and gateway match rtm_family in
* StructRtmsg.
*
@@ -107,7 +176,8 @@
private static boolean matchRouteAddressFamily(@NonNull final InetAddress address,
int family) {
return ((address instanceof Inet4Address) && (family == AF_INET))
- || ((address instanceof Inet6Address) && (family == AF_INET6));
+ || ((address instanceof Inet6Address) &&
+ (family == AF_INET6 || family == RTNL_FAMILY_IP6MR));
}
/**
@@ -121,11 +191,11 @@
@Nullable
public static RtNetlinkRouteMessage parse(@NonNull final StructNlMsgHdr header,
@NonNull final ByteBuffer byteBuffer) {
- final RtNetlinkRouteMessage routeMsg = new RtNetlinkRouteMessage(header);
-
- routeMsg.mRtmsg = StructRtMsg.parse(byteBuffer);
- if (routeMsg.mRtmsg == null) return null;
+ final StructRtMsg rtmsg = StructRtMsg.parse(byteBuffer);
+ if (rtmsg == null) return null;
+ final RtNetlinkRouteMessage routeMsg = new RtNetlinkRouteMessage(header, rtmsg);
int rtmFamily = routeMsg.mRtmsg.family;
+ routeMsg.mIsResolved = ((routeMsg.mRtmsg.flags & RTNH_F_UNRESOLVED) == 0);
// RTA_DST
final int baseOffset = byteBuffer.position();
@@ -139,12 +209,24 @@
routeMsg.mDestination = new IpPrefix(destination, routeMsg.mRtmsg.dstLen);
} else if (rtmFamily == AF_INET) {
routeMsg.mDestination = new IpPrefix(IPV4_ADDR_ANY, 0);
- } else if (rtmFamily == AF_INET6) {
+ } else if (rtmFamily == AF_INET6 || rtmFamily == RTNL_FAMILY_IP6MR) {
routeMsg.mDestination = new IpPrefix(IPV6_ADDR_ANY, 0);
} else {
return null;
}
+ // RTA_SRC
+ byteBuffer.position(baseOffset);
+ nlAttr = StructNlAttr.findNextAttrOfType(RTA_SRC, byteBuffer);
+ if (nlAttr != null) {
+ final InetAddress source = nlAttr.getValueAsInetAddress();
+ // If the RTA_SRC attribute is malformed, return null.
+ if (source == null) return null;
+ // If the address family of destination doesn't match rtm_family, return null.
+ if (!matchRouteAddressFamily(source, rtmFamily)) return null;
+ routeMsg.mSource = new IpPrefix(source, routeMsg.mRtmsg.srcLen);
+ }
+
// RTA_GATEWAY
byteBuffer.position(baseOffset);
nlAttr = StructNlAttr.findNextAttrOfType(RTA_GATEWAY, byteBuffer);
@@ -156,6 +238,17 @@
if (!matchRouteAddressFamily(routeMsg.mGateway, rtmFamily)) return null;
}
+ // RTA_IIF
+ byteBuffer.position(baseOffset);
+ nlAttr = StructNlAttr.findNextAttrOfType(RTA_IIF, byteBuffer);
+ if (nlAttr != null) {
+ Integer iifInteger = nlAttr.getValueAsInteger();
+ if (iifInteger == null) {
+ return null;
+ }
+ routeMsg.mIifIndex = iifInteger;
+ }
+
// RTA_OIF
byteBuffer.position(baseOffset);
nlAttr = StructNlAttr.findNextAttrOfType(RTA_OIF, byteBuffer);
@@ -164,7 +257,7 @@
// the interface index to a name themselves. This may not succeed or may be
// incorrect, because the interface might have been deleted, or even deleted
// and re-added with a different index, since the netlink message was sent.
- routeMsg.mIfindex = nlAttr.getValueAsInt(0 /* 0 isn't a valid ifindex */);
+ routeMsg.mOifIndex = nlAttr.getValueAsInt(0 /* 0 isn't a valid ifindex */);
}
// RTA_CACHEINFO
@@ -174,33 +267,59 @@
routeMsg.mRtaCacheInfo = StructRtaCacheInfo.parse(nlAttr.getValueAsByteBuffer());
}
+ // RTA_EXPIRES
+ byteBuffer.position(baseOffset);
+ nlAttr = StructNlAttr.findNextAttrOfType(RTA_EXPIRES, byteBuffer);
+ if (nlAttr != null) {
+ final Long sinceLastUseCentis = nlAttr.getValueAsLong();
+ // If the RTA_EXPIRES attribute is malformed, return null.
+ if (sinceLastUseCentis == null) return null;
+ // RTA_EXPIRES returns time in clock ticks of USER_HZ(100), which is centiseconds
+ routeMsg.mSinceLastUseMillis = sinceLastUseCentis * 10;
+ }
+
return routeMsg;
}
/**
* Write a rtnetlink address message to {@link ByteBuffer}.
*/
- @VisibleForTesting
- protected void pack(ByteBuffer byteBuffer) {
+ public void pack(ByteBuffer byteBuffer) {
getHeader().pack(byteBuffer);
mRtmsg.pack(byteBuffer);
- final StructNlAttr destination = new StructNlAttr(RTA_DST, mDestination.getAddress());
- destination.pack(byteBuffer);
+ if (mSource != null) {
+ final StructNlAttr source = new StructNlAttr(RTA_SRC, mSource.getAddress());
+ source.pack(byteBuffer);
+ }
+
+ if (mDestination != null) {
+ final StructNlAttr destination = new StructNlAttr(RTA_DST, mDestination.getAddress());
+ destination.pack(byteBuffer);
+ }
if (mGateway != null) {
final StructNlAttr gateway = new StructNlAttr(RTA_GATEWAY, mGateway.getAddress());
gateway.pack(byteBuffer);
}
- if (mIfindex != 0) {
- final StructNlAttr ifindex = new StructNlAttr(RTA_OIF, mIfindex);
- ifindex.pack(byteBuffer);
+ if (mIifIndex != 0) {
+ final StructNlAttr iifindex = new StructNlAttr(RTA_IIF, mIifIndex);
+ iifindex.pack(byteBuffer);
+ }
+ if (mOifIndex != 0) {
+ final StructNlAttr oifindex = new StructNlAttr(RTA_OIF, mOifIndex);
+ oifindex.pack(byteBuffer);
}
if (mRtaCacheInfo != null) {
final StructNlAttr cacheInfo = new StructNlAttr(RTA_CACHEINFO,
mRtaCacheInfo.writeToBytes());
cacheInfo.pack(byteBuffer);
}
+ if (mSinceLastUseMillis >= 0) {
+ final long sinceLastUseCentis = mSinceLastUseMillis / 10;
+ final StructNlAttr expires = new StructNlAttr(RTA_EXPIRES, sinceLastUseCentis);
+ expires.pack(byteBuffer);
+ }
}
@Override
@@ -208,10 +327,14 @@
return "RtNetlinkRouteMessage{ "
+ "nlmsghdr{" + mHeader.toString(OsConstants.NETLINK_ROUTE) + "}, "
+ "Rtmsg{" + mRtmsg.toString() + "}, "
- + "destination{" + mDestination.getAddress().getHostAddress() + "}, "
+ + (mSource == null ? "" : "source{" + mSource.getAddress().getHostAddress() + "}, ")
+ + (mDestination == null ?
+ "" : "destination{" + mDestination.getAddress().getHostAddress() + "}, ")
+ "gateway{" + (mGateway == null ? "" : mGateway.getHostAddress()) + "}, "
- + "ifindex{" + mIfindex + "}, "
+ + (mIifIndex == 0 ? "" : "iifindex{" + mIifIndex + "}, ")
+ + "oifindex{" + mOifIndex + "}, "
+ "rta_cacheinfo{" + (mRtaCacheInfo == null ? "" : mRtaCacheInfo.toString()) + "} "
+ + (mSinceLastUseMillis < 0 ? "" : "sinceLastUseMillis{" + mSinceLastUseMillis + "}")
+ "}";
}
}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java b/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java
index a9b6495..43e8312 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java
@@ -21,6 +21,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import static java.nio.ByteOrder.nativeOrder;
+
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.UnknownHostException;
@@ -152,12 +154,12 @@
nla_type = type;
setValue(new byte[Short.BYTES]);
final ByteBuffer buf = getValueAsByteBuffer();
- final ByteOrder originalOrder = buf.order();
+ // ByteBuffer returned by getValueAsByteBuffer is always in native byte order.
try {
buf.order(order);
buf.putShort(value);
} finally {
- buf.order(originalOrder);
+ buf.order(nativeOrder());
}
}
@@ -169,12 +171,29 @@
nla_type = type;
setValue(new byte[Integer.BYTES]);
final ByteBuffer buf = getValueAsByteBuffer();
- final ByteOrder originalOrder = buf.order();
+ // ByteBuffer returned by getValueAsByteBuffer is always in native byte order.
try {
buf.order(order);
buf.putInt(value);
} finally {
- buf.order(originalOrder);
+ buf.order(nativeOrder());
+ }
+ }
+
+ public StructNlAttr(short type, long value) {
+ this(type, value, ByteOrder.nativeOrder());
+ }
+
+ public StructNlAttr(short type, long value, ByteOrder order) {
+ nla_type = type;
+ setValue(new byte[Long.BYTES]);
+ final ByteBuffer buf = getValueAsByteBuffer();
+ // ByteBuffer returned by getValueAsByteBuffer is always in native byte order.
+ try {
+ buf.order(order);
+ buf.putLong(value);
+ } finally {
+ buf.order(nativeOrder());
}
}
@@ -288,6 +307,7 @@
/**
* Get attribute value as Integer, or null if malformed (e.g., length is not 4 bytes).
+ * The attribute value is assumed to be in native byte order.
*/
public Integer getValueAsInteger() {
final ByteBuffer byteBuffer = getValueAsByteBuffer();
@@ -298,6 +318,18 @@
}
/**
+ * Get attribute value as Long, or null if malformed (e.g., length is not 8 bytes).
+ * The attribute value is assumed to be in native byte order.
+ */
+ public Long getValueAsLong() {
+ final ByteBuffer byteBuffer = getValueAsByteBuffer();
+ if (byteBuffer == null || byteBuffer.remaining() != Long.BYTES) {
+ return null;
+ }
+ return byteBuffer.getLong();
+ }
+
+ /**
* Get attribute value as Int, default value if malformed.
*/
public int getValueAsInt(int defaultValue) {
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructRtMsg.java b/staticlibs/device/com/android/net/module/util/netlink/StructRtMsg.java
index 3cd7292..6d9318c 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/StructRtMsg.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructRtMsg.java
@@ -18,6 +18,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.android.net.module.util.Struct;
import com.android.net.module.util.Struct.Field;
@@ -57,8 +58,9 @@
@Field(order = 8, type = Type.U32)
public final long flags;
- StructRtMsg(short family, short dstLen, short srcLen, short tos, short table, short protocol,
- short scope, short type, long flags) {
+ @VisibleForTesting
+ public StructRtMsg(short family, short dstLen, short srcLen, short tos, short table,
+ short protocol, short scope, short type, long flags) {
this.family = family;
this.dstLen = dstLen;
this.srcLen = srcLen;
diff --git a/staticlibs/device/com/android/net/module/util/structs/StructMf6cctl.java b/staticlibs/device/com/android/net/module/util/structs/StructMf6cctl.java
new file mode 100644
index 0000000..24e0a97
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/StructMf6cctl.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.structs;
+
+import static android.system.OsConstants.AF_INET6;
+
+import com.android.net.module.util.Struct;
+import java.net.Inet6Address;
+import java.util.Set;
+
+/*
+ * Implements the mf6cctl structure which is used to add a multicast forwarding
+ * cache, see /usr/include/linux/mroute6.h
+ */
+public class StructMf6cctl extends Struct {
+ // struct sockaddr_in6 mf6cc_origin, added the fields directly as Struct
+ // doesn't support nested Structs
+ @Field(order = 0, type = Type.U16)
+ public final int originFamily; // AF_INET6
+ @Field(order = 1, type = Type.U16)
+ public final int originPort; // Transport layer port # of origin
+ @Field(order = 2, type = Type.U32)
+ public final long originFlowinfo; // IPv6 flow information
+ @Field(order = 3, type = Type.ByteArray, arraysize = 16)
+ public final byte[] originAddress; //the IPv6 address of origin
+ @Field(order = 4, type = Type.U32)
+ public final long originScopeId; // scope id, not used
+
+ // struct sockaddr_in6 mf6cc_mcastgrp
+ @Field(order = 5, type = Type.U16)
+ public final int groupFamily; // AF_INET6
+ @Field(order = 6, type = Type.U16)
+ public final int groupPort; // Transport layer port # of multicast group
+ @Field(order = 7, type = Type.U32)
+ public final long groupFlowinfo; // IPv6 flow information
+ @Field(order = 8, type = Type.ByteArray, arraysize = 16)
+ public final byte[] groupAddress; //the IPv6 address of multicast group
+ @Field(order = 9, type = Type.U32)
+ public final long groupScopeId; // scope id, not used
+
+ @Field(order = 10, type = Type.U16, padding = 2)
+ public final int mf6ccParent; // incoming interface
+ @Field(order = 11, type = Type.ByteArray, arraysize = 32)
+ public final byte[] mf6ccIfset; // outgoing interfaces
+
+ public StructMf6cctl(final Inet6Address origin, final Inet6Address group,
+ final int mf6ccParent, final Set<Integer> oifset) {
+ this(AF_INET6, 0, (long) 0, origin.getAddress(), (long) 0, AF_INET6,
+ 0, (long) 0, group.getAddress(), (long) 0, mf6ccParent,
+ getMf6ccIfsetBytes(oifset));
+ }
+
+ private StructMf6cctl(int originFamily, int originPort, long originFlowinfo,
+ byte[] originAddress, long originScopeId, int groupFamily, int groupPort,
+ long groupFlowinfo, byte[] groupAddress, long groupScopeId, int mf6ccParent,
+ byte[] mf6ccIfset) {
+ this.originFamily = originFamily;
+ this.originPort = originPort;
+ this.originFlowinfo = originFlowinfo;
+ this.originAddress = originAddress;
+ this.originScopeId = originScopeId;
+ this.groupFamily = groupFamily;
+ this.groupPort = groupPort;
+ this.groupFlowinfo = groupFlowinfo;
+ this.groupAddress = groupAddress;
+ this.groupScopeId = groupScopeId;
+ this.mf6ccParent = mf6ccParent;
+ this.mf6ccIfset = mf6ccIfset;
+ }
+
+ private static byte[] getMf6ccIfsetBytes(final Set<Integer> oifs)
+ throws IllegalArgumentException {
+ byte[] mf6ccIfset = new byte[32];
+ for (int oif : oifs) {
+ int idx = oif / 8;
+ if (idx >= 32) {
+ // invalid oif index, too big to fit in mf6ccIfset
+ throw new IllegalArgumentException("Invalid oif index" + oif);
+ }
+ int offset = oif % 8;
+ mf6ccIfset[idx] |= (byte) (1 << offset);
+ }
+ return mf6ccIfset;
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/StructMif6ctl.java b/staticlibs/device/com/android/net/module/util/structs/StructMif6ctl.java
new file mode 100644
index 0000000..626a170
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/StructMif6ctl.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.structs;
+
+import com.android.net.module.util.Struct;
+
+/*
+ * Implements the mif6ctl structure which is used to add a multicast routing
+ * interface, see /usr/include/linux/mroute6.h
+ */
+public class StructMif6ctl extends Struct {
+ @Field(order = 0, type = Type.U16)
+ public final int mif6cMifi; // Index of MIF
+ @Field(order = 1, type = Type.U8)
+ public final short mif6cFlags; // MIFF_ flags
+ @Field(order = 2, type = Type.U8)
+ public final short vifcThreshold; // ttl limit
+ @Field(order = 3, type = Type.U16)
+ public final int mif6cPifi; //the index of the physical IF
+ @Field(order = 4, type = Type.U32, padding = 2)
+ public final long vifcRateLimit; // Rate limiter values (NI)
+
+ public StructMif6ctl(final int mif6cMifi, final short mif6cFlags, final short vifcThreshold,
+ final int mif6cPifi, final long vifcRateLimit) {
+ this.mif6cMifi = mif6cMifi;
+ this.mif6cFlags = mif6cFlags;
+ this.vifcThreshold = vifcThreshold;
+ this.mif6cPifi = mif6cPifi;
+ this.vifcRateLimit = vifcRateLimit;
+ }
+}
+
diff --git a/staticlibs/device/com/android/net/module/util/structs/StructMrt6Msg.java b/staticlibs/device/com/android/net/module/util/structs/StructMrt6Msg.java
new file mode 100644
index 0000000..569e361
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/StructMrt6Msg.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.structs;
+
+import com.android.net.module.util.Struct;
+import java.net.Inet6Address;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+public class StructMrt6Msg extends Struct {
+ public static final byte MRT6MSG_NOCACHE = 1;
+
+ @Field(order = 0, type = Type.S8)
+ public final byte mbz;
+ @Field(order = 1, type = Type.S8)
+ public final byte msgType; // message type
+ @Field(order = 2, type = Type.U16, padding = 4)
+ public final int mif; // mif received on
+ @Field(order = 3, type = Type.Ipv6Address)
+ public final Inet6Address src;
+ @Field(order = 4, type = Type.Ipv6Address)
+ public final Inet6Address dst;
+
+ public StructMrt6Msg(final byte mbz, final byte msgType, final int mif,
+ final Inet6Address source, final Inet6Address destination) {
+ this.mbz = mbz; // kernel should set it to 0
+ this.msgType = msgType;
+ this.mif = mif;
+ this.src = source;
+ this.dst = destination;
+ }
+
+ public static StructMrt6Msg parse(ByteBuffer byteBuffer) {
+ byteBuffer.order(ByteOrder.nativeOrder());
+ return Struct.parse(StructMrt6Msg.class, byteBuffer);
+ }
+}
+
diff --git a/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java b/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java
index cd1f31c..f6bee69 100644
--- a/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java
+++ b/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java
@@ -189,8 +189,9 @@
* @param message A message describing why the permission was checked. Only needed if this is
* not inside of a two-way binder call from the data receiver
*/
- public boolean checkCallersLocationPermission(String pkgName, @Nullable String featureId,
- int uid, boolean coarseForTargetSdkLessThanQ, @Nullable String message) {
+ public boolean checkCallersLocationPermission(@Nullable String pkgName,
+ @Nullable String featureId, int uid, boolean coarseForTargetSdkLessThanQ,
+ @Nullable String message) {
boolean isTargetSdkLessThanQ = isTargetSdkLessThan(pkgName, Build.VERSION_CODES.Q, uid);
diff --git a/staticlibs/framework/com/android/net/module/util/PermissionUtils.java b/staticlibs/framework/com/android/net/module/util/PermissionUtils.java
index 8315b8f..0d7d96f 100644
--- a/staticlibs/framework/com/android/net/module/util/PermissionUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/PermissionUtils.java
@@ -23,11 +23,14 @@
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
+import android.annotation.CheckResult;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
import android.os.Binder;
+import android.os.UserHandle;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -43,8 +46,9 @@
/**
* Return true if the context has one of given permission.
*/
- public static boolean checkAnyPermissionOf(@NonNull Context context,
- @NonNull String... permissions) {
+ @CheckResult
+ public static boolean hasAnyPermissionOf(@NonNull Context context,
+ @NonNull String... permissions) {
for (String permission : permissions) {
if (context.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED) {
return true;
@@ -54,11 +58,12 @@
}
/**
- * Return true if the context has one of give permission that is allowed
+ * Return true if the context has one of given permission that is allowed
* for a particular process and user ID running in the system.
*/
- public static boolean checkAnyPermissionOf(@NonNull Context context,
- int pid, int uid, @NonNull String... permissions) {
+ @CheckResult
+ public static boolean hasAnyPermissionOf(@NonNull Context context,
+ int pid, int uid, @NonNull String... permissions) {
for (String permission : permissions) {
if (context.checkPermission(permission, pid, uid) == PERMISSION_GRANTED) {
return true;
@@ -72,7 +77,7 @@
*/
public static void enforceAnyPermissionOf(@NonNull Context context,
@NonNull String... permissions) {
- if (!checkAnyPermissionOf(context, permissions)) {
+ if (!hasAnyPermissionOf(context, permissions)) {
throw new SecurityException("Requires one of the following permissions: "
+ String.join(", ", permissions) + ".");
}
@@ -131,7 +136,8 @@
/**
* Return true if the context has DUMP permission.
*/
- public static boolean checkDumpPermission(Context context, String tag, PrintWriter pw) {
+ @CheckResult
+ public static boolean hasDumpPermission(Context context, String tag, PrintWriter pw) {
if (context.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
!= PERMISSION_GRANTED) {
pw.println("Permission Denial: can't dump " + tag + " from from pid="
@@ -183,4 +189,33 @@
}
return result;
}
+
+ /**
+ * Enforces that the given package name belongs to the given uid.
+ *
+ * @param context {@link android.content.Context} for the process.
+ * @param uid User ID to check the package ownership for.
+ * @param packageName Package name to verify.
+ * @throws SecurityException If the package does not belong to the specified uid.
+ */
+ public static void enforcePackageNameMatchesUid(
+ @NonNull Context context, int uid, @Nullable String packageName) {
+ final UserHandle user = UserHandle.getUserHandleForUid(uid);
+ if (getAppUid(context, packageName, user) != uid) {
+ throw new SecurityException(packageName + " does not belong to uid " + uid);
+ }
+ }
+
+ private static int getAppUid(Context context, final String app, final UserHandle user) {
+ final PackageManager pm =
+ context.createContextAsUser(user, 0 /* flags */).getPackageManager();
+ final long token = Binder.clearCallingIdentity();
+ try {
+ return pm.getPackageUid(app, 0 /* flags */);
+ } catch (PackageManager.NameNotFoundException e) {
+ return -1;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
}
diff --git a/staticlibs/lint-baseline.xml b/staticlibs/lint-baseline.xml
new file mode 100644
index 0000000..2ee3a43
--- /dev/null
+++ b/staticlibs/lint-baseline.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.4.0-alpha04" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha04">
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `makeNetlinkSocketAddress`"
+ errorLine1=" Os.bind(fd, makeNetlinkSocketAddress(0, mBindGroups));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/staticlibs/device/com/android/net/module/util/ip/NetlinkMonitor.java"
+ line="111"
+ column="25"/>
+ </issue>
+
+</issues>
diff --git a/staticlibs/native/bpf_headers/Android.bp b/staticlibs/native/bpf_headers/Android.bp
index 41184ea..d55584a 100644
--- a/staticlibs/native/bpf_headers/Android.bp
+++ b/staticlibs/native/bpf_headers/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfMap.h b/staticlibs/native/bpf_headers/include/bpf/BpfMap.h
index 3fede3c..1037beb 100644
--- a/staticlibs/native/bpf_headers/include/bpf/BpfMap.h
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfMap.h
@@ -26,6 +26,8 @@
#include "BpfSyscallWrappers.h"
#include "bpf/BpfUtils.h"
+#include <functional>
+
namespace android {
namespace bpf {
diff --git a/staticlibs/native/bpf_syscall_wrappers/Android.bp b/staticlibs/native/bpf_syscall_wrappers/Android.bp
index b3efc21..1e0cb22 100644
--- a/staticlibs/native/bpf_syscall_wrappers/Android.bp
+++ b/staticlibs/native/bpf_syscall_wrappers/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/staticlibs/native/bpfmapjni/Android.bp b/staticlibs/native/bpfmapjni/Android.bp
index 8babcce..7e6b4ec 100644
--- a/staticlibs/native/bpfmapjni/Android.bp
+++ b/staticlibs/native/bpfmapjni/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/staticlibs/native/bpfutiljni/Android.bp b/staticlibs/native/bpfutiljni/Android.bp
index 39a2795..1ef01a6 100644
--- a/staticlibs/native/bpfutiljni/Android.bp
+++ b/staticlibs/native/bpfutiljni/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/staticlibs/native/ip_checksum/Android.bp b/staticlibs/native/ip_checksum/Android.bp
index 9878d73..e2e118e 100644
--- a/staticlibs/native/ip_checksum/Android.bp
+++ b/staticlibs/native/ip_checksum/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/staticlibs/native/netjniutils/Android.bp b/staticlibs/native/netjniutils/Android.bp
index ca3bbbc..4cab459 100644
--- a/staticlibs/native/netjniutils/Android.bp
+++ b/staticlibs/native/netjniutils/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/staticlibs/native/nettestutils/Android.bp b/staticlibs/native/nettestutils/Android.bp
index df3bb42..ef87f04 100644
--- a/staticlibs/native/nettestutils/Android.bp
+++ b/staticlibs/native/nettestutils/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/staticlibs/native/tcutils/Android.bp b/staticlibs/native/tcutils/Android.bp
index 9a38745..926590d 100644
--- a/staticlibs/native/tcutils/Android.bp
+++ b/staticlibs/native/tcutils/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/staticlibs/netd/Android.bp b/staticlibs/netd/Android.bp
index 637a938..59ef20d 100644
--- a/staticlibs/netd/Android.bp
+++ b/staticlibs/netd/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -241,5 +242,17 @@
min_sdk_version: "30",
},
},
- versions: ["1"],
+ versions_with_info: [
+ {
+ version: "1",
+ imports: [],
+ },
+ {
+ version: "2",
+ imports: [],
+ },
+
+ ],
+ frozen: true,
+
}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/.hash b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/.hash
new file mode 100644
index 0000000..785d42d
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/.hash
@@ -0,0 +1 @@
+0e5d9ad0664b8b3ec9d323534c42333cf6f6ed3d
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/DiscoveryInfo.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/DiscoveryInfo.aidl
new file mode 100644
index 0000000..d31a327
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/DiscoveryInfo.aidl
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.mdns.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable DiscoveryInfo {
+ int id;
+ int result;
+ @utf8InCpp String serviceName;
+ @utf8InCpp String registrationType;
+ @utf8InCpp String domainName;
+ int interfaceIdx;
+ int netId;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/GetAddressInfo.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/GetAddressInfo.aidl
new file mode 100644
index 0000000..2049274
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/GetAddressInfo.aidl
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.mdns.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable GetAddressInfo {
+ int id;
+ int result;
+ @utf8InCpp String hostname;
+ @utf8InCpp String address;
+ int interfaceIdx;
+ int netId;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDns.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDns.aidl
new file mode 100644
index 0000000..d84742b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDns.aidl
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.mdns.aidl;
+/* @hide */
+interface IMDns {
+ /**
+ * @deprecated unimplemented on V+.
+ */
+ void startDaemon();
+ /**
+ * @deprecated unimplemented on V+.
+ */
+ void stopDaemon();
+ /**
+ * @deprecated unimplemented on U+.
+ */
+ void registerService(in android.net.mdns.aidl.RegistrationInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
+ void discover(in android.net.mdns.aidl.DiscoveryInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
+ void resolve(in android.net.mdns.aidl.ResolutionInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
+ void getServiceAddress(in android.net.mdns.aidl.GetAddressInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
+ void stopOperation(int id);
+ /**
+ * @deprecated unimplemented on U+.
+ */
+ void registerEventListener(in android.net.mdns.aidl.IMDnsEventListener listener);
+ /**
+ * @deprecated unimplemented on U+.
+ */
+ void unregisterEventListener(in android.net.mdns.aidl.IMDnsEventListener listener);
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDnsEventListener.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDnsEventListener.aidl
new file mode 100644
index 0000000..187a3d2
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDnsEventListener.aidl
@@ -0,0 +1,62 @@
+/**
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.mdns.aidl;
+/* @hide */
+interface IMDnsEventListener {
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
+ oneway void onServiceRegistrationStatus(in android.net.mdns.aidl.RegistrationInfo status);
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
+ oneway void onServiceDiscoveryStatus(in android.net.mdns.aidl.DiscoveryInfo status);
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
+ oneway void onServiceResolutionStatus(in android.net.mdns.aidl.ResolutionInfo status);
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
+ oneway void onGettingServiceAddressStatus(in android.net.mdns.aidl.GetAddressInfo status);
+ const int SERVICE_DISCOVERY_FAILED = 602;
+ const int SERVICE_FOUND = 603;
+ const int SERVICE_LOST = 604;
+ const int SERVICE_REGISTRATION_FAILED = 605;
+ const int SERVICE_REGISTERED = 606;
+ const int SERVICE_RESOLUTION_FAILED = 607;
+ const int SERVICE_RESOLVED = 608;
+ const int SERVICE_GET_ADDR_FAILED = 611;
+ const int SERVICE_GET_ADDR_SUCCESS = 612;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/RegistrationInfo.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/RegistrationInfo.aidl
new file mode 100644
index 0000000..185111b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/RegistrationInfo.aidl
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.mdns.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable RegistrationInfo {
+ int id;
+ int result;
+ @utf8InCpp String serviceName;
+ @utf8InCpp String registrationType;
+ int port;
+ byte[] txtRecord;
+ int interfaceIdx;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/ResolutionInfo.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/ResolutionInfo.aidl
new file mode 100644
index 0000000..4aa7d79
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/ResolutionInfo.aidl
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.mdns.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable ResolutionInfo {
+ int id;
+ int result;
+ @utf8InCpp String serviceName;
+ @utf8InCpp String registrationType;
+ @utf8InCpp String domain;
+ @utf8InCpp String serviceFullName;
+ @utf8InCpp String hostname;
+ int port;
+ byte[] txtRecord;
+ int interfaceIdx;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDns.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDns.aidl
index ecbe966..d84742b 100644
--- a/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDns.aidl
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDns.aidl
@@ -34,13 +34,40 @@
package android.net.mdns.aidl;
/* @hide */
interface IMDns {
+ /**
+ * @deprecated unimplemented on V+.
+ */
void startDaemon();
+ /**
+ * @deprecated unimplemented on V+.
+ */
void stopDaemon();
+ /**
+ * @deprecated unimplemented on U+.
+ */
void registerService(in android.net.mdns.aidl.RegistrationInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
void discover(in android.net.mdns.aidl.DiscoveryInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
void resolve(in android.net.mdns.aidl.ResolutionInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
void getServiceAddress(in android.net.mdns.aidl.GetAddressInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
void stopOperation(int id);
+ /**
+ * @deprecated unimplemented on U+.
+ */
void registerEventListener(in android.net.mdns.aidl.IMDnsEventListener listener);
+ /**
+ * @deprecated unimplemented on U+.
+ */
void unregisterEventListener(in android.net.mdns.aidl.IMDnsEventListener listener);
}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDnsEventListener.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDnsEventListener.aidl
index 4625cac..187a3d2 100644
--- a/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDnsEventListener.aidl
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDnsEventListener.aidl
@@ -34,9 +34,21 @@
package android.net.mdns.aidl;
/* @hide */
interface IMDnsEventListener {
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
oneway void onServiceRegistrationStatus(in android.net.mdns.aidl.RegistrationInfo status);
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
oneway void onServiceDiscoveryStatus(in android.net.mdns.aidl.DiscoveryInfo status);
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
oneway void onServiceResolutionStatus(in android.net.mdns.aidl.ResolutionInfo status);
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
oneway void onGettingServiceAddressStatus(in android.net.mdns.aidl.GetAddressInfo status);
const int SERVICE_DISCOVERY_FAILED = 602;
const int SERVICE_FOUND = 603;
diff --git a/staticlibs/netd/binder/android/net/mdns/aidl/IMDns.aidl b/staticlibs/netd/binder/android/net/mdns/aidl/IMDns.aidl
index 255d70f..3bf1da8 100644
--- a/staticlibs/netd/binder/android/net/mdns/aidl/IMDns.aidl
+++ b/staticlibs/netd/binder/android/net/mdns/aidl/IMDns.aidl
@@ -28,6 +28,8 @@
* Start the MDNSResponder daemon.
*
* @throws ServiceSpecificException with unix errno EALREADY if daemon is already running.
+ * @throws UnsupportedOperationException on Android V and after.
+ * @deprecated unimplemented on V+.
*/
void startDaemon();
@@ -35,6 +37,8 @@
* Stop the MDNSResponder daemon.
*
* @throws ServiceSpecificException with unix errno EBUSY if daemon is still in use.
+ * @throws UnsupportedOperationException on Android V and after.
+ * @deprecated unimplemented on V+.
*/
void stopDaemon();
@@ -49,6 +53,8 @@
* @throws ServiceSpecificException with one of the following error values:
* - Unix errno EBUSY if request id is already in use.
* - kDNSServiceErr_* list in dns_sd.h if registration fail.
+ * @throws UnsupportedOperationException on Android U and after.
+ * @deprecated unimplemented on U+.
*/
void registerService(in RegistrationInfo info);
@@ -63,6 +69,8 @@
* @throws ServiceSpecificException with one of the following error values:
* - Unix errno EBUSY if request id is already in use.
* - kDNSServiceErr_* list in dns_sd.h if discovery fail.
+ * @throws UnsupportedOperationException on Android U and after.
+ * @deprecated unimplemented on U+.
*/
void discover(in DiscoveryInfo info);
@@ -77,6 +85,8 @@
* @throws ServiceSpecificException with one of the following error values:
* - Unix errno EBUSY if request id is already in use.
* - kDNSServiceErr_* list in dns_sd.h if resolution fail.
+ * @throws UnsupportedOperationException on Android U and after.
+ * @deprecated unimplemented on U+.
*/
void resolve(in ResolutionInfo info);
@@ -92,6 +102,8 @@
* @throws ServiceSpecificException with one of the following error values:
* - Unix errno EBUSY if request id is already in use.
* - kDNSServiceErr_* list in dns_sd.h if getting address fail.
+ * @throws UnsupportedOperationException on Android U and after.
+ * @deprecated unimplemented on U+.
*/
void getServiceAddress(in GetAddressInfo info);
@@ -101,6 +113,8 @@
* @param id the operation id to be stopped.
*
* @throws ServiceSpecificException with unix errno ESRCH if request id is not in use.
+ * @throws UnsupportedOperationException on Android U and after.
+ * @deprecated unimplemented on U+.
*/
void stopOperation(int id);
@@ -112,6 +126,8 @@
* @throws ServiceSpecificException with one of the following error values:
* - Unix errno EINVAL if listener is null.
* - Unix errno EEXIST if register duplicated listener.
+ * @throws UnsupportedOperationException on Android U and after.
+ * @deprecated unimplemented on U+.
*/
void registerEventListener(in IMDnsEventListener listener);
@@ -121,6 +137,8 @@
* @param listener The listener to be unregistered.
*
* @throws ServiceSpecificException with unix errno EINVAL if listener is null.
+ * @throws UnsupportedOperationException on Android U and after.
+ * @deprecated unimplemented on U+.
*/
void unregisterEventListener(in IMDnsEventListener listener);
}
diff --git a/staticlibs/netd/binder/android/net/mdns/aidl/IMDnsEventListener.aidl b/staticlibs/netd/binder/android/net/mdns/aidl/IMDnsEventListener.aidl
index a202a26..f7f028b 100644
--- a/staticlibs/netd/binder/android/net/mdns/aidl/IMDnsEventListener.aidl
+++ b/staticlibs/netd/binder/android/net/mdns/aidl/IMDnsEventListener.aidl
@@ -31,8 +31,8 @@
oneway interface IMDnsEventListener {
/**
* Types for MDNS operation result.
- * These are in sync with frameworks/libs/net/common/netd/libnetdutils/include/netdutils/\
- * ResponseCode.h
+ * These are in sync with packages/modules/Connectivity/staticlibs/netd/libnetdutils/include/\
+ * netdutils/ResponseCode.h
*/
const int SERVICE_DISCOVERY_FAILED = 602;
const int SERVICE_FOUND = 603;
@@ -46,21 +46,29 @@
/**
* Notify service registration status.
+ *
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
*/
void onServiceRegistrationStatus(in RegistrationInfo status);
/**
* Notify service discovery status.
+ *
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
*/
void onServiceDiscoveryStatus(in DiscoveryInfo status);
/**
* Notify service resolution status.
+ *
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
*/
void onServiceResolutionStatus(in ResolutionInfo status);
/**
* Notify getting service address status.
+ *
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
*/
void onGettingServiceAddressStatus(in GetAddressInfo status);
}
diff --git a/staticlibs/netd/libnetdutils/Android.bp b/staticlibs/netd/libnetdutils/Android.bp
index fdb9380..2ae5911 100644
--- a/staticlibs/netd/libnetdutils/Android.bp
+++ b/staticlibs/netd/libnetdutils/Android.bp
@@ -1,4 +1,5 @@
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -22,7 +23,10 @@
"Utils.cpp",
],
defaults: ["netd_defaults"],
- cflags: ["-Wall", "-Werror"],
+ cflags: [
+ "-Wall",
+ "-Werror",
+ ],
shared_libs: [
"libbase",
"liblog",
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/InternetAddresses.h b/staticlibs/netd/libnetdutils/include/netdutils/InternetAddresses.h
index d10cec7..d662739 100644
--- a/staticlibs/netd/libnetdutils/include/netdutils/InternetAddresses.h
+++ b/staticlibs/netd/libnetdutils/include/netdutils/InternetAddresses.h
@@ -21,6 +21,7 @@
#include <stdint.h>
#include <cstring>
#include <limits>
+#include <memory>
#include <string>
#include "netdutils/NetworkConstants.h"
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/Log.h b/staticlibs/netd/libnetdutils/include/netdutils/Log.h
index 77ae649..d266cbc 100644
--- a/staticlibs/netd/libnetdutils/include/netdutils/Log.h
+++ b/staticlibs/netd/libnetdutils/include/netdutils/Log.h
@@ -19,6 +19,7 @@
#include <chrono>
#include <deque>
+#include <functional>
#include <shared_mutex>
#include <string>
#include <type_traits>
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/Slice.h b/staticlibs/netd/libnetdutils/include/netdutils/Slice.h
index 717fbd1..aa12927 100644
--- a/staticlibs/netd/libnetdutils/include/netdutils/Slice.h
+++ b/staticlibs/netd/libnetdutils/include/netdutils/Slice.h
@@ -22,6 +22,7 @@
#include <cstring>
#include <ostream>
#include <tuple>
+#include <type_traits>
#include <vector>
namespace android {
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index 0dfca57..4c226cc 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -3,12 +3,16 @@
//########################################################################
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
android_library {
name: "NetworkStaticLibTestsLib",
- srcs: ["src/**/*.java","src/**/*.kt"],
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
min_sdk_version: "30",
defaults: ["framework-connectivity-test-defaults"],
static_libs: [
@@ -34,8 +38,7 @@
"//packages/modules/NetworkStack/tests/integration",
],
lint: {
- strict_updatability_linting: true,
- test: true
+ test: true,
},
}
@@ -52,5 +55,4 @@
],
jarjar_rules: "jarjar-rules.txt",
test_suites: ["device-tests"],
- lint: { strict_updatability_linting: true },
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/ArpPacketTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/ArpPacketTest.java
index e25d554..29e84c9 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/ArpPacketTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/ArpPacketTest.java
@@ -50,6 +50,8 @@
0x00, 0x1a, 0x11, 0x22, 0x33, 0x33 };
private static final byte[] TEST_TARGET_MAC_ADDR = new byte[] {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
+ private static final MacAddress TEST_DESTINATION_MAC = MacAddress.fromBytes(ETHER_BROADCAST);
+ private static final MacAddress TEST_SOURCE_MAC = MacAddress.fromBytes(TEST_SENDER_MAC_ADDR);
private static final byte[] TEST_ARP_PROBE = new byte[] {
// dst mac address
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
@@ -163,6 +165,8 @@
@Test
public void testParseArpProbePacket() throws Exception {
final ArpPacket packet = ArpPacket.parseArpPacket(TEST_ARP_PROBE, TEST_ARP_PROBE.length);
+ assertEquals(packet.destination, TEST_DESTINATION_MAC);
+ assertEquals(packet.source, TEST_SOURCE_MAC);
assertEquals(packet.opCode, ARP_REQUEST);
assertEquals(packet.senderHwAddress, MacAddress.fromBytes(TEST_SENDER_MAC_ADDR));
assertEquals(packet.targetHwAddress, MacAddress.fromBytes(TEST_TARGET_MAC_ADDR));
@@ -174,6 +178,8 @@
public void testParseArpAnnouncePacket() throws Exception {
final ArpPacket packet = ArpPacket.parseArpPacket(TEST_ARP_ANNOUNCE,
TEST_ARP_ANNOUNCE.length);
+ assertEquals(packet.destination, TEST_DESTINATION_MAC);
+ assertEquals(packet.source, TEST_SOURCE_MAC);
assertEquals(packet.opCode, ARP_REQUEST);
assertEquals(packet.senderHwAddress, MacAddress.fromBytes(TEST_SENDER_MAC_ADDR));
assertEquals(packet.targetHwAddress, MacAddress.fromBytes(TEST_TARGET_MAC_ADDR));
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
index 06b3e2f..f32337d 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
@@ -71,10 +71,6 @@
public class DeviceConfigUtilsTest {
private static final String TEST_NAME_SPACE = "connectivity";
private static final String TEST_EXPERIMENT_FLAG = "experiment_flag";
- private static final String CORE_NETWORKING_TRUNK_STABLE_NAMESPACE = "android_core_networking";
- private static final String TEST_TRUNK_STABLE_FLAG = "trunk_stable_feature";
- private static final String TEST_CORE_NETWORKING_TRUNK_STABLE_FLAG_PROPERTY =
- "com.android.net.flags.trunk_stable_feature";
private static final int TEST_FLAG_VALUE = 28;
private static final String TEST_FLAG_VALUE_STRING = "28";
private static final int TEST_DEFAULT_FLAG_VALUE = 0;
@@ -507,25 +503,4 @@
verify(mContext, never()).getPackageName();
verify(mPm, never()).getPackageInfo(anyString(), anyInt());
}
-
- @Test
- public void testIsCoreNetworkingTrunkStableFeatureEnabled() {
- doReturn(null).when(() -> DeviceConfig.getProperty(
- CORE_NETWORKING_TRUNK_STABLE_NAMESPACE,
- TEST_CORE_NETWORKING_TRUNK_STABLE_FLAG_PROPERTY));
- assertFalse(DeviceConfigUtils.isTrunkStableFeatureEnabled(
- TEST_TRUNK_STABLE_FLAG));
-
- doReturn("false").when(() -> DeviceConfig.getProperty(
- CORE_NETWORKING_TRUNK_STABLE_NAMESPACE,
- TEST_CORE_NETWORKING_TRUNK_STABLE_FLAG_PROPERTY));
- assertFalse(DeviceConfigUtils.isTrunkStableFeatureEnabled(
- TEST_TRUNK_STABLE_FLAG));
-
- doReturn("true").when(() -> DeviceConfig.getProperty(
- CORE_NETWORKING_TRUNK_STABLE_NAMESPACE,
- TEST_CORE_NETWORKING_TRUNK_STABLE_FLAG_PROPERTY));
- assertTrue(DeviceConfigUtils.isTrunkStableFeatureEnabled(
- TEST_TRUNK_STABLE_FLAG));
- }
}
diff --git a/tests/unit/java/com/android/server/HandlerUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/HandlerUtilsTest.kt
similarity index 90%
rename from tests/unit/java/com/android/server/HandlerUtilsTest.kt
rename to staticlibs/tests/unit/src/com/android/net/module/util/HandlerUtilsTest.kt
index 62bb651..f2c902f 100644
--- a/tests/unit/java/com/android/server/HandlerUtilsTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/HandlerUtilsTest.kt
@@ -14,11 +14,11 @@
* limitations under the License.
*/
-package com.android.server
+package com.android.net.module.util
import android.os.HandlerThread
-import com.android.server.connectivity.HandlerUtils
import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.DevSdkIgnoreRunner.MonitorThreadLeak
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import org.junit.After
@@ -27,6 +27,8 @@
const val THREAD_BLOCK_TIMEOUT_MS = 1000L
const val TEST_REPEAT_COUNT = 100
+
+@MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner::class)
class HandlerUtilsTest {
val handlerThread = HandlerThread("HandlerUtilsTestHandlerThread").also {
@@ -39,7 +41,7 @@
// Repeat the test a fair amount of times to ensure that it does not pass by chance.
repeat(TEST_REPEAT_COUNT) {
var result = false
- HandlerUtils.runWithScissors(handler, {
+ HandlerUtils.runWithScissorsForDump(handler, {
assertEquals(Thread.currentThread(), handlerThread)
result = true
}, THREAD_BLOCK_TIMEOUT_MS)
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/PermissionUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/PermissionUtilsTest.kt
index c5a91a4..8586e82 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/PermissionUtilsTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/PermissionUtilsTest.kt
@@ -19,16 +19,18 @@
import android.Manifest.permission.NETWORK_STACK
import android.content.Context
import android.content.pm.PackageManager
+import android.content.pm.PackageManager.NameNotFoundException
import android.content.pm.PackageManager.PERMISSION_DENIED
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
-import com.android.net.module.util.PermissionUtils.checkAnyPermissionOf
import com.android.net.module.util.PermissionUtils.enforceAnyPermissionOf
import com.android.net.module.util.PermissionUtils.enforceNetworkStackPermission
import com.android.net.module.util.PermissionUtils.enforceNetworkStackPermissionOr
+import com.android.net.module.util.PermissionUtils.enforcePackageNameMatchesUid
import com.android.net.module.util.PermissionUtils.enforceSystemFeature
+import com.android.net.module.util.PermissionUtils.hasAnyPermissionOf
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
import kotlin.test.assertEquals
@@ -42,7 +44,10 @@
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.doThrow
import org.mockito.Mockito.mock
/** Tests for PermissionUtils */
@@ -53,6 +58,9 @@
val ignoreRule = DevSdkIgnoreRule()
private val TEST_PERMISSION1 = "android.permission.TEST_PERMISSION1"
private val TEST_PERMISSION2 = "android.permission.TEST_PERMISSION2"
+ private val TEST_UID1 = 1234
+ private val TEST_UID2 = 1235
+ private val TEST_PACKAGE_NAME = "test.package"
private val mockContext = mock(Context::class.java)
private val mockPackageManager = mock(PackageManager::class.java)
@@ -61,6 +69,7 @@
@Before
fun setup() {
doReturn(mockPackageManager).`when`(mockContext).packageManager
+ doReturn(mockContext).`when`(mockContext).createContextAsUser(any(), anyInt())
}
@Test
@@ -69,18 +78,18 @@
.checkCallingOrSelfPermission(TEST_PERMISSION1)
doReturn(PERMISSION_DENIED).`when`(mockContext)
.checkCallingOrSelfPermission(TEST_PERMISSION2)
- assertTrue(checkAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2))
+ assertTrue(hasAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2))
enforceAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2)
doReturn(PERMISSION_DENIED).`when`(mockContext)
.checkCallingOrSelfPermission(TEST_PERMISSION1)
doReturn(PERMISSION_GRANTED).`when`(mockContext)
.checkCallingOrSelfPermission(TEST_PERMISSION2)
- assertTrue(checkAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2))
+ assertTrue(hasAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2))
enforceAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2)
doReturn(PERMISSION_DENIED).`when`(mockContext).checkCallingOrSelfPermission(any())
- assertFalse(checkAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2))
+ assertFalse(hasAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2))
assertFailsWith<SecurityException>("Expect fail but permission granted.") {
enforceAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2)
}
@@ -141,4 +150,24 @@
Assert.fail("Exception should have not been thrown with system feature enabled")
}
}
+
+ @Test
+ fun testEnforcePackageNameMatchesUid() {
+ // Verify name not found throws.
+ doThrow(NameNotFoundException()).`when`(mockPackageManager)
+ .getPackageUid(eq(TEST_PACKAGE_NAME), anyInt())
+ assertFailsWith<SecurityException> {
+ enforcePackageNameMatchesUid(mockContext, TEST_UID1, TEST_PACKAGE_NAME)
+ }
+
+ // Verify uid mismatch throws.
+ doReturn(TEST_UID1).`when`(mockPackageManager)
+ .getPackageUid(eq(TEST_PACKAGE_NAME), anyInt())
+ assertFailsWith<SecurityException> {
+ enforcePackageNameMatchesUid(mockContext, TEST_UID2, TEST_PACKAGE_NAME)
+ }
+
+ // Verify uid match passes.
+ enforcePackageNameMatchesUid(mockContext, TEST_UID1, TEST_PACKAGE_NAME)
+ }
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java
index 65e99f8..b44e428 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java
@@ -32,6 +32,7 @@
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -345,28 +346,28 @@
// Hexadecimal representation of InetDiagMessage
private static final String INET_DIAG_MSG_HEX1 =
// struct nlmsghdr
- "58000000" + // length = 88
- "1400" + // type = SOCK_DIAG_BY_FAMILY
- "0200" + // flags = NLM_F_MULTI
- "00000000" + // seqno
- "f5220000" + // pid
+ "58000000" // length = 88
+ + "1400" // type = SOCK_DIAG_BY_FAMILY
+ + "0200" // flags = NLM_F_MULTI
+ + "00000000" // seqno
+ + "f5220000" // pid
// struct inet_diag_msg
- "0a" + // family = AF_INET6
- "01" + // idiag_state = 1
- "02" + // idiag_timer = 2
- "ff" + // idiag_retrans = 255
+ + "0a" // family = AF_INET6
+ + "01" // idiag_state = 1
+ + "02" // idiag_timer = 2
+ + "ff" // idiag_retrans = 255
// inet_diag_sockid
- "a817" + // idiag_sport = 43031
- "960f" + // idiag_dport = 38415
- "20010db8000000000000000000000001" + // idiag_src = 2001:db8::1
- "20010db8000000000000000000000002" + // idiag_dst = 2001:db8::2
- "07000000" + // idiag_if = 7
- "5800000000000000" + // idiag_cookie = 88
- "04000000" + // idiag_expires = 4
- "05000000" + // idiag_rqueue = 5
- "06000000" + // idiag_wqueue = 6
- "a3270000" + // idiag_uid = 10147
- "a57e19f0"; // idiag_inode = 4028202661
+ + "a817" // idiag_sport = 43031
+ + "960f" // idiag_dport = 38415
+ + "20010db8000000000000000000000001" // idiag_src = 2001:db8::1
+ + "20010db8000000000000000000000002" // idiag_dst = 2001:db8::2
+ + "07000000" // idiag_if = 7
+ + "5800000000000000" // idiag_cookie = 88
+ + "04000000" // idiag_expires = 4
+ + "05000000" // idiag_rqueue = 5
+ + "06000000" // idiag_wqueue = 6
+ + "a3270000" // idiag_uid = 10147
+ + "a57e19f0"; // idiag_inode = 4028202661
private void assertInetDiagMsg1(final NetlinkMessage msg) {
assertNotNull(msg);
@@ -394,33 +395,45 @@
assertEquals(6, inetDiagMsg.inetDiagMsg.idiag_wqueue);
assertEquals(10147, inetDiagMsg.inetDiagMsg.idiag_uid);
assertEquals(4028202661L, inetDiagMsg.inetDiagMsg.idiag_inode);
+
+ // Verify the length of attribute list is 0 as expected since message doesn't
+ // take any attributes
+ assertEquals(0, inetDiagMsg.nlAttrs.size());
}
// Hexadecimal representation of InetDiagMessage
private static final String INET_DIAG_MSG_HEX2 =
// struct nlmsghdr
- "58000000" + // length = 88
- "1400" + // type = SOCK_DIAG_BY_FAMILY
- "0200" + // flags = NLM_F_MULTI
- "00000000" + // seqno
- "f5220000" + // pid
+ "6C000000" // length = 108
+ + "1400" // type = SOCK_DIAG_BY_FAMILY
+ + "0200" // flags = NLM_F_MULTI
+ + "00000000" // seqno
+ + "f5220000" // pid
// struct inet_diag_msg
- "0a" + // family = AF_INET6
- "02" + // idiag_state = 2
- "10" + // idiag_timer = 16
- "20" + // idiag_retrans = 32
+ + "0a" // family = AF_INET6
+ + "02" // idiag_state = 2
+ + "10" // idiag_timer = 16
+ + "20" // idiag_retrans = 32
// inet_diag_sockid
- "a845" + // idiag_sport = 43077
- "01bb" + // idiag_dport = 443
- "20010db8000000000000000000000003" + // idiag_src = 2001:db8::3
- "20010db8000000000000000000000004" + // idiag_dst = 2001:db8::4
- "08000000" + // idiag_if = 8
- "6300000000000000" + // idiag_cookie = 99
- "30000000" + // idiag_expires = 48
- "40000000" + // idiag_rqueue = 64
- "50000000" + // idiag_wqueue = 80
- "39300000" + // idiag_uid = 12345
- "851a0000"; // idiag_inode = 6789
+ + "a845" // idiag_sport = 43077
+ + "01bb" // idiag_dport = 443
+ + "20010db8000000000000000000000003" // idiag_src = 2001:db8::3
+ + "20010db8000000000000000000000004" // idiag_dst = 2001:db8::4
+ + "08000000" // idiag_if = 8
+ + "6300000000000000" // idiag_cookie = 99
+ + "30000000" // idiag_expires = 48
+ + "40000000" // idiag_rqueue = 64
+ + "50000000" // idiag_wqueue = 80
+ + "39300000" // idiag_uid = 12345
+ + "851a0000" // idiag_inode = 6789
+ + "0500" // len = 5
+ + "0800" // type = 8
+ + "00000000" // data
+ + "0800" // len = 8
+ + "0F00" // type = 15(INET_DIAG_MARK)
+ + "850A0C00" // data, socket mark=789125
+ + "0400" // len = 4
+ + "0200"; // type = 2
private void assertInetDiagMsg2(final NetlinkMessage msg) {
assertNotNull(msg);
@@ -448,6 +461,104 @@
assertEquals(80, inetDiagMsg.inetDiagMsg.idiag_wqueue);
assertEquals(12345, inetDiagMsg.inetDiagMsg.idiag_uid);
assertEquals(6789, inetDiagMsg.inetDiagMsg.idiag_inode);
+
+ // Verify the number of nlAttr and their content.
+ assertEquals(3, inetDiagMsg.nlAttrs.size());
+
+ assertEquals(5, inetDiagMsg.nlAttrs.get(0).nla_len);
+ assertEquals(8, inetDiagMsg.nlAttrs.get(0).nla_type);
+ assertArrayEquals(
+ HexEncoding.decode("00".toCharArray(), false),
+ inetDiagMsg.nlAttrs.get(0).nla_value);
+ assertEquals(8, inetDiagMsg.nlAttrs.get(1).nla_len);
+ assertEquals(15, inetDiagMsg.nlAttrs.get(1).nla_type);
+ assertArrayEquals(
+ HexEncoding.decode("850A0C00".toCharArray(), false),
+ inetDiagMsg.nlAttrs.get(1).nla_value);
+ assertEquals(4, inetDiagMsg.nlAttrs.get(2).nla_len);
+ assertEquals(2, inetDiagMsg.nlAttrs.get(2).nla_type);
+ assertNull(inetDiagMsg.nlAttrs.get(2).nla_value);
+ }
+
+ // Hexadecimal representation of InetDiagMessage
+ private static final String INET_DIAG_MSG_HEX_MALFORMED =
+ // struct nlmsghdr
+ "6E000000" // length = 110
+ + "1400" // type = SOCK_DIAG_BY_FAMILY
+ + "0200" // flags = NLM_F_MULTI
+ + "00000000" // seqno
+ + "f5220000" // pid
+ // struct inet_diag_msg
+ + "0a" // family = AF_INET6
+ + "02" // idiag_state = 2
+ + "10" // idiag_timer = 16
+ + "20" // idiag_retrans = 32
+ // inet_diag_sockid
+ + "a845" // idiag_sport = 43077
+ + "01bb" // idiag_dport = 443
+ + "20010db8000000000000000000000005" // idiag_src = 2001:db8::5
+ + "20010db8000000000000000000000006" // idiag_dst = 2001:db8::6
+ + "08000000" // idiag_if = 8
+ + "6300000000000000" // idiag_cookie = 99
+ + "30000000" // idiag_expires = 48
+ + "40000000" // idiag_rqueue = 64
+ + "50000000" // idiag_wqueue = 80
+ + "39300000" // idiag_uid = 12345
+ + "851a0000" // idiag_inode = 6789
+ + "0500" // len = 5
+ + "0800" // type = 8
+ + "00000000" // data
+ + "0800" // len = 8
+ + "0F00" // type = 15(INET_DIAG_MARK)
+ + "850A0C00" // data, socket mark=789125
+ + "0400" // len = 4
+ + "0200" // type = 2
+ + "0100" // len = 1, malformed value
+ + "0100"; // type = 1
+
+ @Test
+ public void testParseInetDiagResponseMalformedNlAttr() throws Exception {
+ final ByteBuffer byteBuffer = ByteBuffer.wrap(
+ HexEncoding.decode((INET_DIAG_MSG_HEX_MALFORMED).toCharArray(), false));
+ byteBuffer.order(ByteOrder.nativeOrder());
+ assertNull(NetlinkMessage.parse(byteBuffer, NETLINK_INET_DIAG));
+ }
+
+ // Hexadecimal representation of InetDiagMessage
+ private static final String INET_DIAG_MSG_HEX_TRUNCATED =
+ // struct nlmsghdr
+ "5E000000" // length = 96
+ + "1400" // type = SOCK_DIAG_BY_FAMILY
+ + "0200" // flags = NLM_F_MULTI
+ + "00000000" // seqno
+ + "f5220000" // pid
+ // struct inet_diag_msg
+ + "0a" // family = AF_INET6
+ + "02" // idiag_state = 2
+ + "10" // idiag_timer = 16
+ + "20" // idiag_retrans = 32
+ // inet_diag_sockid
+ + "a845" // idiag_sport = 43077
+ + "01bb" // idiag_dport = 443
+ + "20010db8000000000000000000000005" // idiag_src = 2001:db8::5
+ + "20010db8000000000000000000000006" // idiag_dst = 2001:db8::6
+ + "08000000" // idiag_if = 8
+ + "6300000000000000" // idiag_cookie = 99
+ + "30000000" // idiag_expires = 48
+ + "40000000" // idiag_rqueue = 64
+ + "50000000" // idiag_wqueue = 80
+ + "39300000" // idiag_uid = 12345
+ + "851a0000" // idiag_inode = 6789
+ + "0800" // len = 8
+ + "0100" // type = 1
+ + "000000"; // data, less than the expected length
+
+ @Test
+ public void testParseInetDiagResponseTruncatedNlAttr() throws Exception {
+ final ByteBuffer byteBuffer = ByteBuffer.wrap(
+ HexEncoding.decode((INET_DIAG_MSG_HEX_TRUNCATED).toCharArray(), false));
+ byteBuffer.order(ByteOrder.nativeOrder());
+ assertNull(NetlinkMessage.parse(byteBuffer, NETLINK_INET_DIAG));
}
private static final byte[] INET_DIAG_MSG_BYTES =
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
index 5a231fc..f64adb8 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
@@ -21,7 +21,9 @@
import static android.system.OsConstants.AF_UNSPEC;
import static android.system.OsConstants.EACCES;
import static android.system.OsConstants.NETLINK_ROUTE;
-
+import static android.system.OsConstants.SOL_SOCKET;
+import static android.system.OsConstants.SO_RCVBUF;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTNL_FAMILY_IP6MR;
import static com.android.net.module.util.netlink.NetlinkUtils.DEFAULT_RECV_BUFSIZE;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
@@ -33,6 +35,8 @@
import static org.junit.Assume.assumeFalse;
import android.content.Context;
+import android.net.util.SocketUtils;
+import android.os.Build;
import android.system.ErrnoException;
import android.system.NetlinkSocketAddress;
import android.system.Os;
@@ -43,6 +47,7 @@
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.Struct;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import libcore.io.IoUtils;
@@ -55,6 +60,9 @@
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -65,19 +73,14 @@
@Test
public void testGetNeighborsQuery() throws Exception {
- final FileDescriptor fd = NetlinkUtils.netlinkSocketForProto(NETLINK_ROUTE);
- assertNotNull(fd);
-
- NetlinkUtils.connectToKernel(fd);
-
- final NetlinkSocketAddress localAddr = (NetlinkSocketAddress) Os.getsockname(fd);
- assertNotNull(localAddr);
- assertEquals(0, localAddr.getGroupsMask());
- assertTrue(0 != localAddr.getPortId());
-
final byte[] req = RtNetlinkNeighborMessage.newGetNeighborsRequest(TEST_SEQNO);
assertNotNull(req);
+ List<RtNetlinkNeighborMessage> msgs = new ArrayList<>();
+ Consumer<RtNetlinkNeighborMessage> handleNlDumpMsg = (msg) -> {
+ msgs.add(msg);
+ };
+
final Context ctx = InstrumentationRegistry.getInstrumentation().getContext();
final int targetSdk =
ctx.getPackageManager()
@@ -94,7 +97,8 @@
assumeFalse("network_stack context is expected to have permission to send RTM_GETNEIGH",
ctxt.startsWith("u:r:network_stack:s0"));
try {
- NetlinkUtils.sendMessage(fd, req, 0, req.length, TEST_TIMEOUT_MS);
+ NetlinkUtils.<RtNetlinkNeighborMessage>getAndProcessNetlinkDumpMessages(req,
+ NETLINK_ROUTE, RtNetlinkNeighborMessage.class, handleNlDumpMsg);
fail("RTM_GETNEIGH is not allowed for apps targeting SDK > 31 on T+ platforms,"
+ " target SDK version: " + targetSdk);
} catch (ErrnoException e) {
@@ -105,106 +109,70 @@
}
// Check that apps targeting lower API levels / running on older platforms succeed
- assertEquals(req.length,
- NetlinkUtils.sendMessage(fd, req, 0, req.length, TEST_TIMEOUT_MS));
+ NetlinkUtils.<RtNetlinkNeighborMessage>getAndProcessNetlinkDumpMessages(req,
+ NETLINK_ROUTE, RtNetlinkNeighborMessage.class, handleNlDumpMsg);
- int neighMessageCount = 0;
- int doneMessageCount = 0;
-
- while (doneMessageCount == 0) {
- ByteBuffer response =
- NetlinkUtils.recvMessage(fd, DEFAULT_RECV_BUFSIZE, TEST_TIMEOUT_MS);
- assertNotNull(response);
- assertTrue(StructNlMsgHdr.STRUCT_SIZE <= response.limit());
- assertEquals(0, response.position());
- assertEquals(ByteOrder.nativeOrder(), response.order());
-
- // Verify the messages at least appears minimally reasonable.
- while (response.remaining() > 0) {
- final NetlinkMessage msg = NetlinkMessage.parse(response, NETLINK_ROUTE);
- assertNotNull(msg);
- final StructNlMsgHdr hdr = msg.getHeader();
- assertNotNull(hdr);
-
- if (hdr.nlmsg_type == NetlinkConstants.NLMSG_DONE) {
- doneMessageCount++;
- continue;
- }
-
- assertEquals(NetlinkConstants.RTM_NEWNEIGH, hdr.nlmsg_type);
- assertTrue(msg instanceof RtNetlinkNeighborMessage);
- assertTrue((hdr.nlmsg_flags & StructNlMsgHdr.NLM_F_MULTI) != 0);
- assertEquals(TEST_SEQNO, hdr.nlmsg_seq);
- assertEquals(localAddr.getPortId(), hdr.nlmsg_pid);
-
- neighMessageCount++;
- }
+ for (var msg : msgs) {
+ assertNotNull(msg);
+ final StructNlMsgHdr hdr = msg.getHeader();
+ assertNotNull(hdr);
+ assertEquals(NetlinkConstants.RTM_NEWNEIGH, hdr.nlmsg_type);
+ assertTrue((hdr.nlmsg_flags & StructNlMsgHdr.NLM_F_MULTI) != 0);
+ assertEquals(TEST_SEQNO, hdr.nlmsg_seq);
}
- assertEquals(1, doneMessageCount);
// TODO: make sure this test passes sanely in airplane mode.
- assertTrue(neighMessageCount > 0);
-
- IoUtils.closeQuietly(fd);
+ assertTrue(msgs.size() > 0);
}
@Test
public void testBasicWorkingGetAddrQuery() throws Exception {
- final FileDescriptor fd = NetlinkUtils.netlinkSocketForProto(NETLINK_ROUTE);
- assertNotNull(fd);
-
- NetlinkUtils.connectToKernel(fd);
-
- final NetlinkSocketAddress localAddr = (NetlinkSocketAddress) Os.getsockname(fd);
- assertNotNull(localAddr);
- assertEquals(0, localAddr.getGroupsMask());
- assertTrue(0 != localAddr.getPortId());
-
final int testSeqno = 8;
final byte[] req = newGetAddrRequest(testSeqno);
assertNotNull(req);
- final long timeout = 500;
- assertEquals(req.length, NetlinkUtils.sendMessage(fd, req, 0, req.length, timeout));
+ List<RtNetlinkAddressMessage> msgs = new ArrayList<>();
+ Consumer<RtNetlinkAddressMessage> handleNlDumpMsg = (msg) -> {
+ msgs.add(msg);
+ };
+ NetlinkUtils.<RtNetlinkAddressMessage>getAndProcessNetlinkDumpMessages(req, NETLINK_ROUTE,
+ RtNetlinkAddressMessage.class, handleNlDumpMsg);
- int addrMessageCount = 0;
+ boolean ipv4LoopbackAddressFound = false;
+ boolean ipv6LoopbackAddressFound = false;
+ final InetAddress loopbackIpv4 = InetAddress.getByName("127.0.0.1");
+ final InetAddress loopbackIpv6 = InetAddress.getByName("::1");
- while (true) {
- ByteBuffer response = NetlinkUtils.recvMessage(fd, DEFAULT_RECV_BUFSIZE, timeout);
- assertNotNull(response);
- assertTrue(StructNlMsgHdr.STRUCT_SIZE <= response.limit());
- assertEquals(0, response.position());
- assertEquals(ByteOrder.nativeOrder(), response.order());
-
- final NetlinkMessage msg = NetlinkMessage.parse(response, NETLINK_ROUTE);
+ for (var msg : msgs) {
assertNotNull(msg);
final StructNlMsgHdr nlmsghdr = msg.getHeader();
assertNotNull(nlmsghdr);
-
- if (nlmsghdr.nlmsg_type == NetlinkConstants.NLMSG_DONE) {
- break;
- }
-
assertEquals(NetlinkConstants.RTM_NEWADDR, nlmsghdr.nlmsg_type);
assertTrue((nlmsghdr.nlmsg_flags & StructNlMsgHdr.NLM_F_MULTI) != 0);
assertEquals(testSeqno, nlmsghdr.nlmsg_seq);
- assertEquals(localAddr.getPortId(), nlmsghdr.nlmsg_pid);
assertTrue(msg instanceof RtNetlinkAddressMessage);
- addrMessageCount++;
-
- // From the query response we can see the RTM_NEWADDR messages representing for IPv4
- // and IPv6 loopback address: 127.0.0.1 and ::1.
+ // When parsing the full response we can see the RTM_NEWADDR messages representing for
+ // IPv4 and IPv6 loopback address: 127.0.0.1 and ::1 and non-loopback addresses.
final StructIfaddrMsg ifaMsg = ((RtNetlinkAddressMessage) msg).getIfaddrHeader();
final InetAddress ipAddress = ((RtNetlinkAddressMessage) msg).getIpAddress();
assertTrue(
"Non-IP address family: " + ifaMsg.family,
ifaMsg.family == AF_INET || ifaMsg.family == AF_INET6);
- assertTrue(ipAddress.isLoopbackAddress());
+ assertNotNull(ipAddress);
+
+ if (ipAddress.equals(loopbackIpv4)) {
+ ipv4LoopbackAddressFound = true;
+ assertTrue(ipAddress.isLoopbackAddress());
+ }
+ if (ipAddress.equals(loopbackIpv6)) {
+ ipv6LoopbackAddressFound = true;
+ assertTrue(ipAddress.isLoopbackAddress());
+ }
}
- assertTrue(addrMessageCount > 0);
-
- IoUtils.closeQuietly(fd);
+ assertTrue(msgs.size() > 0);
+ // Check ipv4 and ipv6 loopback addresses are in the output
+ assertTrue(ipv4LoopbackAddressFound && ipv6LoopbackAddressFound);
}
/** A convenience method to create an RTM_GETADDR request message. */
@@ -228,4 +196,36 @@
return bytes;
}
+
+ @Test
+ public void testGetIpv6MulticastRoutes_doesNotThrow() {
+ var multicastRoutes = NetlinkUtils.getIpv6MulticastRoutes();
+
+ for (var route : multicastRoutes) {
+ assertNotNull(route);
+ assertEquals("Route is not IP6MR: " + route,
+ RTNL_FAMILY_IP6MR, route.getRtmFamily());
+ assertNotNull("Route doesn't contain source: " + route, route.getSource());
+ assertNotNull("Route doesn't contain destination: " + route, route.getDestination());
+ }
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.R) // getsockoptInt requires > R
+ public void testNetlinkSocketForProto_defaultBufferSize() throws Exception {
+ final FileDescriptor fd = NetlinkUtils.netlinkSocketForProto(NETLINK_ROUTE);
+ final int bufferSize = Os.getsockoptInt(fd, SOL_SOCKET, SO_RCVBUF) / 2;
+
+ assertTrue("bufferSize: " + bufferSize, bufferSize > 0); // whatever the default value is
+ SocketUtils.closeSocket(fd);
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.R) // getsockoptInt requires > R
+ public void testNetlinkSocketForProto_setBufferSize() throws Exception {
+ final FileDescriptor fd = NetlinkUtils.netlinkSocketForProto(NETLINK_ROUTE,
+ 8000);
+ final int bufferSize = Os.getsockoptInt(fd, SOL_SOCKET, SO_RCVBUF) / 2;
+
+ assertEquals(8000, bufferSize);
+ SocketUtils.closeSocket(fd);
+ }
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkAddressMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkAddressMessageTest.java
index 01126d2..1d08525 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkAddressMessageTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkAddressMessageTest.java
@@ -42,6 +42,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.net.Inet4Address;
import java.net.Inet6Address;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@@ -179,6 +180,57 @@
}
@Test
+ public void testCreateRtmNewAddressMessage_IPv4Address() {
+ // Hexadecimal representation of our created packet.
+ final String expectedNewAddressHex =
+ // struct nlmsghdr
+ "4c000000" // length = 76
+ + "1400" // type = 20 (RTM_NEWADDR)
+ + "0501" // flags = NLM_F_ACK | NLM_F_REQUEST | NLM_F_REPLACE
+ + "01000000" // seqno = 1
+ + "00000000" // pid = 0 (send to kernel)
+ // struct IfaddrMsg
+ + "02" // family = inet
+ + "18" // prefix len = 24
+ + "00" // flags = 0
+ + "00" // scope = RT_SCOPE_UNIVERSE
+ + "14000000" // ifindex = 20
+ // struct nlattr: IFA_ADDRESS
+ + "0800" // len
+ + "0100" // type
+ + "C0A80491" // IPv4 address = 192.168.4.145
+ // struct nlattr: IFA_CACHEINFO
+ + "1400" // len
+ + "0600" // type
+ + "C0A80000" // preferred = 43200s
+ + "C0A80000" // valid = 43200s
+ + "00000000" // cstamp
+ + "00000000" // tstamp
+ // struct nlattr: IFA_FLAGS
+ + "0800" // len
+ + "0800" // type
+ + "00000000" // flags = 0
+ // struct nlattr: IFA_LOCAL
+ + "0800" // len
+ + "0200" // type
+ + "C0A80491" // local address = 192.168.4.145
+ // struct nlattr: IFA_BROADCAST
+ + "0800" // len
+ + "0400" // type
+ + "C0A804FF"; // broadcast address = 192.168.4.255
+ final byte[] expectedNewAddress =
+ HexEncoding.decode(expectedNewAddressHex.toCharArray(), false);
+
+ final Inet4Address ipAddress =
+ (Inet4Address) InetAddresses.parseNumericAddress("192.168.4.145");
+ final byte[] bytes = RtNetlinkAddressMessage.newRtmNewAddressMessage(1 /* seqno */,
+ ipAddress, (short) 24 /* prefix len */, 0 /* flags */,
+ (byte) RT_SCOPE_UNIVERSE /* scope */, 20 /* ifindex */,
+ (long) 0xA8C0 /* preferred */, (long) 0xA8C0 /* valid */);
+ assertArrayEquals(expectedNewAddress, bytes);
+ }
+
+ @Test
public void testCreateRtmDelAddressMessage() {
// Hexadecimal representation of our created packet.
final String expectedDelAddressHex =
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkRouteMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkRouteMessageTest.java
index 9881653..50b8278 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkRouteMessageTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkRouteMessageTest.java
@@ -16,7 +16,9 @@
package com.android.net.module.util.netlink;
+import static android.system.OsConstants.AF_INET6;
import static android.system.OsConstants.NETLINK_ROUTE;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTNL_FAMILY_IP6MR;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@@ -38,6 +40,7 @@
import java.net.Inet6Address;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import java.util.Arrays;
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -127,6 +130,72 @@
assertEquals(RTM_NEWROUTE_PACK_HEX, HexDump.toHexString(packBuffer.array()));
}
+ private static final String RTM_GETROUTE_MULTICAST_IPV6_HEX =
+ "1C0000001A0001030000000000000000" // struct nlmsghr
+ + "810000000000000000000000"; // struct rtmsg
+
+ private static final String RTM_NEWROUTE_MULTICAST_IPV6_HEX =
+ "88000000180002000000000000000000" // struct nlmsghr
+ + "81808000FE11000500000000" // struct rtmsg
+ + "08000F00FE000000" // RTA_TABLE
+ + "14000200FDACC0F1DBDB000195B7C1A464F944EA" // RTA_SRC
+ + "14000100FF040000000000000000000000001234" // RTA_DST
+ + "0800030014000000" // RTA_IIF
+ + "0C0009000800000111000000" // RTA_MULTIPATH
+ + "1C00110001000000000000009400000000000000" // RTA_STATS
+ + "0000000000000000"
+ + "0C0017007617000000000000"; // RTA_EXPIRES
+
+ @Test
+ public void testParseRtmNewRoute_MulticastIpv6() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_MULTICAST_IPV6_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
+
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ assertNotNull(msg);
+ assertTrue(msg instanceof RtNetlinkRouteMessage);
+ final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+ final StructNlMsgHdr hdr = routeMsg.getHeader();
+ assertNotNull(hdr);
+ assertEquals(136, hdr.nlmsg_len);
+ assertEquals(NetlinkConstants.RTM_NEWROUTE, hdr.nlmsg_type);
+
+ final StructRtMsg rtmsg = routeMsg.getRtMsgHeader();
+ assertNotNull(rtmsg);
+ assertEquals((byte) 129, (byte) rtmsg.family);
+ assertEquals(128, rtmsg.dstLen);
+ assertEquals(128, rtmsg.srcLen);
+ assertEquals(0xFE, rtmsg.table);
+
+ assertEquals(routeMsg.getSource(),
+ new IpPrefix("fdac:c0f1:dbdb:1:95b7:c1a4:64f9:44ea/128"));
+ assertEquals(routeMsg.getDestination(), new IpPrefix("ff04::1234/128"));
+ assertEquals(20, routeMsg.getIifIndex());
+ assertEquals(60060, routeMsg.getSinceLastUseMillis());
+ }
+
+ // NEWROUTE message for multicast IPv6 with the packed attributes
+ private static final String RTM_NEWROUTE_MULTICAST_IPV6_PACK_HEX =
+ "58000000180002000000000000000000" // struct nlmsghr
+ + "81808000FE11000500000000" // struct rtmsg
+ + "14000200FDACC0F1DBDB000195B7C1A464F944EA" // RTA_SRC
+ + "14000100FF040000000000000000000000001234" // RTA_DST
+ + "0800030014000000" // RTA_IIF
+ + "0C0017007617000000000000"; // RTA_EXPIRES
+ @Test
+ public void testPackRtmNewRoute_MulticastIpv6() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_MULTICAST_IPV6_PACK_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+
+ final ByteBuffer packBuffer = ByteBuffer.allocate(88);
+ packBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
+ routeMsg.pack(packBuffer);
+ assertEquals(RTM_NEWROUTE_MULTICAST_IPV6_PACK_HEX,
+ HexDump.toHexString(packBuffer.array()));
+ }
+
private static final String RTM_NEWROUTE_TRUNCATED_HEX =
"48000000180000060000000000000000" // struct nlmsghr
+ "0A400000FC02000100000000" // struct rtmsg
@@ -220,10 +289,79 @@
+ "scope: 0, type: 1, flags: 0}, "
+ "destination{2001:db8:1::}, "
+ "gateway{fe80::1}, "
- + "ifindex{735}, "
+ + "oifindex{735}, "
+ "rta_cacheinfo{clntref: 0, lastuse: 0, expires: 59998, error: 0, used: 0, "
+ "id: 0, ts: 0, tsage: 0} "
+ "}";
assertEquals(expected, routeMsg.toString());
}
+
+ @Test
+ public void testToString_RtmGetRoute() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_GETROUTE_MULTICAST_IPV6_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ assertNotNull(msg);
+ assertTrue(msg instanceof RtNetlinkRouteMessage);
+ final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+ final String expected = "RtNetlinkRouteMessage{ "
+ + "nlmsghdr{"
+ + "StructNlMsgHdr{ nlmsg_len{28}, nlmsg_type{26(RTM_GETROUTE)}, "
+ + "nlmsg_flags{769(NLM_F_REQUEST|NLM_F_DUMP)}, nlmsg_seq{0}, nlmsg_pid{0} }}, "
+ + "Rtmsg{"
+ + "family: 129, dstLen: 0, srcLen: 0, tos: 0, table: 0, protocol: 0, "
+ + "scope: 0, type: 0, flags: 0}, "
+ + "destination{::}, "
+ + "gateway{}, "
+ + "oifindex{0}, "
+ + "rta_cacheinfo{} "
+ + "}";
+ assertEquals(expected, routeMsg.toString());
+ }
+
+ @Test
+ public void testToString_RtmNewRouteMulticastIpv6() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_MULTICAST_IPV6_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ assertNotNull(msg);
+ assertTrue(msg instanceof RtNetlinkRouteMessage);
+ final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+ final String expected = "RtNetlinkRouteMessage{ "
+ + "nlmsghdr{"
+ + "StructNlMsgHdr{ nlmsg_len{136}, nlmsg_type{24(RTM_NEWROUTE)}, "
+ + "nlmsg_flags{2(NLM_F_MULTI)}, nlmsg_seq{0}, nlmsg_pid{0} }}, "
+ + "Rtmsg{"
+ + "family: 129, dstLen: 128, srcLen: 128, tos: 0, table: 254, protocol: 17, "
+ + "scope: 0, type: 5, flags: 0}, "
+ + "source{fdac:c0f1:dbdb:1:95b7:c1a4:64f9:44ea}, "
+ + "destination{ff04::1234}, "
+ + "gateway{}, "
+ + "iifindex{20}, "
+ + "oifindex{0}, "
+ + "rta_cacheinfo{} "
+ + "sinceLastUseMillis{60060}"
+ + "}";
+ assertEquals(expected, routeMsg.toString());
+ }
+
+ @Test
+ public void testGetRtmFamily_RTNL_FAMILY_IP6MR() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_MULTICAST_IPV6_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+
+ assertEquals(RTNL_FAMILY_IP6MR, routeMsg.getRtmFamily());
+ }
+
+ @Test
+ public void testGetRtmFamily_AF_INET6() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+
+ assertEquals(AF_INET6, routeMsg.getRtmFamily());
+ }
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java
index af3fac2..4c3fde6 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java
@@ -92,4 +92,26 @@
assertNull(integer3);
assertEquals(int3, 0x08 /* default value */);
}
+
+ @Test
+ public void testGetValueAsLong() {
+ final Long input = 1234567L;
+ // Not a real netlink attribute, just for testing
+ final StructNlAttr attr = new StructNlAttr(IFA_FLAGS, input);
+
+ final Long output = attr.getValueAsLong();
+
+ assertEquals(input, output);
+ }
+
+ @Test
+ public void testGetValueAsLong_malformed() {
+ final int input = 1234567;
+ // Not a real netlink attribute, just for testing
+ final StructNlAttr attr = new StructNlAttr(IFA_FLAGS, input);
+
+ final Long output = attr.getValueAsLong();
+
+ assertNull(output);
+ }
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMf6cctlTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMf6cctlTest.java
new file mode 100644
index 0000000..a83fc36
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMf6cctlTest.java
@@ -0,0 +1,102 @@
+package com.android.net.module.util.structs;
+
+import static android.system.OsConstants.AF_INET6;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import android.net.InetAddresses;
+import android.util.ArraySet;
+import androidx.test.runner.AndroidJUnit4;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class StructMf6cctlTest {
+ private static final byte[] MSG_BYTES = new byte[] {
+ 10, 0, /* AF_INET6 */
+ 0, 0, /* originPort */
+ 0, 0, 0, 0, /* originFlowinfo */
+ 32, 1, 13, -72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, /* originAddress */
+ 0, 0, 0, 0, /* originScopeId */
+ 10, 0, /* AF_INET6 */
+ 0, 0, /* groupPort */
+ 0, 0, 0, 0, /* groupFlowinfo*/
+ -1, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 52, /*groupAddress*/
+ 0, 0, 0, 0, /* groupScopeId*/
+ 1, 0, /* mf6ccParent */
+ 0, 0, /* padding */
+ 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 /* mf6ccIfset */
+ };
+
+ private static final int OIF = 10;
+ private static final byte[] OIFSET_BYTES = new byte[] {
+ 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+ };
+
+ private static final Inet6Address SOURCE =
+ (Inet6Address) InetAddresses.parseNumericAddress("2001:db8::1");
+ private static final Inet6Address DESTINATION =
+ (Inet6Address) InetAddresses.parseNumericAddress("ff05::1234");
+
+ @Test
+ public void testConstructor() {
+ final Set<Integer> oifset = new ArraySet<>();
+ oifset.add(OIF);
+
+ StructMf6cctl mf6cctl = new StructMf6cctl(SOURCE, DESTINATION,
+ 1 /* mf6ccParent */, oifset);
+
+ assertTrue(Arrays.equals(SOURCE.getAddress(), mf6cctl.originAddress));
+ assertTrue(Arrays.equals(DESTINATION.getAddress(), mf6cctl.groupAddress));
+ assertEquals(1, mf6cctl.mf6ccParent);
+ assertArrayEquals(OIFSET_BYTES, mf6cctl.mf6ccIfset);
+ }
+
+ @Test
+ public void testConstructor_tooBigOifIndex_throwsIllegalArgumentException()
+ throws UnknownHostException {
+ final Set<Integer> oifset = new ArraySet<>();
+ oifset.add(1000);
+
+ assertThrows(IllegalArgumentException.class,
+ () -> new StructMf6cctl(SOURCE, DESTINATION, 1, oifset));
+ }
+
+ @Test
+ public void testParseMf6cctl() {
+ final ByteBuffer buf = ByteBuffer.wrap(MSG_BYTES);
+ buf.order(ByteOrder.nativeOrder());
+ StructMf6cctl mf6cctl = StructMf6cctl.parse(StructMf6cctl.class, buf);
+
+ assertEquals(AF_INET6, mf6cctl.originFamily);
+ assertEquals(AF_INET6, mf6cctl.groupFamily);
+ assertArrayEquals(SOURCE.getAddress(), mf6cctl.originAddress);
+ assertArrayEquals(DESTINATION.getAddress(), mf6cctl.groupAddress);
+ assertEquals(1, mf6cctl.mf6ccParent);
+ assertArrayEquals("mf6ccIfset = " + Arrays.toString(mf6cctl.mf6ccIfset),
+ OIFSET_BYTES, mf6cctl.mf6ccIfset);
+ }
+
+ @Test
+ public void testWriteToBytes() {
+ final Set<Integer> oifset = new ArraySet<>();
+ oifset.add(OIF);
+
+ StructMf6cctl mf6cctl = new StructMf6cctl(SOURCE, DESTINATION,
+ 1 /* mf6ccParent */, oifset);
+ byte[] bytes = mf6cctl.writeToBytes();
+
+ assertArrayEquals("bytes = " + Arrays.toString(bytes), MSG_BYTES, bytes);
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMif6ctlTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMif6ctlTest.java
new file mode 100644
index 0000000..75196e4
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMif6ctlTest.java
@@ -0,0 +1,70 @@
+package com.android.net.module.util.structs;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.util.ArraySet;
+import androidx.test.runner.AndroidJUnit4;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class StructMif6ctlTest {
+ private static final byte[] MSG_BYTES = new byte[] {
+ 1, 0, /* mif6cMifi */
+ 0, /* mif6cFlags */
+ 1, /* vifcThreshold*/
+ 20, 0, /* mif6cPifi */
+ 0, 0, 0, 0, /* vifcRateLimit */
+ 0, 0 /* padding */
+ };
+
+ @Test
+ public void testConstructor() {
+ StructMif6ctl mif6ctl = new StructMif6ctl(10 /* mif6cMifi */,
+ (short) 11 /* mif6cFlags */,
+ (short) 12 /* vifcThreshold */,
+ 13 /* mif6cPifi */,
+ 14L /* vifcRateLimit */);
+
+ assertEquals(10, mif6ctl.mif6cMifi);
+ assertEquals(11, mif6ctl.mif6cFlags);
+ assertEquals(12, mif6ctl.vifcThreshold);
+ assertEquals(13, mif6ctl.mif6cPifi);
+ assertEquals(14, mif6ctl.vifcRateLimit);
+ }
+
+ @Test
+ public void testParseMif6ctl() {
+ final ByteBuffer buf = ByteBuffer.wrap(MSG_BYTES);
+ buf.order(ByteOrder.nativeOrder());
+ StructMif6ctl mif6ctl = StructMif6ctl.parse(StructMif6ctl.class, buf);
+
+ assertEquals(1, mif6ctl.mif6cMifi);
+ assertEquals(0, mif6ctl.mif6cFlags);
+ assertEquals(1, mif6ctl.vifcThreshold);
+ assertEquals(20, mif6ctl.mif6cPifi);
+ assertEquals(0, mif6ctl.vifcRateLimit);
+ }
+
+ @Test
+ public void testWriteToBytes() {
+ StructMif6ctl mif6ctl = new StructMif6ctl(1 /* mif6cMifi */,
+ (short) 0 /* mif6cFlags */,
+ (short) 1 /* vifcThreshold */,
+ 20 /* mif6cPifi */,
+ (long) 0 /* vifcRateLimit */);
+
+ byte[] bytes = mif6ctl.writeToBytes();
+
+ assertArrayEquals("bytes = " + Arrays.toString(bytes), MSG_BYTES, bytes);
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMrt6MsgTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMrt6MsgTest.java
new file mode 100644
index 0000000..f1b75a0
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMrt6MsgTest.java
@@ -0,0 +1,58 @@
+package com.android.net.module.util.structs;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.net.InetAddresses;
+import androidx.test.runner.AndroidJUnit4;
+import com.android.net.module.util.Struct;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class StructMrt6MsgTest {
+
+ private static final byte[] MSG_BYTES = new byte[] {
+ 0, /* mbz = 0 */
+ 1, /* message type = MRT6MSG_NOCACHE */
+ 1, 0, /* mif u16 = 1 */
+ 0, 0, 0, 0, /* padding */
+ 32, 1, 13, -72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, /* source=2001:db8::1 */
+ -1, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 52, /* destination=ff05::1234 */
+ };
+
+ private static final Inet6Address SOURCE =
+ (Inet6Address) InetAddresses.parseNumericAddress("2001:db8::1");
+ private static final Inet6Address GROUP =
+ (Inet6Address) InetAddresses.parseNumericAddress("ff05::1234");
+
+ @Test
+ public void testParseMrt6Msg() {
+ final ByteBuffer buf = ByteBuffer.wrap(MSG_BYTES);
+ StructMrt6Msg mrt6Msg = StructMrt6Msg.parse(buf);
+
+ assertEquals(1, mrt6Msg.mif);
+ assertEquals(StructMrt6Msg.MRT6MSG_NOCACHE, mrt6Msg.msgType);
+ assertEquals(SOURCE, mrt6Msg.src);
+ assertEquals(GROUP, mrt6Msg.dst);
+ }
+
+ @Test
+ public void testWriteToBytes() {
+ StructMrt6Msg msg = new StructMrt6Msg((byte) 0 /* mbz must be 0 */,
+ StructMrt6Msg.MRT6MSG_NOCACHE,
+ 1 /* mif */,
+ SOURCE,
+ GROUP);
+ byte[] bytes = msg.writeToBytes();
+
+ assertArrayEquals(MSG_BYTES, bytes);
+ }
+}
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
index a5e5afb..a8e5a69 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -24,11 +25,11 @@
],
defaults: [
"framework-connectivity-test-defaults",
- "lib_mockito_extended"
+ "lib_mockito_extended",
],
libs: [
"androidx.annotation_annotation",
- "net-utils-device-common-bpf", // TestBpfMap extends IBpfMap.
+ "net-utils-device-common-bpf", // TestBpfMap extends IBpfMap.
],
static_libs: [
"androidx.test.ext.junit",
@@ -42,7 +43,9 @@
"net-utils-device-common-wear",
"modules-utils-build_system",
],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ },
}
java_library {
@@ -72,9 +75,11 @@
"jsr305",
],
static_libs: [
- "kotlin-test"
+ "kotlin-test",
],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ },
}
java_test_host {
@@ -84,6 +89,15 @@
"host/**/*.kt",
],
libs: ["tradefed"],
- test_suites: ["ats", "device-tests", "general-tests", "cts", "mts-networking"],
+ test_suites: [
+ "ats",
+ "device-tests",
+ "general-tests",
+ "cts",
+ "mts-networking",
+ "mcts-networking",
+ "mts-tethering",
+ "mcts-tethering",
+ ],
data: [":ConnectivityTestPreparer"],
}
diff --git a/staticlibs/testutils/app/connectivitychecker/Android.bp b/staticlibs/testutils/app/connectivitychecker/Android.bp
index 049ec9e..5af8c14 100644
--- a/staticlibs/testutils/app/connectivitychecker/Android.bp
+++ b/staticlibs/testutils/app/connectivitychecker/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -30,5 +31,7 @@
"net-tests-utils",
],
host_required: ["net-tests-utils-host-common"],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ },
}
diff --git a/staticlibs/testutils/devicetests/NSResponder.kt b/staticlibs/testutils/devicetests/NSResponder.kt
new file mode 100644
index 0000000..f7619cd
--- /dev/null
+++ b/staticlibs/testutils/devicetests/NSResponder.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.net.MacAddress
+import android.util.Log
+import com.android.net.module.util.Ipv6Utils
+import com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_TLLA
+import com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED
+import com.android.net.module.util.Struct
+import com.android.net.module.util.structs.Icmpv6Header
+import com.android.net.module.util.structs.Ipv6Header
+import com.android.net.module.util.structs.LlaOption
+import com.android.net.module.util.structs.NsHeader
+import com.android.testutils.PacketReflector.IPV6_HEADER_LENGTH
+import java.lang.IllegalArgumentException
+import java.net.Inet6Address
+import java.nio.ByteBuffer
+
+private const val NS_TYPE = 135.toShort()
+
+/**
+ * A class that can be used to reply to Neighbor Solicitation packets on a [TapPacketReader].
+ */
+class NSResponder(
+ reader: TapPacketReader,
+ table: Map<Inet6Address, MacAddress>,
+ name: String = NSResponder::class.java.simpleName
+) : PacketResponder(reader, Icmpv6Filter(), name) {
+ companion object {
+ private val TAG = NSResponder::class.simpleName
+ }
+
+ // Copy the map if not already immutable (toMap) to make sure it is not modified
+ private val table = table.toMap()
+
+ override fun replyToPacket(packet: ByteArray, reader: TapPacketReader) {
+ if (packet.size < IPV6_HEADER_LENGTH) {
+ return
+ }
+ val buf = ByteBuffer.wrap(packet, ETHER_HEADER_LEN, packet.size - ETHER_HEADER_LEN)
+ val ipv6Header = parseOrLog(Ipv6Header::class.java, buf) ?: return
+ val icmpHeader = parseOrLog(Icmpv6Header::class.java, buf) ?: return
+ if (icmpHeader.type != NS_TYPE) {
+ return
+ }
+ val ns = parseOrLog(NsHeader::class.java, buf) ?: return
+ val replyMacAddr = table[ns.target] ?: return
+ val slla = parseOrLog(LlaOption::class.java, buf) ?: return
+ val requesterMac = slla.linkLayerAddress
+
+ val tlla = LlaOption.build(ICMPV6_ND_OPTION_TLLA.toByte(), replyMacAddr)
+ reader.sendResponse(Ipv6Utils.buildNaPacket(
+ replyMacAddr /* srcMac */,
+ requesterMac /* dstMac */,
+ ns.target /* srcIp */,
+ ipv6Header.srcIp /* dstIp */,
+ NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED,
+ ns.target,
+ tlla))
+ }
+
+ private fun <T> parseOrLog(clazz: Class<T>, buf: ByteBuffer): T? where T : Struct {
+ return try {
+ Struct.parse(clazz, buf)
+ } catch (e: IllegalArgumentException) {
+ Log.e(TAG, "Invalid ${clazz.simpleName} in ICMPv6 packet", e)
+ null
+ }
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
index b1d64f8..8090d5b 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
@@ -116,7 +116,10 @@
}
}
- private fun connectToWifiConfig(config: WifiConfiguration) {
+ // Suppress warning because WifiManager methods to connect to a config are
+ // documented not to be deprecated for privileged users.
+ @Suppress("DEPRECATION")
+ fun connectToWifiConfig(config: WifiConfiguration) {
repeat(MAX_WIFI_CONNECT_RETRIES) {
val error = runAsShell(permission.NETWORK_SETTINGS) {
val listener = ConnectWifiListener()
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
index 10accd4..69fdbf8 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
@@ -31,6 +31,7 @@
import org.junit.runner.notification.Failure
import org.junit.runner.notification.RunNotifier
import org.junit.runners.Parameterized
+import org.mockito.Mockito
/**
* A runner that can skip tests based on the development SDK as defined in [DevSdkIgnoreRule].
@@ -124,6 +125,9 @@
notifier.fireTestFailure(Failure(leakMonitorDesc,
IllegalStateException("Unexpected thread changes: $threadsDiff")))
}
+ // Clears up internal state of all inline mocks.
+ // TODO: Call clearInlineMocks() at the end of each test.
+ Mockito.framework().clearInlineMocks()
notifier.fireTestFinished(leakMonitorDesc)
}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DeviceConfigRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/DeviceConfigRule.kt
index 3d98cc3..68248ca 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DeviceConfigRule.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DeviceConfigRule.kt
@@ -22,12 +22,12 @@
import android.util.Log
import com.android.modules.utils.build.SdkLevel
import com.android.testutils.FunctionalUtils.ThrowingRunnable
-import org.junit.rules.TestRule
-import org.junit.runner.Description
-import org.junit.runners.model.Statement
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
private val TAG = DeviceConfigRule::class.simpleName
@@ -147,11 +147,11 @@
return tryTest {
runAsShell(*readWritePermissions) {
DeviceConfig.addOnPropertiesChangedListener(
- DeviceConfig.NAMESPACE_CONNECTIVITY,
+ namespace,
inlineExecutor,
listener)
DeviceConfig.setProperty(
- DeviceConfig.NAMESPACE_CONNECTIVITY,
+ namespace,
key,
value,
false /* makeDefault */)
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ExternalPacketForwarder.kt b/staticlibs/testutils/devicetests/com/android/testutils/ExternalPacketForwarder.kt
new file mode 100644
index 0000000..36eb795
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ExternalPacketForwarder.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.testutils
+
+import java.io.FileDescriptor
+
+class ExternalPacketForwarder(
+ srcFd: FileDescriptor,
+ mtu: Int,
+ dstFd: FileDescriptor,
+ forwardMap: Map<Int, Int>
+) : PacketForwarderBase(srcFd, mtu, dstFd, forwardMap) {
+
+ /**
+ * Prepares a packet for forwarding by potentially updating the
+ * source port based on the specified port remapping rules.
+ *
+ * @param buf The packet data as a byte array.
+ * @param version The IP version of the packet (e.g., 4 for IPv4).
+ */
+ override fun remapPort(buf: ByteArray, version: Int) {
+ val transportOffset = getTransportOffset(version)
+ val intPort = getRemappedPort(buf, transportOffset)
+
+ // Copy remapped source port.
+ if (intPort != 0) {
+ setPortAt(intPort, buf, transportOffset)
+ }
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/InternalPacketForwarder.kt b/staticlibs/testutils/devicetests/com/android/testutils/InternalPacketForwarder.kt
new file mode 100644
index 0000000..58829dc
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/InternalPacketForwarder.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.testutils
+
+import java.io.FileDescriptor
+
+class InternalPacketForwarder(
+ srcFd: FileDescriptor,
+ mtu: Int,
+ dstFd: FileDescriptor,
+ forwardMap: Map<Int, Int>
+) : PacketForwarderBase(srcFd, mtu, dstFd, forwardMap) {
+ /**
+ * Prepares a packet for forwarding by potentially updating the
+ * destination port based on the specified port remapping rules.
+ *
+ * @param buf The packet data as a byte array.
+ * @param version The IP version of the packet (e.g., 4 for IPv4).
+ */
+ override fun remapPort(buf: ByteArray, version: Int) {
+ val transportOffset = getTransportOffset(version) + DESTINATION_PORT_OFFSET
+ val extPort = getRemappedPort(buf, transportOffset)
+
+ // Copy remapped destination port.
+ if (extPort != 0) {
+ setPortAt(extPort, buf, transportOffset)
+ }
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NatExternalPacketForwarder.kt b/staticlibs/testutils/devicetests/com/android/testutils/NatExternalPacketForwarder.kt
deleted file mode 100644
index d7961a0..0000000
--- a/staticlibs/testutils/devicetests/com/android/testutils/NatExternalPacketForwarder.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.testutils
-
-import java.io.FileDescriptor
-import java.net.InetAddress
-
-/**
- * A class that forwards packets from the external {@link TestNetworkInterface} to the internal
- * {@link TestNetworkInterface} with NAT. See {@link NatPacketForwarderBase} for detail.
- */
-class NatExternalPacketForwarder(
- srcFd: FileDescriptor,
- mtu: Int,
- dstFd: FileDescriptor,
- extAddr: InetAddress,
- natMap: PacketBridge.NatMap
-) : NatPacketForwarderBase(srcFd, mtu, dstFd, extAddr, natMap) {
-
- /**
- * Rewrite addresses, ports and fix up checksums for packets received on the external
- * interface.
- *
- * Incoming response from external interface which is being forwarded to the internal
- * interface with translated address, e.g. 1.2.3.4:80 -> 8.8.8.8:1234
- * will be translated into 8.8.8.8:80 -> 192.168.1.1:5678.
- *
- * For packets that are not an incoming response, do not forward them to the
- * internal interface.
- */
- override fun preparePacketForForwarding(buf: ByteArray, len: Int, version: Int, proto: Int) {
- val (addrPos, addrLen) = getAddressPositionAndLength(version)
-
- // TODO: support one external address per ip version.
- val extAddrBuf = mExtAddr.address
- if (addrLen != extAddrBuf.size) throw IllegalStateException("Packet IP version mismatch")
-
- // Get internal address by port.
- val transportOffset =
- if (version == 4) PacketReflector.IPV4_HEADER_LENGTH
- else PacketReflector.IPV6_HEADER_LENGTH
- val dstPort = getPortAt(buf, transportOffset + DESTINATION_PORT_OFFSET)
- val intAddrInfo = synchronized(mNatMap) { mNatMap.fromExternalPort(dstPort) }
- // No mapping, skip. This usually happens if the connection is initiated directly on
- // the external interface, e.g. DNS64 resolution, network validation, etc.
- if (intAddrInfo == null) return
-
- val intAddrBuf = intAddrInfo.address.address
- val intPort = intAddrInfo.port
-
- // Copy the original destination to into the source address.
- for (i in 0 until addrLen) {
- buf[addrPos + i] = buf[addrPos + addrLen + i]
- }
-
- // Copy the internal address into the destination address.
- for (i in 0 until addrLen) {
- buf[addrPos + addrLen + i] = intAddrBuf[i]
- }
-
- // Copy the internal port into the destination port.
- setPortAt(intPort, buf, transportOffset + DESTINATION_PORT_OFFSET)
-
- // Fix IP and Transport layer checksum.
- fixPacketChecksum(buf, len, version, proto.toByte())
- }
-}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NatInternalPacketForwarder.kt b/staticlibs/testutils/devicetests/com/android/testutils/NatInternalPacketForwarder.kt
deleted file mode 100644
index fa39d19..0000000
--- a/staticlibs/testutils/devicetests/com/android/testutils/NatInternalPacketForwarder.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.testutils
-
-import java.io.FileDescriptor
-import java.net.InetAddress
-
-/**
- * A class that forwards packets from the internal {@link TestNetworkInterface} to the external
- * {@link TestNetworkInterface} with NAT. See {@link NatPacketForwarderBase} for detail.
- */
-class NatInternalPacketForwarder(
- srcFd: FileDescriptor,
- mtu: Int,
- dstFd: FileDescriptor,
- extAddr: InetAddress,
- natMap: PacketBridge.NatMap
-) : NatPacketForwarderBase(srcFd, mtu, dstFd, extAddr, natMap) {
-
- /**
- * Rewrite addresses, ports and fix up checksums for packets received on the internal
- * interface.
- *
- * Outgoing packet from the internal interface which is being forwarded to the
- * external interface with translated address, e.g. 192.168.1.1:5678 -> 8.8.8.8:80
- * will be translated into 8.8.8.8:1234 -> 1.2.3.4:80.
- *
- * The external port, e.g. 1234 in the above example, is the port number assigned by
- * the forwarder when creating the mapping to identify the source address and port when
- * the response is coming from the external interface. See {@link PacketBridge.NatMap}
- * for detail.
- */
- override fun preparePacketForForwarding(buf: ByteArray, len: Int, version: Int, proto: Int) {
- val (addrPos, addrLen) = getAddressPositionAndLength(version)
-
- // TODO: support one external address per ip version.
- val extAddrBuf = mExtAddr.address
- if (addrLen != extAddrBuf.size) throw IllegalStateException("Packet IP version mismatch")
-
- val srcAddr = getInetAddressAt(buf, addrPos, addrLen)
-
- // Copy the original destination to into the source address.
- for (i in 0 until addrLen) {
- buf[addrPos + i] = buf[addrPos + addrLen + i]
- }
-
- // Copy the external address into the destination address.
- for (i in 0 until addrLen) {
- buf[addrPos + addrLen + i] = extAddrBuf[i]
- }
-
- // Add an entry to NAT mapping table.
- val transportOffset =
- if (version == 4) PacketReflector.IPV4_HEADER_LENGTH
- else PacketReflector.IPV6_HEADER_LENGTH
- val srcPort = getPortAt(buf, transportOffset)
- val extPort = synchronized(mNatMap) { mNatMap.toExternalPort(srcAddr, srcPort, proto) }
- // Copy the external port to into the source port.
- setPortAt(extPort, buf, transportOffset)
-
- // Fix IP and Transport layer checksum.
- fixPacketChecksum(buf, len, version, proto.toByte())
- }
-}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt b/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt
index d50f78a..0b736d1 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt
@@ -16,6 +16,7 @@
package com.android.testutils
+import android.annotation.SuppressLint
import android.content.Context
import android.net.ConnectivityManager
import android.net.LinkAddress
@@ -31,61 +32,65 @@
import java.net.InetAddress
import libcore.io.IoUtils
-private const val MIN_PORT_NUMBER = 1025
-private const val MAX_PORT_NUMBER = 65535
-
/**
- * A class that set up two {@link TestNetworkInterface} with NAT, and forward packets between them.
+ * A class that set up two {@link TestNetworkInterface}, and forward packets between them.
*
- * See {@link NatPacketForwarder} for more detailed information.
+ * See {@link PacketForwarder} for more detailed information.
*/
class PacketBridge(
context: Context,
- internalAddr: LinkAddress,
- externalAddr: LinkAddress,
- dnsAddr: InetAddress
+ addresses: List<LinkAddress>,
+ dnsAddr: InetAddress,
+ portMapping: List<Pair<Int, Int>>
) {
- private val natMap = NatMap()
private val binder = Binder()
private val cm = context.getSystemService(ConnectivityManager::class.java)!!
private val tnm = context.getSystemService(TestNetworkManager::class.java)!!
- // Create test networks.
- private val internalIface = tnm.createTunInterface(listOf(internalAddr))
- private val externalIface = tnm.createTunInterface(listOf(externalAddr))
+ // Create test networks. The needed permissions should be supplied by the callers.
+ @SuppressLint("MissingPermission")
+ private val internalIface = tnm.createTunInterface(addresses)
+ @SuppressLint("MissingPermission")
+ private val externalIface = tnm.createTunInterface(addresses)
// Register test networks to ConnectivityService.
private val internalNetworkCallback: TestableNetworkCallback
private val externalNetworkCallback: TestableNetworkCallback
+
+ private val internalForwardMap = HashMap<Int, Int>()
+ private val externalForwardMap = HashMap<Int, Int>()
+
val internalNetwork: Network
val externalNetwork: Network
init {
- val (inCb, inNet) = createTestNetwork(internalIface, internalAddr, dnsAddr)
- val (exCb, exNet) = createTestNetwork(externalIface, externalAddr, dnsAddr)
+ val (inCb, inNet) = createTestNetwork(internalIface, addresses, dnsAddr)
+ val (exCb, exNet) = createTestNetwork(externalIface, addresses, dnsAddr)
internalNetworkCallback = inCb
externalNetworkCallback = exCb
internalNetwork = inNet
externalNetwork = exNet
+ for (mapping in portMapping) {
+ internalForwardMap[mapping.first] = mapping.second
+ externalForwardMap[mapping.second] = mapping.first
+ }
}
- // Setup the packet bridge.
+ // Set up the packet bridge.
private val internalFd = internalIface.fileDescriptor.fileDescriptor
private val externalFd = externalIface.fileDescriptor.fileDescriptor
- private val pr1 = NatInternalPacketForwarder(
+ private val pr1 = InternalPacketForwarder(
internalFd,
1500,
externalFd,
- externalAddr.address,
- natMap
+ internalForwardMap
)
- private val pr2 = NatExternalPacketForwarder(
+ private val pr2 = ExternalPacketForwarder(
externalFd,
1500,
internalFd,
- externalAddr.address,
- natMap
+ externalForwardMap
)
fun start() {
@@ -107,7 +112,7 @@
*/
private fun createTestNetwork(
testIface: TestNetworkInterface,
- addr: LinkAddress,
+ addresses: List<LinkAddress>,
dnsAddr: InetAddress
): Pair<TestableNetworkCallback, Network> {
// Make a network request to hold the test network
@@ -120,7 +125,7 @@
cm.requestNetwork(nr, testCb)
val lp = LinkProperties().apply {
- addLinkAddress(addr)
+ setLinkAddresses(addresses)
interfaceName = testIface.interfaceName
addDnsServer(dnsAddr)
}
@@ -130,44 +135,4 @@
val network = testCb.expect<Available>().network
return testCb to network
}
-
- /**
- * A helper class to maintain the mappings between internal addresses/ports and external
- * ports.
- *
- * This class assigns an unused external port number if the mapping between
- * srcaddress:srcport:protocol and the external port does not exist yet.
- *
- * Note that this class is not thread-safe. The instance of the class needs to be
- * synchronized in the callers when being used in multiple threads.
- */
- class NatMap {
- data class AddressInfo(val address: InetAddress, val port: Int, val protocol: Int)
-
- private val mToExternalPort = HashMap<AddressInfo, Int>()
- private val mFromExternalPort = HashMap<Int, AddressInfo>()
-
- // Skip well-known port 0~1024.
- private var nextExternalPort = MIN_PORT_NUMBER
-
- fun toExternalPort(addr: InetAddress, port: Int, protocol: Int): Int {
- val info = AddressInfo(addr, port, protocol)
- val extPort: Int
- if (!mToExternalPort.containsKey(info)) {
- extPort = nextExternalPort++
- if (nextExternalPort > MAX_PORT_NUMBER) {
- throw IllegalStateException("Available ports are exhausted")
- }
- mToExternalPort[info] = extPort
- mFromExternalPort[extPort] = info
- } else {
- extPort = mToExternalPort[info]!!
- }
- return extPort
- }
-
- fun fromExternalPort(port: Int): AddressInfo? {
- return mFromExternalPort[port]
- }
- }
}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java b/staticlibs/testutils/devicetests/com/android/testutils/PacketForwarderBase.java
similarity index 63%
rename from staticlibs/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java
rename to staticlibs/testutils/devicetests/com/android/testutils/PacketForwarderBase.java
index 0a2b5d4..5c79eb0 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketForwarderBase.java
@@ -30,16 +30,14 @@
import android.system.Os;
import android.util.Log;
-import androidx.annotation.GuardedBy;
-
import java.io.FileDescriptor;
import java.io.IOException;
-import java.net.InetAddress;
+import java.util.Map;
import java.util.Objects;
/**
* A class that forwards packets from a {@link TestNetworkInterface} to another
- * {@link TestNetworkInterface} with NAT.
+ * {@link TestNetworkInterface}.
*
* For testing purposes, a {@link TestNetworkInterface} provides a {@link FileDescriptor}
* which allows content injection on the test network. However, this could be hard to use
@@ -54,29 +52,14 @@
*
* To make it work, an internal interface and an external interface are defined, where
* the client might send packets from the internal interface which are originated from
- * multiple addresses to a server that listens on the external address.
- *
- * When forwarding the outgoing packet on the internal interface, a simple NAT mechanism
- * is implemented during forwarding, which will swap the source and destination,
- * but replacing the source address with the external address,
- * e.g. 192.168.1.1:1234 -> 8.8.8.8:80 will be translated into 8.8.8.8:1234 -> 1.2.3.4:80.
- *
- * For the above example, a client who sends http request will have a hallucination that
- * it is talking to a remote server at 8.8.8.8. Also, the server listens on 1.2.3.4 will
- * have a different hallucination that the request is sent from a remote client at 8.8.8.8,
- * to a local address 1.2.3.4.
- *
- * And a NAT mapping is created at the time when the outgoing packet is forwarded.
- * With a different internal source port, the instance learned that when a response with the
- * destination port 1234, it should forward the packet to the internal address 192.168.1.1.
+ * multiple addresses to a server that listens on the different port.
*
* For the incoming packet received from external interface, for example a http response sent
* from the http server, the same mechanism is applied but in a different direction,
- * where the source and destination will be swapped, and the source address will be replaced
- * with the internal address, which is obtained from the NAT mapping described above.
+ * where the source and destination will be swapped.
*/
-public abstract class NatPacketForwarderBase extends Thread {
- private static final String TAG = "NatPacketForwarder";
+public abstract class PacketForwarderBase extends Thread {
+ private static final String TAG = "PacketForwarder";
static final int DESTINATION_PORT_OFFSET = 2;
// The source fd to read packets from.
@@ -88,27 +71,14 @@
// The destination fd to write packets to.
@NonNull
final FileDescriptor mDstFd;
- // The NAT mapping table shared between two NatPacketForwarder instances to map from
- // the source port to the associated internal address. The map can be read/write from two
- // different threads on any given time whenever receiving packets on the
- // {@link TestNetworkInterface}. Thus, synchronize on the object when reading/writing is needed.
- @GuardedBy("mNatMap")
- @NonNull
- final PacketBridge.NatMap mNatMap;
- // The address of the external interface. See {@link NatPacketForwarder}.
- @NonNull
- final InetAddress mExtAddr;
+ @NonNull
+ final Map<Integer, Integer> mPortRemapRules;
/**
- * Construct a {@link NatPacketForwarderBase}.
+ * Construct a {@link PacketForwarderBase}.
*
* This class reads packets from {@code srcFd} of a {@link TestNetworkInterface}, and
- * forwards them to the {@code dstFd} of another {@link TestNetworkInterface} with
- * NAT applied. See {@link NatPacketForwarderBase}.
- *
- * To apply NAT, the address of the external interface needs to be supplied through
- * {@code extAddr} to identify the external interface. And a shared NAT mapping table,
- * {@code natMap} is needed to be shared between these two instances.
+ * forwards them to the {@code dstFd} of another {@link TestNetworkInterface}.
*
* Note that this class is not useful if the instance is not managed by a
* {@link PacketBridge} to set up a two-way communication.
@@ -116,28 +86,50 @@
* @param srcFd {@link FileDescriptor} to read packets from.
* @param mtu MTU of the test network.
* @param dstFd {@link FileDescriptor} to write packets to.
- * @param extAddr the external address, which is the address of the external interface.
- * See {@link NatPacketForwarderBase}.
- * @param natMap the NAT mapping table shared between two {@link NatPacketForwarderBase}
- * instance.
+ * @param portRemapRules port remap rules
*/
- public NatPacketForwarderBase(@NonNull FileDescriptor srcFd, int mtu,
- @NonNull FileDescriptor dstFd, @NonNull InetAddress extAddr,
- @NonNull PacketBridge.NatMap natMap) {
+ public PacketForwarderBase(@NonNull FileDescriptor srcFd, int mtu,
+ @NonNull FileDescriptor dstFd,
+ @NonNull Map<Integer, Integer> portRemapRules) {
super(TAG);
mSrcFd = Objects.requireNonNull(srcFd);
mBuf = new byte[mtu];
mDstFd = Objects.requireNonNull(dstFd);
- mExtAddr = Objects.requireNonNull(extAddr);
- mNatMap = Objects.requireNonNull(natMap);
+ mPortRemapRules = Objects.requireNonNull(portRemapRules);
}
/**
* A method to prepare forwarding packets between two instances of {@link TestNetworkInterface},
- * which includes re-write addresses, ports and fix up checksums.
- * Subclasses should override this method to implement a simple NAT.
+ * which includes ports mapping.
+ * Subclasses should override this method to implement the needed port remapping.
+ * For internal forwarder will remapped destination port,
+ * external forwarder will remapped source port.
+ * Example:
+ * An outgoing packet from the internal interface with
+ * source 1.2.3.4:1234 and destination 8.8.8.8:80
+ * might be translated to 8.8.8.8:1234 -> 1.2.3.4:8080 before forwarding.
+ * An outgoing packet from the external interface with
+ * source 1.2.3.4:8080 and destination 8.8.8.8:1234
+ * might be translated to 8.8.8.8:80 -> 1.2.3.4:1234 before forwarding.
*/
- abstract void preparePacketForForwarding(@NonNull byte[] buf, int len, int version, int proto);
+ abstract void remapPort(@NonNull byte[] buf, int version);
+
+ /**
+ * Retrieves a potentially remapped port number from a packet.
+ *
+ * @param buf The packet data as a byte array.
+ * @param transportOffset The offset within the packet where the transport layer port begins.
+ * @return The remapped port if a mapping exists in the internal forwarding map,
+ * otherwise returns 0 (indicating no remapping).
+ */
+ int getRemappedPort(@NonNull byte[] buf, int transportOffset) {
+ int port = PacketReflectorUtil.getPortAt(buf, transportOffset);
+ return mPortRemapRules.getOrDefault(port, 0);
+ }
+
+ int getTransportOffset(int version) {
+ return version == 4 ? IPV4_HEADER_LENGTH : IPV6_HEADER_LENGTH;
+ }
private void forwardPacket(@NonNull byte[] buf, int len) {
try {
@@ -147,7 +139,13 @@
}
}
- // Reads one packet from mSrcFd, and writes the packet to the mDstFd for supported protocols.
+ /**
+ * Reads one packet from mSrcFd, and writes the packet to the mDestFd for supported protocols.
+ * This includes:
+ * 1.Address Swapping: Swaps source and destination IP addresses.
+ * 2.Port Remapping: Remap port if necessary.
+ * 3.Checksum Recalculation: Updates IP and transport layer checksums to reflect changes.
+ */
private void processPacket() {
final int len = PacketReflectorUtil.readPacket(mSrcFd, mBuf);
if (len < 1) {
@@ -190,12 +188,19 @@
if (len < ipHdrLen + transportHdrLen) {
throw new IllegalStateException("Unexpected buffer length: " + len);
}
- // Re-write addresses, ports and fix up checksums.
- preparePacketForForwarding(mBuf, len, version, proto);
+
+ // Swap source and destination address.
+ PacketReflectorUtil.swapAddresses(mBuf, version);
+
+ // Remapping the port.
+ remapPort(mBuf, version);
+
+ // Fix IP and Transport layer checksum.
+ PacketReflectorUtil.fixPacketChecksum(mBuf, len, version, proto);
+
// Send the packet to the destination fd.
forwardPacket(mBuf, len);
}
-
@Override
public void run() {
Log.i(TAG, "starting fd=" + mSrcFd + " valid=" + mSrcFd.valid());
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketReflector.java b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflector.java
index 69392d4..ce20d67 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/PacketReflector.java
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflector.java
@@ -87,31 +87,6 @@
mBuf = new byte[mtu];
}
- private static void swapBytes(@NonNull byte[] buf, int pos1, int pos2, int len) {
- for (int i = 0; i < len; i++) {
- byte b = buf[pos1 + i];
- buf[pos1 + i] = buf[pos2 + i];
- buf[pos2 + i] = b;
- }
- }
-
- private static void swapAddresses(@NonNull byte[] buf, int version) {
- int addrPos, addrLen;
- switch (version) {
- case 4:
- addrPos = IPV4_ADDR_OFFSET;
- addrLen = IPV4_ADDR_LENGTH;
- break;
- case 6:
- addrPos = IPV6_ADDR_OFFSET;
- addrLen = IPV6_ADDR_LENGTH;
- break;
- default:
- throw new IllegalArgumentException();
- }
- swapBytes(buf, addrPos, addrPos + addrLen, addrLen);
- }
-
// Reflect TCP packets: swap the source and destination addresses, but don't change the ports.
// This is used by the test to "connect to itself" through the VPN.
private void processTcpPacket(@NonNull byte[] buf, int version, int len, int hdrLen) {
@@ -120,7 +95,7 @@
}
// Swap src and dst IP addresses.
- swapAddresses(buf, version);
+ PacketReflectorUtil.swapAddresses(buf, version);
// Send the packet back.
writePacket(buf, len);
@@ -134,11 +109,11 @@
}
// Swap src and dst IP addresses.
- swapAddresses(buf, version);
+ PacketReflectorUtil.swapAddresses(buf, version);
// Swap dst and src ports.
int portOffset = hdrLen;
- swapBytes(buf, portOffset, portOffset + 2, 2);
+ PacketReflectorUtil.swapBytes(buf, portOffset, portOffset + 2, 2);
// Send the packet back.
writePacket(buf, len);
@@ -160,7 +135,7 @@
// Swap src and dst IP addresses, and send the packet back.
// This effectively pings the device to see if it replies.
- swapAddresses(buf, version);
+ PacketReflectorUtil.swapAddresses(buf, version);
writePacket(buf, len);
// The device should have replied, and buf should now contain a ping response.
@@ -202,7 +177,7 @@
}
// Now swap the addresses again and reflect the packet. This sends a ping reply.
- swapAddresses(buf, version);
+ PacketReflectorUtil.swapAddresses(buf, version);
writePacket(buf, len);
}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt
index 498b1a3..ad259c5 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt
@@ -112,3 +112,28 @@
else -> throw IllegalArgumentException("Unsupported protocol: $protocol")
}
}
+
+fun swapBytes(buf: ByteArray, pos1: Int, pos2: Int, len: Int) {
+ for (i in 0 until len) {
+ val b = buf[pos1 + i]
+ buf[pos1 + i] = buf[pos2 + i]
+ buf[pos2 + i] = b
+ }
+}
+
+fun swapAddresses(buf: ByteArray, version: Int) {
+ val addrPos: Int
+ val addrLen: Int
+ when (version) {
+ 4 -> {
+ addrPos = PacketReflector.IPV4_ADDR_OFFSET
+ addrLen = PacketReflector.IPV4_ADDR_LENGTH
+ }
+ 6 -> {
+ addrPos = PacketReflector.IPV6_ADDR_OFFSET
+ addrLen = PacketReflector.IPV6_ADDR_LENGTH
+ }
+ else -> throw java.lang.IllegalArgumentException()
+ }
+ swapBytes(buf, addrPos, addrPos + addrLen, addrLen)
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt
index 740bf63..f1f0c1c 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt
@@ -25,8 +25,10 @@
* A minimal HTTP server running on a random available port.
*
* @param host The host to listen to, or null to listen on all hosts
+ * @param port The port to listen to, or 0 to auto select
*/
-class TestHttpServer(host: String? = null) : NanoHTTPD(host, 0 /* auto-select the port */) {
+class TestHttpServer
+ @JvmOverloads constructor(host: String? = null, port: Int = 0) : NanoHTTPD(host, port) {
// Map of URL path -> HTTP response code
private val responses = HashMap<Request, Response>()
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/PacketFilter.kt b/staticlibs/testutils/hostdevice/com/android/testutils/PacketFilter.kt
index 1bb6d68..a73a58a 100644
--- a/staticlibs/testutils/hostdevice/com/android/testutils/PacketFilter.kt
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/PacketFilter.kt
@@ -110,6 +110,12 @@
override fun test(t: ByteArray) = impl.test(t)
}
+class Icmpv6Filter : Predicate<ByteArray> {
+ private val impl = OffsetFilter(ETHER_TYPE_OFFSET, 0x86.toByte(), 0xdd.toByte() /* IPv6 */).and(
+ OffsetFilter(IPV6_PROTOCOL_OFFSET, 58 /* ICMPv6 */))
+ override fun test(t: ByteArray) = impl.test(t)
+}
+
/**
* A [Predicate] that matches ethernet-encapped DHCP packets sent from a DHCP client.
*/
diff --git a/tests/benchmark/Android.bp b/tests/benchmark/Android.bp
index 6ea5347..7854bb5 100644
--- a/tests/benchmark/Android.bp
+++ b/tests/benchmark/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -40,4 +41,3 @@
test_suites: ["device-tests"],
jarjar_rules: ":connectivity-jarjar-rules",
}
-
diff --git a/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt b/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt
index 585157f..57602f1 100644
--- a/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt
+++ b/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt
@@ -133,7 +133,16 @@
}
@Test
- fun testReadFromRecorder_manyUids() {
+ fun testReadFromRecorder_manyUids_useDataInput() {
+ doTestReadFromRecorder_manyUids(useFastDataInput = false)
+ }
+
+ @Test
+ fun testReadFromRecorder_manyUids_useFastDataInput() {
+ doTestReadFromRecorder_manyUids(useFastDataInput = true)
+ }
+
+ fun doTestReadFromRecorder_manyUids(useFastDataInput: Boolean) {
val mockObserver = mock<NonMonotonicObserver<String>>()
val mockDropBox = mock<DropBoxManager>()
testFilesAssets.forEach {
@@ -146,7 +155,9 @@
PREFIX_UID,
UID_COLLECTION_BUCKET_DURATION_MS,
false /* includeTags */,
- false /* wipeOnError */
+ false /* wipeOnError */,
+ useFastDataInput /* useFastDataInput */,
+ it
)
recorder.orLoadCompleteLocked
}
diff --git a/tests/common/Android.bp b/tests/common/Android.bp
index 7b5c298..6e9d614 100644
--- a/tests/common/Android.bp
+++ b/tests/common/Android.bp
@@ -17,6 +17,7 @@
// Tests in this folder are included both in unit tests and CTS.
// They must be fast and stable, and exercise public or test APIs.
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -93,7 +94,10 @@
name: "ConnectivityCoverageTests",
// Tethering started on SDK 30
min_sdk_version: "30",
- test_suites: ["general-tests", "mts-tethering"],
+ test_suites: [
+ "general-tests",
+ "mts-tethering",
+ ],
defaults: [
"ConnectivityTestsLatestSdkDefaults",
"framework-connectivity-internal-test-defaults",
@@ -185,7 +189,7 @@
// See SuiteModuleLoader.java.
// TODO: why are the modules separated by + instead of being separate entries in the array?
mainline_presubmit_modules = [
- "CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex",
+ "CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex",
]
cc_defaults {
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index 3a3459b..6eb56c7 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -54,13 +54,13 @@
import static android.net.NetworkCapabilities.SIGNAL_STRENGTH_UNSPECIFIED;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_SATELLITE;
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import static android.net.NetworkCapabilities.TRANSPORT_USB;
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
import static android.os.Process.INVALID_UID;
-
import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
import static com.android.modules.utils.build.SdkLevel.isAtLeastV;
@@ -68,7 +68,6 @@
import static com.android.testutils.MiscAsserts.assertEmpty;
import static com.android.testutils.MiscAsserts.assertThrows;
import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
-
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -82,25 +81,22 @@
import android.net.wifi.aware.PeerHandle;
import android.net.wifi.aware.WifiAwareNetworkSpecifier;
import android.os.Build;
-import android.test.suitebuilder.annotation.SmallTest;
import android.util.ArraySet;
import android.util.Range;
-
+import androidx.test.filters.SmallTest;
import com.android.testutils.CompatUtil;
import com.android.testutils.ConnectivityModuleTest;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.DevSdkIgnoreRunner;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mockito;
-
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
@SmallTest
@RunWith(DevSdkIgnoreRunner.class)
@@ -761,6 +757,47 @@
}
@Test
+ public void testSetNetworkSpecifierWithCellularAndSatelliteMultiTransportNc() {
+ final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(1);
+ NetworkCapabilities nc = new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addTransportType(TRANSPORT_SATELLITE)
+ .setNetworkSpecifier(specifier)
+ .build();
+ // Adding a specifier did not crash with 2 transports if it is cellular + satellite
+ assertEquals(specifier, nc.getNetworkSpecifier());
+ }
+
+ @Test
+ public void testSetNetworkSpecifierWithWifiAndSatelliteMultiTransportNc() {
+ final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(1);
+ NetworkCapabilities.Builder nc1 = new NetworkCapabilities.Builder();
+ nc1.addTransportType(TRANSPORT_SATELLITE).addTransportType(TRANSPORT_WIFI);
+ // Adding multiple transports specifier to crash, apart from cellular + satellite
+ // combination
+ assertThrows("Cannot set NetworkSpecifier on a NetworkCapability with multiple transports!",
+ IllegalStateException.class,
+ () -> nc1.build().setNetworkSpecifier(specifier));
+ assertThrows("Cannot set NetworkSpecifier on a NetworkCapability with multiple transports!",
+ IllegalStateException.class,
+ () -> nc1.setNetworkSpecifier(specifier));
+ }
+
+ @Test
+ public void testSetNetworkSpecifierOnTestWithCellularAndSatelliteMultiTransportNc() {
+ final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(1);
+ NetworkCapabilities nc = new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_TEST)
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addTransportType(TRANSPORT_SATELLITE)
+ .setNetworkSpecifier(specifier)
+ .build();
+ // Adding a specifier did not crash with 3 transports , TEST + CELLULAR + SATELLITE and if
+ // one is test
+ assertEquals(specifier, nc.getNetworkSpecifier());
+ }
+
+ @Test
public void testSetNetworkSpecifierOnTestMultiTransportNc() {
final NetworkSpecifier specifier = CompatUtil.makeEthernetNetworkSpecifier("eth0");
NetworkCapabilities nc = new NetworkCapabilities.Builder()
diff --git a/tests/common/java/android/net/nsd/NsdServiceInfoTest.java b/tests/common/java/android/net/nsd/NsdServiceInfoTest.java
index ffe0e91..8e89037 100644
--- a/tests/common/java/android/net/nsd/NsdServiceInfoTest.java
+++ b/tests/common/java/android/net/nsd/NsdServiceInfoTest.java
@@ -18,6 +18,7 @@
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.Assert.fail;
@@ -40,6 +41,7 @@
import java.util.Arrays;
import java.util.List;
import java.util.Map;
+import java.util.Set;
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
@@ -114,8 +116,10 @@
NsdServiceInfo fullInfo = new NsdServiceInfo();
fullInfo.setServiceName("kitten");
fullInfo.setServiceType("_kitten._tcp");
+ fullInfo.setSubtypes(Set.of("_thread", "_matter"));
fullInfo.setPort(4242);
fullInfo.setHostAddresses(List.of(IPV4_ADDRESS));
+ fullInfo.setHostname("home");
fullInfo.setNetwork(new Network(123));
fullInfo.setInterfaceIndex(456);
checkParcelable(fullInfo);
@@ -131,6 +135,7 @@
attributedInfo.setServiceType("_kitten._tcp");
attributedInfo.setPort(4242);
attributedInfo.setHostAddresses(List.of(IPV6_ADDRESS, IPV4_ADDRESS));
+ attributedInfo.setHostname("home");
attributedInfo.setAttribute("color", "pink");
attributedInfo.setAttribute("sound", (new String("にゃあ")).getBytes("UTF-8"));
attributedInfo.setAttribute("adorable", (String) null);
@@ -149,7 +154,7 @@
assertFalse(attributedInfo.getAttributes().keySet().contains("sticky"));
}
- public void checkParcelable(NsdServiceInfo original) {
+ private static void checkParcelable(NsdServiceInfo original) {
// Write to parcel.
Parcel p = Parcel.obtain();
Bundle writer = new Bundle();
@@ -166,6 +171,7 @@
assertEquals(original.getServiceName(), result.getServiceName());
assertEquals(original.getServiceType(), result.getServiceType());
assertEquals(original.getHost(), result.getHost());
+ assertEquals(original.getHostname(), result.getHostname());
assertTrue(original.getPort() == result.getPort());
assertEquals(original.getNetwork(), result.getNetwork());
assertEquals(original.getInterfaceIndex(), result.getInterfaceIndex());
@@ -179,11 +185,20 @@
}
}
- public void assertEmptyServiceInfo(NsdServiceInfo shouldBeEmpty) {
+ private static void assertEmptyServiceInfo(NsdServiceInfo shouldBeEmpty) {
byte[] txtRecord = shouldBeEmpty.getTxtRecord();
if (txtRecord == null || txtRecord.length == 0) {
return;
}
fail("NsdServiceInfo.getTxtRecord did not return null but " + Arrays.toString(txtRecord));
}
+
+ @Test
+ public void testSubtypesValidSubtypesSuccess() {
+ NsdServiceInfo info = new NsdServiceInfo();
+
+ info.setSubtypes(Set.of("_thread", "_matter"));
+
+ assertEquals(Set.of("_thread", "_matter"), info.getSubtypes());
+ }
}
diff --git a/tests/cts/hostside/Android.bp b/tests/cts/hostside/Android.bp
index e55ba63..f6c0430 100644
--- a/tests/cts/hostside/Android.bp
+++ b/tests/cts/hostside/Android.bp
@@ -12,17 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-next_app_data = [ ":CtsHostsideNetworkTestsAppNext" ]
+next_app_data = [":CtsHostsideNetworkTestsAppNext"]
// The above line is put in place to prevent any future automerger merge conflict between aosp,
// downstream branches. The CtsHostsideNetworkTestsAppNext target will not exist in
// some downstream branches, but it should exist in aosp and some downstream branches.
-
-
-
-
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -30,7 +27,10 @@
name: "CtsHostsideNetworkTests",
defaults: ["cts_defaults"],
// Only compile source java files in this apk.
- srcs: ["src/**/*.java"],
+ srcs: [
+ "src/**/*.java",
+ ":ArgumentConstants",
+ ],
libs: [
"net-tests-utils-host-device-common",
"cts-tradefed",
@@ -43,7 +43,9 @@
test_suites: [
"cts",
"general-tests",
- "sts"
+ "mcts-tethering",
+ "mts-tethering",
+ "sts",
],
data: [
":CtsHostsideNetworkTestsApp",
diff --git a/tests/cts/hostside/aidl/Android.bp b/tests/cts/hostside/aidl/Android.bp
index 2751f6f..18a5897 100644
--- a/tests/cts/hostside/aidl/Android.bp
+++ b/tests/cts/hostside/aidl/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/tests/cts/hostside/app/Android.bp b/tests/cts/hostside/app/Android.bp
index 470bb17..cf4afa9 100644
--- a/tests/cts/hostside/app/Android.bp
+++ b/tests/cts/hostside/app/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -35,7 +36,10 @@
"android.test.runner",
"android.test.base",
],
- srcs: ["src/**/*.java"],
+ srcs: [
+ "src/**/*.java",
+ ":ArgumentConstants",
+ ],
// Tag this module as a cts test artifact
test_suites: [
"general-tests",
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractBatterySaverModeTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractBatterySaverModeTestCase.java
index 04d054d..0d7365f 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractBatterySaverModeTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractBatterySaverModeTestCase.java
@@ -59,13 +59,13 @@
setBatterySaverMode(false);
launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
setBatterySaverMode(true);
- assertForegroundNetworkAccess();
+ assertTopNetworkAccess(true);
// Although it should not have access while the screen is off.
turnScreenOff();
assertBackgroundNetworkAccess(false);
turnScreenOn();
- assertForegroundNetworkAccess();
+ assertTopNetworkAccess(true);
// Goes back to background state.
finishActivity();
@@ -75,7 +75,7 @@
setBatterySaverMode(false);
launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
setBatterySaverMode(true);
- assertForegroundNetworkAccess();
+ assertForegroundServiceNetworkAccess();
stopForegroundService();
assertBackgroundNetworkAccess(false);
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDefaultRestrictionsTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDefaultRestrictionsTest.java
new file mode 100644
index 0000000..8a3e790
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDefaultRestrictionsTest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.net.hostside;
+
+import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING;
+
+import static org.junit.Assume.assumeTrue;
+
+import android.os.SystemClock;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Base class for default, always-on network restrictions.
+ */
+abstract class AbstractDefaultRestrictionsTest extends AbstractRestrictBackgroundNetworkTestCase {
+
+ @Before
+ public final void setUp() throws Exception {
+ super.setUp();
+
+ removePowerSaveModeWhitelist(TEST_APP2_PKG);
+ removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
+
+ registerBroadcastReceiver();
+ assumeTrue("Feature not enabled", isNetworkBlockedForTopSleepingAndAbove());
+ }
+
+ @After
+ public final void tearDown() throws Exception {
+ super.tearDown();
+
+ removePowerSaveModeWhitelist(TEST_APP2_PKG);
+ removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
+ }
+
+ @Test
+ public void testFgsNetworkAccess() throws Exception {
+ assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+ SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+ assertNetworkAccess(false, null);
+
+ launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
+ }
+
+ @Test
+ public void testActivityNetworkAccess() throws Exception {
+ assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+ SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+ assertNetworkAccess(false, null);
+
+ launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
+ }
+
+ @Test
+ public void testBackgroundNetworkAccess_inFullAllowlist() throws Exception {
+ assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+ SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+ assertNetworkAccess(false, null);
+
+ addPowerSaveModeWhitelist(TEST_APP2_PKG);
+ assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+ assertNetworkAccess(true, null);
+ }
+
+ @Test
+ public void testBackgroundNetworkAccess_inExceptIdleAllowlist() throws Exception {
+ assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+ SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+ assertNetworkAccess(false, null);
+
+ addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
+ assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+ assertNetworkAccess(true, null);
+ }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java
index e0ce4ea..b037953 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java
@@ -16,6 +16,8 @@
package com.android.cts.net.hostside;
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+
import static com.android.cts.net.hostside.Property.DOZE_MODE;
import static com.android.cts.net.hostside.Property.NOT_LOW_RAM_DEVICE;
@@ -62,9 +64,9 @@
setDozeMode(false);
launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
setDozeMode(true);
- assertForegroundNetworkAccess();
+ assertForegroundServiceNetworkAccess();
stopForegroundService();
- assertBackgroundState();
+ assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
assertBackgroundNetworkAccess(false);
}
@@ -136,6 +138,6 @@
protected void assertsForegroundAlwaysHasNetworkAccess() throws Exception {
launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
stopForegroundService();
- assertBackgroundState();
+ assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
}
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
index 198b009..2ca8832 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
@@ -16,10 +16,14 @@
package com.android.cts.net.hostside;
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+import static android.app.ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
+import static android.app.ActivityManager.PROCESS_STATE_TOP;
import static android.app.job.JobScheduler.RESULT_SUCCESS;
import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
import static android.os.BatteryManager.BATTERY_PLUGGED_ANY;
+import static com.android.cts.net.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
import static com.android.cts.net.hostside.NetworkPolicyTestUtils.executeShellCommand;
import static com.android.cts.net.hostside.NetworkPolicyTestUtils.forceRunJob;
import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getConnectivityManager;
@@ -38,7 +42,6 @@
import static org.junit.Assert.fail;
import android.annotation.NonNull;
-import android.app.ActivityManager;
import android.app.Instrumentation;
import android.app.NotificationManager;
import android.app.job.JobInfo;
@@ -63,10 +66,13 @@
import android.util.Pair;
import androidx.annotation.Nullable;
+import androidx.test.platform.app.InstrumentationRegistry;
import com.android.compatibility.common.util.AmUtils;
import com.android.compatibility.common.util.BatteryUtils;
import com.android.compatibility.common.util.DeviceConfigStateHelper;
+import com.android.compatibility.common.util.ThrowingRunnable;
+import com.android.modules.utils.build.SdkLevel;
import org.junit.Rule;
import org.junit.rules.RuleChain;
@@ -76,6 +82,7 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
/**
* Superclass for tests related to background network restrictions.
@@ -86,6 +93,8 @@
protected static final String TEST_PKG = "com.android.cts.net.hostside";
protected static final String TEST_APP2_PKG = "com.android.cts.net.hostside.app2";
+ // TODO(b/321797685): Configure it via device-config once it is available.
+ protected static final long PROCESS_STATE_TRANSITION_DELAY_MS = TimeUnit.SECONDS.toMillis(5);
private static final String TEST_APP2_ACTIVITY_CLASS = TEST_APP2_PKG + ".MyActivity";
private static final String TEST_APP2_SERVICE_CLASS = TEST_APP2_PKG + ".MyForegroundService";
@@ -93,7 +102,6 @@
private static final ComponentName TEST_JOB_COMPONENT = new ComponentName(
TEST_APP2_PKG, TEST_APP2_JOB_SERVICE_CLASS);
-
private static final int TEST_JOB_ID = 7357437;
private static final int SLEEP_TIME_SEC = 1;
@@ -126,8 +134,6 @@
private static final int SECOND_IN_MS = 1000;
static final int NETWORK_TIMEOUT_MS = 15 * SECOND_IN_MS;
- private static int PROCESS_STATE_FOREGROUND_SERVICE;
-
private static final String KEY_NETWORK_STATE_OBSERVER = TEST_PKG + ".observer";
private static final String KEY_SKIP_VALIDATION_CHECKS = TEST_PKG + ".skip_validation_checks";
@@ -150,8 +156,6 @@
private static final IntentFilter BATTERY_CHANGED_FILTER =
new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
- private static final String APP_NOT_FOREGROUND_ERROR = "app_not_fg";
-
protected static final long TEMP_POWERSAVE_WHITELIST_DURATION_MS = 20_000; // 20 sec
private static final long BROADCAST_TIMEOUT_MS = 5_000;
@@ -171,9 +175,6 @@
.around(new MeterednessConfigurationRule());
protected void setUp() throws Exception {
- // TODO: Annotate these constants with @TestApi instead of obtaining them using reflection
- PROCESS_STATE_FOREGROUND_SERVICE = (Integer) ActivityManager.class
- .getDeclaredField("PROCESS_STATE_FOREGROUND_SERVICE").get(null);
mInstrumentation = getInstrumentation();
mContext = getContext();
mCm = getConnectivityManager();
@@ -182,7 +183,16 @@
mUid = getUid(TEST_APP2_PKG);
mMyUid = getUid(mContext.getPackageName());
mServiceClient = new MyServiceClient(mContext);
- mServiceClient.bind();
+
+ final Bundle args = InstrumentationRegistry.getArguments();
+ final int bindPriorityFlags;
+ if (Boolean.valueOf(args.getString(ARG_WAIVE_BIND_PRIORITY, "false"))) {
+ bindPriorityFlags = Context.BIND_WAIVE_PRIORITY;
+ } else {
+ bindPriorityFlags = Context.BIND_NOT_FOREGROUND;
+ }
+ mServiceClient.bind(bindPriorityFlags);
+
mPowerManager = mContext.getSystemService(PowerManager.class);
executeShellCommand("cmd netpolicy start-watching " + mUid);
// Some of the test cases assume that Data saver mode is initially disabled, which might not
@@ -206,6 +216,22 @@
if (null != lock && lock.isHeld()) lock.release();
}
+ /**
+ * Check if the feature blocking network for top_sleeping and lower priority proc-states is
+ * enabled. This is a manual check because the feature flag infrastructure may not be available
+ * in all the branches that will get this code.
+ * TODO: b/322115994 - Use @RequiresFlagsEnabled with
+ * Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE once the tests are moved to cts.
+ */
+ protected boolean isNetworkBlockedForTopSleepingAndAbove() {
+ if (!SdkLevel.isAtLeastV()) {
+ return false;
+ }
+ final String output = executeShellCommand("device_config get backstage_power"
+ + " com.android.server.net.network_blocked_for_top_sleeping_and_above");
+ return Boolean.parseBoolean(output);
+ }
+
protected int getUid(String packageName) throws Exception {
return mContext.getPackageManager().getPackageUid(packageName, 0);
}
@@ -284,44 +310,20 @@
restrictBackgroundValueToString(Integer.parseInt(status)));
}
- protected void assertBackgroundNetworkAccess(boolean expectAllowed) throws Exception {
- assertBackgroundNetworkAccess(expectAllowed, null);
- }
-
/**
- * Asserts whether the active network is available or not for the background app. If the network
- * is unavailable, also checks whether it is blocked by the expected error.
- *
- * @param expectAllowed expect background network access to be allowed or not.
- * @param expectedUnavailableError the expected error when {@code expectAllowed} is false. It's
- * meaningful only when the {@code expectAllowed} is 'false'.
- * Throws an IllegalArgumentException when {@code expectAllowed}
- * is true and this parameter is not null. When the
- * {@code expectAllowed} is 'false' and this parameter is null,
- * this function does not compare error type of the networking
- * access failure.
+ * @deprecated The definition of "background" can be ambiguous. Use separate calls to
+ * {@link #assertProcessStateBelow(int)} with
+ * {@link #assertNetworkAccess(boolean, boolean, String)} to be explicit, instead.
*/
- protected void assertBackgroundNetworkAccess(boolean expectAllowed,
- @Nullable final String expectedUnavailableError) throws Exception {
- assertBackgroundState();
- if (expectAllowed && expectedUnavailableError != null) {
- throw new IllegalArgumentException("expectedUnavailableError is not null");
- }
- assertNetworkAccess(expectAllowed /* expectAvailable */, false /* needScreenOn */,
- expectedUnavailableError);
+ @Deprecated
+ protected void assertBackgroundNetworkAccess(boolean expectAllowed) throws Exception {
+ assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
+ assertNetworkAccess(expectAllowed, false, null);
}
- protected void assertForegroundNetworkAccess() throws Exception {
- assertForegroundNetworkAccess(true);
- }
-
- protected void assertForegroundNetworkAccess(boolean expectAllowed) throws Exception {
- assertForegroundState();
- // We verified that app is in foreground state but if the screen turns-off while
- // verifying for network access, the app will go into background state (in case app's
- // foreground status was due to top activity). So, turn the screen on when verifying
- // network connectivity.
- assertNetworkAccess(expectAllowed /* expectAvailable */, true /* needScreenOn */);
+ protected void assertTopNetworkAccess(boolean expectAllowed) throws Exception {
+ assertTopState();
+ assertNetworkAccess(expectAllowed, true /* needScreenOn */);
}
protected void assertForegroundServiceNetworkAccess() throws Exception {
@@ -355,75 +357,65 @@
finishExpeditedJob();
}
- protected final void assertBackgroundState() throws Exception {
- final int maxTries = 30;
- ProcessState state = null;
- for (int i = 1; i <= maxTries; i++) {
- state = getProcessStateByUid(mUid);
- Log.v(TAG, "assertBackgroundState(): status for app2 (" + mUid + ") on attempt #" + i
- + ": " + state);
- if (isBackground(state.state)) {
- return;
- }
- Log.d(TAG, "App not on background state (" + state + ") on attempt #" + i
- + "; sleeping 1s before trying again");
- // No sleep after the last turn
- if (i < maxTries) {
- SystemClock.sleep(SECOND_IN_MS);
- }
- }
- fail("App2 (" + mUid + ") is not on background state after "
- + maxTries + " attempts: " + state);
+ /**
+ * Asserts that the process state of the test app is below, in priority, to the given
+ * {@link android.app.ActivityManager.ProcessState}.
+ */
+ protected final void assertProcessStateBelow(int processState) throws Exception {
+ assertProcessState(ps -> ps.state > processState, null);
}
- protected final void assertForegroundState() throws Exception {
- final int maxTries = 30;
- ProcessState state = null;
- for (int i = 1; i <= maxTries; i++) {
- state = getProcessStateByUid(mUid);
- Log.v(TAG, "assertForegroundState(): status for app2 (" + mUid + ") on attempt #" + i
- + ": " + state);
- if (!isBackground(state.state)) {
- return;
- }
- Log.d(TAG, "App not on foreground state on attempt #" + i
- + "; sleeping 1s before trying again");
- turnScreenOn();
- // No sleep after the last turn
- if (i < maxTries) {
- SystemClock.sleep(SECOND_IN_MS);
- }
- }
- fail("App2 (" + mUid + ") is not on foreground state after "
- + maxTries + " attempts: " + state);
+ protected final void assertTopState() throws Exception {
+ assertProcessState(ps -> ps.state == PROCESS_STATE_TOP, () -> turnScreenOn());
}
protected final void assertForegroundServiceState() throws Exception {
+ assertProcessState(ps -> ps.state == PROCESS_STATE_FOREGROUND_SERVICE, null);
+ }
+
+ private void assertProcessState(Predicate<ProcessState> statePredicate,
+ ThrowingRunnable onRetry) throws Exception {
final int maxTries = 30;
ProcessState state = null;
for (int i = 1; i <= maxTries; i++) {
+ if (onRetry != null) {
+ onRetry.run();
+ }
state = getProcessStateByUid(mUid);
- Log.v(TAG, "assertForegroundServiceState(): status for app2 (" + mUid + ") on attempt #"
- + i + ": " + state);
- if (state.state == PROCESS_STATE_FOREGROUND_SERVICE) {
+ Log.v(TAG, "assertProcessState(): status for app2 (" + mUid + ") on attempt #" + i
+ + ": " + state);
+ if (statePredicate.test(state)) {
return;
}
- Log.d(TAG, "App not on foreground service state on attempt #" + i
+ Log.i(TAG, "App not in desired process state on attempt #" + i
+ "; sleeping 1s before trying again");
- // No sleep after the last turn
if (i < maxTries) {
SystemClock.sleep(SECOND_IN_MS);
}
}
- fail("App2 (" + mUid + ") is not on foreground service state after "
- + maxTries + " attempts: " + state);
+ fail("App2 (" + mUid + ") is not in the desired process state after " + maxTries
+ + " attempts: " + state);
}
/**
- * Returns whether an app state should be considered "background" for restriction purposes.
+ * Asserts whether the active network is available or not. If the network is unavailable, also
+ * checks whether it is blocked by the expected error.
+ *
+ * @param expectAllowed expect background network access to be allowed or not.
+ * @param expectedUnavailableError the expected error when {@code expectAllowed} is false. It's
+ * meaningful only when the {@code expectAllowed} is 'false'.
+ * Throws an IllegalArgumentException when {@code expectAllowed}
+ * is true and this parameter is not null. When the
+ * {@code expectAllowed} is 'false' and this parameter is null,
+ * this function does not compare error type of the networking
+ * access failure.
*/
- protected boolean isBackground(int state) {
- return state > PROCESS_STATE_FOREGROUND_SERVICE;
+ protected void assertNetworkAccess(boolean expectAllowed, String expectedUnavailableError)
+ throws Exception {
+ if (expectAllowed && expectedUnavailableError != null) {
+ throw new IllegalArgumentException("expectedUnavailableError is not null");
+ }
+ assertNetworkAccess(expectAllowed, false, expectedUnavailableError);
}
/**
@@ -958,7 +950,7 @@
} else if (resultCode == INetworkStateObserver.RESULT_ERROR_UNEXPECTED_PROC_STATE) {
Log.d(TAG, resultData);
// App didn't come to foreground when the activity is started, so try again.
- assertForegroundNetworkAccess();
+ assertTopNetworkAccess(true);
} else {
fail("Unexpected resultCode=" + resultCode + "; received=[" + resultData + "]");
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java
index 10775d0..c1d576d 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java
@@ -17,6 +17,9 @@
package com.android.cts.net.hostside;
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING;
+
import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getUiDevice;
import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground;
import static com.android.cts.net.hostside.Property.APP_STANDBY_MODE;
@@ -26,8 +29,13 @@
import static com.android.cts.net.hostside.Property.METERED_NETWORK;
import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
+import static org.junit.Assume.assumeTrue;
+
+import android.os.SystemClock;
import android.util.Log;
+import com.android.compatibility.common.util.ThrowingRunnable;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -61,14 +69,14 @@
@RequiredProperties({BATTERY_SAVER_MODE})
public void testStartActivity_batterySaver() throws Exception {
setBatterySaverMode(true);
- assertLaunchedActivityHasNetworkAccess("testStartActivity_batterySaver");
+ assertLaunchedActivityHasNetworkAccess("testStartActivity_batterySaver", null);
}
@Test
@RequiredProperties({DATA_SAVER_MODE, METERED_NETWORK})
public void testStartActivity_dataSaver() throws Exception {
setRestrictBackground(true);
- assertLaunchedActivityHasNetworkAccess("testStartActivity_dataSaver");
+ assertLaunchedActivityHasNetworkAccess("testStartActivity_dataSaver", null);
}
@Test
@@ -77,7 +85,7 @@
setDozeMode(true);
// TODO (235284115): We need to turn on Doze every time before starting
// the activity.
- assertLaunchedActivityHasNetworkAccess("testStartActivity_doze");
+ assertLaunchedActivityHasNetworkAccess("testStartActivity_doze", null);
}
@Test
@@ -87,15 +95,28 @@
setAppIdle(true);
// TODO (235284115): We need to put the app into app standby mode every
// time before starting the activity.
- assertLaunchedActivityHasNetworkAccess("testStartActivity_appStandby");
+ assertLaunchedActivityHasNetworkAccess("testStartActivity_appStandby", null);
}
- private void assertLaunchedActivityHasNetworkAccess(String testName) throws Exception {
+ @Test
+ public void testStartActivity_default() throws Exception {
+ assumeTrue("Feature not enabled", isNetworkBlockedForTopSleepingAndAbove());
+ assertLaunchedActivityHasNetworkAccess("testStartActivity_default", () -> {
+ assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+ SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+ });
+ }
+
+ private void assertLaunchedActivityHasNetworkAccess(String testName,
+ ThrowingRunnable onBeginIteration) throws Exception {
for (int i = 0; i < TEST_ITERATION_COUNT; ++i) {
+ if (onBeginIteration != null) {
+ onBeginIteration.run();
+ }
Log.i(TAG, testName + " start #" + i);
launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
getUiDevice().pressHome();
- assertBackgroundState();
+ assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
Log.i(TAG, testName + " end #" + i);
}
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DataSaverModeTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DataSaverModeTest.java
index 2f30536..790e031 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DataSaverModeTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DataSaverModeTest.java
@@ -108,7 +108,7 @@
setRestrictBackground(false);
launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
setRestrictBackground(true);
- assertForegroundNetworkAccess();
+ assertTopNetworkAccess(true);
// Although it should not have access while the screen is off.
turnScreenOff();
@@ -119,7 +119,7 @@
if (isTV()) {
startActivity();
}
- assertForegroundNetworkAccess();
+ assertTopNetworkAccess(true);
// Goes back to background state.
finishActivity();
@@ -129,7 +129,7 @@
setRestrictBackground(false);
launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
setRestrictBackground(true);
- assertForegroundNetworkAccess();
+ assertForegroundServiceNetworkAccess();
stopForegroundService();
assertBackgroundNetworkAccess(false);
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DefaultRestrictionsMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DefaultRestrictionsMeteredTest.java
new file mode 100644
index 0000000..f3a1026
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DefaultRestrictionsMeteredTest.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.net.hostside;
+
+import static com.android.cts.net.hostside.Property.METERED_NETWORK;
+
+@RequiredProperties({METERED_NETWORK})
+public class DefaultRestrictionsMeteredTest extends AbstractDefaultRestrictionsTest {
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DefaultRestrictionsNonMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DefaultRestrictionsNonMeteredTest.java
new file mode 100644
index 0000000..5651dd0
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DefaultRestrictionsNonMeteredTest.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.net.hostside;
+
+import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
+
+@RequiredProperties({NON_METERED_NETWORK})
+public class DefaultRestrictionsNonMeteredTest extends AbstractDefaultRestrictionsTest {
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
index 93cc911..980ecd5 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
@@ -34,26 +34,30 @@
private Context mContext;
private ServiceConnection mServiceConnection;
- private IMyService mService;
+ private volatile IMyService mService;
+ private final ConditionVariable mServiceCondition = new ConditionVariable();
public MyServiceClient(Context context) {
mContext = context;
}
- public void bind() {
+ /**
+ * Binds to a service in the test app to communicate state.
+ * @param bindPriorityFlags Flags to influence the process-state of the bound app.
+ */
+ public void bind(int bindPriorityFlags) {
if (mService != null) {
throw new IllegalStateException("Already bound");
}
-
- final ConditionVariable cv = new ConditionVariable();
mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mService = IMyService.Stub.asInterface(service);
- cv.open();
+ mServiceCondition.open();
}
@Override
public void onServiceDisconnected(ComponentName name) {
+ mServiceCondition.close();
mService = null;
}
};
@@ -63,12 +67,8 @@
// Needs to use BIND_NOT_FOREGROUND so app2 does not run in
// the same process state as app
mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE
- | Context.BIND_NOT_FOREGROUND);
- cv.block(TIMEOUT_MS);
- if (mService == null) {
- throw new IllegalStateException(
- "Could not bind to MyService service after " + TIMEOUT_MS + "ms");
- }
+ | bindPriorityFlags);
+ ensureServiceConnection();
}
public void unbind() {
@@ -77,37 +77,56 @@
}
}
+ private void ensureServiceConnection() {
+ if (mService != null) {
+ return;
+ }
+ mServiceCondition.block(TIMEOUT_MS);
+ if (mService == null) {
+ throw new IllegalStateException(
+ "Could not bind to MyService service after " + TIMEOUT_MS + "ms");
+ }
+ }
+
public void registerBroadcastReceiver() throws RemoteException {
+ ensureServiceConnection();
mService.registerBroadcastReceiver();
}
public int getCounters(String receiverName, String action) throws RemoteException {
+ ensureServiceConnection();
return mService.getCounters(receiverName, action);
}
public String checkNetworkStatus() throws RemoteException {
+ ensureServiceConnection();
return mService.checkNetworkStatus();
}
public String getRestrictBackgroundStatus() throws RemoteException {
+ ensureServiceConnection();
return mService.getRestrictBackgroundStatus();
}
public void sendNotification(int notificationId, String notificationType)
throws RemoteException {
+ ensureServiceConnection();
mService.sendNotification(notificationId, notificationType);
}
public void registerNetworkCallback(final NetworkRequest request, INetworkCallback cb)
throws RemoteException {
+ ensureServiceConnection();
mService.registerNetworkCallback(request, cb);
}
public void unregisterNetworkCallback() throws RemoteException {
+ ensureServiceConnection();
mService.unregisterNetworkCallback();
}
public int scheduleJob(JobInfo jobInfo) throws RemoteException {
+ ensureServiceConnection();
return mService.scheduleJob(jobInfo);
}
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
index 82f4a65..5552b8f 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
@@ -16,6 +16,8 @@
package com.android.cts.net.hostside;
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
import static android.net.NetworkCapabilities.SIGNAL_STRENGTH_UNSPECIFIED;
@@ -33,6 +35,7 @@
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.cts.util.CtsNetUtils;
+import android.os.SystemClock;
import android.util.Log;
import com.android.modules.utils.build.SdkLevel;
@@ -42,6 +45,7 @@
import org.junit.Rule;
import org.junit.Test;
+import java.util.ArrayList;
import java.util.Objects;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
@@ -144,12 +148,22 @@
public Network expectAvailableCallbackAndGetNetwork() {
final CallbackInfo cb = nextCallback(TEST_CONNECT_TIMEOUT_MS);
if (cb.state != CallbackState.AVAILABLE) {
- fail("Network is not available. Instead obtained the following callback :"
- + cb);
+ fail("Network is not available. Instead obtained the following callback :" + cb);
}
return cb.network;
}
+ public void drainAndWaitForIdle() {
+ try {
+ do {
+ mCallbacks.drainTo(new ArrayList<>());
+ } while (mCallbacks.poll(TEST_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS) != null);
+ } catch (InterruptedException ie) {
+ Log.e(TAG, "Interrupted while draining callback queue", ie);
+ Thread.currentThread().interrupt();
+ }
+ }
+
public void expectBlockedStatusCallback(Network expectedNetwork, boolean expectBlocked) {
expectCallback(CallbackState.BLOCKED_STATUS, expectedNetwork, expectBlocked);
}
@@ -203,6 +217,7 @@
// Initial state
setBatterySaverMode(false);
setRestrictBackground(false);
+ setAppIdle(false);
// Get transports of the active network, this has to be done before changing meteredness,
// since wifi will be disconnected when changing from non-metered to metered.
@@ -223,7 +238,7 @@
// Check that the network is metered.
mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork,
false /* hasCapability */, NET_CAPABILITY_NOT_METERED);
- mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, false);
+ mTestNetworkCallback.drainAndWaitForIdle();
// Before Android T, DNS queries over private DNS should be but are not restricted by Power
// Saver or Data Saver. The issue is fixed in mainline update and apps can no longer request
@@ -312,7 +327,8 @@
// Enable Power Saver
setBatterySaverMode(true);
if (SdkLevel.isAtLeastT()) {
- assertBackgroundNetworkAccess(false, "java.net.UnknownHostException");
+ assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
+ assertNetworkAccess(false, "java.net.UnknownHostException");
} else {
assertBackgroundNetworkAccess(false);
}
@@ -336,7 +352,8 @@
// Enable Power Saver
setBatterySaverMode(true);
if (SdkLevel.isAtLeastT()) {
- assertBackgroundNetworkAccess(false, "java.net.UnknownHostException");
+ assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
+ assertNetworkAccess(false, "java.net.UnknownHostException");
} else {
assertBackgroundNetworkAccess(false);
}
@@ -353,6 +370,58 @@
}
}
+ @Test
+ public void testOnBlockedStatusChanged_default() throws Exception {
+ assumeTrue("Feature not enabled", isNetworkBlockedForTopSleepingAndAbove());
+
+ try {
+ assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+ assertNetworkAccess(false, null);
+ assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
+
+ launchActivity();
+ assertTopState();
+ assertNetworkAccess(true, null);
+ mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
+ assertNetworkAccessBlockedByBpf(false, mUid, true /* metered */);
+
+ finishActivity();
+ assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+ SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+ assertNetworkAccess(false, null);
+ mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
+ assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
+
+ } finally {
+ mMeterednessConfiguration.resetNetworkMeteredness();
+ }
+
+ // Set to non-metered network
+ mMeterednessConfiguration.configureNetworkMeteredness(false);
+ mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork,
+ true /* hasCapability */, NET_CAPABILITY_NOT_METERED);
+ try {
+ assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+ assertNetworkAccess(false, null);
+ assertNetworkAccessBlockedByBpf(true, mUid, false /* metered */);
+
+ launchActivity();
+ assertTopState();
+ assertNetworkAccess(true, null);
+ mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
+ assertNetworkAccessBlockedByBpf(false, mUid, false /* metered */);
+
+ finishActivity();
+ assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+ SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+ assertNetworkAccess(false, null);
+ mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
+ assertNetworkAccessBlockedByBpf(true, mUid, false /* metered */);
+ } finally {
+ mMeterednessConfiguration.resetNetworkMeteredness();
+ }
+ }
+
// TODO: 1. test against VPN lockdown.
// 2. test against multiple networks.
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyManagerTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyManagerTest.java
index a0d88c9..968e270 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyManagerTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyManagerTest.java
@@ -16,6 +16,8 @@
package com.android.cts.net.hostside;
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING;
import static android.os.Process.SYSTEM_UID;
import static com.android.cts.net.hostside.NetworkPolicyTestUtils.assertIsUidRestrictedOnMeteredNetworks;
@@ -27,6 +29,9 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import android.os.SystemClock;
import org.junit.After;
import org.junit.Before;
@@ -137,13 +142,13 @@
// Make TEST_APP2_PKG go to foreground and mUid will be allowed temporarily.
launchActivity();
- assertForegroundState();
+ assertTopState();
assertNetworkingBlockedStatusForUid(mUid, METERED,
false /* expectedResult */); // Match NTWK_ALLOWED_TMP_ALLOWLIST
// Back to background.
finishActivity();
- assertBackgroundState();
+ assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
assertNetworkingBlockedStatusForUid(mUid, METERED,
true /* expectedResult */); // Match NTWK_BLOCKED_BG_RESTRICT
} finally {
@@ -219,11 +224,11 @@
// Make TEST_APP2_PKG go to foreground and isUidRestrictedOnMeteredNetworks() will
// return false.
launchActivity();
- assertForegroundState();
+ assertTopState();
assertIsUidRestrictedOnMeteredNetworks(mUid, false /* expectedResult */);
// Back to background.
finishActivity();
- assertBackgroundState();
+ assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
// Add mUid into restrict background whitelist and isUidRestrictedOnMeteredNetworks()
// will return false.
@@ -237,4 +242,33 @@
assertIsUidRestrictedOnMeteredNetworks(mUid, false /* expectedResult */);
}
}
+
+ @Test
+ public void testIsUidNetworkingBlocked_whenInBackground() throws Exception {
+ assumeTrue("Feature not enabled", isNetworkBlockedForTopSleepingAndAbove());
+
+ try {
+ assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+ SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+ assertNetworkingBlockedStatusForUid(mUid, METERED, true /* expectedResult */);
+ assertTrue(isUidNetworkingBlocked(mUid, NON_METERED));
+
+ launchActivity();
+ assertTopState();
+ assertNetworkingBlockedStatusForUid(mUid, METERED, false /* expectedResult */);
+ assertFalse(isUidNetworkingBlocked(mUid, NON_METERED));
+
+ finishActivity();
+ assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+ SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+ assertNetworkingBlockedStatusForUid(mUid, METERED, true /* expectedResult */);
+ assertTrue(isUidNetworkingBlocked(mUid, NON_METERED));
+
+ addPowerSaveModeWhitelist(TEST_APP2_PKG);
+ assertNetworkingBlockedStatusForUid(mUid, METERED, false /* expectedResult */);
+ assertFalse(isUidNetworkingBlocked(mUid, NON_METERED));
+ } finally {
+ removePowerSaveModeWhitelist(TEST_APP2_PKG);
+ }
+ }
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/RestrictedModeTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RestrictedModeTest.java
index 35f1f1c..4777bf4 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/RestrictedModeTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RestrictedModeTest.java
@@ -38,7 +38,7 @@
// go to foreground state and enable restricted mode
launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
setRestrictedNetworkingMode(true);
- assertForegroundNetworkAccess(false);
+ assertTopNetworkAccess(false);
// go to background state
finishActivity();
@@ -47,7 +47,7 @@
// disable restricted mode and assert network access in foreground and background states
setRestrictedNetworkingMode(false);
launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
- assertForegroundNetworkAccess(true);
+ assertTopNetworkAccess(true);
// go to background state
finishActivity();
diff --git a/tests/cts/hostside/app2/Android.bp b/tests/cts/hostside/app2/Android.bp
index db92f5c..c526172 100644
--- a/tests/cts/hostside/app2/Android.bp
+++ b/tests/cts/hostside/app2/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/tests/cts/hostside/certs/Android.bp b/tests/cts/hostside/certs/Android.bp
index 60b5476..301973e 100644
--- a/tests/cts/hostside/certs/Android.bp
+++ b/tests/cts/hostside/certs/Android.bp
@@ -1,4 +1,5 @@
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/common/TrunkStable.bp b/tests/cts/hostside/instrumentation_arguments/Android.bp
similarity index 73%
rename from common/TrunkStable.bp
rename to tests/cts/hostside/instrumentation_arguments/Android.bp
index 59874c2..cdede36 100644
--- a/common/TrunkStable.bp
+++ b/tests/cts/hostside/instrumentation_arguments/Android.bp
@@ -1,5 +1,4 @@
-//
-// Copyright (C) 2023 The Android Open Source Project
+// Copyright (C) 2024 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -12,5 +11,12 @@
// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+filegroup {
+ name: "ArgumentConstants",
+ srcs: ["src/**/*.java"],
+}
diff --git a/tests/cts/hostside/instrumentation_arguments/src/com/android/cts/net/arguments/InstrumentationArguments.java b/tests/cts/hostside/instrumentation_arguments/src/com/android/cts/net/arguments/InstrumentationArguments.java
new file mode 100644
index 0000000..472e347
--- /dev/null
+++ b/tests/cts/hostside/instrumentation_arguments/src/com/android/cts/net/arguments/InstrumentationArguments.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.net.arguments;
+
+public interface InstrumentationArguments {
+ String ARG_WAIVE_BIND_PRIORITY = "waive_bind_priority";
+}
diff --git a/tests/cts/hostside/networkslicingtestapp/Android.bp b/tests/cts/hostside/networkslicingtestapp/Android.bp
index 2aa3f69..100b6e4 100644
--- a/tests/cts/hostside/networkslicingtestapp/Android.bp
+++ b/tests/cts/hostside/networkslicingtestapp/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -39,8 +40,8 @@
android_test_helper_app {
name: "CtsHostsideNetworkCapTestsAppWithoutProperty",
defaults: [
- "cts_support_defaults",
- "CtsHostsideNetworkCapTestsAppDefaults"
+ "cts_support_defaults",
+ "CtsHostsideNetworkCapTestsAppDefaults",
],
manifest: "AndroidManifestWithoutProperty.xml",
}
@@ -48,8 +49,8 @@
android_test_helper_app {
name: "CtsHostsideNetworkCapTestsAppWithProperty",
defaults: [
- "cts_support_defaults",
- "CtsHostsideNetworkCapTestsAppDefaults"
+ "cts_support_defaults",
+ "CtsHostsideNetworkCapTestsAppDefaults",
],
manifest: "AndroidManifestWithProperty.xml",
}
@@ -57,8 +58,8 @@
android_test_helper_app {
name: "CtsHostsideNetworkCapTestsAppSdk33",
defaults: [
- "cts_support_defaults",
- "CtsHostsideNetworkCapTestsAppDefaults"
+ "cts_support_defaults",
+ "CtsHostsideNetworkCapTestsAppDefaults",
],
target_sdk_version: "33",
manifest: "AndroidManifestWithoutProperty.xml",
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java b/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java
index 849ac7c..880e826 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java
@@ -16,6 +16,8 @@
package com.android.cts.net;
+import static com.android.cts.net.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
+
import android.platform.test.annotations.FlakyTest;
import com.android.testutils.SkipPresubmit;
@@ -26,9 +28,12 @@
import org.junit.Test;
+import java.util.Map;
+
@SkipPresubmit(reason = "Out of SLO flakiness")
public class HostsideConnOnActivityStartTest extends HostsideNetworkTestCase {
private static final String TEST_CLASS = TEST_PKG + ".ConnOnActivityStartTest";
+
@BeforeClassWithInfo
public static void setUpOnce(TestInformation testInfo) throws Exception {
uninstallPackage(testInfo, TEST_APP2_PKG, false);
@@ -60,4 +65,11 @@
public void testStartActivity_appStandby() throws Exception {
runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_appStandby");
}
+
+ // TODO(b/321848487): Annotate with @RequiresFlagsEnabled to mirror the device-side test.
+ @Test
+ public void testStartActivity_default() throws Exception {
+ runDeviceTestsWithArgs(TEST_PKG, TEST_CLASS, "testStartActivity_default",
+ Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
+ }
}
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideDefaultNetworkRestrictionsTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideDefaultNetworkRestrictionsTests.java
new file mode 100644
index 0000000..0d01fc1
--- /dev/null
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideDefaultNetworkRestrictionsTests.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.net;
+
+import static com.android.cts.net.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
+
+import com.android.testutils.SkipPresubmit;
+import com.android.tradefed.device.DeviceNotAvailableException;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Map;
+
+// TODO(b/321848487): Annotate with @RequiresFlagsEnabled to mirror the device-side tests.
+@SkipPresubmit(reason = "Monitoring for flakiness")
+public class HostsideDefaultNetworkRestrictionsTests extends HostsideNetworkTestCase {
+ private static final String METERED_TEST_CLASS = TEST_PKG + ".DefaultRestrictionsMeteredTest";
+ private static final String NON_METERED_TEST_CLASS =
+ TEST_PKG + ".DefaultRestrictionsNonMeteredTest";
+
+ @Before
+ public void setUp() throws Exception {
+ uninstallPackage(TEST_APP2_PKG, false);
+ installPackage(TEST_APP2_APK);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ uninstallPackage(TEST_APP2_PKG, true);
+ }
+
+ private void runMeteredTest(String methodName) throws DeviceNotAvailableException {
+ runDeviceTestsWithArgs(TEST_PKG, METERED_TEST_CLASS, methodName,
+ Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
+ }
+
+ private void runNonMeteredTest(String methodName) throws DeviceNotAvailableException {
+ runDeviceTestsWithArgs(TEST_PKG, NON_METERED_TEST_CLASS, methodName,
+ Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
+ }
+
+ @Test
+ public void testMeteredNetworkAccess_defaultRestrictions_testActivityNetworkAccess()
+ throws Exception {
+ runMeteredTest("testActivityNetworkAccess");
+ }
+
+ @Test
+ public void testMeteredNetworkAccess_defaultRestrictions_testFgsNetworkAccess()
+ throws Exception {
+ runMeteredTest("testFgsNetworkAccess");
+ }
+
+ @Test
+ public void testMeteredNetworkAccess_defaultRestrictions_inFullAllowlist() throws Exception {
+ runMeteredTest("testBackgroundNetworkAccess_inFullAllowlist");
+ }
+
+ @Test
+ public void testMeteredNetworkAccess_defaultRestrictions_inExceptIdleAllowlist()
+ throws Exception {
+ runMeteredTest("testBackgroundNetworkAccess_inExceptIdleAllowlist");
+ }
+
+ @Test
+ public void testNonMeteredNetworkAccess_defaultRestrictions_testActivityNetworkAccess()
+ throws Exception {
+ runNonMeteredTest("testActivityNetworkAccess");
+ }
+
+ @Test
+ public void testNonMeteredNetworkAccess_defaultRestrictions_testFgsNetworkAccess()
+ throws Exception {
+ runNonMeteredTest("testFgsNetworkAccess");
+ }
+
+ @Test
+ public void testNonMeteredNetworkAccess_defaultRestrictions_inFullAllowlist() throws Exception {
+ runNonMeteredTest("testBackgroundNetworkAccess_inFullAllowlist");
+ }
+
+ @Test
+ public void testNonMeteredNetworkAccess_defaultRestrictions_inExceptIdleAllowlist()
+ throws Exception {
+ runNonMeteredTest("testBackgroundNetworkAccess_inExceptIdleAllowlist");
+ }
+}
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java
index 04bd1ad..361f7c7 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java
@@ -15,12 +15,16 @@
*/
package com.android.cts.net;
+import static com.android.cts.net.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
+
import com.android.testutils.SkipPresubmit;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
+import java.util.Map;
+
@SkipPresubmit(reason = "Out of SLO flakiness")
public class HostsideNetworkCallbackTests extends HostsideNetworkTestCase {
@@ -46,5 +50,12 @@
runDeviceTests(TEST_PKG,
TEST_PKG + ".NetworkCallbackTest", "testOnBlockedStatusChanged_powerSaver");
}
+
+ // TODO(b/321848487): Annotate with @RequiresFlagsEnabled to mirror the device-side test.
+ @Test
+ public void testOnBlockedStatusChanged_default() throws Exception {
+ runDeviceTestsWithArgs(TEST_PKG, TEST_PKG + ".NetworkCallbackTest",
+ "testOnBlockedStatusChanged_default", Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
+ }
}
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkPolicyManagerTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkPolicyManagerTests.java
index 3ddb88b..e97db58 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkPolicyManagerTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkPolicyManagerTests.java
@@ -16,10 +16,14 @@
package com.android.cts.net;
+import static com.android.cts.net.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
+import java.util.Map;
+
public class HostsideNetworkPolicyManagerTests extends HostsideNetworkTestCase {
@Before
public void setUp() throws Exception {
@@ -71,4 +75,12 @@
runDeviceTests(TEST_PKG,
TEST_PKG + ".NetworkPolicyManagerTest", "testIsUidRestrictedOnMeteredNetworks");
}
+
+ // TODO(b/321848487): Annotate with @RequiresFlagsEnabled to mirror the device-side test.
+ @Test
+ public void testIsUidNetworkingBlocked_whenInBackground() throws Exception {
+ runDeviceTestsWithArgs(TEST_PKG, TEST_PKG + ".NetworkPolicyManagerTest",
+ "testIsUidNetworkingBlocked_whenInBackground",
+ Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
+ }
}
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
index 3358fd7..ca95ed6 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
@@ -31,10 +31,13 @@
import com.android.tradefed.testtype.junit4.AfterClassWithInfo;
import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
import com.android.tradefed.testtype.junit4.BeforeClassWithInfo;
+import com.android.tradefed.testtype.junit4.DeviceTestRunOptions;
import com.android.tradefed.util.RunUtil;
import org.junit.runner.RunWith;
+import java.util.Map;
+
@RunWith(DeviceJUnit4ClassRunner.class)
abstract class HostsideNetworkTestCase extends BaseHostJUnit4Test {
protected static final boolean DEBUG = false;
@@ -146,6 +149,17 @@
+ packageName + ", u=" + currentUser);
}
+ protected boolean runDeviceTestsWithArgs(String packageName, String className,
+ String methodName, Map<String, String> args) throws DeviceNotAvailableException {
+ final DeviceTestRunOptions deviceTestRunOptions = new DeviceTestRunOptions(packageName)
+ .setTestClassName(className)
+ .setTestMethodName(methodName);
+ for (Map.Entry<String, String> arg : args.entrySet()) {
+ deviceTestRunOptions.addInstrumentationArg(arg.getKey(), arg.getValue());
+ }
+ return runDeviceTests(deviceTestRunOptions);
+ }
+
protected String runCommand(String command) throws DeviceNotAvailableException {
Log.d(TAG, "Command: '" + command + "'");
final String output = getDevice().executeShellCommand(command);
diff --git a/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java b/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
index aa90f5f..fa68e3e 100644
--- a/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
+++ b/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
@@ -191,6 +191,6 @@
String path = "/proc/sys/net/ipv4/tcp_congestion_control";
String value = mDevice.executeAdbCommand("shell", "cat", path).trim();
- assertEquals(value, "cubic");
+ assertEquals("cubic", value);
}
}
diff --git a/tests/cts/multidevices/Android.bp b/tests/cts/multidevices/Android.bp
new file mode 100644
index 0000000..5ac4229
--- /dev/null
+++ b/tests/cts/multidevices/Android.bp
@@ -0,0 +1,42 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+python_test_host {
+ name: "CtsConnectivityMultiDevicesTestCases",
+ main: "connectivity_multi_devices_test.py",
+ srcs: ["connectivity_multi_devices_test.py"],
+ libs: [
+ "mobly",
+ ],
+ test_suites: [
+ "cts",
+ "general-tests",
+ ],
+ test_options: {
+ unit_test: false,
+ },
+ data: [
+ // Package the snippet with the mobly test
+ ":connectivity_multi_devices_snippet",
+ ],
+ version: {
+ py3: {
+ embedded_launcher: true,
+ },
+ },
+}
diff --git a/tests/cts/multidevices/AndroidTest.xml b/tests/cts/multidevices/AndroidTest.xml
new file mode 100644
index 0000000..5312b4d
--- /dev/null
+++ b/tests/cts/multidevices/AndroidTest.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration description="Config for CTS Connectivity multi devices test cases">
+ <option name="test-suite-tag" value="cts" />
+ <option name="config-descriptor:metadata" key="component" value="networking" />
+ <option name="config-descriptor:metadata" key="token" value="SIM_CARD" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+
+ <device name="device1">
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="test-file-name" value="connectivity_multi_devices_snippet.apk" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ </target_preparer>
+ </device>
+ <device name="device2">
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="test-file-name" value="connectivity_multi_devices_snippet.apk" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ </target_preparer>
+ </device>
+
+ <test class="com.android.tradefed.testtype.mobly.MoblyBinaryHostTest">
+ <!-- The mobly-par-file-name should match the module name -->
+ <option name="mobly-par-file-name" value="CtsConnectivityMultiDevicesTestCases" />
+ <!-- Timeout limit in milliseconds for all test cases of the python binary -->
+ <option name="mobly-test-timeout" value="180000" />
+ </test>
+</configuration>
+
diff --git a/tests/cts/multidevices/connectivity_multi_devices_test.py b/tests/cts/multidevices/connectivity_multi_devices_test.py
new file mode 100644
index 0000000..ab88504
--- /dev/null
+++ b/tests/cts/multidevices/connectivity_multi_devices_test.py
@@ -0,0 +1,110 @@
+# Lint as: python3
+"""Connectivity multi devices tests."""
+import base64
+import sys
+import uuid
+
+from mobly import asserts
+from mobly import base_test
+from mobly import test_runner
+from mobly import utils
+from mobly.controllers import android_device
+
+CONNECTIVITY_MULTI_DEVICES_SNIPPET_PACKAGE = "com.google.snippet.connectivity"
+
+
+class UpstreamType:
+ CELLULAR = 1
+ WIFI = 2
+
+
+class ConnectivityMultiDevicesTest(base_test.BaseTestClass):
+
+ def setup_class(self):
+ # Declare that two Android devices are needed.
+ self.clientDevice, self.serverDevice = self.register_controller(
+ android_device, min_number=2
+ )
+
+ def setup_device(device):
+ device.load_snippet(
+ "connectivity_multi_devices_snippet",
+ CONNECTIVITY_MULTI_DEVICES_SNIPPET_PACKAGE,
+ )
+
+ # Set up devices in parallel to save time.
+ utils.concurrent_exec(
+ setup_device,
+ ((self.clientDevice,), (self.serverDevice,)),
+ max_workers=2,
+ raise_on_exception=True,
+ )
+
+ @staticmethod
+ def generate_uuid32_base64():
+ """Generates a UUID32 and encodes it in Base64.
+
+ Returns:
+ str: The Base64-encoded UUID32 string. Which is 22 characters.
+ """
+ return base64.b64encode(uuid.uuid1().bytes).decode("utf-8").strip("=")
+
+ def _do_test_hotspot_for_upstream_type(self, upstream_type):
+ """Test hotspot with the specified upstream type.
+
+ This test create a hotspot, make the client connect
+ to it, and verify the packet is forwarded by the hotspot.
+ """
+ server = self.serverDevice.connectivity_multi_devices_snippet
+ client = self.clientDevice.connectivity_multi_devices_snippet
+
+ # Assert pre-conditions specific to each upstream type.
+ asserts.skip_if(not client.hasWifiFeature(), "Client requires Wifi feature")
+ asserts.skip_if(
+ not server.hasHotspotFeature(), "Server requires hotspot feature"
+ )
+ if upstream_type == UpstreamType.CELLULAR:
+ asserts.skip_if(
+ not server.hasTelephonyFeature(), "Server requires Telephony feature"
+ )
+ server.requestCellularAndEnsureDefault()
+ elif upstream_type == UpstreamType.WIFI:
+ asserts.skip_if(
+ not server.isStaApConcurrencySupported(),
+ "Server requires Wifi AP + STA concurrency",
+ )
+ server.ensureWifiIsDefault()
+ else:
+ raise ValueError(f"Invalid upstream type: {upstream_type}")
+
+ # Generate ssid/passphrase with random characters to make sure nearby devices won't
+ # connect unexpectedly. Note that total length of ssid cannot go over 32.
+ testSsid = "HOTSPOT-" + self.generate_uuid32_base64()
+ testPassphrase = self.generate_uuid32_base64()
+
+ try:
+ # Create a hotspot with fixed SSID and password.
+ server.startHotspot(testSsid, testPassphrase)
+
+ # Make the client connects to the hotspot.
+ client.connectToWifi(testSsid, testPassphrase, True)
+
+ finally:
+ if upstream_type == UpstreamType.CELLULAR:
+ server.unrequestCellular()
+ # Teardown the hotspot.
+ server.stopAllTethering()
+
+ def test_hotspot_upstream_wifi(self):
+ self._do_test_hotspot_for_upstream_type(UpstreamType.WIFI)
+
+ def test_hotspot_upstream_cellular(self):
+ self._do_test_hotspot_for_upstream_type(UpstreamType.CELLULAR)
+
+
+if __name__ == "__main__":
+ # Take test args
+ if "--" in sys.argv:
+ index = sys.argv.index("--")
+ sys.argv = sys.argv[:1] + sys.argv[index + 1 :]
+ test_runner.main()
diff --git a/tests/cts/multidevices/snippet/Android.bp b/tests/cts/multidevices/snippet/Android.bp
new file mode 100644
index 0000000..5940cbb
--- /dev/null
+++ b/tests/cts/multidevices/snippet/Android.bp
@@ -0,0 +1,37 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+ name: "connectivity_multi_devices_snippet",
+ defaults: [
+ "ConnectivityTestsLatestSdkDefaults",
+ "cts_defaults",
+ "framework-connectivity-test-defaults",
+ ],
+ srcs: [
+ "ConnectivityMultiDevicesSnippet.kt",
+ ],
+ manifest: "AndroidManifest.xml",
+ static_libs: [
+ "androidx.test.runner",
+ "mobly-snippet-lib",
+ "cts-net-utils",
+ ],
+ platform_apis: true,
+ min_sdk_version: "30", // R
+}
diff --git a/tests/cts/multidevices/snippet/AndroidManifest.xml b/tests/cts/multidevices/snippet/AndroidManifest.xml
new file mode 100644
index 0000000..9ed8146
--- /dev/null
+++ b/tests/cts/multidevices/snippet/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.snippet.connectivity">
+ <!-- Declare the minimum Android SDK version and internet permission,
+ which are required by Mobly Snippet Lib since it uses network socket. -->
+ <uses-sdk android:minSdkVersion="30" />
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+ <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+ <uses-permission android:name="android.permission.INTERNET" />
+ <application>
+ <!-- Add any classes that implement the Snippet interface as meta-data, whose
+ value is a comma-separated string, each section being the package path
+ of a snippet class -->
+ <meta-data
+ android:name="mobly-snippets"
+ android:value="com.google.snippet.connectivity.ConnectivityMultiDevicesSnippet" />
+ </application>
+ <!-- Add an instrumentation tag so that the app can be launched through an
+ instrument command. The runner `com.google.android.mobly.snippet.SnippetRunner`
+ is derived from `AndroidJUnitRunner`, and is required to use the
+ Mobly Snippet Lib. -->
+ <instrumentation
+ android:name="com.google.android.mobly.snippet.SnippetRunner"
+ android:targetPackage="com.google.snippet.connectivity" />
+</manifest>
diff --git a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
new file mode 100644
index 0000000..115210b
--- /dev/null
+++ b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.snippet.connectivity
+
+import android.Manifest.permission.OVERRIDE_WIFI_CONFIG
+import android.content.pm.PackageManager.FEATURE_TELEPHONY
+import android.content.pm.PackageManager.FEATURE_WIFI
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.cts.util.CtsNetUtils
+import android.net.cts.util.CtsTetheringUtils
+import android.net.wifi.ScanResult
+import android.net.wifi.SoftApConfiguration
+import android.net.wifi.SoftApConfiguration.SECURITY_TYPE_WPA2_PSK
+import android.net.wifi.WifiConfiguration
+import android.net.wifi.WifiManager
+import android.net.wifi.WifiNetworkSpecifier
+import android.net.wifi.WifiSsid
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.testutils.ConnectUtil
+import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.runAsShell
+import com.google.android.mobly.snippet.Snippet
+import com.google.android.mobly.snippet.rpc.Rpc
+
+class ConnectivityMultiDevicesSnippet : Snippet {
+ private val context = InstrumentationRegistry.getInstrumentation().getTargetContext()
+ private val wifiManager = context.getSystemService(WifiManager::class.java)!!
+ private val cm = context.getSystemService(ConnectivityManager::class.java)!!
+ private val pm = context.packageManager
+ private val ctsNetUtils = CtsNetUtils(context)
+ private val ctsTetheringUtils = CtsTetheringUtils(context)
+ private var oldSoftApConfig: SoftApConfiguration? = null
+
+ @Rpc(description = "Check whether the device has wifi feature.")
+ fun hasWifiFeature() = pm.hasSystemFeature(FEATURE_WIFI)
+
+ @Rpc(description = "Check whether the device has telephony feature.")
+ fun hasTelephonyFeature() = pm.hasSystemFeature(FEATURE_TELEPHONY)
+
+ @Rpc(description = "Check whether the device supporters AP + STA concurrency.")
+ fun isStaApConcurrencySupported() {
+ wifiManager.isStaApConcurrencySupported()
+ }
+
+ @Rpc(description = "Request cellular connection and ensure it is the default network.")
+ fun requestCellularAndEnsureDefault() {
+ ctsNetUtils.disableWifi()
+ val network = ctsNetUtils.connectToCell()
+ ctsNetUtils.expectNetworkIsSystemDefault(network)
+ }
+
+ @Rpc(description = "Unrequest cellular connection.")
+ fun unrequestCellular() {
+ ctsNetUtils.disconnectFromCell()
+ }
+
+ @Rpc(description = "Ensure any wifi is connected and is the default network.")
+ fun ensureWifiIsDefault() {
+ val network = ctsNetUtils.ensureWifiConnected()
+ ctsNetUtils.expectNetworkIsSystemDefault(network)
+ }
+
+ @Rpc(description = "Connect to specified wifi network.")
+ // Suppress warning because WifiManager methods to connect to a config are
+ // documented not to be deprecated for privileged users.
+ @Suppress("DEPRECATION")
+ fun connectToWifi(ssid: String, passphrase: String, requireValidation: Boolean): Network {
+ val specifier = WifiNetworkSpecifier.Builder()
+ .setSsid(ssid)
+ .setWpa2Passphrase(passphrase)
+ .setBand(ScanResult.WIFI_BAND_24_GHZ)
+ .build()
+ val wifiConfig = WifiConfiguration()
+ wifiConfig.SSID = "\"" + ssid + "\""
+ wifiConfig.preSharedKey = "\"" + passphrase + "\""
+ wifiConfig.hiddenSSID = true
+ wifiConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA2_PSK)
+ wifiConfig.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP)
+ wifiConfig.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP)
+
+ // Register network callback for the specific wifi.
+ val networkCallback = TestableNetworkCallback()
+ val wifiRequest = NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI)
+ .setNetworkSpecifier(specifier)
+ .build()
+ cm.registerNetworkCallback(wifiRequest, networkCallback)
+
+ try {
+ // Add the test configuration and connect to it.
+ val connectUtil = ConnectUtil(context)
+ connectUtil.connectToWifiConfig(wifiConfig)
+
+ val event = networkCallback.expect<Available>()
+ if (requireValidation) {
+ networkCallback.eventuallyExpect<CapabilitiesChanged> {
+ it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
+ }
+ }
+ return event.network
+ } finally {
+ cm.unregisterNetworkCallback(networkCallback)
+ }
+ }
+
+ @Rpc(description = "Check whether the device supports hotspot feature.")
+ fun hasHotspotFeature(): Boolean {
+ val tetheringCallback = ctsTetheringUtils.registerTetheringEventCallback()
+ try {
+ return tetheringCallback.isWifiTetheringSupported(context)
+ } finally {
+ ctsTetheringUtils.unregisterTetheringEventCallback(tetheringCallback)
+ }
+ }
+
+ @Rpc(description = "Start a hotspot with given SSID and passphrase.")
+ fun startHotspot(ssid: String, passphrase: String) {
+ // Store old config.
+ runAsShell(OVERRIDE_WIFI_CONFIG) {
+ oldSoftApConfig = wifiManager.getSoftApConfiguration()
+ }
+
+ val softApConfig = SoftApConfiguration.Builder()
+ .setWifiSsid(WifiSsid.fromBytes(ssid.toByteArray()))
+ .setPassphrase(passphrase, SECURITY_TYPE_WPA2_PSK)
+ .setBand(SoftApConfiguration.BAND_2GHZ)
+ .build()
+ runAsShell(OVERRIDE_WIFI_CONFIG) {
+ wifiManager.setSoftApConfiguration(softApConfig)
+ }
+ val tetheringCallback = ctsTetheringUtils.registerTetheringEventCallback()
+ try {
+ tetheringCallback.expectNoTetheringActive()
+ ctsTetheringUtils.startWifiTethering(tetheringCallback)
+ } finally {
+ ctsTetheringUtils.unregisterTetheringEventCallback(tetheringCallback)
+ }
+ }
+
+ @Rpc(description = "Stop all tethering.")
+ fun stopAllTethering() {
+ ctsTetheringUtils.stopAllTethering()
+
+ // Restore old config.
+ oldSoftApConfig?.let {
+ runAsShell(OVERRIDE_WIFI_CONFIG) {
+ wifiManager.setSoftApConfiguration(it)
+ }
+ }
+ }
+}
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index 9310888..074c587 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -54,27 +55,26 @@
"junit",
"junit-params",
"modules-utils-build",
+ "net-tests-utils",
"net-utils-framework-common",
"truth",
"TetheringIntegrationTestsBaseLib",
],
- // uncomment when b/13249961 is fixed
- // sdk_version: "current",
- platform_apis: true,
+ min_sdk_version: "30",
per_testcase_directory: true,
host_required: ["net-tests-utils-host-common"],
test_config_template: "AndroidTestTemplate.xml",
data: [
":ConnectivityTestPreparer",
":CtsCarrierServicePackage",
- ]
+ ],
}
// Networking CTS tests for development and release. These tests always target the platform SDK
// version, and are subject to all the restrictions appropriate to that version. Before SDK
-// finalization, these tests have a min_sdk_version of 10000, and cannot be installed on release
-// devices.
+// finalization, these tests have a min_sdk_version of 10000, but they can still be installed on
+// release devices as their min_sdk_version is set to a production version.
android_test {
name: "CtsNetTestCases",
defaults: [
@@ -87,6 +87,14 @@
],
test_suites: [
"cts",
+ "mts-dnsresolver",
+ "mts-networking",
+ "mts-tethering",
+ "mts-wifi",
+ "mcts-dnsresolver",
+ "mcts-networking",
+ "mcts-tethering",
+ "mcts-wifi",
"general-tests",
],
}
@@ -131,6 +139,7 @@
"cts",
"general-tests",
"mts-tethering",
+ "mcts-tethering",
],
}
diff --git a/tests/cts/net/api23Test/Android.bp b/tests/cts/net/api23Test/Android.bp
index 1f1dd5d..2ec3a70 100644
--- a/tests/cts/net/api23Test/Android.bp
+++ b/tests/cts/net/api23Test/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityManagerApi23Test.java b/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityManagerApi23Test.java
index 8d68c5f..af1af43 100644
--- a/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityManagerApi23Test.java
+++ b/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityManagerApi23Test.java
@@ -65,7 +65,7 @@
}
ConnectivityReceiver.prepare();
- mCtsNetUtils.toggleWifi();
+ mCtsNetUtils.reconnectWifiAndWaitForConnectivityAction();
// The connectivity broadcast has been sent; push through a terminal broadcast
// to wait for in the receive to confirm it didn't see the connectivity change.
@@ -88,7 +88,7 @@
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
Thread.sleep(200);
- mCtsNetUtils.toggleWifi();
+ mCtsNetUtils.reconnectWifiAndWaitForConnectivityAction();
Intent getConnectivityCount = new Intent(GET_WIFI_CONNECTIVITY_ACTION_COUNT);
assertEquals(2, sendOrderedBroadcastAndReturnResultCode(
@@ -106,7 +106,7 @@
filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
mContext.registerReceiver(receiver, filter);
- mCtsNetUtils.toggleWifi();
+ mCtsNetUtils.reconnectWifiAndWaitForConnectivityAction();
Intent finalIntent = new Intent(ConnectivityReceiver.FINAL_ACTION);
finalIntent.setClass(mContext, ConnectivityReceiver.class);
mContext.sendBroadcast(finalIntent);
diff --git a/tests/cts/net/appForApi23/Android.bp b/tests/cts/net/appForApi23/Android.bp
index b39690f..d300743 100644
--- a/tests/cts/net/appForApi23/Android.bp
+++ b/tests/cts/net/appForApi23/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/tests/cts/net/jni/Android.bp b/tests/cts/net/jni/Android.bp
index a421349..fbf4f29 100644
--- a/tests/cts/net/jni/Android.bp
+++ b/tests/cts/net/jni/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/tests/cts/net/native/Android.bp b/tests/cts/net/native/Android.bp
index 153ff51..3f24592 100644
--- a/tests/cts/net/native/Android.bp
+++ b/tests/cts/net/native/Android.bp
@@ -15,6 +15,7 @@
// Build the unit tests.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/tests/cts/net/native/dns/Android.bp b/tests/cts/net/native/dns/Android.bp
index da4fe28..8e24fba 100644
--- a/tests/cts/net/native/dns/Android.bp
+++ b/tests/cts/net/native/dns/Android.bp
@@ -1,4 +1,5 @@
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -49,5 +50,7 @@
"general-tests",
"mts-dnsresolver",
"mts-networking",
+ "mcts-dnsresolver",
+ "mcts-networking",
],
}
diff --git a/tests/cts/net/src/android/net/cts/BatteryStatsManagerTest.java b/tests/cts/net/src/android/net/cts/BatteryStatsManagerTest.java
index 466514c..3e5d0ba 100644
--- a/tests/cts/net/src/android/net/cts/BatteryStatsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/BatteryStatsManagerTest.java
@@ -199,7 +199,8 @@
Log.d(TAG, "Generate traffic on wifi network.");
generateNetworkTraffic(wifiNetwork, url);
// Wifi battery stats are updated when wifi on.
- mCtsNetUtils.toggleWifi();
+ mCtsNetUtils.disableWifi();
+ mCtsNetUtils.ensureWifiConnected();
// Check wifi battery stats are updated.
runAsShell(UPDATE_DEVICE_STATS,
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
index e0fe929..ceb48d4 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
@@ -298,17 +298,6 @@
},
android.Manifest.permission.MODIFY_PHONE_STATE);
- // TODO(b/157779832): This should use android.permission.CHANGE_NETWORK_STATE. However, the
- // shell does not have CHANGE_NETWORK_STATE, so use CONNECTIVITY_INTERNAL until the shell
- // permissions are updated.
- runWithShellPermissionIdentity(
- () -> mConnectivityManager.requestNetwork(
- CELLULAR_NETWORK_REQUEST, testNetworkCallback),
- android.Manifest.permission.CONNECTIVITY_INTERNAL);
-
- final Network network = testNetworkCallback.waitForAvailable();
- assertNotNull(network);
-
assertTrue("Didn't receive broadcast for ACTION_CARRIER_CONFIG_CHANGED for subId=" + subId,
carrierConfigReceiver.waitForCarrierConfigChanged());
@@ -324,6 +313,17 @@
Thread.sleep(5_000);
+ // TODO(b/157779832): This should use android.permission.CHANGE_NETWORK_STATE. However, the
+ // shell does not have CHANGE_NETWORK_STATE, so use CONNECTIVITY_INTERNAL until the shell
+ // permissions are updated.
+ runWithShellPermissionIdentity(
+ () -> mConnectivityManager.requestNetwork(
+ CELLULAR_NETWORK_REQUEST, testNetworkCallback),
+ android.Manifest.permission.CONNECTIVITY_INTERNAL);
+
+ final Network network = testNetworkCallback.waitForAvailable();
+ assertNotNull(network);
+
// TODO(b/217559768): Receiving carrier config change and immediately checking carrier
// privileges is racy, as the CP status is updated after receiving the same signal. Move
// the CP check after sleep to temporarily reduce the flakiness. This will soon be fixed
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 544f300..cdf8340 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -802,7 +802,9 @@
assertNull(redactedNormal.getUids());
assertNull(redactedNormal.getSsid());
assertNull(redactedNormal.getUnderlyingNetworks());
- assertEquals(0, redactedNormal.getSubscriptionIds().size());
+ // TODO: Make subIds public and update to verify the size is 2
+ final int subIdsSize = redactedNormal.getSubscriptionIds().size();
+ assertTrue(subIdsSize == 0 || subIdsSize == 2);
assertEquals(WifiInfo.DEFAULT_MAC_ADDRESS,
((WifiInfo) redactedNormal.getTransportInfo()).getBSSID());
assertEquals(rssi, ((WifiInfo) redactedNormal.getTransportInfo()).getRssi());
@@ -1353,9 +1355,7 @@
public void testToggleWifiConnectivityAction() throws Exception {
assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
- // toggleWifi calls connectToWifi and disconnectFromWifi, which both wait for
- // CONNECTIVITY_ACTION broadcasts.
- mCtsNetUtils.toggleWifi();
+ mCtsNetUtils.reconnectWifiAndWaitForConnectivityAction();
}
/** Verify restricted networks cannot be requested. */
@@ -1554,6 +1554,40 @@
}
}
+ @Test @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @ConnectivityModuleTest
+ public void testSetBackgroundNetworkingShellCommand() {
+ final int testUid = 54352;
+ runShellCommand("cmd connectivity set-background-networking-enabled-for-uid " + testUid
+ + " true");
+ int rule = runAsShell(NETWORK_SETTINGS,
+ () -> mCm.getUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, testUid));
+ assertEquals(rule, FIREWALL_RULE_ALLOW);
+
+ runShellCommand("cmd connectivity set-background-networking-enabled-for-uid " + testUid
+ + " false");
+ rule = runAsShell(NETWORK_SETTINGS,
+ () -> mCm.getUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, testUid));
+ assertEquals(rule, FIREWALL_RULE_DENY);
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @ConnectivityModuleTest
+ public void testGetBackgroundNetworkingShellCommand() {
+ final int testUid = 54312;
+ runAsShell(NETWORK_SETTINGS,
+ () -> mCm.setUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, testUid,
+ FIREWALL_RULE_ALLOW));
+ String output = runShellCommand(
+ "cmd connectivity get-background-networking-enabled-for-uid " + testUid);
+ assertTrue(output.contains("allow"));
+
+ runAsShell(NETWORK_SETTINGS,
+ () -> mCm.setUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, testUid,
+ FIREWALL_RULE_DEFAULT));
+ output = runShellCommand(
+ "cmd connectivity get-background-networking-enabled-for-uid " + testUid);
+ assertTrue(output.contains("deny"));
+ }
+
// TODO: move the following socket keep alive test to dedicated test class.
/**
* Callback used in tcp keepalive offload that allows caller to wait callback fires.
diff --git a/tests/cts/net/src/android/net/cts/DiscoveryRequestTest.kt b/tests/cts/net/src/android/net/cts/DiscoveryRequestTest.kt
new file mode 100644
index 0000000..909a5bc
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/DiscoveryRequestTest.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts
+
+import android.net.Network
+import android.net.nsd.DiscoveryRequest
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.assertParcelingIsLossless
+import com.android.testutils.assertThrows
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** CTS tests for {@link DiscoveryRequest}. */
+@IgnoreUpTo(Build.VERSION_CODES.S_V2)
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@ConnectivityModuleTest
+class DiscoveryRequestTest {
+ @Test
+ fun testParcelingIsLossLess() {
+ val requestWithNullFields =
+ DiscoveryRequest.Builder("_ipps._tcp").build()
+ val requestWithAllFields =
+ DiscoveryRequest.Builder("_ipps._tcp")
+ .setSubtype("_xyz")
+ .setNetwork(Network(1))
+ .build()
+
+ assertParcelingIsLossless(requestWithNullFields)
+ assertParcelingIsLossless(requestWithAllFields)
+ }
+
+ @Test
+ fun testBuilder_success() {
+ val request = DiscoveryRequest.Builder("_ipps._tcp")
+ .setSubtype("_xyz")
+ .setNetwork(Network(1))
+ .build()
+
+ assertEquals("_ipps._tcp", request.serviceType)
+ assertEquals("_xyz", request.subtype)
+ assertEquals(Network(1), request.network)
+ }
+
+ @Test
+ fun testBuilderConstructor_emptyServiceType_throwsIllegalArgument() {
+ assertThrows(IllegalArgumentException::class.java) {
+ DiscoveryRequest.Builder("")
+ }
+ }
+
+ @Test
+ fun testEquality() {
+ val request1 = DiscoveryRequest.Builder("_ipps._tcp").build()
+ val request2 = DiscoveryRequest.Builder("_ipps._tcp").build()
+ val request3 = DiscoveryRequest.Builder("_ipps._tcp")
+ .setSubtype("_xyz")
+ .setNetwork(Network(1))
+ .build()
+ val request4 = DiscoveryRequest.Builder("_ipps._tcp")
+ .setSubtype("_xyz")
+ .setNetwork(Network(1))
+ .build()
+
+ assertEquals(request1, request2)
+ assertEquals(request3, request4)
+ assertNotEquals(request1, request3)
+ assertNotEquals(request2, request4)
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/DnsResolverTest.java b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
index 9ff0f2f..752891f 100644
--- a/tests/cts/net/src/android/net/cts/DnsResolverTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
@@ -23,6 +23,7 @@
import static android.net.DnsResolver.TYPE_AAAA;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
+import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
import static android.system.OsConstants.ETIMEDOUT;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
@@ -59,11 +60,14 @@
import com.android.net.module.util.DnsPacket;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DeviceConfigRule;
import com.android.testutils.DnsResolverModuleTest;
import com.android.testutils.SkipPresubmit;
import org.junit.After;
import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -80,6 +84,8 @@
@AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps")
@RunWith(AndroidJUnit4.class)
public class DnsResolverTest {
+ @ClassRule
+ public static final DeviceConfigRule DEVICE_CONFIG_CLASS_RULE = new DeviceConfigRule();
@Rule
public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
@@ -123,6 +129,20 @@
private TestNetworkCallback mWifiRequestCallback = null;
+ /**
+ * @see BeforeClass
+ */
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ // Use async private DNS resolution to avoid flakes due to races applying the setting
+ DEVICE_CONFIG_CLASS_RULE.setConfig(NAMESPACE_CONNECTIVITY,
+ "networkmonitor_async_privdns_resolution", "1");
+ // Make sure NetworkMonitor is restarted before and after the test so the flag is applied
+ // and cleaned up.
+ maybeToggleWifiAndCell();
+ DEVICE_CONFIG_CLASS_RULE.runAfterNextCleanup(DnsResolverTest::maybeToggleWifiAndCell);
+ }
+
@Before
public void setUp() throws Exception {
mContext = InstrumentationRegistry.getContext();
@@ -144,6 +164,12 @@
}
}
+ private static void maybeToggleWifiAndCell() throws Exception {
+ final CtsNetUtils utils = new CtsNetUtils(InstrumentationRegistry.getContext());
+ utils.reconnectWifiIfSupported();
+ utils.reconnectCellIfSupported();
+ }
+
private static String byteArrayToHexString(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int i = 0; i < bytes.length; ++i) {
diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
index 6b7954a..f6a025a 100644
--- a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
+++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
@@ -648,7 +648,7 @@
testIpv6Only, requiresValidation, testSessionKey , testIkeTunConnParams)));
}
- @Test
+ @Test @IgnoreUpTo(SC_V2)
public void testStartStopVpnProfileV4() throws Exception {
doTestStartStopVpnProfile(false /* testIpv6Only */, false /* requiresValidation */,
false /* testSessionKey */, false /* testIkeTunConnParams */);
@@ -660,7 +660,7 @@
false /* testSessionKey */, false /* testIkeTunConnParams */);
}
- @Test
+ @Test @IgnoreUpTo(SC_V2)
public void testStartStopVpnProfileV6() throws Exception {
doTestStartStopVpnProfile(true /* testIpv6Only */, false /* requiresValidation */,
false /* testSessionKey */, false /* testIkeTunConnParams */);
diff --git a/tests/cts/net/src/android/net/cts/IpSecTransformStateTest.java b/tests/cts/net/src/android/net/cts/IpSecTransformStateTest.java
new file mode 100644
index 0000000..7b42306
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/IpSecTransformStateTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.fail;
+
+import android.net.IpSecTransformState;
+import android.os.Build;
+import android.os.SystemClock;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+@RunWith(DevSdkIgnoreRunner.class)
+public class IpSecTransformStateTest {
+ @Rule public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
+ private static final long TIMESTAMP_MILLIS = 1000L;
+ private static final long HIGHEST_SEQ_NUMBER_TX = 10000L;
+ private static final long HIGHEST_SEQ_NUMBER_RX = 20000L;
+ private static final long PACKET_COUNT = 9000L;
+ private static final long BYTE_COUNT = 900000L;
+
+ private static final int REPLAY_BITMAP_LEN_BYTE = 512;
+ private static final byte[] REPLAY_BITMAP_NO_PACKETS = new byte[REPLAY_BITMAP_LEN_BYTE];
+ private static final byte[] REPLAY_BITMAP_ALL_RECEIVED = new byte[REPLAY_BITMAP_LEN_BYTE];
+
+ static {
+ for (int i = 0; i < REPLAY_BITMAP_ALL_RECEIVED.length; i++) {
+ REPLAY_BITMAP_ALL_RECEIVED[i] = (byte) 0xff;
+ }
+ }
+
+ @Test
+ public void testBuildAndGet() {
+ final IpSecTransformState state =
+ new IpSecTransformState.Builder()
+ .setTimestampMillis(TIMESTAMP_MILLIS)
+ .setTxHighestSequenceNumber(HIGHEST_SEQ_NUMBER_TX)
+ .setRxHighestSequenceNumber(HIGHEST_SEQ_NUMBER_RX)
+ .setPacketCount(PACKET_COUNT)
+ .setByteCount(BYTE_COUNT)
+ .setReplayBitmap(REPLAY_BITMAP_ALL_RECEIVED)
+ .build();
+
+ assertEquals(TIMESTAMP_MILLIS, state.getTimestampMillis());
+ assertEquals(HIGHEST_SEQ_NUMBER_TX, state.getTxHighestSequenceNumber());
+ assertEquals(HIGHEST_SEQ_NUMBER_RX, state.getRxHighestSequenceNumber());
+ assertEquals(PACKET_COUNT, state.getPacketCount());
+ assertEquals(BYTE_COUNT, state.getByteCount());
+ assertArrayEquals(REPLAY_BITMAP_ALL_RECEIVED, state.getReplayBitmap());
+ }
+
+ @Test
+ public void testSelfGeneratedTimestampMillis() {
+ final long elapsedRealtimeBefore = SystemClock.elapsedRealtime();
+
+ final IpSecTransformState state =
+ new IpSecTransformState.Builder().setReplayBitmap(REPLAY_BITMAP_NO_PACKETS).build();
+
+ final long elapsedRealtimeAfter = SystemClock.elapsedRealtime();
+
+ // Verify elapsedRealtimeBefore <= state.getTimestampMillis() <= elapsedRealtimeAfter
+ assertFalse(elapsedRealtimeBefore > state.getTimestampMillis());
+ assertFalse(elapsedRealtimeAfter < state.getTimestampMillis());
+ }
+
+ @Test
+ public void testBuildWithoutReplayBitmap() throws Exception {
+ try {
+ new IpSecTransformState.Builder().build();
+ fail("Expected expcetion if replay bitmap is not set");
+ } catch (NullPointerException expected) {
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt b/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt
index eef3f87..5ba6c4c 100644
--- a/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt
+++ b/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt
@@ -23,11 +23,15 @@
import com.android.net.module.util.ArrayTrackRecord
import com.android.net.module.util.DnsPacket
import com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN
+import com.android.net.module.util.NetworkStackConstants.IPV6_DST_ADDR_OFFSET
import com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN
import com.android.net.module.util.NetworkStackConstants.UDP_HEADER_LEN
import com.android.net.module.util.TrackRecord
import com.android.testutils.IPv6UdpFilter
import com.android.testutils.TapPacketReader
+import java.net.Inet6Address
+import java.net.InetAddress
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
@@ -236,19 +240,28 @@
private fun getMdnsPayload(packet: ByteArray) = packet.copyOfRange(
ETHER_HEADER_LEN + IPV6_HEADER_LEN + UDP_HEADER_LEN, packet.size)
+private fun getDstAddr(packet: ByteArray): Inet6Address {
+ val v6AddrPos = ETHER_HEADER_LEN + IPV6_DST_ADDR_OFFSET
+ return Inet6Address.getByAddress(packet.copyOfRange(v6AddrPos, v6AddrPos + IPV6_ADDR_LEN))
+ as Inet6Address
+}
+
fun TapPacketReader.pollForMdnsPacket(
timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS,
predicate: (TestDnsPacket) -> Boolean
): TestDnsPacket? {
val mdnsProbeFilter = IPv6UdpFilter(srcPort = MDNS_PORT, dstPort = MDNS_PORT).and {
+ val dst = getDstAddr(it)
val mdnsPayload = getMdnsPayload(it)
try {
- predicate(TestDnsPacket(mdnsPayload))
+ predicate(TestDnsPacket(mdnsPayload, dst))
} catch (e: DnsPacket.ParseException) {
false
}
}
- return poll(timeoutMs, mdnsProbeFilter)?.let { TestDnsPacket(getMdnsPayload(it)) }
+ return poll(timeoutMs, mdnsProbeFilter)?.let {
+ TestDnsPacket(getMdnsPayload(it), getDstAddr(it))
+ }
}
fun TapPacketReader.pollForProbe(
@@ -281,7 +294,7 @@
it.isReplyFor("$serviceName.$serviceType.local")
}
-class TestDnsPacket(data: ByteArray) : DnsPacket(data) {
+class TestDnsPacket(data: ByteArray, val dstAddr: InetAddress) : DnsPacket(data) {
val header: DnsHeader
get() = mHeader
val records: Array<List<DnsRecord>>
@@ -290,9 +303,10 @@
it.dName == name && it.nsType == DnsResolver.TYPE_ANY
}
- fun isReplyFor(name: String): Boolean = mRecords[ANSECTION].any {
- it.dName == name && it.nsType == DnsResolver.TYPE_SRV
- }
+ fun isReplyFor(name: String, type: Int = DnsResolver.TYPE_SRV): Boolean =
+ mRecords[ANSECTION].any {
+ it.dName == name && it.nsType == type
+ }
fun isQueryFor(name: String, vararg requiredTypes: Int): Boolean = requiredTypes.all { type ->
mRecords[QDSECTION].any {
diff --git a/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
index 17a9ca2..7ab73c2 100644
--- a/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
+++ b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
@@ -16,7 +16,17 @@
package android.net.cts;
+import static android.content.pm.PackageManager.FEATURE_TELEPHONY;
+import static android.content.pm.PackageManager.FEATURE_WIFI;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
+
+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.fail;
import android.content.ContentResolver;
import android.content.Context;
@@ -28,9 +38,25 @@
import android.platform.test.annotations.AppModeFull;
import android.system.ErrnoException;
import android.system.OsConstants;
-import android.test.AndroidTestCase;
+import android.util.ArraySet;
-public class MultinetworkApiTest extends AndroidTestCase {
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.testutils.DeviceConfigRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+public class MultinetworkApiTest {
+ @Rule
+ public final DeviceConfigRule mDeviceConfigRule = new DeviceConfigRule();
static {
System.loadLibrary("nativemultinetwork_jni");
@@ -56,24 +82,27 @@
private ContentResolver mCR;
private ConnectivityManager mCM;
private CtsNetUtils mCtsNetUtils;
- private String mOldMode;
- private String mOldDnsSpecifier;
+ private Context mContext;
+ private Network mRequestedCellNetwork;
- @Override
- protected void setUp() throws Exception {
- super.setUp();
- mCM = (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
- mCR = getContext().getContentResolver();
- mCtsNetUtils = new CtsNetUtils(getContext());
+ @Before
+ public void setUp() throws Exception {
+ mContext = InstrumentationRegistry.getInstrumentation().getContext();
+ mCM = mContext.getSystemService(ConnectivityManager.class);
+ mCR = mContext.getContentResolver();
+ mCtsNetUtils = new CtsNetUtils(mContext);
}
- @Override
- protected void tearDown() throws Exception {
- super.tearDown();
+ @After
+ public void tearDown() {
+ if (mCtsNetUtils.cellConnectAttempted()) {
+ mCtsNetUtils.disconnectFromCell();
+ }
}
- public void testGetaddrinfo() throws ErrnoException {
- for (Network network : mCtsNetUtils.getTestableNetworks()) {
+ @Test
+ public void testGetaddrinfo() throws Exception {
+ for (Network network : getTestableNetworks()) {
int errno = runGetaddrinfoCheck(network.getNetworkHandle());
if (errno != 0) {
throw new ErrnoException(
@@ -82,13 +111,14 @@
}
}
+ @Test
@AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
- public void testSetprocnetwork() throws ErrnoException {
+ public void testSetprocnetwork() throws Exception {
// Hopefully no prior test in this process space has set a default network.
assertNull(mCM.getProcessDefaultNetwork());
assertEquals(0, NetworkUtils.getBoundNetworkForProcess());
- for (Network network : mCtsNetUtils.getTestableNetworks()) {
+ for (Network network : getTestableNetworks()) {
mCM.setProcessDefaultNetwork(null);
assertNull(mCM.getProcessDefaultNetwork());
@@ -107,7 +137,7 @@
mCM.setProcessDefaultNetwork(null);
}
- for (Network network : mCtsNetUtils.getTestableNetworks()) {
+ for (Network network : getTestableNetworks()) {
NetworkUtils.bindProcessToNetwork(0);
assertNull(mCM.getBoundNetworkForProcess());
@@ -125,9 +155,10 @@
}
}
+ @Test
@AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
- public void testSetsocknetwork() throws ErrnoException {
- for (Network network : mCtsNetUtils.getTestableNetworks()) {
+ public void testSetsocknetwork() throws Exception {
+ for (Network network : getTestableNetworks()) {
int errno = runSetsocknetwork(network.getNetworkHandle());
if (errno != 0) {
throw new ErrnoException(
@@ -136,8 +167,9 @@
}
}
- public void testNativeDatagramTransmission() throws ErrnoException {
- for (Network network : mCtsNetUtils.getTestableNetworks()) {
+ @Test
+ public void testNativeDatagramTransmission() throws Exception {
+ for (Network network : getTestableNetworks()) {
int errno = runDatagramCheck(network.getNetworkHandle());
if (errno != 0) {
throw new ErrnoException(
@@ -146,7 +178,8 @@
}
}
- public void testNoSuchNetwork() {
+ @Test
+ public void testNoSuchNetwork() throws Exception {
final Network eNoNet = new Network(54321);
assertNull(mCM.getNetworkInfo(eNoNet));
@@ -158,9 +191,10 @@
// assertEquals(-OsConstants.ENONET, runGetaddrinfoCheck(eNoNetHandle));
}
- public void testNetworkHandle() {
+ @Test
+ public void testNetworkHandle() throws Exception {
// Test Network -> NetworkHandle -> Network results in the same Network.
- for (Network network : mCtsNetUtils.getTestableNetworks()) {
+ for (Network network : getTestableNetworks()) {
long networkHandle = network.getNetworkHandle();
Network newNetwork = Network.fromNetworkHandle(networkHandle);
assertEquals(newNetwork, network);
@@ -181,10 +215,9 @@
} catch (IllegalArgumentException e) {}
}
+ @Test
public void testResNApi() throws Exception {
- final Network[] testNetworks = mCtsNetUtils.getTestableNetworks();
-
- for (Network network : testNetworks) {
+ for (Network network : getTestableNetworks()) {
// Throws AssertionError directly in jni function if test fail.
runResNqueryCheck(network.getNetworkHandle());
runResNsendCheck(network.getNetworkHandle());
@@ -201,14 +234,26 @@
}
}
+ @Test
@AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps")
- public void testResNApiNXDomainPrivateDns() throws InterruptedException {
+ public void testResNApiNXDomainPrivateDns() throws Exception {
+ // Use async private DNS resolution to avoid flakes due to races applying the setting
+ mDeviceConfigRule.setConfig(NAMESPACE_CONNECTIVITY,
+ "networkmonitor_async_privdns_resolution", "1");
+ mCtsNetUtils.reconnectWifiIfSupported();
+ mCtsNetUtils.reconnectCellIfSupported();
+
mCtsNetUtils.storePrivateDnsSetting();
+
+ mDeviceConfigRule.runAfterNextCleanup(() -> {
+ mCtsNetUtils.reconnectWifiIfSupported();
+ mCtsNetUtils.reconnectCellIfSupported();
+ });
// Enable private DNS strict mode and set server to dns.google before doing NxDomain test.
// b/144521720
try {
mCtsNetUtils.setPrivateDnsStrictMode(GOOGLE_PRIVATE_DNS_SERVER);
- for (Network network : mCtsNetUtils.getTestableNetworks()) {
+ for (Network network : getTestableNetworks()) {
// Wait for private DNS setting to propagate.
mCtsNetUtils.awaitPrivateDnsSetting("NxDomain test wait private DNS setting timeout",
network, GOOGLE_PRIVATE_DNS_SERVER, true);
@@ -218,4 +263,44 @@
mCtsNetUtils.restorePrivateDnsSetting();
}
}
+
+ /**
+ * Get all testable Networks with internet capability.
+ */
+ private Set<Network> getTestableNetworks() throws InterruptedException {
+ // Obtain cell and Wi-Fi through CtsNetUtils (which uses NetworkCallbacks), as they may have
+ // just been reconnected by the test using NetworkCallbacks, so synchronous calls may not
+ // yet return them (synchronous calls and callbacks should not be mixed for a given
+ // Network).
+ final Set<Network> testableNetworks = new ArraySet<>();
+ if (mContext.getPackageManager().hasSystemFeature(FEATURE_TELEPHONY)) {
+ if (!mCtsNetUtils.cellConnectAttempted()) {
+ mRequestedCellNetwork = mCtsNetUtils.connectToCell();
+ }
+ assertNotNull("Cell network requested but not obtained", mRequestedCellNetwork);
+ testableNetworks.add(mRequestedCellNetwork);
+ }
+
+ if (mContext.getPackageManager().hasSystemFeature(FEATURE_WIFI)) {
+ testableNetworks.add(mCtsNetUtils.ensureWifiConnected());
+ }
+
+ // Obtain other networks through the synchronous API, if any.
+ for (Network network : mCtsNetUtils.getTestableNetworks()) {
+ final NetworkCapabilities nc = mCM.getNetworkCapabilities(network);
+ if (nc != null
+ && !nc.hasTransport(TRANSPORT_WIFI)
+ && !nc.hasTransport(TRANSPORT_CELLULAR)) {
+ testableNetworks.add(network);
+ }
+ }
+
+ // In practice this should not happen as getTestableNetworks throws if there is no network
+ // at all.
+ assertFalse("This device does not support WiFi nor cell data, and does not have any other "
+ + "network connected. This test requires at least one internet-providing "
+ + "network.",
+ testableNetworks.isEmpty());
+ return testableNetworks;
+ }
}
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index fe2f813..beb9274 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -21,6 +21,7 @@
import android.app.Instrumentation
import android.content.Context
import android.content.pm.PackageManager
+import android.content.pm.PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION
import android.net.ConnectivityManager
import android.net.EthernetNetworkSpecifier
import android.net.INetworkAgent
@@ -70,6 +71,7 @@
import android.net.TelephonyNetworkSpecifier
import android.net.TestNetworkInterface
import android.net.TestNetworkManager
+import android.net.TransportInfo
import android.net.Uri
import android.net.VpnManager
import android.net.VpnTransportInfo
@@ -150,6 +152,7 @@
import kotlin.test.assertTrue
import kotlin.test.fail
import org.junit.After
+import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -574,27 +577,13 @@
}
private fun doTestAllowedUids(
- subId: Int,
- transport: Int,
- uid: Int,
- expectUidsPresent: Boolean
- ) {
- doTestAllowedUids(subId, intArrayOf(transport), uid, expectUidsPresent)
- }
-
- private fun doTestAllowedUids(
- subId: Int,
transports: IntArray,
uid: Int,
- expectUidsPresent: Boolean
+ expectUidsPresent: Boolean,
+ specifier: NetworkSpecifier?,
+ transportInfo: TransportInfo?
) {
val callback = TestableNetworkCallback(DEFAULT_TIMEOUT_MS)
- val specifier = when {
- transports.size != 1 -> null
- TRANSPORT_ETHERNET in transports -> EthernetNetworkSpecifier("testInterface")
- TRANSPORT_CELLULAR in transports -> TelephonyNetworkSpecifier(subId)
- else -> null
- }
val agent = createNetworkAgent(initialNc = NetworkCapabilities.Builder().run {
addTransportType(TRANSPORT_TEST)
transports.forEach { addTransportType(it) }
@@ -602,10 +591,7 @@
addCapability(NET_CAPABILITY_NOT_SUSPENDED)
removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
setNetworkSpecifier(specifier)
- if (TRANSPORT_WIFI in transports && SdkLevel.isAtLeastV()) {
- // setSubscriptionId only exists in V+
- setTransportInfo(WifiInfo.Builder().setSubscriptionId(subId).build())
- }
+ setTransportInfo(transportInfo)
setAllowedUids(setOf(uid))
setOwnerUid(Process.myUid())
setAdministratorUids(intArrayOf(Process.myUid()))
@@ -626,9 +612,49 @@
}
}
agent.unregister()
+ callback.eventuallyExpect<Lost> { it.network == agent.network }
// callback will be unregistered in tearDown()
}
+ private fun doTestAllowedUids(
+ transport: Int,
+ uid: Int,
+ expectUidsPresent: Boolean
+ ) {
+ doTestAllowedUids(intArrayOf(transport), uid, expectUidsPresent,
+ specifier = null, transportInfo = null)
+ }
+
+ private fun doTestAllowedUidsWithSubId(
+ subId: Int,
+ transport: Int,
+ uid: Int,
+ expectUidsPresent: Boolean
+ ) {
+ doTestAllowedUidsWithSubId(subId, intArrayOf(transport), uid, expectUidsPresent)
+ }
+
+ private fun doTestAllowedUidsWithSubId(
+ subId: Int,
+ transports: IntArray,
+ uid: Int,
+ expectUidsPresent: Boolean
+ ) {
+ val specifier = when {
+ transports.size != 1 -> null
+ TRANSPORT_ETHERNET in transports -> EthernetNetworkSpecifier("testInterface")
+ TRANSPORT_CELLULAR in transports -> TelephonyNetworkSpecifier(subId)
+ else -> null
+ }
+ val transportInfo = if (TRANSPORT_WIFI in transports && SdkLevel.isAtLeastV()) {
+ // setSubscriptionId only exists in V+
+ WifiInfo.Builder().setSubscriptionId(subId).build()
+ } else {
+ null
+ }
+ doTestAllowedUids(transports, uid, expectUidsPresent, specifier, transportInfo)
+ }
+
private fun setHoldCarrierPrivilege(hold: Boolean, subId: Int) {
fun getCertHash(): String {
val pkgInfo = realContext.packageManager.getPackageInfo(realContext.opPackageName,
@@ -722,6 +748,19 @@
@Test
@IgnoreUpTo(Build.VERSION_CODES.S)
fun testAllowedUids() {
+ doTestAllowedUids(TRANSPORT_CELLULAR, Process.myUid(), expectUidsPresent = false)
+ doTestAllowedUids(TRANSPORT_WIFI, Process.myUid(), expectUidsPresent = false)
+ doTestAllowedUids(TRANSPORT_BLUETOOTH, Process.myUid(), expectUidsPresent = false)
+
+ // TODO(b/315136340): Allow ownerUid to see allowedUids and add cases that expect uids
+ // present
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S)
+ fun testAllowedUids_WithCarrierServicePackage() {
+ assumeTrue(realContext.packageManager.hasSystemFeature(FEATURE_TELEPHONY_SUBSCRIPTION))
+
// Use a different package than this one to make sure that a package that doesn't hold
// carrier service permission can be set as an allowed UID.
val servicePackage = "android.net.cts.carrierservicepackage"
@@ -734,12 +773,17 @@
val tm = realContext.getSystemService(TelephonyManager::class.java)!!
val defaultSubId = SubscriptionManager.getDefaultSubscriptionId()
+ assertTrue(defaultSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID,
+ "getDefaultSubscriptionId returns INVALID_SUBSCRIPTION_ID")
tryTest {
// This process is not the carrier service UID, so allowedUids should be ignored in all
// the following cases.
- doTestAllowedUids(defaultSubId, TRANSPORT_CELLULAR, uid, expectUidsPresent = false)
- doTestAllowedUids(defaultSubId, TRANSPORT_WIFI, uid, expectUidsPresent = false)
- doTestAllowedUids(defaultSubId, TRANSPORT_BLUETOOTH, uid, expectUidsPresent = false)
+ doTestAllowedUidsWithSubId(defaultSubId, TRANSPORT_CELLULAR, uid,
+ expectUidsPresent = false)
+ doTestAllowedUidsWithSubId(defaultSubId, TRANSPORT_WIFI, uid,
+ expectUidsPresent = false)
+ doTestAllowedUidsWithSubId(defaultSubId, TRANSPORT_BLUETOOTH, uid,
+ expectUidsPresent = false)
// The tools to set the carrier service package override do not exist before U,
// so there is no way to test the rest of this test on < U.
@@ -782,9 +826,10 @@
// TODO(b/315136340): Allow ownerUid to see allowedUids and enable below test case
// doTestAllowedUids(defaultSubId, TRANSPORT_WIFI, uid, expectUidsPresent = true)
}
- doTestAllowedUids(defaultSubId, TRANSPORT_BLUETOOTH, uid, expectUidsPresent = false)
- doTestAllowedUids(defaultSubId, intArrayOf(TRANSPORT_CELLULAR, TRANSPORT_WIFI), uid,
+ doTestAllowedUidsWithSubId(defaultSubId, TRANSPORT_BLUETOOTH, uid,
expectUidsPresent = false)
+ doTestAllowedUidsWithSubId(defaultSubId, intArrayOf(TRANSPORT_CELLULAR, TRANSPORT_WIFI),
+ uid, expectUidsPresent = false)
} cleanupStep {
if (SdkLevel.isAtLeastU()) setCarrierServicePackageOverride(defaultSubId, null)
} cleanup {
diff --git a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
index 594f3fb..6ec4e62 100644
--- a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
@@ -32,6 +32,8 @@
import static com.android.testutils.DevSdkIgnoreRuleKt.VANILLA_ICE_CREAM;
+import static com.google.common.truth.Truth.assertThat;
+
import static junit.framework.Assert.fail;
import static org.junit.Assert.assertArrayEquals;
@@ -62,6 +64,7 @@
import com.android.networkstack.apishim.NetworkRequestShimImpl;
import com.android.networkstack.apishim.common.NetworkRequestShim;
import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
+import com.android.testutils.ConnectivityModuleTest;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
@@ -72,6 +75,7 @@
import java.util.Set;
@RunWith(AndroidJUnit4.class)
+@ConnectivityModuleTest
public class NetworkRequestTest {
@Rule
public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
@@ -173,6 +177,20 @@
}
@Test
+ @IgnoreUpTo(Build.VERSION_CODES.S)
+ public void testSubscriptionIds() {
+ int[] subIds = {1, 2};
+ assertTrue(
+ new NetworkRequest.Builder().build()
+ .getSubscriptionIds().isEmpty());
+ assertThat(new NetworkRequest.Builder()
+ .setSubscriptionIds(Set.of(subIds[0], subIds[1]))
+ .build()
+ .getSubscriptionIds())
+ .containsExactly(subIds[0], subIds[1]);
+ }
+
+ @Test
@IgnoreUpTo(Build.VERSION_CODES.Q)
public void testRequestorPackageName() {
assertNull(new NetworkRequest.Builder().build().getRequestorPackageName());
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
index f374181..1b1f367 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
@@ -62,7 +62,7 @@
@Test
fun testMdnsDiscoveryCanSendPacketOnLocalOnlyDownstreamTetheringInterface() {
- assumeFalse(isInterfaceForTetheringAvailable)
+ assumeFalse(isInterfaceForTetheringAvailable())
var downstreamIface: TestNetworkInterface? = null
var tetheringEventCallback: MyTetheringEventCallback? = null
@@ -104,7 +104,7 @@
@Test
fun testMdnsDiscoveryWorkOnTetheringInterface() {
- assumeFalse(isInterfaceForTetheringAvailable)
+ assumeFalse(isInterfaceForTetheringAvailable())
setIncludeTestInterfaces(true)
var downstreamIface: TestNetworkInterface? = null
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index e1ea2b9..ce2c2c1 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -43,6 +43,7 @@
import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped
import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.ServiceFound
import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.ServiceLost
+import android.net.cts.NsdRegistrationRecord.RegistrationEvent.RegistrationFailed
import android.net.cts.NsdRegistrationRecord.RegistrationEvent.ServiceRegistered
import android.net.cts.NsdRegistrationRecord.RegistrationEvent.ServiceUnregistered
import android.net.cts.NsdResolveRecord.ResolveEvent.ResolutionStopped
@@ -52,6 +53,7 @@
import android.net.cts.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdatedLost
import android.net.cts.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.UnregisterCallbackSucceeded
import android.net.cts.util.CtsNetUtils
+import android.net.nsd.DiscoveryRequest
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import android.net.nsd.OffloadEngine
@@ -60,6 +62,7 @@
import android.os.Handler
import android.os.HandlerThread
import android.platform.test.annotations.AppModeFull
+import android.provider.DeviceConfig.NAMESPACE_TETHERING
import android.system.ErrnoException
import android.system.Os
import android.system.OsConstants.AF_INET6
@@ -68,6 +71,7 @@
import android.system.OsConstants.ETH_P_IPV6
import android.system.OsConstants.IPPROTO_IPV6
import android.system.OsConstants.IPPROTO_UDP
+import android.system.OsConstants.RT_SCOPE_LINK
import android.system.OsConstants.SOCK_DGRAM
import android.util.Log
import androidx.test.filters.SmallTest
@@ -77,12 +81,14 @@
import com.android.modules.utils.build.SdkLevel.isAtLeastU
import com.android.net.module.util.DnsPacket
import com.android.net.module.util.HexDump
+import com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN
import com.android.net.module.util.PacketBuilder
import com.android.testutils.ConnectivityModuleTest
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.DeviceConfigRule
+import com.android.testutils.NSResponder
import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
import com.android.testutils.TapPacketReader
@@ -132,6 +138,7 @@
private const val TEST_PORT = 12345
private const val MDNS_PORT = 5353.toShort()
private val multicastIpv6Addr = parseNumericAddress("ff02::fb") as Inet6Address
+private val testSrcAddr = parseNumericAddress("2001:db8::123") as Inet6Address
@AppModeFull(reason = "Socket cannot bind in instant app mode")
@RunWith(DevSdkIgnoreRunner::class)
@@ -143,6 +150,9 @@
@get:Rule
val ignoreRule = DevSdkIgnoreRule()
+ @get:Rule
+ val deviceConfigRule = DeviceConfigRule()
+
private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
private val nsdManager by lazy {
context.getSystemService(NsdManager::class.java) ?: fail("Could not get NsdManager service")
@@ -151,7 +161,11 @@
private val cm by lazy { context.getSystemService(ConnectivityManager::class.java)!! }
private val serviceName = "NsdTest%09d".format(Random().nextInt(1_000_000_000))
private val serviceName2 = "NsdTest%09d".format(Random().nextInt(1_000_000_000))
+ private val serviceName3 = "NsdTest%09d".format(Random().nextInt(1_000_000_000))
private val serviceType = "_nmt%09d._tcp".format(Random().nextInt(1_000_000_000))
+ private val serviceType2 = "_nmt%09d._tcp".format(Random().nextInt(1_000_000_000))
+ private val customHostname = "NsdTestHost%09d".format(Random().nextInt(1_000_000_000))
+ private val customHostname2 = "NsdTestHost%09d".format(Random().nextInt(1_000_000_000))
private val handlerThread = HandlerThread(NsdManagerTest::class.java.simpleName)
private val ctsNetUtils by lazy{ CtsNetUtils(context) }
@@ -670,6 +684,48 @@
}
}
+ @Test
+ fun testRegisterService_twoServicesWithSameNameButDifferentTypes_registeredAndDiscoverable() {
+ val si1 = NsdServiceInfo().also {
+ it.network = testNetwork1.network
+ it.serviceName = serviceName
+ it.serviceType = serviceType
+ it.port = TEST_PORT
+ }
+ val si2 = NsdServiceInfo().also {
+ it.network = testNetwork1.network
+ it.serviceName = serviceName
+ it.serviceType = serviceType2
+ it.port = TEST_PORT + 1
+ }
+ val registrationRecord1 = NsdRegistrationRecord()
+ val registrationRecord2 = NsdRegistrationRecord()
+ val discoveryRecord1 = NsdDiscoveryRecord()
+ val discoveryRecord2 = NsdDiscoveryRecord()
+ tryTest {
+ registerService(registrationRecord1, si1)
+ registerService(registrationRecord2, si2)
+
+ nsdManager.discoverServices(serviceType,
+ NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, discoveryRecord1)
+ nsdManager.discoverServices(serviceType2,
+ NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, discoveryRecord2)
+
+ discoveryRecord1.waitForServiceDiscovered(serviceName, serviceType,
+ testNetwork1.network)
+ discoveryRecord2.waitForServiceDiscovered(serviceName, serviceType2,
+ testNetwork1.network)
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(discoveryRecord1)
+ nsdManager.stopServiceDiscovery(discoveryRecord2)
+ } cleanup {
+ nsdManager.unregisterService(registrationRecord1)
+ nsdManager.unregisterService(registrationRecord2)
+ }
+ }
+
fun checkOffloadServiceInfo(serviceInfo: OffloadServiceInfo, si: NsdServiceInfo) {
val expectedServiceType = si.serviceType.split(",")[0]
assertEquals(si.serviceName, serviceInfo.key.serviceName)
@@ -677,11 +733,12 @@
assertEquals(listOf("_subtype"), serviceInfo.subtypes)
assertTrue(serviceInfo.hostname.startsWith("Android_"))
assertTrue(serviceInfo.hostname.endsWith("local"))
- assertEquals(0, serviceInfo.priority)
+ // Test service types should not be in the priority list
+ assertEquals(Integer.MAX_VALUE, serviceInfo.priority)
assertEquals(OffloadEngine.OFFLOAD_TYPE_REPLY.toLong(), serviceInfo.offloadType)
val offloadPayload = serviceInfo.offloadPayload
assertNotNull(offloadPayload)
- val dnsPacket = TestDnsPacket(offloadPayload)
+ val dnsPacket = TestDnsPacket(offloadPayload, dstAddr = multicastIpv6Addr)
assertEquals(0x8400, dnsPacket.header.flags)
assertEquals(0, dnsPacket.records[DnsPacket.QDSECTION].size)
assertTrue(dnsPacket.records[DnsPacket.ANSECTION].size >= 5)
@@ -992,6 +1049,199 @@
}
@Test
+ fun testSubtypeAdvertisingAndDiscovery_withSetSubtypesApi() {
+ runSubtypeAdvertisingAndDiscoveryTest(useLegacySpecifier = false)
+ }
+
+ @Test
+ fun testSubtypeAdvertisingAndDiscovery_withSetSubtypesApiAndLegacySpecifier() {
+ runSubtypeAdvertisingAndDiscoveryTest(useLegacySpecifier = true)
+ }
+
+ private fun runSubtypeAdvertisingAndDiscoveryTest(useLegacySpecifier: Boolean) {
+ val si = makeTestServiceInfo(network = testNetwork1.network)
+ if (useLegacySpecifier) {
+ si.subtypes = setOf("_subtype1")
+
+ // Test "_type._tcp.local,_subtype" syntax with the registration
+ si.serviceType = si.serviceType + ",_subtype2"
+ } else {
+ si.subtypes = setOf("_subtype1", "_subtype2")
+ }
+
+ val registrationRecord = NsdRegistrationRecord()
+
+ val baseTypeDiscoveryRecord = NsdDiscoveryRecord()
+ val subtype1DiscoveryRecord = NsdDiscoveryRecord()
+ val subtype2DiscoveryRecord = NsdDiscoveryRecord()
+ val otherSubtypeDiscoveryRecord = NsdDiscoveryRecord()
+ tryTest {
+ registerService(registrationRecord, si)
+
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, baseTypeDiscoveryRecord)
+
+ // Test "<subtype>._type._tcp.local" syntax with discovery. Note this is not
+ // "<subtype>._sub._type._tcp.local".
+ nsdManager.discoverServices("_othersubtype.$serviceType",
+ NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, otherSubtypeDiscoveryRecord)
+ nsdManager.discoverServices("_subtype1.$serviceType",
+ NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, subtype1DiscoveryRecord)
+
+ nsdManager.discoverServices(
+ DiscoveryRequest.Builder(serviceType).setSubtype("_subtype2")
+ .setNetwork(testNetwork1.network).build(),
+ Executor { it.run() }, subtype2DiscoveryRecord)
+
+ val info1 = subtype1DiscoveryRecord.waitForServiceDiscovered(
+ serviceName, serviceType, testNetwork1.network)
+ assertTrue(info1.subtypes.contains("_subtype1"))
+ val info2 = subtype2DiscoveryRecord.waitForServiceDiscovered(
+ serviceName, serviceType, testNetwork1.network)
+ assertTrue(info2.subtypes.contains("_subtype2"))
+ baseTypeDiscoveryRecord.waitForServiceDiscovered(
+ serviceName, serviceType, testNetwork1.network)
+ otherSubtypeDiscoveryRecord.expectCallback<DiscoveryStarted>()
+ // The subtype callback was registered later but called, no need for an extra delay
+ otherSubtypeDiscoveryRecord.assertNoCallback(timeoutMs = 0)
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(baseTypeDiscoveryRecord)
+ nsdManager.stopServiceDiscovery(subtype1DiscoveryRecord)
+ nsdManager.stopServiceDiscovery(subtype2DiscoveryRecord)
+ nsdManager.stopServiceDiscovery(otherSubtypeDiscoveryRecord)
+
+ baseTypeDiscoveryRecord.expectCallback<DiscoveryStopped>()
+ subtype1DiscoveryRecord.expectCallback<DiscoveryStopped>()
+ subtype2DiscoveryRecord.expectCallback<DiscoveryStopped>()
+ otherSubtypeDiscoveryRecord.expectCallback<DiscoveryStopped>()
+ } cleanup {
+ nsdManager.unregisterService(registrationRecord)
+ }
+ }
+
+ @Test
+ fun testMultipleSubTypeAdvertisingAndDiscovery_withUpdate() {
+ val si1 = makeTestServiceInfo(network = testNetwork1.network).apply {
+ serviceType += ",_subtype1"
+ }
+ val si2 = makeTestServiceInfo(network = testNetwork1.network).apply {
+ serviceType += ",_subtype2"
+ }
+ val registrationRecord = NsdRegistrationRecord()
+ val subtype3DiscoveryRecord = NsdDiscoveryRecord()
+ tryTest {
+ registerService(registrationRecord, si1)
+ updateService(registrationRecord, si2)
+ nsdManager.discoverServices("_subtype2.$serviceType",
+ NsdManager.PROTOCOL_DNS_SD, testNetwork1.network,
+ { it.run() }, subtype3DiscoveryRecord)
+ subtype3DiscoveryRecord.waitForServiceDiscovered(serviceName,
+ serviceType, testNetwork1.network)
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(subtype3DiscoveryRecord)
+ subtype3DiscoveryRecord.expectCallback<DiscoveryStopped>()
+ } cleanup {
+ nsdManager.unregisterService(registrationRecord)
+ }
+ }
+
+ @Test
+ fun testSubtypeAdvertisingAndDiscovery_nonAlphanumericalSubtypes() {
+ // All non-alphanumerical characters between 0x20 and 0x7e, with a leading underscore
+ val nonAlphanumSubtype = "_ !\"#\$%&'()*+-/:;<=>?@[\\]^_`{|}"
+ // Test both legacy syntax and the subtypes setter, on different networks
+ val si1 = makeTestServiceInfo(network = testNetwork1.network).apply {
+ serviceType = "$serviceType,_test1,$nonAlphanumSubtype"
+ }
+ val si2 = makeTestServiceInfo(network = testNetwork2.network).apply {
+ subtypes = setOf("_test2", nonAlphanumSubtype)
+ }
+
+ val registrationRecord1 = NsdRegistrationRecord()
+ val registrationRecord2 = NsdRegistrationRecord()
+ val subtypeDiscoveryRecord1 = NsdDiscoveryRecord()
+ val subtypeDiscoveryRecord2 = NsdDiscoveryRecord()
+ tryTest {
+ registerService(registrationRecord1, si1)
+ registerService(registrationRecord2, si2)
+ nsdManager.discoverServices(DiscoveryRequest.Builder(serviceType)
+ .setSubtype(nonAlphanumSubtype)
+ .setNetwork(testNetwork1.network)
+ .build(), { it.run() }, subtypeDiscoveryRecord1)
+ nsdManager.discoverServices("$nonAlphanumSubtype.$serviceType",
+ NsdManager.PROTOCOL_DNS_SD, testNetwork2.network, { it.run() },
+ subtypeDiscoveryRecord2)
+
+ val discoveredInfo1 = subtypeDiscoveryRecord1.waitForServiceDiscovered(serviceName,
+ serviceType, testNetwork1.network)
+ val discoveredInfo2 = subtypeDiscoveryRecord2.waitForServiceDiscovered(serviceName,
+ serviceType, testNetwork2.network)
+ assertTrue(discoveredInfo1.subtypes.contains(nonAlphanumSubtype))
+ assertTrue(discoveredInfo2.subtypes.contains(nonAlphanumSubtype))
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(subtypeDiscoveryRecord1)
+ subtypeDiscoveryRecord1.expectCallback<DiscoveryStopped>()
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(subtypeDiscoveryRecord2)
+ subtypeDiscoveryRecord2.expectCallback<DiscoveryStopped>()
+ } cleanup {
+ nsdManager.unregisterService(registrationRecord1)
+ nsdManager.unregisterService(registrationRecord2)
+ }
+ }
+
+ @Test
+ fun testSubtypeDiscovery_typeMatchButSubtypeNotMatch_notDiscovered() {
+ val si1 = makeTestServiceInfo(network = testNetwork1.network).apply {
+ serviceType += ",_subtype1"
+ }
+ val registrationRecord = NsdRegistrationRecord()
+ val subtype2DiscoveryRecord = NsdDiscoveryRecord()
+ tryTest {
+ registerService(registrationRecord, si1)
+ val request = DiscoveryRequest.Builder(serviceType)
+ .setSubtype("_subtype2").setNetwork(testNetwork1.network).build()
+ nsdManager.discoverServices(request, { it.run() }, subtype2DiscoveryRecord)
+ subtype2DiscoveryRecord.expectCallback<DiscoveryStarted>()
+ subtype2DiscoveryRecord.assertNoCallback(timeoutMs = 2000)
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(subtype2DiscoveryRecord)
+ subtype2DiscoveryRecord.expectCallback<DiscoveryStopped>()
+ } cleanup {
+ nsdManager.unregisterService(registrationRecord)
+ }
+ }
+
+ @Test
+ fun testSubtypeAdvertising_tooManySubtypes_returnsFailureBadParameters() {
+ val si = makeTestServiceInfo(network = testNetwork1.network)
+ // Sets 101 subtypes in total
+ val seq = generateSequence(1) { it + 1}
+ si.subtypes = seq.take(100).toList().map {it -> "_subtype" + it}.toSet()
+ si.serviceType = si.serviceType + ",_subtype"
+
+ val record = NsdRegistrationRecord()
+ nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, Executor { it.run() }, record)
+
+ val failedCb = record.expectCallback<RegistrationFailed>(REGISTRATION_TIMEOUT_MS)
+ assertEquals(NsdManager.FAILURE_BAD_PARAMETERS, failedCb.errorCode)
+ }
+
+ @Test
+ fun testSubtypeAdvertising_emptySubtypeLabel_returnsFailureBadParameters() {
+ val si = makeTestServiceInfo(network = testNetwork1.network)
+ si.subtypes = setOf("")
+
+ val record = NsdRegistrationRecord()
+ nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, Executor { it.run() }, record)
+
+ val failedCb = record.expectCallback<RegistrationFailed>(REGISTRATION_TIMEOUT_MS)
+ assertEquals(NsdManager.FAILURE_BAD_PARAMETERS, failedCb.errorCode)
+ }
+
+ @Test
fun testRegisterWithConflictDuringProbing() {
// This test requires shims supporting T+ APIs (NsdServiceInfo.network)
assumeTrue(TestUtils.shouldTestTApis())
@@ -1029,6 +1279,83 @@
}
@Test
+ fun testRegisterServiceWithCustomHostAndAddresses_conflictDuringProbing_hostRenamed() {
+ val si = makeTestServiceInfo(testNetwork1.network).apply {
+ hostname = customHostname
+ hostAddresses = listOf(
+ parseNumericAddress("192.0.2.24"),
+ parseNumericAddress("2001:db8::3"))
+ }
+
+ val packetReader = TapPacketReader(Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ packetReader.startAsyncForTest()
+ handlerThread.waitForIdle(TIMEOUT_MS)
+
+ // Register service on testNetwork1
+ val registrationRecord = NsdRegistrationRecord()
+ nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, { it.run() },
+ registrationRecord)
+
+ tryTest {
+ assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
+ "Did not find a probe for the service")
+ packetReader.sendResponse(buildConflictingAnnouncementForCustomHost())
+
+ // Registration must use an updated hostname to avoid the conflict
+ val cb = registrationRecord.expectCallback<ServiceRegistered>(REGISTRATION_TIMEOUT_MS)
+ // Service name is not renamed because there's no conflict on the service name.
+ assertEquals(serviceName, cb.serviceInfo.serviceName)
+ val hostname = cb.serviceInfo.hostname ?: fail("Missing hostname")
+ hostname.let {
+ assertTrue("Unexpected registered hostname: $it",
+ it.startsWith(customHostname) && it != customHostname)
+ }
+ } cleanupStep {
+ nsdManager.unregisterService(registrationRecord)
+ registrationRecord.expectCallback<ServiceUnregistered>()
+ } cleanup {
+ packetReader.handler.post { packetReader.stop() }
+ handlerThread.waitForIdle(TIMEOUT_MS)
+ }
+ }
+
+ @Test
+ fun testRegisterServiceWithCustomHostNoAddresses_noConflictDuringProbing_notRenamed() {
+ val si = makeTestServiceInfo(testNetwork1.network).apply {
+ hostname = customHostname
+ }
+
+ val packetReader = TapPacketReader(Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ packetReader.startAsyncForTest()
+ handlerThread.waitForIdle(TIMEOUT_MS)
+
+ // Register service on testNetwork1
+ val registrationRecord = NsdRegistrationRecord()
+ nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, { it.run() },
+ registrationRecord)
+
+ tryTest {
+ assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
+ "Did not find a probe for the service")
+ // Not a conflict because no record is registered for the hostname
+ packetReader.sendResponse(buildConflictingAnnouncementForCustomHost())
+
+ // Registration is not renamed because there's no conflict
+ val cb = registrationRecord.expectCallback<ServiceRegistered>(REGISTRATION_TIMEOUT_MS)
+ assertEquals(serviceName, cb.serviceInfo.serviceName)
+ assertEquals(customHostname, cb.serviceInfo.hostname)
+ } cleanupStep {
+ nsdManager.unregisterService(registrationRecord)
+ registrationRecord.expectCallback<ServiceUnregistered>()
+ } cleanup {
+ packetReader.handler.post { packetReader.stop() }
+ handlerThread.waitForIdle(TIMEOUT_MS)
+ }
+ }
+
+ @Test
fun testRegisterWithConflictAfterProbing() {
// This test requires shims supporting T+ APIs (NsdServiceInfo.network)
assumeTrue(TestUtils.shouldTestTApis())
@@ -1103,6 +1430,121 @@
}
}
+ @Test
+ fun testRegisterServiceWithCustomHostAndAddresses_conflictAfterProbing_hostRenamed() {
+ val si = makeTestServiceInfo(testNetwork1.network).apply {
+ hostname = customHostname
+ hostAddresses = listOf(
+ parseNumericAddress("192.0.2.24"),
+ parseNumericAddress("2001:db8::3"))
+ }
+
+ // Register service on testNetwork1
+ val registrationRecord = NsdRegistrationRecord()
+ val discoveryRecord = NsdDiscoveryRecord()
+ val registeredService = registerService(registrationRecord, si)
+ val packetReader = TapPacketReader(
+ Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ packetReader.startAsyncForTest()
+ handlerThread.waitForIdle(TIMEOUT_MS)
+
+ tryTest {
+ repeat(3) {
+ assertNotNull(packetReader.pollForAdvertisement(serviceName, serviceType),
+ "Expect 3 announcements sent after initial probing")
+ }
+
+ assertEquals(si.serviceName, registeredService.serviceName)
+ assertEquals(si.hostname, registeredService.hostname)
+
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, { it.run() }, discoveryRecord)
+ val discoveredInfo = discoveryRecord.waitForServiceDiscovered(
+ si.serviceName, serviceType)
+
+ // Send a conflicting announcement
+ val conflictingAnnouncement = buildConflictingAnnouncementForCustomHost()
+ packetReader.sendResponse(conflictingAnnouncement)
+
+ // Expect to see probes (RFC6762 9., service is reset to probing state)
+ assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
+ "Probe not received within timeout after conflict")
+
+ // Send the conflicting packet again to reply to the probe
+ packetReader.sendResponse(conflictingAnnouncement)
+
+ val newRegistration =
+ registrationRecord
+ .expectCallbackEventually<ServiceRegistered>(REGISTRATION_TIMEOUT_MS) {
+ it.serviceInfo.serviceName == serviceName
+ && it.serviceInfo.hostname.let { hostname ->
+ hostname != null
+ && hostname.startsWith(customHostname)
+ && hostname != customHostname
+ }
+ }
+
+ val resolvedInfo = resolveService(discoveredInfo)
+ assertEquals(newRegistration.serviceInfo.serviceName, resolvedInfo.serviceName)
+ assertEquals(newRegistration.serviceInfo.hostname, resolvedInfo.hostname)
+
+ discoveryRecord.assertNoCallback()
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(discoveryRecord)
+ discoveryRecord.expectCallback<DiscoveryStopped>()
+ } cleanupStep {
+ nsdManager.unregisterService(registrationRecord)
+ registrationRecord.expectCallback<ServiceUnregistered>()
+ } cleanup {
+ packetReader.handler.post { packetReader.stop() }
+ handlerThread.waitForIdle(TIMEOUT_MS)
+ }
+ }
+
+ @Test
+ fun testRegisterServiceWithCustomHostNoAddresses_noConflictAfterProbing_notRenamed() {
+ val si = makeTestServiceInfo(testNetwork1.network).apply {
+ hostname = customHostname
+ }
+
+ // Register service on testNetwork1
+ val registrationRecord = NsdRegistrationRecord()
+ val discoveryRecord = NsdDiscoveryRecord()
+ val registeredService = registerService(registrationRecord, si)
+ val packetReader = TapPacketReader(Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ packetReader.startAsyncForTest()
+ handlerThread.waitForIdle(TIMEOUT_MS)
+
+ tryTest {
+ assertNotNull(packetReader.pollForAdvertisement(serviceName, serviceType),
+ "No announcements sent after initial probing")
+
+ assertEquals(si.serviceName, registeredService.serviceName)
+ assertEquals(si.hostname, registeredService.hostname)
+
+ // Send a conflicting announcement
+ val conflictingAnnouncement = buildConflictingAnnouncementForCustomHost()
+ packetReader.sendResponse(conflictingAnnouncement)
+
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, { it.run() }, discoveryRecord)
+
+ // The service is not renamed
+ discoveryRecord.waitForServiceDiscovered(si.serviceName, serviceType)
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(discoveryRecord)
+ discoveryRecord.expectCallback<DiscoveryStopped>()
+ } cleanupStep {
+ nsdManager.unregisterService(registrationRecord)
+ registrationRecord.expectCallback<ServiceUnregistered>()
+ } cleanup {
+ packetReader.handler.post { packetReader.stop() }
+ handlerThread.waitForIdle(TIMEOUT_MS)
+ }
+ }
+
// Test that even if only a PTR record is received as a reply when discovering, without the
// SRV, TXT, address records as recommended (but not mandated) by RFC 6763 12, the service can
// still be discovered.
@@ -1161,7 +1603,8 @@
// Resolve service on testNetwork1
val resolveRecord = NsdResolveRecord()
val packetReader = TapPacketReader(Handler(handlerThread.looper),
- testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */
+ )
packetReader.startAsyncForTest()
handlerThread.waitForIdle(TIMEOUT_MS)
@@ -1224,6 +1667,445 @@
serviceResolved.serviceInfo.hostAddresses.toSet())
}
+ @Test
+ fun testUnicastReplyUsedWhenQueryUnicastFlagSet() {
+ // The flag may be removed in the future but unicast replies should be enabled by default
+ // in that case. The rule will reset flags automatically on teardown.
+ deviceConfigRule.setConfig(NAMESPACE_TETHERING, "test_nsd_unicast_reply_enabled", "1")
+
+ val si = makeTestServiceInfo(testNetwork1.network)
+
+ // Register service on testNetwork1
+ val registrationRecord = NsdRegistrationRecord()
+ var nsResponder: NSResponder? = null
+ tryTest {
+ registerService(registrationRecord, si)
+ val packetReader = TapPacketReader(Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ packetReader.startAsyncForTest()
+
+ handlerThread.waitForIdle(TIMEOUT_MS)
+ /*
+ Send a "query unicast" query.
+ Generated with:
+ scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, qd =
+ scapy.DNSQR(qname='_nmt123456789._tcp.local', qtype='PTR', qclass=0x8001)
+ )).hex()
+ */
+ val mdnsPayload = HexDump.hexStringToByteArray("0000000000010000000000000d5f6e6d74313" +
+ "233343536373839045f746370056c6f63616c00000c8001")
+ replaceServiceNameAndTypeWithTestSuffix(mdnsPayload)
+
+ val testSrcAddr = makeLinkLocalAddressOfOtherDeviceOnPrefix(testNetwork1.network)
+ nsResponder = NSResponder(packetReader, mapOf(
+ testSrcAddr to MacAddress.fromString("01:02:03:04:05:06")
+ )).apply { start() }
+
+ packetReader.sendResponse(buildMdnsPacket(mdnsPayload, testSrcAddr))
+ // The reply is sent unicast to the source address. There may be announcements sent
+ // multicast around this time, so filter by destination address.
+ val reply = packetReader.pollForMdnsPacket { pkt ->
+ pkt.isReplyFor("$serviceType.local", DnsResolver.TYPE_PTR) &&
+ pkt.dstAddr == testSrcAddr
+ }
+ assertNotNull(reply)
+ } cleanup {
+ nsResponder?.stop()
+ nsdManager.unregisterService(registrationRecord)
+ registrationRecord.expectCallback<ServiceUnregistered>()
+ }
+ }
+
+ @Test
+ fun testReplyWhenKnownAnswerSuppressionFlagSet() {
+ // The flag may be removed in the future but known-answer suppression should be enabled by
+ // default in that case. The rule will reset flags automatically on teardown.
+ deviceConfigRule.setConfig(NAMESPACE_TETHERING, "test_nsd_known_answer_suppression", "1")
+ deviceConfigRule.setConfig(NAMESPACE_TETHERING, "test_nsd_unicast_reply_enabled", "1")
+
+ val si = makeTestServiceInfo(testNetwork1.network)
+
+ // Register service on testNetwork1
+ val registrationRecord = NsdRegistrationRecord()
+ var nsResponder: NSResponder? = null
+ tryTest {
+ registerService(registrationRecord, si)
+ val packetReader = TapPacketReader(Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ packetReader.startAsyncForTest()
+
+ handlerThread.waitForIdle(TIMEOUT_MS)
+ /*
+ Send a query with a known answer. Expect to receive a response containing TXT record
+ only.
+ Generated with:
+ scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, qd =
+ scapy.DNSQR(qname='_nmt123456789._tcp.local', qtype='PTR',
+ qclass=0x8001) /
+ scapy.DNSQR(qname='NsdTest123456789._nmt123456789._tcp.local', qtype='TXT',
+ qclass=0x8001),
+ an = scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=4500,
+ rdata='NsdTest123456789._nmt123456789._tcp.local')
+ )).hex()
+ */
+ val query = HexDump.hexStringToByteArray("0000000000020001000000000d5f6e6d74313233343" +
+ "536373839045f746370056c6f63616c00000c8001104e7364546573743132333435363738390" +
+ "d5f6e6d74313233343536373839045f746370056c6f63616c00001080010d5f6e6d743132333" +
+ "43536373839045f746370056c6f63616c00000c000100001194002b104e73645465737431323" +
+ "33435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00")
+ replaceServiceNameAndTypeWithTestSuffix(query)
+
+ val testSrcAddr = makeLinkLocalAddressOfOtherDeviceOnPrefix(testNetwork1.network)
+ nsResponder = NSResponder(packetReader, mapOf(
+ testSrcAddr to MacAddress.fromString("01:02:03:04:05:06")
+ )).apply { start() }
+
+ packetReader.sendResponse(buildMdnsPacket(query, testSrcAddr))
+ // The reply is sent unicast to the source address. There may be announcements sent
+ // multicast around this time, so filter by destination address.
+ val reply = packetReader.pollForMdnsPacket { pkt ->
+ pkt.isReplyFor("$serviceName.$serviceType.local", DnsResolver.TYPE_TXT) &&
+ !pkt.isReplyFor("$serviceType.local", DnsResolver.TYPE_PTR) &&
+ pkt.dstAddr == testSrcAddr
+ }
+ assertNotNull(reply)
+
+ /*
+ Send a query with a known answer (TTL is less than half). Expect to receive a response
+ containing both PTR and TXT records.
+ Generated with:
+ scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, qd =
+ scapy.DNSQR(qname='_nmt123456789._tcp.local', qtype='PTR',
+ qclass=0x8001) /
+ scapy.DNSQR(qname='NsdTest123456789._nmt123456789._tcp.local', qtype='TXT',
+ qclass=0x8001),
+ an = scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=2150,
+ rdata='NsdTest123456789._nmt123456789._tcp.local')
+ )).hex()
+ */
+ val query2 = HexDump.hexStringToByteArray("0000000000020001000000000d5f6e6d7431323334" +
+ "3536373839045f746370056c6f63616c00000c8001104e736454657374313233343536373839" +
+ "0d5f6e6d74313233343536373839045f746370056c6f63616c00001080010d5f6e6d74313233" +
+ "343536373839045f746370056c6f63616c00000c000100000866002b104e7364546573743132" +
+ "333435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00")
+ replaceServiceNameAndTypeWithTestSuffix(query2)
+
+ packetReader.sendResponse(buildMdnsPacket(query2, testSrcAddr))
+ // The reply is sent unicast to the source address. There may be announcements sent
+ // multicast around this time, so filter by destination address.
+ val reply2 = packetReader.pollForMdnsPacket { pkt ->
+ pkt.isReplyFor("$serviceName.$serviceType.local", DnsResolver.TYPE_TXT) &&
+ pkt.isReplyFor("$serviceType.local", DnsResolver.TYPE_PTR) &&
+ pkt.dstAddr == testSrcAddr
+ }
+ assertNotNull(reply2)
+ } cleanup {
+ nsResponder?.stop()
+ nsdManager.unregisterService(registrationRecord)
+ registrationRecord.expectCallback<ServiceUnregistered>()
+ }
+ }
+
+ @Test
+ fun testReplyWithMultipacketWhenKnownAnswerSuppressionFlagSet() {
+ // The flag may be removed in the future but known-answer suppression should be enabled by
+ // default in that case. The rule will reset flags automatically on teardown.
+ deviceConfigRule.setConfig(NAMESPACE_TETHERING, "test_nsd_known_answer_suppression", "1")
+ deviceConfigRule.setConfig(NAMESPACE_TETHERING, "test_nsd_unicast_reply_enabled", "1")
+
+ val si = makeTestServiceInfo(testNetwork1.network)
+
+ // Register service on testNetwork1
+ val registrationRecord = NsdRegistrationRecord()
+ var nsResponder: NSResponder? = null
+ tryTest {
+ registerService(registrationRecord, si)
+ val packetReader = TapPacketReader(Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ packetReader.startAsyncForTest()
+
+ handlerThread.waitForIdle(TIMEOUT_MS)
+ /*
+ Send a query with truncated bit set.
+ Generated with:
+ scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, tc=1, qd=
+ scapy.DNSQR(qname='_nmt123456789._tcp.local', qtype='PTR',
+ qclass=0x8001) /
+ scapy.DNSQR(qname='NsdTest123456789._nmt123456789._tcp.local', qtype='TXT',
+ qclass=0x8001)
+ )).hex()
+ */
+ val query = HexDump.hexStringToByteArray("0000020000020000000000000d5f6e6d74313233343" +
+ "536373839045f746370056c6f63616c00000c8001104e7364546573743132333435363738390" +
+ "d5f6e6d74313233343536373839045f746370056c6f63616c0000108001")
+ replaceServiceNameAndTypeWithTestSuffix(query)
+ /*
+ Send a known answer packet (other service) with truncated bit set.
+ Generated with:
+ scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, tc=1, qd=None,
+ an = scapy.DNSRR(rrname='_test._tcp.local', type='PTR', ttl=4500,
+ rdata='NsdTest._test._tcp.local')
+ )).hex()
+ */
+ val knownAnswer1 = HexDump.hexStringToByteArray("000002000000000100000000055f74657374" +
+ "045f746370056c6f63616c00000c000100001194001a074e736454657374055f74657374045f" +
+ "746370056c6f63616c00")
+ replaceServiceNameAndTypeWithTestSuffix(knownAnswer1)
+ /*
+ Send a known answer packet.
+ Generated with:
+ scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, qd=None,
+ an = scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=4500,
+ rdata='NsdTest123456789._nmt123456789._tcp.local')
+ )).hex()
+ */
+ val knownAnswer2 = HexDump.hexStringToByteArray("0000000000000001000000000d5f6e6d7431" +
+ "3233343536373839045f746370056c6f63616c00000c000100001194002b104e736454657374" +
+ "3132333435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00")
+ replaceServiceNameAndTypeWithTestSuffix(knownAnswer2)
+
+ val testSrcAddr = makeLinkLocalAddressOfOtherDeviceOnPrefix(testNetwork1.network)
+ nsResponder = NSResponder(packetReader, mapOf(
+ testSrcAddr to MacAddress.fromString("01:02:03:04:05:06")
+ )).apply { start() }
+
+ packetReader.sendResponse(buildMdnsPacket(query, testSrcAddr))
+ packetReader.sendResponse(buildMdnsPacket(knownAnswer1, testSrcAddr))
+ packetReader.sendResponse(buildMdnsPacket(knownAnswer2, testSrcAddr))
+ // The reply is sent unicast to the source address. There may be announcements sent
+ // multicast around this time, so filter by destination address.
+ val reply = packetReader.pollForMdnsPacket { pkt ->
+ pkt.isReplyFor("$serviceName.$serviceType.local", DnsResolver.TYPE_TXT) &&
+ !pkt.isReplyFor("$serviceType.local", DnsResolver.TYPE_PTR) &&
+ pkt.dstAddr == testSrcAddr
+ }
+ assertNotNull(reply)
+ } cleanup {
+ nsResponder?.stop()
+ nsdManager.unregisterService(registrationRecord)
+ registrationRecord.expectCallback<ServiceUnregistered>()
+ }
+ }
+
+ private fun makeLinkLocalAddressOfOtherDeviceOnPrefix(network: Network): Inet6Address {
+ val lp = cm.getLinkProperties(network) ?: fail("No LinkProperties for net $network")
+ // Expect to have a /64 link-local address
+ val linkAddr = lp.linkAddresses.firstOrNull {
+ it.isIPv6 && it.scope == RT_SCOPE_LINK && it.prefixLength == 64
+ } ?: fail("No /64 link-local address found in ${lp.linkAddresses} for net $network")
+
+ // Add one to the device address to simulate the address of another device on the prefix
+ val addrBytes = linkAddr.address.address
+ addrBytes[IPV6_ADDR_LEN - 1]++
+ return Inet6Address.getByAddress(addrBytes) as Inet6Address
+ }
+
+ @Test
+ fun testAdvertisingAndDiscovery_servicesWithCustomHost_customHostAddressesFound() {
+ val hostAddresses1 = listOf(
+ parseNumericAddress("192.0.2.23"),
+ parseNumericAddress("2001:db8::1"),
+ parseNumericAddress("2001:db8::2"))
+ val hostAddresses2 = listOf(
+ parseNumericAddress("192.0.2.24"),
+ parseNumericAddress("2001:db8::3"))
+ val si1 = NsdServiceInfo().also {
+ it.network = testNetwork1.network
+ it.serviceName = serviceName
+ it.serviceType = serviceType
+ it.port = TEST_PORT
+ it.hostname = customHostname
+ it.hostAddresses = hostAddresses1
+ }
+ val si2 = NsdServiceInfo().also {
+ it.network = testNetwork1.network
+ it.serviceName = serviceName2
+ it.serviceType = serviceType
+ it.port = TEST_PORT + 1
+ it.hostname = customHostname2
+ it.hostAddresses = hostAddresses2
+ }
+ val registrationRecord1 = NsdRegistrationRecord()
+ val registrationRecord2 = NsdRegistrationRecord()
+
+ val discoveryRecord1 = NsdDiscoveryRecord()
+ val discoveryRecord2 = NsdDiscoveryRecord()
+ tryTest {
+ registerService(registrationRecord1, si1)
+
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, discoveryRecord1)
+
+ val discoveredInfo = discoveryRecord1.waitForServiceDiscovered(
+ serviceName, serviceType, testNetwork1.network)
+ val resolvedInfo = resolveService(discoveredInfo)
+
+ assertEquals(TEST_PORT, resolvedInfo.port)
+ assertEquals(si1.hostname, resolvedInfo.hostname)
+ assertAddressEquals(hostAddresses1, resolvedInfo.hostAddresses)
+
+ registerService(registrationRecord2, si2)
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, discoveryRecord2)
+
+ val discoveredInfo2 = discoveryRecord2.waitForServiceDiscovered(
+ serviceName2, serviceType, testNetwork1.network)
+ val resolvedInfo2 = resolveService(discoveredInfo2)
+
+ assertEquals(TEST_PORT + 1, resolvedInfo2.port)
+ assertEquals(si2.hostname, resolvedInfo2.hostname)
+ assertAddressEquals(hostAddresses2, resolvedInfo2.hostAddresses)
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(discoveryRecord1)
+ nsdManager.stopServiceDiscovery(discoveryRecord2)
+
+ discoveryRecord1.expectCallbackEventually<DiscoveryStopped>()
+ discoveryRecord2.expectCallbackEventually<DiscoveryStopped>()
+ } cleanup {
+ nsdManager.unregisterService(registrationRecord1)
+ nsdManager.unregisterService(registrationRecord2)
+ }
+ }
+
+ @Test
+ fun testAdvertisingAndDiscovery_multipleRegistrationsForSameCustomHost_unionOfAddressesFound() {
+ val hostAddresses1 = listOf(
+ parseNumericAddress("192.0.2.23"),
+ parseNumericAddress("2001:db8::1"),
+ parseNumericAddress("2001:db8::2"))
+ val hostAddresses2 = listOf(
+ parseNumericAddress("192.0.2.24"),
+ parseNumericAddress("2001:db8::3"))
+ val hostAddresses3 = listOf(
+ parseNumericAddress("2001:db8::3"),
+ parseNumericAddress("2001:db8::5"))
+ val si1 = NsdServiceInfo().also {
+ it.network = testNetwork1.network
+ it.hostname = customHostname
+ it.hostAddresses = hostAddresses1
+ }
+ val si2 = NsdServiceInfo().also {
+ it.network = testNetwork1.network
+ it.serviceName = serviceName
+ it.serviceType = serviceType
+ it.port = TEST_PORT
+ it.hostname = customHostname
+ it.hostAddresses = hostAddresses2
+ }
+ val si3 = NsdServiceInfo().also {
+ it.network = testNetwork1.network
+ it.serviceName = serviceName3
+ it.serviceType = serviceType
+ it.port = TEST_PORT + 1
+ it.hostname = customHostname
+ it.hostAddresses = hostAddresses3
+ }
+
+ val registrationRecord1 = NsdRegistrationRecord()
+ val registrationRecord2 = NsdRegistrationRecord()
+ val registrationRecord3 = NsdRegistrationRecord()
+
+ val discoveryRecord = NsdDiscoveryRecord()
+ tryTest {
+ registerService(registrationRecord1, si1)
+ registerService(registrationRecord2, si2)
+
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, discoveryRecord)
+
+ val discoveredInfo1 = discoveryRecord.waitForServiceDiscovered(
+ serviceName, serviceType, testNetwork1.network)
+ val resolvedInfo1 = resolveService(discoveredInfo1)
+
+ assertEquals(TEST_PORT, resolvedInfo1.port)
+ assertEquals(si1.hostname, resolvedInfo1.hostname)
+ assertAddressEquals(
+ hostAddresses1 + hostAddresses2,
+ resolvedInfo1.hostAddresses)
+
+ registerService(registrationRecord3, si3)
+
+ val discoveredInfo2 = discoveryRecord.waitForServiceDiscovered(
+ serviceName3, serviceType, testNetwork1.network)
+ val resolvedInfo2 = resolveService(discoveredInfo2)
+
+ assertEquals(TEST_PORT + 1, resolvedInfo2.port)
+ assertEquals(si2.hostname, resolvedInfo2.hostname)
+ assertAddressEquals(
+ hostAddresses1 + hostAddresses2 + hostAddresses3,
+ resolvedInfo2.hostAddresses)
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(discoveryRecord)
+
+ discoveryRecord.expectCallbackEventually<DiscoveryStopped>()
+ } cleanup {
+ nsdManager.unregisterService(registrationRecord1)
+ nsdManager.unregisterService(registrationRecord2)
+ nsdManager.unregisterService(registrationRecord3)
+ }
+ }
+
+ @Test
+ fun testAdvertisingAndDiscovery_servicesWithTheSameCustomHostAddressOmitted_addressesFound() {
+ val hostAddresses = listOf(
+ parseNumericAddress("192.0.2.23"),
+ parseNumericAddress("2001:db8::1"),
+ parseNumericAddress("2001:db8::2"))
+ val si1 = NsdServiceInfo().also {
+ it.network = testNetwork1.network
+ it.serviceType = serviceType
+ it.serviceName = serviceName
+ it.port = TEST_PORT
+ it.hostname = customHostname
+ it.hostAddresses = hostAddresses
+ }
+ val si2 = NsdServiceInfo().also {
+ it.network = testNetwork1.network
+ it.serviceType = serviceType
+ it.serviceName = serviceName2
+ it.port = TEST_PORT + 1
+ it.hostname = customHostname
+ }
+
+ val registrationRecord1 = NsdRegistrationRecord()
+ val registrationRecord2 = NsdRegistrationRecord()
+
+ val discoveryRecord = NsdDiscoveryRecord()
+ tryTest {
+ registerService(registrationRecord1, si1)
+
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, discoveryRecord)
+
+ val discoveredInfo1 = discoveryRecord.waitForServiceDiscovered(
+ serviceName, serviceType, testNetwork1.network)
+ val resolvedInfo1 = resolveService(discoveredInfo1)
+
+ assertEquals(serviceName, discoveredInfo1.serviceName)
+ assertEquals(TEST_PORT, resolvedInfo1.port)
+ assertEquals(si1.hostname, resolvedInfo1.hostname)
+ assertAddressEquals(hostAddresses, resolvedInfo1.hostAddresses)
+
+ registerService(registrationRecord2, si2)
+
+ val discoveredInfo2 = discoveryRecord.waitForServiceDiscovered(
+ serviceName2, serviceType, testNetwork1.network)
+ val resolvedInfo2 = resolveService(discoveredInfo2)
+
+ assertEquals(serviceName2, discoveredInfo2.serviceName)
+ assertEquals(TEST_PORT + 1, resolvedInfo2.port)
+ assertEquals(si2.hostname, resolvedInfo2.hostname)
+ assertAddressEquals(hostAddresses, resolvedInfo2.hostAddresses)
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(discoveryRecord)
+
+ discoveryRecord.expectCallback<DiscoveryStopped>()
+ } cleanup {
+ nsdManager.unregisterService(registrationRecord1)
+ nsdManager.unregisterService(registrationRecord2)
+ }
+ }
+
private fun buildConflictingAnnouncement(): ByteBuffer {
/*
Generated with:
@@ -1240,6 +2122,22 @@
return buildMdnsPacket(mdnsPayload)
}
+ private fun buildConflictingAnnouncementForCustomHost(): ByteBuffer {
+ /*
+ Generated with scapy:
+ raw(DNS(rd=0, qr=1, aa=1, qd = None, an =
+ DNSRR(rrname='NsdTestHost123456789.local', type=28, rclass=1, ttl=120,
+ rdata='2001:db8::321')
+ )).hex()
+ */
+ val mdnsPayload = HexDump.hexStringToByteArray("000084000000000100000000144e7364" +
+ "54657374486f7374313233343536373839056c6f63616c00001c000100000078001020010db80000" +
+ "00000000000000000321")
+ replaceCustomHostnameWithTestSuffix(mdnsPayload)
+
+ return buildMdnsPacket(mdnsPayload)
+ }
+
/**
* Replaces occurrences of "NsdTest123456789" and "_nmt123456789" in mDNS payload with the
* actual random name and type that are used by the test.
@@ -1256,6 +2154,19 @@
replaceAll(packetBuffer, testPacketTypePrefix, encodedTypePrefix)
}
+ /**
+ * Replaces occurrences of "NsdTestHost123456789" in mDNS payload with the
+ * actual random host name that are used by the test.
+ */
+ private fun replaceCustomHostnameWithTestSuffix(mdnsPayload: ByteArray) {
+ // Test custom hostnames have consistent length and are always ASCII
+ val testPacketName = "NsdTestHost123456789".encodeToByteArray()
+ val encodedHostname = customHostname.encodeToByteArray()
+
+ val packetBuffer = ByteBuffer.wrap(mdnsPayload)
+ replaceAll(packetBuffer, testPacketName, encodedHostname)
+ }
+
private tailrec fun replaceAll(buffer: ByteBuffer, source: ByteArray, replacement: ByteArray) {
assertEquals(source.size, replacement.size)
val index = buffer.array().indexOf(source)
@@ -1268,7 +2179,10 @@
replaceAll(buffer, source, replacement)
}
- private fun buildMdnsPacket(mdnsPayload: ByteArray): ByteBuffer {
+ private fun buildMdnsPacket(
+ mdnsPayload: ByteArray,
+ srcAddr: Inet6Address = testSrcAddr
+ ): ByteBuffer {
val packetBuffer = PacketBuilder.allocate(true /* hasEther */, IPPROTO_IPV6,
IPPROTO_UDP, mdnsPayload.size)
val packetBuilder = PacketBuilder(packetBuffer)
@@ -1283,7 +2197,7 @@
0x60000000, // version=6, traffic class=0x0, flowlabel=0x0
IPPROTO_UDP.toByte(),
64 /* hop limit */,
- parseNumericAddress("2001:db8::123") as Inet6Address /* srcIp */,
+ srcAddr,
multicastIpv6Addr /* dstIp */)
packetBuilder.writeUdpHeader(MDNS_PORT /* srcPort */, MDNS_PORT /* dstPort */)
packetBuffer.put(mdnsPayload)
@@ -1305,6 +2219,18 @@
return cb.serviceInfo
}
+ /**
+ * Update a service.
+ */
+ private fun updateService(
+ record: NsdRegistrationRecord,
+ si: NsdServiceInfo,
+ executor: Executor = Executor { it.run() }
+ ) {
+ nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, executor, record)
+ // TODO: add the callback check for the update.
+ }
+
private fun resolveService(discoveredInfo: NsdServiceInfo): NsdServiceInfo {
val record = NsdResolveRecord()
nsdManager.resolveService(discoveredInfo, Executor { it.run() }, record)
@@ -1339,3 +2265,9 @@
if (this == null) return ""
return String(this, StandardCharsets.UTF_8)
}
+
+private fun assertAddressEquals(expected: List<InetAddress>, actual: List<InetAddress>) {
+ // No duplicate addresses in the actual address list
+ assertEquals(actual.toSet().size, actual.size)
+ assertEquals(expected.toSet(), actual.toSet())
+}
\ No newline at end of file
diff --git a/tests/cts/net/src/android/net/cts/OffloadServiceInfoTest.kt b/tests/cts/net/src/android/net/cts/OffloadServiceInfoTest.kt
new file mode 100644
index 0000000..36de4f2
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/OffloadServiceInfoTest.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts
+
+import android.net.nsd.OffloadEngine.OFFLOAD_TYPE_FILTER_QUERIES
+import android.net.nsd.OffloadServiceInfo
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import kotlin.test.assertContentEquals
+import kotlin.test.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** CTS tests for {@link OffloadServiceInfo}. */
+@IgnoreUpTo(Build.VERSION_CODES.S_V2)
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@ConnectivityModuleTest
+class OffloadServiceInfoTest {
+ @Test
+ fun testCreateOffloadServiceInfo() {
+ val offloadServiceInfo = OffloadServiceInfo(
+ OffloadServiceInfo.Key("_testService", "_testType"),
+ listOf("_sub1", "_sub2"),
+ "Android.local",
+ byteArrayOf(0x1, 0x2, 0x3),
+ 1 /* priority */,
+ OFFLOAD_TYPE_FILTER_QUERIES.toLong()
+ )
+
+ assertEquals(OffloadServiceInfo.Key("_testService", "_testType"), offloadServiceInfo.key)
+ assertEquals(listOf("_sub1", "_sub2"), offloadServiceInfo.subtypes)
+ assertEquals("Android.local", offloadServiceInfo.hostname)
+ assertContentEquals(byteArrayOf(0x1, 0x2, 0x3), offloadServiceInfo.offloadPayload)
+ assertEquals(1, offloadServiceInfo.priority)
+ assertEquals(OFFLOAD_TYPE_FILTER_QUERIES.toLong(), offloadServiceInfo.offloadType)
+ }
+}
diff --git a/tests/cts/net/util/Android.bp b/tests/cts/net/util/Android.bp
index fffd30f..644634b 100644
--- a/tests/cts/net/util/Android.bp
+++ b/tests/cts/net/util/Android.bp
@@ -16,12 +16,16 @@
// Common utilities for cts net tests.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
java_library {
name: "cts-net-utils",
- srcs: ["java/**/*.java", "java/**/*.kt"],
+ srcs: [
+ "java/**/*.java",
+ "java/**/*.kt",
+ ],
static_libs: [
"compatibility-device-util-axt",
"junit",
diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
index 96330e2..3d828a4 100644
--- a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
+++ b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
@@ -173,21 +173,39 @@
return cb;
}
- // Toggle WiFi twice, leaving it in the state it started in
- public void toggleWifi() throws Exception {
- if (mWifiManager.isWifiEnabled()) {
- Network wifiNetwork = getWifiNetwork();
- // Ensure system default network is WIFI because it's expected in disconnectFromWifi()
- expectNetworkIsSystemDefault(wifiNetwork);
- disconnectFromWifi(wifiNetwork);
- connectToWifi();
- } else {
- connectToWifi();
- Network wifiNetwork = getWifiNetwork();
- // Ensure system default network is WIFI because it's expected in disconnectFromWifi()
- expectNetworkIsSystemDefault(wifiNetwork);
- disconnectFromWifi(wifiNetwork);
+ /**
+ * Toggle Wi-Fi off and on, waiting for the {@link ConnectivityManager#CONNECTIVITY_ACTION}
+ * broadcast in both cases.
+ */
+ public void reconnectWifiAndWaitForConnectivityAction() throws Exception {
+ assertTrue(mWifiManager.isWifiEnabled());
+ Network wifiNetwork = getWifiNetwork();
+ // Ensure system default network is WIFI because it's expected in disconnectFromWifi()
+ expectNetworkIsSystemDefault(wifiNetwork);
+ disconnectFromWifi(wifiNetwork, true /* expectLegacyBroadcast */);
+ connectToWifi(true /* expectLegacyBroadcast */);
+ }
+
+ /**
+ * Turn Wi-Fi off, then back on and make sure it connects, if it is supported.
+ */
+ public void reconnectWifiIfSupported() throws Exception {
+ if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI)) {
+ return;
}
+ disableWifi();
+ ensureWifiConnected();
+ }
+
+ /**
+ * Turn cell data off, then back on and make sure it connects, if it is supported.
+ */
+ public void reconnectCellIfSupported() throws Exception {
+ if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
+ return;
+ }
+ setMobileDataEnabled(false);
+ setMobileDataEnabled(true);
}
public Network expectNetworkIsSystemDefault(Network network)
diff --git a/tests/cts/netpermission/internetpermission/Android.bp b/tests/cts/netpermission/internetpermission/Android.bp
index 5314396..7d5ca2f 100644
--- a/tests/cts/netpermission/internetpermission/Android.bp
+++ b/tests/cts/netpermission/internetpermission/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/tests/cts/netpermission/updatestatspermission/Android.bp b/tests/cts/netpermission/updatestatspermission/Android.bp
index 40474db..2fde1ce 100644
--- a/tests/cts/netpermission/updatestatspermission/Android.bp
+++ b/tests/cts/netpermission/updatestatspermission/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/tests/cts/tethering/Android.bp b/tests/cts/tethering/Android.bp
index 4284f56..3928961 100644
--- a/tests/cts/tethering/Android.bp
+++ b/tests/cts/tethering/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
index 274596f..81608f7 100644
--- a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
+++ b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
@@ -71,6 +71,8 @@
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
+import com.android.testutils.ParcelUtils;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -236,6 +238,26 @@
}
@Test
+ public void testTetheringRequestParcelable() {
+ final LinkAddress localAddr = new LinkAddress("192.168.24.5/24");
+ final LinkAddress clientAddr = new LinkAddress("192.168.24.100/24");
+ final TetheringRequest unparceled = new TetheringRequest.Builder(TETHERING_USB)
+ .setStaticIpv4Addresses(localAddr, clientAddr)
+ .setExemptFromEntitlementCheck(true)
+ .setShouldShowEntitlementUi(false).build();
+ final TetheringRequest parceled = ParcelUtils.parcelingRoundTrip(unparceled);
+ assertEquals(unparceled.getTetheringType(), parceled.getTetheringType());
+ assertEquals(unparceled.getConnectivityScope(), parceled.getConnectivityScope());
+ assertEquals(unparceled.getLocalIpv4Address(), parceled.getLocalIpv4Address());
+ assertEquals(unparceled.getClientStaticIpv4Address(),
+ parceled.getClientStaticIpv4Address());
+ assertEquals(unparceled.isExemptFromEntitlementCheck(),
+ parceled.isExemptFromEntitlementCheck());
+ assertEquals(unparceled.getShouldShowEntitlementUi(),
+ parceled.getShouldShowEntitlementUi());
+ }
+
+ @Test
public void testRegisterTetheringEventCallback() throws Exception {
final TestTetheringEventCallback tetherEventCallback =
mCtsTetheringUtils.registerTetheringEventCallback();
diff --git a/tests/deflake/Android.bp b/tests/deflake/Android.bp
index 8205f1c..726e504 100644
--- a/tests/deflake/Android.bp
+++ b/tests/deflake/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp
index f705e34..349529dd 100644
--- a/tests/integration/Android.bp
+++ b/tests/integration/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -74,7 +75,10 @@
java_library {
name: "frameworks-net-integration-testutils",
defaults: ["framework-connectivity-test-defaults"],
- srcs: ["util/**/*.java", "util/**/*.kt"],
+ srcs: [
+ "util/**/*.java",
+ "util/**/*.kt",
+ ],
static_libs: [
"androidx.annotation_annotation",
"androidx.test.rules",
diff --git a/tests/integration/AndroidManifest.xml b/tests/integration/AndroidManifest.xml
index cea83c7..1821329 100644
--- a/tests/integration/AndroidManifest.xml
+++ b/tests/integration/AndroidManifest.xml
@@ -42,6 +42,9 @@
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
<!-- Register UidFrozenStateChangedCallback -->
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
+ <!-- Permission required for CTS test - NetworkStatsIntegrationTest -->
+ <uses-permission android:name="android.permission.INTERNET"/>
+ <uses-permission android:name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS"/>
<application android:debuggable="true">
<uses-library android:name="android.test.runner"/>
diff --git a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
index 496d163..361d68c 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -37,28 +37,35 @@
import android.net.Uri
import android.net.metrics.IpConnectivityLog
import android.os.ConditionVariable
+import android.os.Handler
+import android.os.HandlerThread
import android.os.IBinder
import android.os.SystemConfigManager
import android.os.UserHandle
import android.os.VintfRuntimeInfo
+import android.telephony.TelephonyManager
import android.testing.TestableContext
import android.util.Log
-import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.android.compatibility.common.util.SystemUtil
import com.android.connectivity.resources.R
import com.android.net.module.util.BpfUtils
+import com.android.networkstack.apishim.TelephonyManagerShimImpl
import com.android.server.BpfNetMaps
import com.android.server.ConnectivityService
import com.android.server.NetworkAgentWrapper
import com.android.server.TestNetIdManager
+import com.android.server.connectivity.CarrierPrivilegeAuthenticator
import com.android.server.connectivity.ConnectivityResources
import com.android.server.connectivity.MockableSystemProperties
import com.android.server.connectivity.MultinetworkPolicyTracker
import com.android.server.connectivity.ProxyTracker
+import com.android.server.connectivity.SatelliteAccessController
+import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.DeviceInfoUtils
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.tryTest
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
@@ -74,14 +81,14 @@
import org.mockito.Mock
import org.mockito.Mockito.any
import org.mockito.Mockito.anyInt
-import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.doNothing
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.eq
import org.mockito.Mockito.mock
-import org.mockito.Mockito.spy
import org.mockito.MockitoAnnotations
import org.mockito.Spy
+import java.util.function.Consumer
+import java.util.function.BiConsumer
const val SERVICE_BIND_TIMEOUT_MS = 5_000L
const val TEST_TIMEOUT_MS = 10_000L
@@ -90,7 +97,8 @@
* Test that exercises an instrumented version of ConnectivityService against an instrumented
* NetworkStack in a different test process.
*/
-@RunWith(AndroidJUnit4::class)
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRunner.MonitorThreadLeak
class ConnectivityServiceIntegrationTest {
// lateinit used here for mocks as they need to be reinitialized between each test and the test
// should crash if they are used before being initialized.
@@ -117,6 +125,8 @@
private lateinit var service: ConnectivityService
private lateinit var cm: ConnectivityManager
+ private val handlerThreads = mutableListOf<HandlerThread>()
+
companion object {
// lateinit for this binder token, as it must be initialized before any test code is run
// and use of it before init should crash the test.
@@ -197,7 +207,7 @@
networkStackClient = TestNetworkStackClient(realContext)
networkStackClient.start()
- service = TestConnectivityService(makeDependencies())
+ service = TestConnectivityService(TestDependencies())
cm = ConnectivityManager(context, service)
context.addMockSystemService(Context.CONNECTIVITY_SERVICE, cm)
context.addMockSystemService(Context.NETWORK_STATS_SERVICE, statsManager)
@@ -208,31 +218,61 @@
private inner class TestConnectivityService(deps: Dependencies) : ConnectivityService(
context, dnsResolver, log, netd, deps)
- private fun makeDependencies(): ConnectivityService.Dependencies {
- val deps = spy(ConnectivityService.Dependencies())
- doReturn(networkStackClient).`when`(deps).networkStack
- doReturn(mock(ProxyTracker::class.java)).`when`(deps).makeProxyTracker(any(), any())
- doReturn(mock(MockableSystemProperties::class.java)).`when`(deps).systemProperties
- doReturn(TestNetIdManager()).`when`(deps).makeNetIdManager()
- doReturn(mock(BpfNetMaps::class.java)).`when`(deps).getBpfNetMaps(any(), any())
- doAnswer { inv ->
- MultinetworkPolicyTracker(inv.getArgument(0),
- inv.getArgument(1),
- inv.getArgument(2),
- object : MultinetworkPolicyTracker.Dependencies() {
- override fun getResourcesForActiveSubId(
- connResources: ConnectivityResources,
- activeSubId: Int
- ) = resources
- })
- }.`when`(deps).makeMultinetworkPolicyTracker(any(), any(), any())
- return deps
+ private inner class TestDependencies : ConnectivityService.Dependencies() {
+ override fun getNetworkStack() = networkStackClient
+ override fun makeProxyTracker(context: Context, connServiceHandler: Handler) =
+ mock(ProxyTracker::class.java)
+ override fun getSystemProperties() = mock(MockableSystemProperties::class.java)
+ override fun makeNetIdManager() = TestNetIdManager()
+ override fun getBpfNetMaps(context: Context?, netd: INetd?) = mock(BpfNetMaps::class.java)
+
+ override fun makeMultinetworkPolicyTracker(
+ c: Context,
+ h: Handler,
+ r: Runnable
+ ) = MultinetworkPolicyTracker(c, h, r,
+ object : MultinetworkPolicyTracker.Dependencies() {
+ override fun getResourcesForActiveSubId(
+ connResources: ConnectivityResources,
+ activeSubId: Int
+ ) = resources
+ })
+
+ override fun makeHandlerThread(tag: String): HandlerThread =
+ super.makeHandlerThread(tag).also { handlerThreads.add(it) }
+
+ override fun makeCarrierPrivilegeAuthenticator(
+ context: Context,
+ tm: TelephonyManager,
+ requestRestrictedWifiEnabled: Boolean,
+ listener: BiConsumer<Int, Int>
+ ): CarrierPrivilegeAuthenticator {
+ return CarrierPrivilegeAuthenticator(context,
+ object : CarrierPrivilegeAuthenticator.Dependencies() {
+ override fun makeHandlerThread(): HandlerThread =
+ super.makeHandlerThread().also { handlerThreads.add(it) }
+ },
+ tm, TelephonyManagerShimImpl.newInstance(tm),
+ requestRestrictedWifiEnabled, listener)
+ }
+
+ override fun makeSatelliteAccessController(
+ context: Context,
+ updateSatellitePreferredUid: Consumer<MutableSet<Int>>?,
+ connectivityServiceInternalHandler: Handler
+ ): SatelliteAccessController? = mock(
+ SatelliteAccessController::class.java)
}
@After
fun tearDown() {
nsInstrumentation.clearAllState()
ConnectivityResources.setResourcesContextForTest(null)
+ handlerThreads.forEach {
+ it.quitSafely()
+ it.join()
+ }
+ handlerThreads.clear()
}
@Test
@@ -254,13 +294,18 @@
na.addCapability(NET_CAPABILITY_INTERNET)
na.connect()
- testCallback.expectAvailableThenValidatedCallbacks(na.network, TEST_TIMEOUT_MS)
- val requestedSize = nsInstrumentation.getRequestUrls().size
- if (requestedSize == 2 || (requestedSize == 1 &&
- nsInstrumentation.getRequestUrls()[0] == httpsProbeUrl)) {
- return
+ tryTest {
+ testCallback.expectAvailableThenValidatedCallbacks(na.network, TEST_TIMEOUT_MS)
+ val requestedSize = nsInstrumentation.getRequestUrls().size
+ if (requestedSize == 2 || (requestedSize == 1 &&
+ nsInstrumentation.getRequestUrls()[0] == httpsProbeUrl)
+ ) {
+ return@tryTest
+ }
+ fail("Unexpected request urls: ${nsInstrumentation.getRequestUrls()}")
+ } cleanup {
+ na.destroy()
}
- fail("Unexpected request urls: ${nsInstrumentation.getRequestUrls()}")
}
@Test
@@ -292,24 +337,32 @@
val lp = LinkProperties()
lp.captivePortalApiUrl = Uri.parse(apiUrl)
val na = NetworkAgentWrapper(TRANSPORT_CELLULAR, lp, null /* ncTemplate */, context)
- networkStackClient.verifyNetworkMonitorCreated(na.network, TEST_TIMEOUT_MS)
- na.addCapability(NET_CAPABILITY_INTERNET)
- na.connect()
+ tryTest {
+ networkStackClient.verifyNetworkMonitorCreated(na.network, TEST_TIMEOUT_MS)
- testCb.expectAvailableCallbacks(na.network, validated = false, tmt = TEST_TIMEOUT_MS)
+ na.addCapability(NET_CAPABILITY_INTERNET)
+ na.connect()
- val capportData = testCb.expect<LinkPropertiesChanged>(na, TEST_TIMEOUT_MS) {
- it.lp.captivePortalData != null
- }.lp.captivePortalData
- assertNotNull(capportData)
- assertTrue(capportData.isCaptive)
- assertEquals(Uri.parse("https://login.capport.android.com"), capportData.userPortalUrl)
- assertEquals(Uri.parse("https://venueinfo.capport.android.com"), capportData.venueInfoUrl)
+ testCb.expectAvailableCallbacks(na.network, validated = false, tmt = TEST_TIMEOUT_MS)
- testCb.expectCaps(na, TEST_TIMEOUT_MS) {
- it.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL) &&
- !it.hasCapability(NET_CAPABILITY_VALIDATED)
+ val capportData = testCb.expect<LinkPropertiesChanged>(na, TEST_TIMEOUT_MS) {
+ it.lp.captivePortalData != null
+ }.lp.captivePortalData
+ assertNotNull(capportData)
+ assertTrue(capportData.isCaptive)
+ assertEquals(Uri.parse("https://login.capport.android.com"), capportData.userPortalUrl)
+ assertEquals(
+ Uri.parse("https://venueinfo.capport.android.com"),
+ capportData.venueInfoUrl
+ )
+
+ testCb.expectCaps(na, TEST_TIMEOUT_MS) {
+ it.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL) &&
+ !it.hasCapability(NET_CAPABILITY_VALIDATED)
+ }
+ } cleanup {
+ na.destroy()
}
}
diff --git a/tests/integration/src/com/android/server/net/integrationtests/NetworkStatsIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/NetworkStatsIntegrationTest.kt
new file mode 100644
index 0000000..765e56e
--- /dev/null
+++ b/tests/integration/src/com/android/server/net/integrationtests/NetworkStatsIntegrationTest.kt
@@ -0,0 +1,597 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.net.integrationtests
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.annotation.TargetApi
+import android.app.usage.NetworkStats
+import android.app.usage.NetworkStats.Bucket
+import android.app.usage.NetworkStats.Bucket.TAG_NONE
+import android.app.usage.NetworkStatsManager
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.TYPE_TEST
+import android.net.InetAddresses
+import android.net.IpPrefix
+import android.net.LinkAddress
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import android.net.NetworkTemplate
+import android.net.NetworkTemplate.MATCH_TEST
+import android.net.TestNetworkSpecifier
+import android.net.TrafficStats
+import android.os.Build
+import android.os.Process
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.net.integrationtests.NetworkStatsIntegrationTest.Direction.DOWNLOAD
+import com.android.server.net.integrationtests.NetworkStatsIntegrationTest.Direction.UPLOAD
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.PacketBridge
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.TestDnsServer
+import com.android.testutils.TestHttpServer
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.runAsShell
+import fi.iki.elonen.NanoHTTPD
+import java.io.BufferedInputStream
+import java.io.BufferedOutputStream
+import java.net.HttpURLConnection
+import java.net.HttpURLConnection.HTTP_OK
+import java.net.InetSocketAddress
+import java.net.URL
+import java.nio.charset.Charset
+import kotlin.math.ceil
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TEST_TAG = 0xF00D
+
+@RunWith(DevSdkIgnoreRunner::class)
+@TargetApi(Build.VERSION_CODES.S)
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class NetworkStatsIntegrationTest {
+ private val TAG = NetworkStatsIntegrationTest::class.java.simpleName
+ private val LOCAL_V6ADDR =
+ LinkAddress(InetAddresses.parseNumericAddress("2001:db8::1234"), 64)
+
+ // Remote address, both the client and server will have a hallucination that
+ // they are talking to this address.
+ private val REMOTE_V6ADDR =
+ LinkAddress(InetAddresses.parseNumericAddress("dead:beef::808:808"), 64)
+ private val REMOTE_V4ADDR =
+ LinkAddress(InetAddresses.parseNumericAddress("8.8.8.8"), 32)
+ private val DEFAULT_MTU = 1500
+ private val DEFAULT_BUFFER_SIZE = 1500 // Any size greater than or equal to mtu
+ private val CONNECTION_TIMEOUT_MILLIS = 15000
+ private val TEST_DOWNLOAD_SIZE = 10000L
+ private val TEST_UPLOAD_SIZE = 20000L
+ private val HTTP_SERVER_NAME = "test.com"
+ private val HTTP_SERVER_PORT = 8080 // Use port > 1024 to avoid restrictions on system ports
+ private val DNS_INTERNAL_SERVER_PORT = 53
+ private val DNS_EXTERNAL_SERVER_PORT = 1053
+ private val TCP_ACK_SIZE = 72
+
+ // Packet overheads that are not part of the actual data transmission, these
+ // include DNS packets, TCP handshake/termination packets, and HTTP header
+ // packets. These overheads were gathered from real samples and may not
+ // be perfectly accurate because of DNS caches and TCP retransmissions, etc.
+ private val CONSTANT_PACKET_OVERHEAD = 8
+
+ // 130 is an observed average.
+ private val CONSTANT_BYTES_OVERHEAD = 130 * CONSTANT_PACKET_OVERHEAD
+ private val TOLERANCE = 1.3
+
+ // Set up the packet bridge with two IPv6 address only test networks.
+ private val inst = InstrumentationRegistry.getInstrumentation()
+ private val context = inst.getContext()
+ private val packetBridge = runAsShell(MANAGE_TEST_NETWORKS) {
+ PacketBridge(
+ context,
+ listOf(LOCAL_V6ADDR),
+ REMOTE_V6ADDR.address,
+ listOf(
+ Pair(DNS_INTERNAL_SERVER_PORT, DNS_EXTERNAL_SERVER_PORT)
+ )
+ )
+ }
+ private val cm = context.getSystemService(ConnectivityManager::class.java)!!
+
+ // Set up DNS server for testing server and DNS64.
+ private val fakeDns = TestDnsServer(
+ packetBridge.externalNetwork,
+ InetSocketAddress(LOCAL_V6ADDR.address, DNS_EXTERNAL_SERVER_PORT)
+ ).apply {
+ start()
+ setAnswer(
+ "ipv4only.arpa",
+ listOf(IpPrefix(REMOTE_V6ADDR.address, REMOTE_V6ADDR.prefixLength).address)
+ )
+ setAnswer(HTTP_SERVER_NAME, listOf(REMOTE_V4ADDR.address))
+ }
+
+ // Start up test http server.
+ private val httpServer = TestHttpServer(
+ LOCAL_V6ADDR.address.hostAddress,
+ HTTP_SERVER_PORT
+ ).apply {
+ start()
+ }
+
+ @Before
+ fun setUp() {
+ assumeTrue(shouldRunTests())
+ packetBridge.start()
+ }
+
+ // For networkstack tests, it is not guaranteed that the tethering module will be
+ // updated at the same time. If the tethering module is not new enough, it may not contain
+ // the necessary abilities to run these tests. For example, The tests depends on test
+ // network stats being counted, which can only be achieved when they are marked as TYPE_TEST.
+ // If the tethering module does not support TYPE_TEST stats, then these tests will need
+ // to be skipped.
+ fun shouldRunTests() = cm.getNetworkInfo(packetBridge.internalNetwork)!!.type == TYPE_TEST
+
+ @After
+ fun tearDown() {
+ packetBridge.stop()
+ fakeDns.stop()
+ httpServer.stop()
+ }
+
+ private fun waitFor464XlatReady(network: Network): String {
+ val iface = cm.getLinkProperties(network)!!.interfaceName!!
+
+ // Make a network request to listen to the specific test network.
+ val nr = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+ .setNetworkSpecifier(TestNetworkSpecifier(iface))
+ .build()
+ val testCb = TestableNetworkCallback()
+ cm.registerNetworkCallback(nr, testCb)
+
+ // Wait for the stacked address to be available.
+ testCb.eventuallyExpect<LinkPropertiesChanged> {
+ it.lp.stackedLinks.getOrNull(0)?.linkAddresses?.getOrNull(0) != null
+ }
+
+ return iface
+ }
+
+ private val Network.mtu: Int get() {
+ val lp = cm.getLinkProperties(this)!!
+ val mtuStacked = if (lp.stackedLinks[0]?.mtu != 0) lp.stackedLinks[0].mtu else DEFAULT_MTU
+ val mtuInterface = if (lp.mtu != 0) lp.mtu else DEFAULT_MTU
+ return mtuInterface.coerceAtMost(mtuStacked)
+ }
+
+ /**
+ * Verify data usage download stats with test 464xlat networks.
+ *
+ * This test starts two test networks and binds them together, the internal one is for the
+ * client to make http traffic on the test network, and the external one is for the mocked
+ * http and dns server to bind to and provide responses.
+ *
+ * After Clat setup, the client will use clat v4 address to send packets to the mocked
+ * server v4 address, which will be translated into a v6 packet by the clat daemon with
+ * NAT64 prefix learned from the mocked DNS64 response. And send to the interface.
+ *
+ * While the packets are being forwarded to the external interface, the servers will see
+ * the packets originated from the mocked v6 address, and destined to a local v6 address.
+ */
+ @Test
+ fun test464XlatTcpStats() {
+ // Wait for 464Xlat to be ready.
+ val internalInterfaceName = waitFor464XlatReady(packetBridge.internalNetwork)
+ val mtu = packetBridge.internalNetwork.mtu
+
+ val snapshotBeforeTest = StatsSnapshot(context, internalInterfaceName)
+
+ // Generate the download traffic.
+ genHttpTraffic(packetBridge.internalNetwork, uploadSize = 0L, TEST_DOWNLOAD_SIZE)
+
+ // In practice, for one way 10k download payload, the download usage is about
+ // 11222~12880 bytes, with 14~17 packets. And the upload usage is about 1279~1626 bytes
+ // with 14~17 packets, which is majorly contributed by TCP ACK packets.
+ val snapshotAfterDownload = StatsSnapshot(context, internalInterfaceName)
+ val (expectedDownloadLower, expectedDownloadUpper) = getExpectedStatsBounds(
+ TEST_DOWNLOAD_SIZE,
+ mtu,
+ DOWNLOAD
+ )
+ assertOnlyNonTaggedStatsIncreases(
+ snapshotBeforeTest,
+ snapshotAfterDownload,
+ expectedDownloadLower,
+ expectedDownloadUpper
+ )
+
+ // Generate upload traffic with tag to verify tagged data accounting as well.
+ genHttpTrafficWithTag(
+ packetBridge.internalNetwork,
+ TEST_UPLOAD_SIZE,
+ downloadSize = 0L,
+ TEST_TAG
+ )
+
+ // Verify upload data usage accounting.
+ val snapshotAfterUpload = StatsSnapshot(context, internalInterfaceName)
+ val (expectedUploadLower, expectedUploadUpper) = getExpectedStatsBounds(
+ TEST_UPLOAD_SIZE,
+ mtu,
+ UPLOAD
+ )
+ assertAllStatsIncreases(
+ snapshotAfterDownload,
+ snapshotAfterUpload,
+ expectedUploadLower,
+ expectedUploadUpper
+ )
+ }
+
+ private enum class Direction {
+ DOWNLOAD,
+ UPLOAD
+ }
+
+ private fun getExpectedStatsBounds(
+ transmittedSize: Long,
+ mtu: Int,
+ direction: Direction
+ ): Pair<BareStats, BareStats> {
+ // This is already an underestimated value since the input doesn't include TCP/IP
+ // layer overhead.
+ val txBytesLower = transmittedSize
+ // Include TCP/IP header overheads and retransmissions in the upper bound.
+ val txBytesUpper = (transmittedSize * TOLERANCE).toLong()
+ val txPacketsLower = txBytesLower / mtu + (CONSTANT_PACKET_OVERHEAD / TOLERANCE).toLong()
+ val estTransmissionPacketsUpper = ceil(txBytesUpper / mtu.toDouble()).toLong()
+ val txPacketsUpper = estTransmissionPacketsUpper +
+ (CONSTANT_PACKET_OVERHEAD * TOLERANCE).toLong()
+ // Assume ACK only sent once for the entire transmission.
+ val rxPacketsLower = 1L + (CONSTANT_PACKET_OVERHEAD / TOLERANCE).toLong()
+ // Assume ACK sent for every RX packet.
+ val rxPacketsUpper = txPacketsUpper
+ val rxBytesLower = 1L * TCP_ACK_SIZE + (CONSTANT_BYTES_OVERHEAD / TOLERANCE).toLong()
+ val rxBytesUpper = estTransmissionPacketsUpper * TCP_ACK_SIZE +
+ (CONSTANT_BYTES_OVERHEAD * TOLERANCE).toLong()
+
+ return if (direction == UPLOAD) {
+ BareStats(rxBytesLower, rxPacketsLower, txBytesLower, txPacketsLower) to
+ BareStats(rxBytesUpper, rxPacketsUpper, txBytesUpper, txPacketsUpper)
+ } else {
+ BareStats(txBytesLower, txPacketsLower, rxBytesLower, rxPacketsLower) to
+ BareStats(txBytesUpper, txPacketsUpper, rxBytesUpper, rxPacketsUpper)
+ }
+ }
+
+ private fun genHttpTraffic(network: Network, uploadSize: Long, downloadSize: Long) =
+ genHttpTrafficWithTag(network, uploadSize, downloadSize, NetworkStats.Bucket.TAG_NONE)
+
+ private fun genHttpTrafficWithTag(
+ network: Network,
+ uploadSize: Long,
+ downloadSize: Long,
+ tag: Int
+ ) {
+ val path = "/test_upload_download"
+ val buf = ByteArray(DEFAULT_BUFFER_SIZE)
+
+ httpServer.addResponse(
+ TestHttpServer.Request(path, NanoHTTPD.Method.POST), NanoHTTPD.Response.Status.OK,
+ content = getRandomString(downloadSize)
+ )
+ var httpConnection: HttpURLConnection? = null
+ try {
+ TrafficStats.setThreadStatsTag(tag)
+ val spec = "http://$HTTP_SERVER_NAME:${httpServer.listeningPort}$path"
+ val url = URL(spec)
+ httpConnection = network.openConnection(url) as HttpURLConnection
+ httpConnection.connectTimeout = CONNECTION_TIMEOUT_MILLIS
+ httpConnection.requestMethod = "POST"
+ httpConnection.doOutput = true
+ // Tell the server that the response should not be compressed. Otherwise, the data usage
+ // accounted will be less than expected.
+ httpConnection.setRequestProperty("Accept-Encoding", "identity")
+ // Tell the server that to close connection after this request, this is needed to
+ // prevent from reusing the same socket that has different tagging requirement.
+ httpConnection.setRequestProperty("Connection", "close")
+
+ // Send http body.
+ val outputStream = BufferedOutputStream(httpConnection.outputStream)
+ outputStream.write(getRandomString(uploadSize).toByteArray(Charset.forName("UTF-8")))
+ outputStream.close()
+ assertEquals(HTTP_OK, httpConnection.responseCode)
+
+ // Receive response from the server.
+ val inputStream = BufferedInputStream(httpConnection.getInputStream())
+ var total = 0L
+ while (true) {
+ val count = inputStream.read(buf)
+ if (count == -1) break // End-of-Stream
+ total += count
+ }
+ assertEquals(downloadSize, total)
+ } finally {
+ httpConnection?.inputStream?.close()
+ TrafficStats.clearThreadStatsTag()
+ }
+ }
+
+ // NetworkStats.Bucket cannot be written. So another class is needed to
+ // perform arithmetic operations.
+ data class BareStats(
+ val rxBytes: Long,
+ val rxPackets: Long,
+ val txBytes: Long,
+ val txPackets: Long
+ ) {
+ operator fun plus(other: BareStats): BareStats {
+ return BareStats(
+ this.rxBytes + other.rxBytes, this.rxPackets + other.rxPackets,
+ this.txBytes + other.txBytes, this.txPackets + other.txPackets
+ )
+ }
+
+ operator fun minus(other: BareStats): BareStats {
+ return BareStats(
+ this.rxBytes - other.rxBytes, this.rxPackets - other.rxPackets,
+ this.txBytes - other.txBytes, this.txPackets - other.txPackets
+ )
+ }
+
+ fun reverse(): BareStats =
+ BareStats(
+ rxBytes = txBytes,
+ rxPackets = txPackets,
+ txBytes = rxBytes,
+ txPackets = rxPackets
+ )
+
+ override fun toString(): String {
+ return "BareStats{rx/txBytes=$rxBytes/$txBytes, rx/txPackets=$rxPackets/$txPackets}"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is BareStats) return false
+
+ if (rxBytes != other.rxBytes) return false
+ if (rxPackets != other.rxPackets) return false
+ if (txBytes != other.txBytes) return false
+ if (txPackets != other.txPackets) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return (rxBytes * 11 + rxPackets * 13 + txBytes * 17 + txPackets * 19).toInt()
+ }
+
+ companion object {
+ val EMPTY = BareStats(0L, 0L, 0L, 0L)
+ }
+ }
+
+ data class StatsSnapshot(val context: Context, val iface: String) {
+ val statsSummary = getNetworkSummary(iface)
+ val statsUid = getUidDetail(iface, TAG_NONE)
+ val taggedSummary = getTaggedNetworkSummary(iface, TEST_TAG)
+ val taggedUid = getUidDetail(iface, TEST_TAG)
+ val trafficStatsIface = getTrafficStatsIface(iface)
+ val trafficStatsUid = getTrafficStatsUid(Process.myUid())
+
+ private fun getUidDetail(iface: String, tag: Int): BareStats {
+ return getNetworkStatsThat(iface, tag) { nsm, template ->
+ nsm.queryDetailsForUidTagState(
+ template, Long.MIN_VALUE, Long.MAX_VALUE,
+ Process.myUid(), tag, Bucket.STATE_ALL
+ )
+ }
+ }
+
+ private fun getNetworkSummary(iface: String): BareStats {
+ return getNetworkStatsThat(iface, TAG_NONE) { nsm, template ->
+ nsm.querySummary(template, Long.MIN_VALUE, Long.MAX_VALUE)
+ }
+ }
+
+ private fun getTaggedNetworkSummary(iface: String, tag: Int): BareStats {
+ return getNetworkStatsThat(iface, tag) { nsm, template ->
+ nsm.queryTaggedSummary(template, Long.MIN_VALUE, Long.MAX_VALUE)
+ }
+ }
+
+ private fun getNetworkStatsThat(
+ iface: String,
+ tag: Int,
+ queryApi: (nsm: NetworkStatsManager, template: NetworkTemplate) -> NetworkStats
+ ): BareStats {
+ val nsm = context.getSystemService(NetworkStatsManager::class.java)!!
+ nsm.forceUpdate()
+ val testTemplate = NetworkTemplate.Builder(MATCH_TEST)
+ .setWifiNetworkKeys(setOf(iface)).build()
+ val stats = queryApi.invoke(nsm, testTemplate)
+ val filteredBuckets =
+ stats.buckets().filter { it.uid == Process.myUid() && it.tag == tag }
+ return filteredBuckets.fold(BareStats.EMPTY) { acc, it ->
+ acc + BareStats(
+ it.rxBytes,
+ it.rxPackets,
+ it.txBytes,
+ it.txPackets
+ )
+ }
+ }
+
+ // Helper function to iterate buckets in app.usage.NetworkStats.
+ private fun NetworkStats.buckets() = object : Iterable<NetworkStats.Bucket> {
+ override fun iterator() = object : Iterator<NetworkStats.Bucket> {
+ override operator fun hasNext() = hasNextBucket()
+ override operator fun next() =
+ NetworkStats.Bucket().also { assertTrue(getNextBucket(it)) }
+ }
+ }
+
+ private fun getTrafficStatsIface(iface: String): BareStats = BareStats(
+ TrafficStats.getRxBytes(iface),
+ TrafficStats.getRxPackets(iface),
+ TrafficStats.getTxBytes(iface),
+ TrafficStats.getTxPackets(iface)
+ )
+
+ private fun getTrafficStatsUid(uid: Int): BareStats = BareStats(
+ TrafficStats.getUidRxBytes(uid),
+ TrafficStats.getUidRxPackets(uid),
+ TrafficStats.getUidTxBytes(uid),
+ TrafficStats.getUidTxPackets(uid)
+ )
+ }
+
+ private fun assertAllStatsIncreases(
+ before: StatsSnapshot,
+ after: StatsSnapshot,
+ lower: BareStats,
+ upper: BareStats
+ ) {
+ assertNonTaggedStatsIncreases(before, after, lower, upper)
+ assertTaggedStatsIncreases(before, after, lower, upper)
+ }
+
+ private fun assertOnlyNonTaggedStatsIncreases(
+ before: StatsSnapshot,
+ after: StatsSnapshot,
+ lower: BareStats,
+ upper: BareStats
+ ) {
+ assertNonTaggedStatsIncreases(before, after, lower, upper)
+ assertTaggedStatsEquals(before, after)
+ }
+
+ private fun assertNonTaggedStatsIncreases(
+ before: StatsSnapshot,
+ after: StatsSnapshot,
+ lower: BareStats,
+ upper: BareStats
+ ) {
+ assertInRange(
+ "Unexpected iface traffic stats",
+ after.iface,
+ before.trafficStatsIface, after.trafficStatsIface,
+ lower, upper
+ )
+ // Uid traffic stats are counted in both direction because the external network
+ // traffic is also attributed to the test uid.
+ assertInRange(
+ "Unexpected uid traffic stats",
+ after.iface,
+ before.trafficStatsUid, after.trafficStatsUid,
+ lower + lower.reverse(), upper + upper.reverse()
+ )
+ assertInRange(
+ "Unexpected non-tagged summary stats",
+ after.iface,
+ before.statsSummary, after.statsSummary,
+ lower, upper
+ )
+ assertInRange(
+ "Unexpected non-tagged uid stats",
+ after.iface,
+ before.statsUid, after.statsUid,
+ lower, upper
+ )
+ }
+
+ private fun assertTaggedStatsEquals(before: StatsSnapshot, after: StatsSnapshot) {
+ // Increment of tagged data should be zero since no tagged traffic was generated.
+ assertEquals(
+ before.taggedSummary,
+ after.taggedSummary,
+ "Unexpected tagged summary stats: ${after.iface}"
+ )
+ assertEquals(
+ before.taggedUid,
+ after.taggedUid,
+ "Unexpected tagged uid stats: ${Process.myUid()} on ${after.iface}"
+ )
+ }
+
+ private fun assertTaggedStatsIncreases(
+ before: StatsSnapshot,
+ after: StatsSnapshot,
+ lower: BareStats,
+ upper: BareStats
+ ) {
+ assertInRange(
+ "Unexpected tagged summary stats",
+ after.iface,
+ before.taggedSummary, after.taggedSummary,
+ lower,
+ upper
+ )
+ assertInRange(
+ "Unexpected tagged uid stats: ${Process.myUid()}",
+ after.iface,
+ before.taggedUid, after.taggedUid,
+ lower,
+ upper
+ )
+ }
+
+ /** Verify the given BareStats is in range [lower, upper] */
+ private fun assertInRange(
+ tag: String,
+ iface: String,
+ before: BareStats,
+ after: BareStats,
+ lower: BareStats,
+ upper: BareStats
+ ) {
+ // Passing the value after operation and the value before operation to dump the actual
+ // numbers if it fails.
+ assertTrue(checkInRange(before, after, lower, upper),
+ "$tag on $iface: $after - $before is not within range [$lower, $upper]"
+ )
+ }
+
+ private fun checkInRange(
+ before: BareStats,
+ after: BareStats,
+ lower: BareStats,
+ upper: BareStats
+ ): Boolean {
+ val value = after - before
+ return value.rxBytes in lower.rxBytes..upper.rxBytes &&
+ value.rxPackets in lower.rxPackets..upper.rxPackets &&
+ value.txBytes in lower.txBytes..upper.txBytes &&
+ value.txPackets in lower.txPackets..upper.txPackets
+ }
+
+ fun getRandomString(length: Long): String {
+ val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
+ return (1..length)
+ .map { allowedChars.random() }
+ .joinToString("")
+ }
+}
diff --git a/tests/integration/util/com/android/server/NetworkAgentWrapper.java b/tests/integration/util/com/android/server/NetworkAgentWrapper.java
index ec09f9e..960c6ca 100644
--- a/tests/integration/util/com/android/server/NetworkAgentWrapper.java
+++ b/tests/integration/util/com/android/server/NetworkAgentWrapper.java
@@ -36,6 +36,7 @@
import static org.junit.Assert.fail;
import android.annotation.NonNull;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.LinkProperties;
@@ -51,6 +52,7 @@
import android.os.ConditionVariable;
import android.os.HandlerThread;
import android.os.Message;
+import android.util.CloseGuard;
import android.util.Log;
import android.util.Range;
@@ -65,11 +67,14 @@
import java.util.function.Consumer;
public class NetworkAgentWrapper implements TestableNetworkCallback.HasNetwork {
+ private static final long DESTROY_TIMEOUT_MS = 10_000L;
+
// Note : Please do not add any new instrumentation here. If you need new instrumentation,
// please add it in CSAgentWrapper and use subclasses of CSTest instead of adding more
// tools in ConnectivityServiceTest.
private final NetworkCapabilities mNetworkCapabilities;
private final HandlerThread mHandlerThread;
+ private final CloseGuard mCloseGuard;
private final Context mContext;
private final String mLogTag;
private final NetworkAgentConfig mNetworkAgentConfig;
@@ -157,6 +162,8 @@
mLogTag = "Mock-" + typeName;
mHandlerThread = new HandlerThread(mLogTag);
mHandlerThread.start();
+ mCloseGuard = new CloseGuard();
+ mCloseGuard.open("destroy");
// extraInfo is set to "" by default in NetworkAgentConfig.
final String extraInfo = (transport == TRANSPORT_CELLULAR) ? "internet.apn" : "";
@@ -359,6 +366,35 @@
mNetworkAgent.unregister();
}
+ /**
+ * Destroy the network agent and stop its looper.
+ *
+ * <p>This must always be called.
+ */
+ public void destroy() {
+ mHandlerThread.quitSafely();
+ try {
+ mHandlerThread.join(DESTROY_TIMEOUT_MS);
+ } catch (InterruptedException e) {
+ Log.e(mLogTag, "Interrupted when waiting for handler thread on destroy", e);
+ }
+ mCloseGuard.close();
+ }
+
+ @SuppressLint("Finalize") // Follows the recommended pattern for CloseGuard
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ // Note that mCloseGuard could be null if the constructor threw.
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+ destroy();
+ } finally {
+ super.finalize();
+ }
+ }
+
@Override
public Network getNetwork() {
return mNetworkAgent.getNetwork();
diff --git a/tests/mts/Android.bp b/tests/mts/Android.bp
index 6425223..336be2e 100644
--- a/tests/mts/Android.bp
+++ b/tests/mts/Android.bp
@@ -14,6 +14,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -38,5 +39,5 @@
"bpf_existence_test.cpp",
],
compile_multilib: "first",
- min_sdk_version: "30", // Ensure test runs on R and above.
+ min_sdk_version: "30", // Ensure test runs on R and above.
}
diff --git a/tests/native/connectivity_native_test/Android.bp b/tests/native/connectivity_native_test/Android.bp
index 8825aa4..2f66d17 100644
--- a/tests/native/connectivity_native_test/Android.bp
+++ b/tests/native/connectivity_native_test/Android.bp
@@ -1,4 +1,5 @@
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/tests/native/connectivity_native_test/connectivity_native_test.cpp b/tests/native/connectivity_native_test/connectivity_native_test.cpp
index 27a9d35..f62a30b 100644
--- a/tests/native/connectivity_native_test/connectivity_native_test.cpp
+++ b/tests/native/connectivity_native_test/connectivity_native_test.cpp
@@ -41,13 +41,14 @@
void SetUp() override {
restoreBlockedPorts = false;
+
// Skip test case if not on U.
- if (!android::modules::sdklevel::IsAtLeastU()) GTEST_SKIP() <<
- "Should be at least T device.";
+ if (!android::modules::sdklevel::IsAtLeastU())
+ GTEST_SKIP() << "Should be at least U device.";
// Skip test case if not on 5.4 kernel which is required by bpf prog.
- if (!android::bpf::isAtLeastKernelVersion(5, 4, 0)) GTEST_SKIP() <<
- "Kernel should be at least 5.4.";
+ if (!android::bpf::isAtLeastKernelVersion(5, 4, 0))
+ GTEST_SKIP() << "Kernel should be at least 5.4.";
// Necessary to use dlopen/dlsym since the lib is only available on U and there
// is no Sdk34ModuleController in tradefed yet.
diff --git a/tests/native/utilities/Android.bp b/tests/native/utilities/Android.bp
index 4706b3d..48a5414 100644
--- a/tests/native/utilities/Android.bp
+++ b/tests/native/utilities/Android.bp
@@ -14,14 +14,17 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
+// TODO: delete this as it is a cross-module api boundary violation
cc_test_library {
name: "libconnectivity_native_test_utils",
+ visibility: ["//packages/modules/DnsResolver/tests:__subpackages__"],
defaults: [
"netd_defaults",
- "resolv_test_defaults"
+ "resolv_test_defaults",
],
srcs: [
"firewall.cpp",
diff --git a/tests/native/utilities/firewall.cpp b/tests/native/utilities/firewall.cpp
index 669b76a..34b4f07 100644
--- a/tests/native/utilities/firewall.cpp
+++ b/tests/native/utilities/firewall.cpp
@@ -60,10 +60,10 @@
// iif should be non-zero if and only if match == MATCH_IIF
if (match == IIF_MATCH && iif == 0) {
return Errorf("Interface match {} must have nonzero interface index",
- static_cast<int>(match));
+ static_cast<uint32_t>(match));
} else if (match != IIF_MATCH && iif != 0) {
return Errorf("Non-interface match {} must have zero interface index",
- static_cast<int>(match));
+ static_cast<uint32_t>(match));
}
std::lock_guard guard(mMutex);
@@ -71,14 +71,14 @@
if (oldMatch.ok()) {
UidOwnerValue newMatch = {
.iif = iif ? iif : oldMatch.value().iif,
- .rule = static_cast<uint8_t>(oldMatch.value().rule | match),
+ .rule = oldMatch.value().rule | match,
};
auto res = mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY);
if (!res.ok()) return Errorf("Failed to update rule: {}", res.error().message());
} else {
UidOwnerValue newMatch = {
.iif = iif,
- .rule = static_cast<uint8_t>(match),
+ .rule = match,
};
auto res = mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY);
if (!res.ok()) return Errorf("Failed to add rule: {}", res.error().message());
@@ -93,7 +93,7 @@
UidOwnerValue newMatch = {
.iif = (match == IIF_MATCH) ? 0 : oldMatch.value().iif,
- .rule = static_cast<uint8_t>(oldMatch.value().rule & ~match),
+ .rule = oldMatch.value().rule & ~match,
};
if (newMatch.rule == 0) {
auto res = mUidOwnerMap.deleteValue(uid);
diff --git a/tests/smoketest/Android.bp b/tests/smoketest/Android.bp
index 4ab24fc..121efa1 100644
--- a/tests/smoketest/Android.bp
+++ b/tests/smoketest/Android.bp
@@ -10,6 +10,7 @@
// TODO: remove this hack when there is a better solution for jni_libs that includes
// dependent libraries.
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 8b286a0..20d457f 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -2,6 +2,7 @@
// Build FrameworksNetTests package
//########################################################################
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "Android-Apache-2.0"
@@ -65,15 +66,13 @@
"java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt",
"java/com/android/internal/net/NetworkUtilsInternalTest.java",
"java/com/android/internal/net/VpnProfileTest.java",
- "java/com/android/server/VpnManagerServiceTest.java",
"java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java",
"java/com/android/server/connectivity/IpConnectivityMetricsTest.java",
"java/com/android/server/connectivity/MetricsTestUtil.java",
"java/com/android/server/connectivity/MultipathPolicyTrackerTest.java",
"java/com/android/server/connectivity/NetdEventListenerServiceTest.java",
- "java/com/android/server/connectivity/VpnTest.java",
"java/com/android/server/net/ipmemorystore/*.java",
- ]
+ ],
}
// Subset of services-core used to by ConnectivityService tests to test VPN realistically.
@@ -115,7 +114,7 @@
"service-connectivity-tiramisu-pre-jarjar",
"services.core-vpn",
"testables",
- "cts-net-utils"
+ "cts-net-utils",
],
libs: [
"android.net.ipsec.ike.stubs.module_lib",
diff --git a/tests/unit/java/android/net/NetworkStatsCollectionTest.java b/tests/unit/java/android/net/NetworkStatsCollectionTest.java
index a6e9e95..81557f8 100644
--- a/tests/unit/java/android/net/NetworkStatsCollectionTest.java
+++ b/tests/unit/java/android/net/NetworkStatsCollectionTest.java
@@ -64,6 +64,7 @@
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
@@ -90,7 +91,8 @@
@SmallTest
@DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
public class NetworkStatsCollectionTest {
-
+ @Rule
+ public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
private static final String TEST_FILE = "test.bin";
private static final String TEST_IMSI = "310260000000000";
private static final int TEST_SUBID = 1;
@@ -199,6 +201,33 @@
77017831L, 100995L, 35436758L, 92344L);
}
+ private InputStream getUidInputStreamFromRes(int uidRes) throws Exception {
+ final File testFile =
+ new File(InstrumentationRegistry.getContext().getFilesDir(), TEST_FILE);
+ stageFile(uidRes, testFile);
+
+ final NetworkStatsCollection collection = new NetworkStatsCollection(30 * MINUTE_IN_MILLIS);
+ collection.readLegacyUid(testFile, true);
+
+ // now export into a unified format
+ final ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ collection.write(bos);
+ return new ByteArrayInputStream(bos.toByteArray());
+ }
+
+ @Test
+ public void testFastDataInputRead() throws Exception {
+ final NetworkStatsCollection legacyCollection =
+ new NetworkStatsCollection(30 * MINUTE_IN_MILLIS, false /* useFastDataInput */);
+ final NetworkStatsCollection fastReadCollection =
+ new NetworkStatsCollection(30 * MINUTE_IN_MILLIS, true /* useFastDataInput */);
+ final InputStream bis = getUidInputStreamFromRes(R.raw.netstats_uid_v4);
+ legacyCollection.read(bis);
+ bis.reset();
+ fastReadCollection.read(bis);
+ assertCollectionEntries(legacyCollection.getEntries(), fastReadCollection);
+ }
+
@Test
public void testStartEndAtomicBuckets() throws Exception {
final NetworkStatsCollection collection = new NetworkStatsCollection(HOUR_IN_MILLIS);
diff --git a/tests/unit/java/android/net/NetworkStatsRecorderTest.java b/tests/unit/java/android/net/NetworkStatsRecorderTest.java
index fad11a3..7d039b6 100644
--- a/tests/unit/java/android/net/NetworkStatsRecorderTest.java
+++ b/tests/unit/java/android/net/NetworkStatsRecorderTest.java
@@ -16,8 +16,17 @@
package com.android.server.net;
+import static android.net.NetworkStats.SET_DEFAULT;
+import static android.net.NetworkStats.SET_FOREGROUND;
+import static android.net.NetworkStats.TAG_NONE;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID_TAG;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_XT;
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UID;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UIDTAG;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_XT;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.mockito.Mockito.any;
@@ -29,21 +38,31 @@
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import android.annotation.NonNull;
+import android.net.NetworkIdentity;
+import android.net.NetworkIdentitySet;
import android.net.NetworkStats;
+import android.net.NetworkStatsCollection;
import android.os.DropBoxManager;
import androidx.test.filters.SmallTest;
import com.android.internal.util.FileRotator;
+import com.android.metrics.NetworkStatsMetricsLogger;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
+import libcore.testing.io.TestIoUtils;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
import java.io.IOException;
@RunWith(DevSdkIgnoreRunner.class)
@@ -53,6 +72,8 @@
private static final String TAG = NetworkStatsRecorderTest.class.getSimpleName();
private static final String TEST_PREFIX = "test";
+ private static final int TEST_UID1 = 1234;
+ private static final int TEST_UID2 = 1235;
@Mock private DropBoxManager mDropBox;
@Mock private NetworkStats.NonMonotonicObserver mObserver;
@@ -64,7 +85,8 @@
private NetworkStatsRecorder buildRecorder(FileRotator rotator, boolean wipeOnError) {
return new NetworkStatsRecorder(rotator, mObserver, mDropBox, TEST_PREFIX,
- HOUR_IN_MILLIS, false /* includeTags */, wipeOnError);
+ HOUR_IN_MILLIS, false /* includeTags */, wipeOnError,
+ false /* useFastDataInput */, null /* baseDir */);
}
@Test
@@ -85,4 +107,110 @@
// Verify that the rotator won't delete files.
verify(rotator, never()).deleteAll();
}
+
+ @Test
+ public void testFileReadingMetrics_empty() {
+ final NetworkStatsCollection collection = new NetworkStatsCollection(30);
+ final NetworkStatsMetricsLogger.Dependencies deps =
+ mock(NetworkStatsMetricsLogger.Dependencies.class);
+ final NetworkStatsMetricsLogger logger = new NetworkStatsMetricsLogger(deps);
+ logger.logRecorderFileReading(PREFIX_XT, 888, null /* statsDir */, collection,
+ false /* useFastDataInput */);
+ verify(deps).writeRecorderFileReadingStats(
+ NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_XT,
+ 1 /* readIndex */,
+ 888 /* readLatencyMillis */,
+ 0 /* fileCount */,
+ 0 /* totalFileSize */,
+ 0 /* keys */,
+ 0 /* uids */,
+ 0 /* totalHistorySize */,
+ false /* useFastDataInput */
+ );
+
+ // Write second time, verify the index increases.
+ logger.logRecorderFileReading(PREFIX_XT, 567, null /* statsDir */, collection,
+ true /* useFastDataInput */);
+ verify(deps).writeRecorderFileReadingStats(
+ NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_XT,
+ 2 /* readIndex */,
+ 567 /* readLatencyMillis */,
+ 0 /* fileCount */,
+ 0 /* totalFileSize */,
+ 0 /* keys */,
+ 0 /* uids */,
+ 0 /* totalHistorySize */,
+ true /* useFastDataInput */
+ );
+ }
+
+ @Test
+ public void testFileReadingMetrics() {
+ final NetworkStatsCollection collection = new NetworkStatsCollection(30);
+ final NetworkStats.Entry entry = new NetworkStats.Entry();
+ final NetworkIdentitySet identSet = new NetworkIdentitySet();
+ identSet.add(new NetworkIdentity.Builder().build());
+ // Empty entries will be skipped, put some ints to make sure they can be recorded.
+ entry.rxBytes = 1;
+
+ collection.recordData(identSet, TEST_UID1, SET_DEFAULT, TAG_NONE, 0, 60, entry);
+ collection.recordData(identSet, TEST_UID2, SET_DEFAULT, TAG_NONE, 0, 60, entry);
+ collection.recordData(identSet, TEST_UID2, SET_FOREGROUND, TAG_NONE, 30, 60, entry);
+
+ final NetworkStatsMetricsLogger.Dependencies deps =
+ mock(NetworkStatsMetricsLogger.Dependencies.class);
+ final NetworkStatsMetricsLogger logger = new NetworkStatsMetricsLogger(deps);
+ logger.logRecorderFileReading(PREFIX_UID, 123, null /* statsDir */, collection,
+ false /* useFastDataInput */);
+ verify(deps).writeRecorderFileReadingStats(
+ NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UID,
+ 1 /* readIndex */,
+ 123 /* readLatencyMillis */,
+ 0 /* fileCount */,
+ 0 /* totalFileSize */,
+ 3 /* keys */,
+ 2 /* uids */,
+ 5 /* totalHistorySize */,
+ false /* useFastDataInput */
+ );
+ }
+
+ @Test
+ public void testFileReadingMetrics_fileAttributes() throws IOException {
+ final NetworkStatsCollection collection = new NetworkStatsCollection(30);
+
+ // Create files for testing. Only the first and the third files should be counted,
+ // with total 26 (each char takes 2 bytes) bytes in the content.
+ final File statsDir = TestIoUtils.createTemporaryDirectory(getClass().getSimpleName());
+ write(statsDir, "uid_tag.1024-2048", "wanted");
+ write(statsDir, "uid_tag.1024-2048.backup", "");
+ write(statsDir, "uid_tag.2048-", "wanted2");
+ write(statsDir, "uid.2048-4096", "unwanted");
+ write(statsDir, "uid.2048-4096.backup", "unwanted2");
+
+ final NetworkStatsMetricsLogger.Dependencies deps =
+ mock(NetworkStatsMetricsLogger.Dependencies.class);
+ final NetworkStatsMetricsLogger logger = new NetworkStatsMetricsLogger(deps);
+ logger.logRecorderFileReading(PREFIX_UID_TAG, 678, statsDir, collection,
+ false /* useFastDataInput */);
+ verify(deps).writeRecorderFileReadingStats(
+ NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UIDTAG,
+ 1 /* readIndex */,
+ 678 /* readLatencyMillis */,
+ 2 /* fileCount */,
+ 26 /* totalFileSize */,
+ 0 /* keys */,
+ 0 /* uids */,
+ 0 /* totalHistorySize */,
+ false /* useFastDataInput */
+ );
+ }
+
+ private void write(@NonNull File baseDir, @NonNull String name,
+ @NonNull String value) throws IOException {
+ final DataOutputStream out = new DataOutputStream(
+ new FileOutputStream(new File(baseDir, name)));
+ out.writeChars(value);
+ out.close();
+ }
}
diff --git a/tests/unit/java/android/net/NetworkUtilsTest.java b/tests/unit/java/android/net/NetworkUtilsTest.java
index 5d789b4..e453c02 100644
--- a/tests/unit/java/android/net/NetworkUtilsTest.java
+++ b/tests/unit/java/android/net/NetworkUtilsTest.java
@@ -21,8 +21,14 @@
import static android.system.OsConstants.SOCK_DGRAM;
import static android.system.OsConstants.SOL_SOCKET;
import static android.system.OsConstants.SO_RCVTIMEO;
+
+import static com.android.compatibility.common.util.PropertyUtil.getVsrApiLevel;
+
import static junit.framework.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
import android.os.Build;
import android.system.ErrnoException;
import android.system.Os;
@@ -38,7 +44,6 @@
import org.junit.runner.RunWith;
import java.io.FileDescriptor;
-import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@@ -154,10 +159,9 @@
return timeval;
}
- @Test
- public void testSetSockOptBytes() throws ErrnoException {
- final FileDescriptor sock = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6);
- final StructTimeval writeTimeval = StructTimeval.fromMillis(1200);
+ private void testSetSockOptBytes(FileDescriptor sock, long timeValMillis)
+ throws ErrnoException {
+ final StructTimeval writeTimeval = StructTimeval.fromMillis(timeValMillis);
byte[] timeval = getTimevalBytes(writeTimeval);
final StructTimeval readTimeval;
@@ -165,6 +169,22 @@
readTimeval = Os.getsockoptTimeval(sock, SOL_SOCKET, SO_RCVTIMEO);
assertEquals(writeTimeval, readTimeval);
+ }
+
+ @Test
+ public void testSetSockOptBytes() throws ErrnoException {
+ final FileDescriptor sock = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6);
+
+ testSetSockOptBytes(sock, 3000);
+
+ testSetSockOptBytes(sock, 5000);
+
SocketUtils.closeSocketQuietly(sock);
}
+
+ @Test
+ public void testIsKernel64Bit() {
+ assumeTrue(getVsrApiLevel() > Build.VERSION_CODES.TIRAMISU);
+ assertTrue(NetworkUtils.isKernel64Bit());
+ }
}
diff --git a/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt b/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt
new file mode 100644
index 0000000..332f2a3
--- /dev/null
+++ b/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.nsd
+
+import android.net.nsd.AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY
+import android.net.nsd.NsdManager.PROTOCOL_DNS_SD
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.parcelingRoundTrip
+import java.time.Duration
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+// TODO: move this class to CTS tests when AdvertisingRequest is made public
+/** Unit tests for {@link AdvertisingRequest}. */
+@IgnoreUpTo(Build.VERSION_CODES.S_V2)
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@ConnectivityModuleTest
+class AdvertisingRequestTest {
+ @Test
+ fun testParcelingIsLossLess() {
+ val info = NsdServiceInfo().apply {
+ serviceType = "_ipp._tcp"
+ }
+ val beforeParcel = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
+ .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+ .setTtl(Duration.ofSeconds(30L))
+ .build()
+
+ val afterParcel = parcelingRoundTrip(beforeParcel)
+
+ assertEquals(beforeParcel.serviceInfo.serviceType, afterParcel.serviceInfo.serviceType)
+ assertEquals(beforeParcel.advertisingConfig, afterParcel.advertisingConfig)
+ }
+
+@Test
+fun testBuilder_setNullTtl_success() {
+ val info = NsdServiceInfo().apply {
+ serviceType = "_ipp._tcp"
+ }
+ val request = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
+ .setTtl(null)
+ .build()
+
+ assertNull(request.ttl)
+}
+
+ @Test
+ fun testBuilder_setPropertiesSuccess() {
+ val info = NsdServiceInfo().apply {
+ serviceType = "_ipp._tcp"
+ }
+ val request = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
+ .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+ .setTtl(Duration.ofSeconds(100L))
+ .build()
+
+ assertEquals("_ipp._tcp", request.serviceInfo.serviceType)
+ assertEquals(PROTOCOL_DNS_SD, request.protocolType)
+ assertEquals(NSD_ADVERTISING_UPDATE_ONLY, request.advertisingConfig)
+ assertEquals(Duration.ofSeconds(100L), request.ttl)
+ }
+
+ @Test
+ fun testEquality() {
+ val info = NsdServiceInfo().apply {
+ serviceType = "_ipp._tcp"
+ }
+ val request1 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD).build()
+ val request2 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD).build()
+ val request3 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
+ .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+ .setTtl(Duration.ofSeconds(120L))
+ .build()
+ val request4 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
+ .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+ .setTtl(Duration.ofSeconds(120L))
+ .build()
+
+ assertEquals(request1, request2)
+ assertEquals(request3, request4)
+ assertNotEquals(request1, request3)
+ assertNotEquals(request2, request4)
+ }
+}
diff --git a/tests/unit/java/android/net/nsd/NsdManagerTest.java b/tests/unit/java/android/net/nsd/NsdManagerTest.java
index 550a9ee..76a649e 100644
--- a/tests/unit/java/android/net/nsd/NsdManagerTest.java
+++ b/tests/unit/java/android/net/nsd/NsdManagerTest.java
@@ -23,6 +23,7 @@
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -38,6 +39,7 @@
import androidx.test.filters.SmallTest;
+import com.android.modules.utils.build.SdkLevel;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
import com.android.testutils.FunctionalUtils.ThrowingConsumer;
@@ -51,6 +53,10 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.net.InetAddress;
+import java.util.List;
+import java.time.Duration;
+
@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
@@ -86,73 +92,81 @@
@Test
@EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testResolveServiceS() throws Exception {
- verify(mServiceConn, never()).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ false);
doTestResolveService();
}
@Test
@DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testResolveServicePreS() throws Exception {
- verify(mServiceConn).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ true);
doTestResolveService();
}
@Test
@EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testDiscoverServiceS() throws Exception {
- verify(mServiceConn, never()).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ false);
doTestDiscoverService();
}
@Test
@DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testDiscoverServicePreS() throws Exception {
- verify(mServiceConn).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ true);
doTestDiscoverService();
}
@Test
@EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testParallelResolveServiceS() throws Exception {
- verify(mServiceConn, never()).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ false);
doTestParallelResolveService();
}
@Test
@DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testParallelResolveServicePreS() throws Exception {
- verify(mServiceConn).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ true);
doTestParallelResolveService();
}
@Test
@EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testInvalidCallsS() throws Exception {
- verify(mServiceConn, never()).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ false);
doTestInvalidCalls();
}
@Test
@DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testInvalidCallsPreS() throws Exception {
- verify(mServiceConn).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ true);
doTestInvalidCalls();
}
@Test
@EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testRegisterServiceS() throws Exception {
- verify(mServiceConn, never()).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ false);
doTestRegisterService();
}
@Test
@DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testRegisterServicePreS() throws Exception {
- verify(mServiceConn).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ true);
doTestRegisterService();
}
+ private void verifyDaemonStarted(boolean targetSdkPreS) throws Exception {
+ if (targetSdkPreS && !SdkLevel.isAtLeastV()) {
+ verify(mServiceConn).startDaemon();
+ } else {
+ verify(mServiceConn, never()).startDaemon();
+ }
+ }
+
private void doTestResolveService() throws Exception {
NsdManager manager = mManager;
@@ -196,6 +210,39 @@
verify(listener2, timeout(mTimeoutMs).times(1)).onServiceResolved(reply);
}
+ @Test
+ public void testRegisterServiceWithAdvertisingRequest() throws Exception {
+ final NsdManager manager = mManager;
+ final NsdServiceInfo request = new NsdServiceInfo("another_name2", "another_type2");
+ request.setPort(2203);
+ final AdvertisingRequest advertisingRequest = new AdvertisingRequest.Builder(request,
+ PROTOCOL).build();
+ final NsdManager.RegistrationListener listener = mock(
+ NsdManager.RegistrationListener.class);
+
+ manager.registerService(advertisingRequest, Runnable::run, listener);
+ int key4 = getRequestKey(req -> verify(mServiceConn).registerService(req.capture(), any()));
+ mCallback.onRegisterServiceSucceeded(key4, request);
+ verify(listener, timeout(mTimeoutMs).times(1)).onServiceRegistered(request);
+ }
+
+ @Test
+ public void testRegisterServiceWithCustomTtl() throws Exception {
+ final NsdManager manager = mManager;
+ final NsdServiceInfo info = new NsdServiceInfo("another_name2", "another_type2");
+ info.setPort(2203);
+ final AdvertisingRequest request = new AdvertisingRequest.Builder(info, PROTOCOL)
+ .setTtl(Duration.ofSeconds(30)).build();
+ final NsdManager.RegistrationListener listener = mock(
+ NsdManager.RegistrationListener.class);
+
+ manager.registerService(request, Runnable::run, listener);
+
+ AdvertisingRequest capturedRequest = getAdvertisingRequest(
+ req -> verify(mServiceConn).registerService(anyInt(), req.capture()));
+ assertEquals(request, capturedRequest);
+ }
+
private void doTestRegisterService() throws Exception {
NsdManager manager = mManager;
@@ -260,6 +307,7 @@
private void doTestDiscoverService() throws Exception {
NsdManager manager = mManager;
+ DiscoveryRequest request1 = new DiscoveryRequest.Builder("a_type").build();
NsdServiceInfo reply1 = new NsdServiceInfo("a_name", "a_type");
NsdServiceInfo reply2 = new NsdServiceInfo("another_name", "a_type");
NsdServiceInfo reply3 = new NsdServiceInfo("a_third_name", "a_type");
@@ -280,7 +328,7 @@
int key2 = getRequestKey(req ->
verify(mServiceConn, times(2)).discoverServices(req.capture(), any()));
- mCallback.onDiscoverServicesStarted(key2, reply1);
+ mCallback.onDiscoverServicesStarted(key2, request1);
verify(listener, timeout(mTimeoutMs).times(1)).onDiscoveryStarted("a_type");
@@ -320,7 +368,7 @@
int key3 = getRequestKey(req ->
verify(mServiceConn, times(3)).discoverServices(req.capture(), any()));
- mCallback.onDiscoverServicesStarted(key3, reply1);
+ mCallback.onDiscoverServicesStarted(key3, request1);
verify(listener, timeout(mTimeoutMs).times(1)).onDiscoveryStarted("a_type");
// Client unregisters immediately, it fails
@@ -344,10 +392,52 @@
NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class);
NsdManager.DiscoveryListener listener2 = mock(NsdManager.DiscoveryListener.class);
NsdManager.ResolveListener listener3 = mock(NsdManager.ResolveListener.class);
+ NsdManager.RegistrationListener listener4 = mock(NsdManager.RegistrationListener.class);
+ NsdManager.RegistrationListener listener5 = mock(NsdManager.RegistrationListener.class);
+ NsdManager.RegistrationListener listener6 = mock(NsdManager.RegistrationListener.class);
NsdServiceInfo invalidService = new NsdServiceInfo(null, null);
- NsdServiceInfo validService = new NsdServiceInfo("a_name", "a_type");
+ NsdServiceInfo validService = new NsdServiceInfo("a_name", "_a_type._tcp");
+ NsdServiceInfo otherServiceWithSubtype = new NsdServiceInfo("b_name", "_a_type._tcp,_sub1");
+ NsdServiceInfo validServiceDuplicate = new NsdServiceInfo("a_name", "_a_type._tcp");
+ NsdServiceInfo validServiceSubtypeUpdate = new NsdServiceInfo("a_name",
+ "_a_type._tcp,_sub1,_s2");
+ NsdServiceInfo otherSubtypeUpdate = new NsdServiceInfo("a_name", "_a_type._tcp,_sub1,_s3");
+ NsdServiceInfo dotSyntaxSubtypeUpdate = new NsdServiceInfo("a_name", "_sub1._a_type._tcp");
+
validService.setPort(2222);
+ otherServiceWithSubtype.setPort(2222);
+ validServiceDuplicate.setPort(2222);
+ validServiceSubtypeUpdate.setPort(2222);
+ otherSubtypeUpdate.setPort(2222);
+ dotSyntaxSubtypeUpdate.setPort(2222);
+
+ NsdServiceInfo invalidMissingHostnameWithAddresses = new NsdServiceInfo(null, null);
+ invalidMissingHostnameWithAddresses.setHostAddresses(
+ List.of(
+ InetAddress.parseNumericAddress("192.168.82.14"),
+ InetAddress.parseNumericAddress("2001::1")));
+
+ NsdServiceInfo validCustomHostWithAddresses = new NsdServiceInfo(null, null);
+ validCustomHostWithAddresses.setHostname("a_host");
+ validCustomHostWithAddresses.setHostAddresses(
+ List.of(
+ InetAddress.parseNumericAddress("192.168.82.14"),
+ InetAddress.parseNumericAddress("2001::1")));
+
+ NsdServiceInfo validServiceWithCustomHostAndAddresses =
+ new NsdServiceInfo("a_name", "_a_type._tcp");
+ validServiceWithCustomHostAndAddresses.setPort(2222);
+ validServiceWithCustomHostAndAddresses.setHostname("a_host");
+ validServiceWithCustomHostAndAddresses.setHostAddresses(
+ List.of(
+ InetAddress.parseNumericAddress("192.168.82.14"),
+ InetAddress.parseNumericAddress("2001::1")));
+
+ NsdServiceInfo validServiceWithCustomHostNoAddresses =
+ new NsdServiceInfo("a_name", "_a_type._tcp");
+ validServiceWithCustomHostNoAddresses.setPort(2222);
+ validServiceWithCustomHostNoAddresses.setHostname("a_host");
// Service registration
// - invalid arguments
@@ -357,13 +447,38 @@
mustFail(() -> { manager.registerService(invalidService, PROTOCOL, listener1); });
mustFail(() -> { manager.registerService(validService, -1, listener1); });
mustFail(() -> { manager.registerService(validService, PROTOCOL, null); });
+ mustFail(() -> {
+ manager.registerService(invalidMissingHostnameWithAddresses, PROTOCOL, listener1); });
manager.registerService(validService, PROTOCOL, listener1);
- // - listener already registered
+ // - update without subtype is not allowed
+ mustFail(() -> { manager.registerService(validServiceDuplicate, PROTOCOL, listener1); });
+ // - update with subtype is allowed
+ manager.registerService(validServiceSubtypeUpdate, PROTOCOL, listener1);
+ // - re-updating to the same subtype is allowed
+ manager.registerService(validServiceSubtypeUpdate, PROTOCOL, listener1);
+ // - updating to other subtypes is allowed
+ manager.registerService(otherSubtypeUpdate, PROTOCOL, listener1);
+ // - update back to the service without subtype is allowed
+ manager.registerService(validService, PROTOCOL, listener1);
+ // - updating to a subtype with _sub._type syntax is not allowed
+ mustFail(() -> { manager.registerService(dotSyntaxSubtypeUpdate, PROTOCOL, listener1); });
+ // - updating to a different service name is not allowed
+ mustFail(() -> { manager.registerService(otherServiceWithSubtype, PROTOCOL, listener1); });
+ // - listener already registered, and not using subtypes
mustFail(() -> { manager.registerService(validService, PROTOCOL, listener1); });
manager.unregisterService(listener1);
// TODO: make listener immediately reusable
//mustFail(() -> { manager.unregisterService(listener1); });
//manager.registerService(validService, PROTOCOL, listener1);
+ // - registering a custom host without a service is valid
+ manager.registerService(validCustomHostWithAddresses, PROTOCOL, listener4);
+ manager.unregisterService(listener4);
+ // - registering a service with a custom host is valid
+ manager.registerService(validServiceWithCustomHostAndAddresses, PROTOCOL, listener5);
+ manager.unregisterService(listener5);
+ // - registering a service with a custom host with no addresses is valid
+ manager.registerService(validServiceWithCustomHostNoAddresses, PROTOCOL, listener6);
+ manager.unregisterService(listener6);
// Discover service
// - invalid arguments
@@ -405,4 +520,12 @@
verifier.accept(captor);
return captor.getValue();
}
+
+ AdvertisingRequest getAdvertisingRequest(
+ ThrowingConsumer<ArgumentCaptor<AdvertisingRequest>> verifier) throws Exception {
+ final ArgumentCaptor<AdvertisingRequest> captor =
+ ArgumentCaptor.forClass(AdvertisingRequest.class);
+ verifier.accept(captor);
+ return captor.getValue();
+ }
}
diff --git a/tests/unit/java/android/net/util/KeepaliveUtilsTest.kt b/tests/unit/java/android/net/util/KeepaliveUtilsTest.kt
index cb3a315..470274d 100644
--- a/tests/unit/java/android/net/util/KeepaliveUtilsTest.kt
+++ b/tests/unit/java/android/net/util/KeepaliveUtilsTest.kt
@@ -95,11 +95,11 @@
// Check resource with invalid transport type.
assertRunWithException(arrayOf("-1,3"))
- assertRunWithException(arrayOf("10,3"))
+ assertRunWithException(arrayOf("11,3"))
// Check valid customization generates expected array.
val validRes = arrayOf("0,3", "1,0", "4,4")
- val expectedValidRes = intArrayOf(3, 0, 0, 0, 4, 0, 0, 0, 0, 0)
+ val expectedValidRes = intArrayOf(3, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0)
val mockContext = getMockedContextWithStringArrayRes(
R.array.config_networkSupportedKeepaliveCount,
diff --git a/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt b/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt
index 3043d50..53baee1 100644
--- a/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt
+++ b/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt
@@ -16,6 +16,7 @@
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.net.NetworkScore
+import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
import android.net.NetworkScore.POLICY_EXITING
import android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY
import android.os.Build
@@ -86,7 +87,10 @@
.addCapability(NET_CAPABILITY_NOT_SUSPENDED)
.addCapability(NET_CAPABILITY_NOT_ROAMING)
.build()
- val wifi1Score = NetworkScore.Builder().setExiting(true).build()
+ val wifi1Score = NetworkScore.Builder()
+ .setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST)
+ .setExiting(true)
+ .build()
val agentWifi1 = Agent(nc = wifi1Caps, score = FromS(wifi1Score)).also { it.connect() }
val wifi2Caps = NetworkCapabilities.Builder()
@@ -96,7 +100,10 @@
.addCapability(NET_CAPABILITY_NOT_ROAMING)
.addEnterpriseId(NET_ENTERPRISE_ID_3)
.build()
- val wifi2Score = NetworkScore.Builder().setTransportPrimary(true).build()
+ val wifi2Score = NetworkScore.Builder()
+ .setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST)
+ .setTransportPrimary(true)
+ .build()
val agentWifi2 = Agent(nc = wifi2Caps, score = FromS(wifi2Score)).also { it.connect() }
val cellCaps = NetworkCapabilities.Builder()
@@ -107,7 +114,9 @@
.addCapability(NET_CAPABILITY_NOT_ROAMING)
.addEnterpriseId(NET_ENTERPRISE_ID_1)
.build()
- val cellScore = NetworkScore.Builder().build()
+ val cellScore = NetworkScore.Builder()
+ .setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST)
+ .build()
val agentCell = Agent(nc = cellCaps, score = FromS(cellScore)).also { it.connect() }
val stats = csHandler.onHandler { service.sampleConnectivityState() }
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 8f5fd7c..c534025 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -157,6 +157,7 @@
import static android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH;
import static android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_LOW;
+import static com.android.server.ConnectivityService.ALLOW_SATALLITE_NETWORK_FALLBACK;
import static com.android.server.ConnectivityService.DELAY_DESTROY_FROZEN_SOCKETS_VERSION;
import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
import static com.android.server.ConnectivityService.ALLOW_SYSUI_CONNECTIVITY_REPORTS;
@@ -420,6 +421,7 @@
import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
import com.android.server.connectivity.ProxyTracker;
import com.android.server.connectivity.QosCallbackTracker;
+import com.android.server.connectivity.SatelliteAccessController;
import com.android.server.connectivity.TcpKeepaliveController;
import com.android.server.connectivity.UidRangeUtils;
import com.android.server.connectivity.VpnProfileStore;
@@ -485,6 +487,7 @@
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
@@ -502,6 +505,7 @@
// to enable faster testing of smaller groups of functionality.
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
+@DevSdkIgnoreRunner.MonitorThreadLeak
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
public class ConnectivityServiceTest {
private static final String TAG = "ConnectivityServiceTest";
@@ -522,7 +526,7 @@
// between a LOST callback that arrives immediately and a LOST callback that arrives after
// the linger/nascent timeout. For this, our assertions should run fast enough to leave
// less than (mService.mLingerDelayMs - TEST_CALLBACK_TIMEOUT_MS) between the time callbacks are
- // supposedly fired, and the time we call expectCallback.
+ // supposedly fired, and the time we call expectCapChanged.
private static final int TEST_CALLBACK_TIMEOUT_MS = 250;
// Chosen to be less than TEST_CALLBACK_TIMEOUT_MS. This ensures that requests have time to
// complete before callbacks are verified.
@@ -561,6 +565,7 @@
private static final int TEST_PACKAGE_UID2 = 321;
private static final int TEST_PACKAGE_UID3 = 456;
private static final int NETWORK_ACTIVITY_NO_UID = -1;
+ private static final int TEST_SUBSCRIPTION_ID = 1;
private static final int PACKET_WAKEUP_MARK_MASK = 0x80000000;
@@ -589,6 +594,7 @@
private TestNetworkAgentWrapper mWiFiAgent;
private TestNetworkAgentWrapper mCellAgent;
private TestNetworkAgentWrapper mEthernetAgent;
+ private final List<TestNetworkAgentWrapper> mCreatedAgents = new ArrayList<>();
private MockVpn mMockVpn;
private Context mContext;
private NetworkPolicyCallback mPolicyCallback;
@@ -639,6 +645,7 @@
@Mock DestroySocketsWrapper mDestroySocketsWrapper;
@Mock SubscriptionManager mSubscriptionManager;
@Mock KeepaliveTracker.Dependencies mMockKeepaliveTrackerDependencies;
+ @Mock SatelliteAccessController mSatelliteAccessController;
// BatteryStatsManager is final and cannot be mocked with regular mockito, so just mock the
// underlying binder calls.
@@ -1092,6 +1099,7 @@
NetworkCapabilities ncTemplate, NetworkProvider provider,
NetworkAgentWrapper.Callbacks callbacks) throws Exception {
super(transport, linkProperties, ncTemplate, provider, callbacks, mServiceContext);
+ mCreatedAgents.add(this);
// Waits for the NetworkAgent to be registered, which includes the creation of the
// NetworkMonitor.
@@ -2050,11 +2058,21 @@
@Override
public CarrierPrivilegeAuthenticator makeCarrierPrivilegeAuthenticator(
@NonNull final Context context,
- @NonNull final TelephonyManager tm) {
+ @NonNull final TelephonyManager tm,
+ final boolean requestRestrictedWifiEnabled,
+ BiConsumer<Integer, Integer> listener) {
return mDeps.isAtLeastT() ? mCarrierPrivilegeAuthenticator : null;
}
@Override
+ public SatelliteAccessController makeSatelliteAccessController(
+ @NonNull final Context context,
+ Consumer<Set<Integer>> updateSatelliteNetworkFallbackUidCallback,
+ @NonNull final Handler connectivityServiceInternalHandler) {
+ return mSatelliteAccessController;
+ }
+
+ @Override
public boolean intentFilterEquals(final PendingIntent a, final PendingIntent b) {
return runAsShell(GET_INTENT_SENDER_INTENT, () -> a.intentFilterEquals(b));
}
@@ -2144,6 +2162,8 @@
case ConnectivityFlags.NO_REMATCH_ALL_REQUESTS_ON_REGISTER:
case ConnectivityFlags.CARRIER_SERVICE_CHANGED_USE_CALLBACK:
return true;
+ case ConnectivityFlags.REQUEST_RESTRICTED_WIFI:
+ return true;
case KEY_DESTROY_FROZEN_SOCKETS_VERSION:
return true;
case DELAY_DESTROY_FROZEN_SOCKETS_VERSION:
@@ -2160,6 +2180,8 @@
return true;
case LOG_BPF_RC:
return true;
+ case ALLOW_SATALLITE_NETWORK_FALLBACK:
+ return true;
default:
return super.isFeatureNotChickenedOut(context, name);
}
@@ -2404,6 +2426,11 @@
FakeSettingsProvider.clearSettingsProvider();
ConnectivityResources.setResourcesContextForTest(null);
+ for (TestNetworkAgentWrapper agent : mCreatedAgents) {
+ agent.destroy();
+ }
+ mCreatedAgents.clear();
+
mCsHandlerThread.quitSafely();
mCsHandlerThread.join();
mAlarmManagerThread.quitSafely();
@@ -11460,7 +11487,7 @@
doTestInterfaceClassActivityChanged(TRANSPORT_CELLULAR);
}
- private void doTestOnNetworkActive_NewNetworkConnects(int transportType, boolean expectCallback)
+ private void doTestOnNetworkActive_NewNetworkConnects(int transportType, boolean expectCapChanged)
throws Exception {
final ConditionVariable onNetworkActiveCv = new ConditionVariable();
final ConnectivityManager.OnNetworkActiveListener listener = onNetworkActiveCv::open;
@@ -11472,7 +11499,7 @@
testAndCleanup(() -> {
mCm.addDefaultNetworkActiveListener(listener);
agent.connect(true);
- if (expectCallback) {
+ if (expectCapChanged) {
assertTrue(onNetworkActiveCv.block(TEST_CALLBACK_TIMEOUT_MS));
} else {
assertFalse(onNetworkActiveCv.block(TEST_CALLBACK_TIMEOUT_MS));
@@ -11487,7 +11514,7 @@
@Test
public void testOnNetworkActive_NewCellConnects_CallbackCalled() throws Exception {
- doTestOnNetworkActive_NewNetworkConnects(TRANSPORT_CELLULAR, true /* expectCallback */);
+ doTestOnNetworkActive_NewNetworkConnects(TRANSPORT_CELLULAR, true /* expectCapChanged */);
}
@Test
@@ -11496,8 +11523,8 @@
// networks that tracker adds the idle timer to. And the tracker does not set the idle timer
// for the ethernet network.
// So onNetworkActive is not called when the ethernet becomes the default network
- final boolean expectCallback = mDeps.isAtLeastV();
- doTestOnNetworkActive_NewNetworkConnects(TRANSPORT_ETHERNET, expectCallback);
+ final boolean expectCapChanged = mDeps.isAtLeastV();
+ doTestOnNetworkActive_NewNetworkConnects(TRANSPORT_ETHERNET, expectCapChanged);
}
@Test
@@ -12921,7 +12948,7 @@
mServiceContext.setPermission(NETWORK_STACK, PERMISSION_GRANTED);
assertTrue(
"NetworkStack permission not applied",
- mService.checkConnectivityDiagnosticsPermissions(
+ mService.hasConnectivityDiagnosticsPermissions(
Process.myPid(), Process.myUid(), naiWithoutUid,
mContext.getOpPackageName()));
}
@@ -12933,7 +12960,7 @@
mServiceContext.setPermission(STATUS_BAR_SERVICE, PERMISSION_GRANTED);
assertTrue(
"SysUi permission (STATUS_BAR_SERVICE) not applied",
- mService.checkConnectivityDiagnosticsPermissions(
+ mService.hasConnectivityDiagnosticsPermissions(
Process.myPid(), Process.myUid(), naiWithoutUid,
mContext.getOpPackageName()));
}
@@ -12950,7 +12977,7 @@
assertFalse(
"Mismatched uid/package name should not pass the location permission check",
- mService.checkConnectivityDiagnosticsPermissions(
+ mService.hasConnectivityDiagnosticsPermissions(
Process.myPid() + 1, wrongUid, naiWithUid, mContext.getOpPackageName()));
}
@@ -12961,7 +12988,7 @@
assertEquals(
"Unexpected ConnDiags permission",
expectPermission,
- mService.checkConnectivityDiagnosticsPermissions(
+ mService.hasConnectivityDiagnosticsPermissions(
Process.myPid(), Process.myUid(), info, mContext.getOpPackageName()));
}
@@ -13003,7 +13030,7 @@
waitForIdle();
assertTrue(
"Active VPN permission not applied",
- mService.checkConnectivityDiagnosticsPermissions(
+ mService.hasConnectivityDiagnosticsPermissions(
Process.myPid(), Process.myUid(), naiWithoutUid,
mContext.getOpPackageName()));
@@ -13011,7 +13038,7 @@
waitForIdle();
assertFalse(
"VPN shouldn't receive callback on non-underlying network",
- mService.checkConnectivityDiagnosticsPermissions(
+ mService.hasConnectivityDiagnosticsPermissions(
Process.myPid(), Process.myUid(), naiWithoutUid,
mContext.getOpPackageName()));
}
@@ -13028,7 +13055,7 @@
assertTrue(
"NetworkCapabilities administrator uid permission not applied",
- mService.checkConnectivityDiagnosticsPermissions(
+ mService.hasConnectivityDiagnosticsPermissions(
Process.myPid(), Process.myUid(), naiWithUid, mContext.getOpPackageName()));
}
@@ -13046,7 +13073,7 @@
// Use wrong pid and uid
assertFalse(
"Permissions allowed when they shouldn't be granted",
- mService.checkConnectivityDiagnosticsPermissions(
+ mService.hasConnectivityDiagnosticsPermissions(
Process.myPid() + 1, Process.myUid() + 1, naiWithUid,
mContext.getOpPackageName()));
}
@@ -17345,6 +17372,14 @@
.build();
}
+ private NetworkRequest getRestrictedRequestForWifiWithSubIds() {
+ return new NetworkRequest.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+ .setSubscriptionIds(Collections.singleton(TEST_SUBSCRIPTION_ID))
+ .build();
+ }
+
@Test
public void testNetworkRequestWithSubIdsWithNetworkFactoryPermission() throws Exception {
mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_GRANTED);
@@ -17378,6 +17413,141 @@
}
@Test
+ @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+ public void testCarrierConfigAppSendNetworkRequestForRestrictedWifi() throws Exception {
+ mServiceContext.setPermission(CONNECTIVITY_USE_RESTRICTED_NETWORKS, PERMISSION_DENIED);
+ doReturn(true).when(mCarrierPrivilegeAuthenticator)
+ .isCarrierServiceUidForNetworkCapabilities(anyInt(), any());
+ final PendingIntent pendingIntent = PendingIntent.getBroadcast(
+ mContext, 0 /* requestCode */, new Intent("a"), FLAG_IMMUTABLE);
+ final NetworkCallback networkCallback1 = new NetworkCallback();
+ final NetworkCallback networkCallback2 = new NetworkCallback();
+
+ mCm.requestNetwork(
+ getRestrictedRequestForWifiWithSubIds(), networkCallback1);
+ mCm.requestNetwork(
+ getRestrictedRequestForWifiWithSubIds(), pendingIntent);
+ mCm.registerNetworkCallback(
+ getRestrictedRequestForWifiWithSubIds(), networkCallback2);
+
+ mCm.unregisterNetworkCallback(networkCallback1);
+ mCm.releaseNetworkRequest(pendingIntent);
+ mCm.unregisterNetworkCallback(networkCallback2);
+ }
+
+ private void doTestNetworkRequestWithCarrierPrivilegesLost(
+ boolean shouldGrantRestrictedNetworkPermission,
+ int lostPrivilegeUid,
+ int lostPrivilegeSubId,
+ boolean expectUnavailable,
+ boolean expectCapChanged) throws Exception {
+ if (shouldGrantRestrictedNetworkPermission) {
+ mServiceContext.setPermission(CONNECTIVITY_USE_RESTRICTED_NETWORKS, PERMISSION_GRANTED);
+ } else {
+ mServiceContext.setPermission(CONNECTIVITY_USE_RESTRICTED_NETWORKS, PERMISSION_DENIED);
+ }
+
+ NetworkCapabilities filter =
+ getRestrictedRequestForWifiWithSubIds().networkCapabilities;
+ final HandlerThread handlerThread = new HandlerThread("testRestrictedFactoryRequests");
+ handlerThread.start();
+
+ final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(),
+ mServiceContext, "testFactory", filter, mCsHandlerThread);
+ testFactory.register();
+ testFactory.assertRequestCountEquals(0);
+
+ doReturn(true).when(mCarrierPrivilegeAuthenticator)
+ .isCarrierServiceUidForNetworkCapabilities(eq(Process.myUid()), any());
+ final TestNetworkCallback networkCallback = new TestNetworkCallback();
+ final NetworkRequest networkrequest =
+ getRestrictedRequestForWifiWithSubIds();
+ mCm.requestNetwork(networkrequest, networkCallback);
+ testFactory.expectRequestAdd();
+ testFactory.assertRequestCountEquals(1);
+
+ NetworkCapabilities nc = new NetworkCapabilities.Builder(filter)
+ .setAllowedUids(Set.of(Process.myUid()))
+ .build();
+ mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, new LinkProperties(), nc);
+ mWiFiAgent.connect(false);
+ networkCallback.expectAvailableCallbacksUnvalidated(mWiFiAgent);
+ final NetworkAgentInfo nai = mService.getNetworkAgentInfoForNetwork(
+ mWiFiAgent.getNetwork());
+
+ doReturn(false).when(mCarrierPrivilegeAuthenticator)
+ .isCarrierServiceUidForNetworkCapabilities(eq(Process.myUid()), any());
+ doReturn(TEST_SUBSCRIPTION_ID).when(mCarrierPrivilegeAuthenticator)
+ .getSubIdFromNetworkCapabilities(any());
+ mService.onCarrierPrivilegesLost(lostPrivilegeUid, lostPrivilegeSubId);
+ waitForIdle();
+
+ if (expectCapChanged) {
+ networkCallback.expect(NETWORK_CAPS_UPDATED);
+ }
+ if (expectUnavailable) {
+ networkCallback.expect(UNAVAILABLE);
+ }
+ if (!expectCapChanged && !expectUnavailable) {
+ networkCallback.assertNoCallback();
+ }
+
+ mWiFiAgent.disconnect();
+ waitForIdle();
+
+ if (expectUnavailable) {
+ testFactory.assertRequestCountEquals(0);
+ } else {
+ testFactory.assertRequestCountEquals(1);
+ }
+
+ handlerThread.quitSafely();
+ handlerThread.join();
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+ public void testRestrictedRequestRemovedDueToCarrierPrivilegesLost() throws Exception {
+ doTestNetworkRequestWithCarrierPrivilegesLost(
+ false /* shouldGrantRestrictedNetworkPermission */,
+ Process.myUid(),
+ TEST_SUBSCRIPTION_ID,
+ true /* expectUnavailable */,
+ true /* expectCapChanged */);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+ public void testRequestNotRemoved_MismatchSubId() throws Exception {
+ doTestNetworkRequestWithCarrierPrivilegesLost(
+ false /* shouldGrantRestrictedNetworkPermission */,
+ Process.myUid(),
+ TEST_SUBSCRIPTION_ID + 1,
+ false /* expectUnavailable */,
+ false /* expectCapChanged */);
+ }
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+ public void testRequestNotRemoved_MismatchUid() throws Exception {
+ doTestNetworkRequestWithCarrierPrivilegesLost(
+ false /* shouldGrantRestrictedNetworkPermission */,
+ Process.myUid() + 1,
+ TEST_SUBSCRIPTION_ID,
+ false /* expectUnavailable */,
+ false /* expectCapChanged */);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+ public void testRequestNotRemoved_HasRestrictedNetworkPermission() throws Exception {
+ doTestNetworkRequestWithCarrierPrivilegesLost(
+ true /* shouldGrantRestrictedNetworkPermission */,
+ Process.myUid(),
+ TEST_SUBSCRIPTION_ID,
+ false /* expectUnavailable */,
+ true /* expectCapChanged */);
+ }
+ @Test
public void testAllowedUids() throws Exception {
final int preferenceOrder =
ConnectivityService.PREFERENCE_ORDER_IRRELEVANT_BECAUSE_NOT_DEFAULT;
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 32014c2..881de56 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -34,6 +34,7 @@
import static android.net.connectivity.ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER;
import static android.net.nsd.NsdManager.FAILURE_BAD_PARAMETERS;
import static android.net.nsd.NsdManager.FAILURE_INTERNAL_ERROR;
+import static android.net.nsd.NsdManager.FAILURE_MAX_LIMIT;
import static android.net.nsd.NsdManager.FAILURE_OPERATION_NOT_RUNNING;
import static com.android.networkstack.apishim.api33.ConstantsShim.REGISTER_NSD_OFFLOAD_ENGINE;
@@ -82,6 +83,7 @@
import android.net.mdns.aidl.IMDnsEventListener;
import android.net.mdns.aidl.RegistrationInfo;
import android.net.mdns.aidl.ResolutionInfo;
+import android.net.nsd.AdvertisingRequest;
import android.net.nsd.INsdManagerCallback;
import android.net.nsd.INsdServiceConnector;
import android.net.nsd.MDnsManager;
@@ -100,6 +102,7 @@
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
+import android.os.Process;
import android.os.RemoteException;
import android.util.Pair;
@@ -109,6 +112,7 @@
import com.android.metrics.NetworkNsdReportedMetrics;
import com.android.server.NsdService.Dependencies;
import com.android.server.connectivity.mdns.MdnsAdvertiser;
+import com.android.server.connectivity.mdns.MdnsAdvertisingOptions;
import com.android.server.connectivity.mdns.MdnsDiscoveryManager;
import com.android.server.connectivity.mdns.MdnsInterfaceSocket;
import com.android.server.connectivity.mdns.MdnsSearchOptions;
@@ -131,15 +135,20 @@
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import java.net.InetAddress;
import java.net.UnknownHostException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Queue;
+import java.util.Set;
// TODOs:
// - test client can send requests and receive replies
@@ -149,6 +158,9 @@
@SmallTest
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
public class NsdServiceTest {
+ @Rule
+ public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
static final int PROTOCOL = NsdManager.PROTOCOL_DNS_SD;
private static final long CLEANUP_DELAY_MS = 500;
private static final long TIMEOUT_MS = 500;
@@ -230,7 +242,8 @@
doReturn(mSocketProvider).when(mDeps).makeMdnsSocketProvider(any(), any(), any(), any());
doReturn(DEFAULT_RUNNING_APP_ACTIVE_IMPORTANCE_CUTOFF).when(mDeps).getDeviceConfigInt(
eq(NsdService.MDNS_CONFIG_RUNNING_APP_ACTIVE_IMPORTANCE_CUTOFF), anyInt());
- doReturn(mAdvertiser).when(mDeps).makeMdnsAdvertiser(any(), any(), any(), any(), any());
+ doReturn(mAdvertiser).when(mDeps).makeMdnsAdvertiser(any(), any(), any(), any(), any(),
+ any());
doReturn(mMetrics).when(mDeps).makeNetworkNsdReportedMetrics(anyInt());
doReturn(mClock).when(mDeps).makeClock();
doReturn(TEST_TIME_MS).when(mClock).elapsedRealtime();
@@ -252,8 +265,14 @@
mThread.quitSafely();
mThread.join();
}
+
+ // Clear inline mocks as there are possible memory leaks if not done (see mockito
+ // doc for clearInlineMocks), and some tests create many of them.
+ Mockito.framework().clearInlineMocks();
}
+ // Native mdns provided by Netd is removed after U.
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Test
@DisableCompatChanges({
RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER,
@@ -286,6 +305,7 @@
@Test
@EnableCompatChanges(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testNoDaemonStartedWhenClientsConnect() throws Exception {
// Creating an NsdManager will not cause daemon startup.
connectClient(mService);
@@ -321,6 +341,7 @@
@Test
@EnableCompatChanges(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testClientRequestsAreGCedAtDisconnection() throws Exception {
final NsdManager client = connectClient(mService);
final INsdManagerCallback cb1 = getCallback();
@@ -365,6 +386,7 @@
@Test
@EnableCompatChanges(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testCleanupDelayNoRequestActive() throws Exception {
final NsdManager client = connectClient(mService);
@@ -401,6 +423,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testDiscoverOnTetheringDownstream() throws Exception {
final NsdManager client = connectClient(mService);
final int interfaceIdx = 123;
@@ -499,6 +522,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testDiscoverOnBlackholeNetwork() throws Exception {
final NsdManager client = connectClient(mService);
final DiscoveryListener discListener = mock(DiscoveryListener.class);
@@ -531,6 +555,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testServiceRegistrationSuccessfulAndFailed() throws Exception {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -585,6 +610,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testServiceDiscoveryFailed() throws Exception {
final NsdManager client = connectClient(mService);
final DiscoveryListener discListener = mock(DiscoveryListener.class);
@@ -617,6 +643,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testServiceResolutionFailed() throws Exception {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -652,6 +679,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testGettingAddressFailed() throws Exception {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -701,8 +729,89 @@
true /* isLegacy */, getAddrId, 10L /* durationMs */);
}
+ @EnableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @Test
+ public void testPerClientListenerLimit() throws Exception {
+ final NsdManager client1 = connectClient(mService);
+ final NsdManager client2 = connectClient(mService);
+
+ final String testType1 = "_testtype1._tcp";
+ final NsdServiceInfo testServiceInfo1 = new NsdServiceInfo("MyTestService1", testType1);
+ testServiceInfo1.setPort(12345);
+ final String testType2 = "_testtype2._tcp";
+ final NsdServiceInfo testServiceInfo2 = new NsdServiceInfo("MyTestService2", testType2);
+ testServiceInfo2.setPort(12345);
+
+ // Each client can register 200 requests (for example 100 discover and 100 register).
+ final int numEachListener = 100;
+ final ArrayList<DiscoveryListener> discListeners = new ArrayList<>(numEachListener);
+ final ArrayList<RegistrationListener> regListeners = new ArrayList<>(numEachListener);
+ for (int i = 0; i < numEachListener; i++) {
+ final DiscoveryListener discListener1 = mock(DiscoveryListener.class);
+ discListeners.add(discListener1);
+ final RegistrationListener regListener1 = mock(RegistrationListener.class);
+ regListeners.add(regListener1);
+ final DiscoveryListener discListener2 = mock(DiscoveryListener.class);
+ discListeners.add(discListener2);
+ final RegistrationListener regListener2 = mock(RegistrationListener.class);
+ regListeners.add(regListener2);
+ client1.discoverServices(testType1, NsdManager.PROTOCOL_DNS_SD,
+ (Network) null, Runnable::run, discListener1);
+ client1.registerService(testServiceInfo1, NsdManager.PROTOCOL_DNS_SD, Runnable::run,
+ regListener1);
+
+ client2.registerService(testServiceInfo2, NsdManager.PROTOCOL_DNS_SD, Runnable::run,
+ regListener2);
+ client2.discoverServices(testType2, NsdManager.PROTOCOL_DNS_SD,
+ (Network) null, Runnable::run, discListener2);
+ }
+
+ // Use a longer timeout than usual for the handler to process all the events. The
+ // registrations take about 1s on a high-end 2013 device.
+ HandlerUtils.waitForIdle(mHandler, 30_000L);
+ for (int i = 0; i < discListeners.size(); i++) {
+ // Callbacks are sent on the manager handler which is different from mHandler, so use
+ // a short timeout (each callback should come quickly after the previous one).
+ verify(discListeners.get(i), timeout(TEST_TIME_MS))
+ .onDiscoveryStarted(i % 2 == 0 ? testType1 : testType2);
+
+ // registerService does not get a callback before probing finishes (will not happen as
+ // this is mocked)
+ verifyNoMoreInteractions(regListeners.get(i));
+ }
+
+ // The next registrations should fail
+ final DiscoveryListener failDiscListener1 = mock(DiscoveryListener.class);
+ final RegistrationListener failRegListener1 = mock(RegistrationListener.class);
+ final DiscoveryListener failDiscListener2 = mock(DiscoveryListener.class);
+ final RegistrationListener failRegListener2 = mock(RegistrationListener.class);
+
+ client1.discoverServices(testType1, NsdManager.PROTOCOL_DNS_SD,
+ (Network) null, Runnable::run, failDiscListener1);
+ verify(failDiscListener1, timeout(TEST_TIME_MS))
+ .onStartDiscoveryFailed(testType1, FAILURE_MAX_LIMIT);
+
+ client1.registerService(testServiceInfo1, NsdManager.PROTOCOL_DNS_SD, Runnable::run,
+ failRegListener1);
+ verify(failRegListener1, timeout(TEST_TIME_MS)).onRegistrationFailed(
+ argThat(a -> testServiceInfo1.getServiceName().equals(a.getServiceName())),
+ eq(FAILURE_MAX_LIMIT));
+
+ client1.discoverServices(testType2, NsdManager.PROTOCOL_DNS_SD,
+ (Network) null, Runnable::run, failDiscListener2);
+ verify(failDiscListener2, timeout(TEST_TIME_MS))
+ .onStartDiscoveryFailed(testType2, FAILURE_MAX_LIMIT);
+
+ client1.registerService(testServiceInfo2, NsdManager.PROTOCOL_DNS_SD, Runnable::run,
+ failRegListener2);
+ verify(failRegListener2, timeout(TEST_TIME_MS)).onRegistrationFailed(
+ argThat(a -> testServiceInfo2.getServiceName().equals(a.getServiceName())),
+ eq(FAILURE_MAX_LIMIT));
+ }
+
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testNoCrashWhenProcessResolutionAfterBinderDied() throws Exception {
final NsdManager client = connectClient(mService);
final INsdManagerCallback cb = getCallback();
@@ -723,6 +832,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testStopServiceResolution() {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -749,6 +859,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testStopResolutionFailed() {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -774,6 +885,7 @@
@Test @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testStopResolutionDuringGettingAddress() throws RemoteException {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -864,7 +976,8 @@
List.of() /* textStrings */,
List.of() /* textEntries */,
1234,
- network);
+ network,
+ Instant.MAX /* expirationTime */);
// Callbacks for query sent.
listener.onDiscoveryQuerySent(Collections.emptyList(), 1 /* transactionId */);
@@ -894,7 +1007,8 @@
List.of() /* textStrings */,
List.of() /* textEntries */,
1234,
- network);
+ network,
+ Instant.MAX /* expirationTime */);
// Verify onServiceUpdated callback.
listener.onServiceUpdated(updatedServiceInfo);
@@ -955,6 +1069,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testMdnsDiscoveryManagerFeature() {
// Create NsdService w/o feature enabled.
final NsdManager client = connectClient(mService);
@@ -1025,7 +1140,8 @@
List.of(), /* textStrings */
List.of(), /* textEntries */
1234, /* interfaceIndex */
- network);
+ network,
+ Instant.MAX /* expirationTime */);
// Verify onServiceNameDiscovered callback
listener.onServiceNameDiscovered(foundInfo, false /* isServiceFromCache */);
@@ -1046,7 +1162,8 @@
null, /* textStrings */
null, /* textEntries */
1234, /* interfaceIndex */
- network);
+ network,
+ Instant.MAX /* expirationTime */);
// Verify onServiceNameRemoved callback
listener.onServiceNameRemoved(removedInfo);
verify(discListener, timeout(TIMEOUT_MS)).onServiceLost(argThat(info ->
@@ -1117,7 +1234,8 @@
waitForIdle();
verify(mAdvertiser).addOrUpdateService(anyInt(), argThat(s ->
"Instance".equals(s.getServiceName())
- && SERVICE_TYPE.equals(s.getServiceType())), eq("_subtype"), any());
+ && SERVICE_TYPE.equals(s.getServiceType())
+ && s.getSubtypes().equals(Set.of("_subtype"))), any(), anyInt());
final DiscoveryListener discListener = mock(DiscoveryListener.class);
client.discoverServices(typeWithSubtype, PROTOCOL, network, Runnable::run, discListener);
@@ -1167,7 +1285,8 @@
List.of(MdnsServiceInfo.TextEntry.fromBytes(new byte[]{
'k', 'e', 'y', '=', (byte) 0xFF, (byte) 0xFE})) /* textEntries */,
1234,
- network);
+ network,
+ Instant.ofEpochSecond(1000_000L) /* expirationTime */);
// Verify onServiceFound callback
doReturn(TEST_TIME_MS + 10L).when(mClock).elapsedRealtime();
@@ -1192,6 +1311,7 @@
assertTrue(info.getHostAddresses().stream().anyMatch(
address -> address.equals(parseNumericAddress("2001:db8::2"))));
assertEquals(network, info.getNetwork());
+ assertEquals(Instant.ofEpochSecond(1000_000L), info.getExpirationTime());
// Verify the listener has been unregistered.
verify(mDiscoveryManager, timeout(TIMEOUT_MS))
@@ -1201,6 +1321,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testMdnsAdvertiserFeatureFlagging() {
// Create NsdService w/o feature enabled.
final NsdManager client = connectClient(mService);
@@ -1223,7 +1344,7 @@
final ArgumentCaptor<Integer> serviceIdCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mAdvertiser).addOrUpdateService(serviceIdCaptor.capture(),
- argThat(info -> matches(info, regInfo)), eq(null) /* subtype */, any());
+ argThat(info -> matches(info, regInfo)), any(), anyInt());
client.unregisterService(regListenerWithoutFeature);
waitForIdle();
@@ -1239,6 +1360,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testTypeSpecificFeatureFlagging() {
doReturn("_type1._tcp:flag1,_type2._tcp:flag2").when(mDeps).getTypeAllowlistFlags();
doReturn(true).when(mDeps).isFeatureEnabled(any(),
@@ -1251,7 +1373,7 @@
service1.setHostAddresses(List.of(parseNumericAddress("2001:db8::123")));
service1.setPort(1234);
final NsdServiceInfo service2 = new NsdServiceInfo(SERVICE_NAME, "_type2._tcp");
- service2.setHostAddresses(List.of(parseNumericAddress("2001:db8::123")));
+ service1.setHostAddresses(List.of(parseNumericAddress("2001:db8::123")));
service2.setPort(1234);
client.discoverServices(service1.getServiceType(),
@@ -1283,9 +1405,9 @@
// The advertiser is enabled for _type2 but not _type1
verify(mAdvertiser, never()).addOrUpdateService(anyInt(),
- argThat(info -> matches(info, service1)), eq(null) /* subtype */, any());
+ argThat(info -> matches(info, service1)), any(), anyInt());
verify(mAdvertiser).addOrUpdateService(anyInt(), argThat(info -> matches(info, service2)),
- eq(null) /* subtype */, any());
+ any(), anyInt());
}
@Test
@@ -1297,7 +1419,7 @@
// final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
- verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any(), any());
+ verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any(), any(), any());
final NsdServiceInfo regInfo = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
regInfo.setHost(parseNumericAddress("192.0.2.123"));
@@ -1310,7 +1432,7 @@
verify(mSocketProvider).startMonitoringSockets();
final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mAdvertiser).addOrUpdateService(idCaptor.capture(), argThat(info ->
- matches(info, regInfo)), eq(null) /* subtype */, any());
+ matches(info, regInfo)), any(), anyInt());
// Verify onServiceRegistered callback
final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
@@ -1348,7 +1470,7 @@
// final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
- verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any(), any());
+ verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any(), any(), any());
final NsdServiceInfo regInfo = new NsdServiceInfo(SERVICE_NAME, "invalid_type");
regInfo.setHost(parseNumericAddress("192.0.2.123"));
@@ -1358,7 +1480,7 @@
client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
waitForIdle();
- verify(mAdvertiser, never()).addOrUpdateService(anyInt(), any(), any(), any());
+ verify(mAdvertiser, never()).addOrUpdateService(anyInt(), any(), any(), anyInt());
verify(regListener, timeout(TIMEOUT_MS)).onRegistrationFailed(
argThat(info -> matches(info, regInfo)), eq(FAILURE_INTERNAL_ERROR));
@@ -1375,7 +1497,7 @@
// final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
- verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any(), any());
+ verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any(), any(), any());
final NsdServiceInfo regInfo = new NsdServiceInfo("a".repeat(70), SERVICE_TYPE);
regInfo.setHost(parseNumericAddress("192.0.2.123"));
@@ -1387,9 +1509,12 @@
waitForIdle();
final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
// Service name is truncated to 63 characters
- verify(mAdvertiser).addOrUpdateService(idCaptor.capture(),
- argThat(info -> info.getServiceName().equals("a".repeat(63))),
- eq(null) /* subtype */, any());
+ verify(mAdvertiser)
+ .addOrUpdateService(
+ idCaptor.capture(),
+ argThat(info -> info.getServiceName().equals("a".repeat(63))),
+ any(),
+ anyInt());
// Verify onServiceRegistered callback
final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
@@ -1404,6 +1529,82 @@
}
@Test
+ public void testAdvertiseCustomTtl_validTtl_success() {
+ runValidTtlAdvertisingTest(30L);
+ runValidTtlAdvertisingTest(10 * 3600L);
+ }
+
+ @Test
+ public void testAdvertiseCustomTtl_ttlSmallerThan30SecondsButClientIsSystemServer_success() {
+ when(mDeps.getCallingUid()).thenReturn(Process.SYSTEM_UID);
+
+ runValidTtlAdvertisingTest(29L);
+ }
+
+ @Test
+ public void testAdvertiseCustomTtl_ttlLargerThan10HoursButClientIsSystemServer_success() {
+ when(mDeps.getCallingUid()).thenReturn(Process.SYSTEM_UID);
+
+ runValidTtlAdvertisingTest(10 * 3600L + 1);
+ runValidTtlAdvertisingTest(0xffffffffL);
+ }
+
+ private void runValidTtlAdvertisingTest(long validTtlSeconds) {
+ setMdnsAdvertiserEnabled();
+
+ final NsdManager client = connectClient(mService);
+ final RegistrationListener regListener = mock(RegistrationListener.class);
+ final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
+ ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
+ verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any(), any(), any());
+
+ final NsdServiceInfo regInfo = new NsdServiceInfo("Service custom TTL", SERVICE_TYPE);
+ regInfo.setPort(1234);
+ final AdvertisingRequest request =
+ new AdvertisingRequest.Builder(regInfo, NsdManager.PROTOCOL_DNS_SD)
+ .setTtl(Duration.ofSeconds(validTtlSeconds)).build();
+
+ client.registerService(request, Runnable::run, regListener);
+ waitForIdle();
+
+ final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
+ final MdnsAdvertisingOptions expectedAdverstingOptions =
+ MdnsAdvertisingOptions.newBuilder().setTtl(request.getTtl()).build();
+ verify(mAdvertiser).addOrUpdateService(idCaptor.capture(), any(),
+ eq(expectedAdverstingOptions), anyInt());
+
+ // Verify onServiceRegistered callback
+ final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
+ final int regId = idCaptor.getValue();
+ cb.onRegisterServiceSucceeded(regId, regInfo);
+
+ verify(regListener, timeout(TIMEOUT_MS)).onServiceRegistered(
+ argThat(info -> matches(info, new NsdServiceInfo(regInfo.getServiceName(), null))));
+ }
+
+ @Test
+ public void testAdvertiseCustomTtl_invalidTtl_FailsWithBadParameters() {
+ setMdnsAdvertiserEnabled();
+ final long invalidTtlSeconds = 29L;
+ final NsdManager client = connectClient(mService);
+ final RegistrationListener regListener = mock(RegistrationListener.class);
+ final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
+ ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
+ verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any(), any(), any());
+
+ final NsdServiceInfo regInfo = new NsdServiceInfo("Service custom TTL", SERVICE_TYPE);
+ regInfo.setPort(1234);
+ final AdvertisingRequest request =
+ new AdvertisingRequest.Builder(regInfo, NsdManager.PROTOCOL_DNS_SD)
+ .setTtl(Duration.ofSeconds(invalidTtlSeconds)).build();
+ client.registerService(request, Runnable::run, regListener);
+ waitForIdle();
+
+ verify(regListener, timeout(TIMEOUT_MS))
+ .onRegistrationFailed(any(), eq(FAILURE_BAD_PARAMETERS));
+ }
+
+ @Test
public void testStopServiceResolutionWithMdnsDiscoveryManager() {
setMdnsDiscoveryManagerEnabled();
@@ -1453,14 +1654,22 @@
final String serviceType5 = "_TEST._999._tcp.";
final String serviceType6 = "_998._tcp.,_TEST";
final String serviceType7 = "_997._tcp,_TEST";
+ final String serviceType8 = "_997._tcp,_test1,_test2,_test3";
+ final String serviceType9 = "_test4._997._tcp,_test1,_test2,_test3";
assertNull(parseTypeAndSubtype(serviceType1));
assertNull(parseTypeAndSubtype(serviceType2));
assertNull(parseTypeAndSubtype(serviceType3));
- assertEquals(new Pair<>("_123._udp", null), parseTypeAndSubtype(serviceType4));
- assertEquals(new Pair<>("_999._tcp", "_TEST"), parseTypeAndSubtype(serviceType5));
- assertEquals(new Pair<>("_998._tcp", "_TEST"), parseTypeAndSubtype(serviceType6));
- assertEquals(new Pair<>("_997._tcp", "_TEST"), parseTypeAndSubtype(serviceType7));
+ assertEquals(new Pair<>("_123._udp", Collections.emptyList()),
+ parseTypeAndSubtype(serviceType4));
+ assertEquals(new Pair<>("_999._tcp", List.of("_TEST")), parseTypeAndSubtype(serviceType5));
+ assertEquals(new Pair<>("_998._tcp", List.of("_TEST")), parseTypeAndSubtype(serviceType6));
+ assertEquals(new Pair<>("_997._tcp", List.of("_TEST")), parseTypeAndSubtype(serviceType7));
+
+ assertEquals(new Pair<>("_997._tcp", List.of("_test1", "_test2", "_test3")),
+ parseTypeAndSubtype(serviceType8));
+ assertEquals(new Pair<>("_997._tcp", List.of("_test4")),
+ parseTypeAndSubtype(serviceType9));
}
@Test
@@ -1479,7 +1688,7 @@
client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
waitForIdle();
verify(mSocketProvider).startMonitoringSockets();
- verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any(), any());
+ verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any(), anyInt());
// Verify the discovery uses MdnsDiscoveryManager
final DiscoveryListener discListener = mock(DiscoveryListener.class);
@@ -1512,7 +1721,7 @@
client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
waitForIdle();
verify(mSocketProvider).startMonitoringSockets();
- verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any(), any());
+ verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any(), anyInt());
final Network wifiNetwork1 = new Network(123);
final Network wifiNetwork2 = new Network(124);
diff --git a/tests/unit/java/com/android/server/VpnManagerServiceTest.java b/tests/unit/java/com/android/server/VpnManagerServiceTest.java
deleted file mode 100644
index bf23cd1..0000000
--- a/tests/unit/java/com/android/server/VpnManagerServiceTest.java
+++ /dev/null
@@ -1,409 +0,0 @@
-/*
- * 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 com.android.server;
-
-import static android.os.Build.VERSION_CODES.R;
-
-import static com.android.testutils.ContextUtils.mockService;
-import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
-import static com.android.testutils.MiscAsserts.assertThrows;
-
-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.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import android.annotation.UserIdInt;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.net.ConnectivityManager;
-import android.net.INetd;
-import android.net.Uri;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.INetworkManagementService;
-import android.os.Looper;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.security.Credentials;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.internal.net.VpnProfile;
-import com.android.server.connectivity.Vpn;
-import com.android.server.connectivity.VpnProfileStore;
-import com.android.server.net.LockdownVpnTracker;
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRunner;
-import com.android.testutils.HandlerUtils;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.nio.charset.StandardCharsets;
-import java.util.List;
-
-@RunWith(DevSdkIgnoreRunner.class)
-@IgnoreUpTo(R) // VpnManagerService is not available before R
-@SmallTest
-public class VpnManagerServiceTest extends VpnTestBase {
- private static final String CONTEXT_ATTRIBUTION_TAG = "VPN_MANAGER";
-
- @Rule
- public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
-
- private static final int TIMEOUT_MS = 2_000;
-
- @Mock Context mContext;
- @Mock Context mContextWithoutAttributionTag;
- @Mock Context mSystemContext;
- @Mock Context mUserAllContext;
- private HandlerThread mHandlerThread;
- @Mock private Vpn mVpn;
- @Mock private INetworkManagementService mNms;
- @Mock private ConnectivityManager mCm;
- @Mock private UserManager mUserManager;
- @Mock private INetd mNetd;
- @Mock private PackageManager mPackageManager;
- @Mock private VpnProfileStore mVpnProfileStore;
- @Mock private LockdownVpnTracker mLockdownVpnTracker;
-
- private VpnManagerServiceDependencies mDeps;
- private VpnManagerService mService;
- private BroadcastReceiver mUserPresentReceiver;
- private BroadcastReceiver mIntentReceiver;
- private final String mNotMyVpnPkg = "com.not.my.vpn";
-
- class VpnManagerServiceDependencies extends VpnManagerService.Dependencies {
- @Override
- public HandlerThread makeHandlerThread() {
- return mHandlerThread;
- }
-
- @Override
- public INetworkManagementService getINetworkManagementService() {
- return mNms;
- }
-
- @Override
- public INetd getNetd() {
- return mNetd;
- }
-
- @Override
- public Vpn createVpn(Looper looper, Context context, INetworkManagementService nms,
- INetd netd, @UserIdInt int userId) {
- return mVpn;
- }
-
- @Override
- public VpnProfileStore getVpnProfileStore() {
- return mVpnProfileStore;
- }
-
- @Override
- public LockdownVpnTracker createLockDownVpnTracker(Context context, Handler handler,
- Vpn vpn, VpnProfile profile) {
- return mLockdownVpnTracker;
- }
-
- @Override
- public @UserIdInt int getMainUserId() {
- return UserHandle.USER_SYSTEM;
- }
- }
-
- @Before
- public void setUp() throws Exception {
- MockitoAnnotations.initMocks(this);
-
- mHandlerThread = new HandlerThread("TestVpnManagerService");
- mDeps = new VpnManagerServiceDependencies();
-
- // The attribution tag is a dependency for IKE library to collect VPN metrics correctly
- // and thus should not be changed without updating the IKE code.
- doReturn(mContext)
- .when(mContextWithoutAttributionTag)
- .createAttributionContext(CONTEXT_ATTRIBUTION_TAG);
-
- doReturn(mUserAllContext).when(mContext).createContextAsUser(UserHandle.ALL, 0);
- doReturn(mSystemContext).when(mContext).createContextAsUser(UserHandle.SYSTEM, 0);
- doReturn(mPackageManager).when(mContext).getPackageManager();
- setMockedPackages(mPackageManager, sPackages);
-
- mockService(mContext, ConnectivityManager.class, Context.CONNECTIVITY_SERVICE, mCm);
- mockService(mContext, UserManager.class, Context.USER_SERVICE, mUserManager);
- doReturn(SYSTEM_USER).when(mUserManager).getUserInfo(eq(SYSTEM_USER_ID));
-
- mService = new VpnManagerService(mContextWithoutAttributionTag, mDeps);
- mService.systemReady();
-
- final ArgumentCaptor<BroadcastReceiver> intentReceiverCaptor =
- ArgumentCaptor.forClass(BroadcastReceiver.class);
- final ArgumentCaptor<BroadcastReceiver> userPresentReceiverCaptor =
- ArgumentCaptor.forClass(BroadcastReceiver.class);
- verify(mSystemContext).registerReceiver(
- userPresentReceiverCaptor.capture(), any(), any(), any());
- verify(mUserAllContext, times(2)).registerReceiver(
- intentReceiverCaptor.capture(), any(), any(), any());
- mUserPresentReceiver = userPresentReceiverCaptor.getValue();
- mIntentReceiver = intentReceiverCaptor.getValue();
-
- // Add user to create vpn in mVpn
- onUserStarted(SYSTEM_USER_ID);
- assertNotNull(mService.mVpns.get(SYSTEM_USER_ID));
- }
-
- @Test
- public void testUpdateAppExclusionList() {
- // Start vpn
- mService.startVpnProfile(TEST_VPN_PKG);
- verify(mVpn).startVpnProfile(eq(TEST_VPN_PKG));
-
- // Remove package due to package replaced.
- onPackageRemoved(PKGS[0], PKG_UIDS[0], true /* isReplacing */);
- verify(mVpn, never()).refreshPlatformVpnAppExclusionList();
-
- // Add package due to package replaced.
- onPackageAdded(PKGS[0], PKG_UIDS[0], true /* isReplacing */);
- verify(mVpn, never()).refreshPlatformVpnAppExclusionList();
-
- // Remove package
- onPackageRemoved(PKGS[0], PKG_UIDS[0], false /* isReplacing */);
- verify(mVpn).refreshPlatformVpnAppExclusionList();
-
- // Add the package back
- onPackageAdded(PKGS[0], PKG_UIDS[0], false /* isReplacing */);
- verify(mVpn, times(2)).refreshPlatformVpnAppExclusionList();
- }
-
- @Test
- public void testStartVpnProfileFromDiffPackage() {
- assertThrows(
- SecurityException.class, () -> mService.startVpnProfile(mNotMyVpnPkg));
- }
-
- @Test
- public void testStopVpnProfileFromDiffPackage() {
- assertThrows(SecurityException.class, () -> mService.stopVpnProfile(mNotMyVpnPkg));
- }
-
- @Test
- public void testGetProvisionedVpnProfileStateFromDiffPackage() {
- assertThrows(SecurityException.class, () ->
- mService.getProvisionedVpnProfileState(mNotMyVpnPkg));
- }
-
- @Test
- public void testGetProvisionedVpnProfileState() {
- mService.getProvisionedVpnProfileState(TEST_VPN_PKG);
- verify(mVpn).getProvisionedVpnProfileState(TEST_VPN_PKG);
- }
-
- private Intent buildIntent(String action, String packageName, int userId, int uid,
- boolean isReplacing) {
- final Intent intent = new Intent(action);
- intent.putExtra(Intent.EXTRA_USER_HANDLE, userId);
- intent.putExtra(Intent.EXTRA_UID, uid);
- intent.putExtra(Intent.EXTRA_REPLACING, isReplacing);
- if (packageName != null) {
- intent.setData(Uri.fromParts("package" /* scheme */, packageName, null /* fragment */));
- }
-
- return intent;
- }
-
- private void sendIntent(Intent intent) {
- sendIntent(mIntentReceiver, mContext, intent);
- }
-
- private void sendIntent(BroadcastReceiver receiver, Context context, Intent intent) {
- final Handler h = mHandlerThread.getThreadHandler();
-
- // Send in handler thread.
- h.post(() -> receiver.onReceive(context, intent));
- HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
- }
-
- private void onUserStarted(int userId) {
- sendIntent(buildIntent(Intent.ACTION_USER_STARTED,
- null /* packageName */, userId, -1 /* uid */, false /* isReplacing */));
- }
-
- private void onUserUnlocked(int userId) {
- sendIntent(buildIntent(Intent.ACTION_USER_UNLOCKED,
- null /* packageName */, userId, -1 /* uid */, false /* isReplacing */));
- }
-
- private void onUserStopped(int userId) {
- sendIntent(buildIntent(Intent.ACTION_USER_STOPPED,
- null /* packageName */, userId, -1 /* uid */, false /* isReplacing */));
- }
-
- private void onLockDownReset() {
- sendIntent(buildIntent(LockdownVpnTracker.ACTION_LOCKDOWN_RESET, null /* packageName */,
- UserHandle.USER_SYSTEM, -1 /* uid */, false /* isReplacing */));
- }
-
- private void onPackageAdded(String packageName, int userId, int uid, boolean isReplacing) {
- sendIntent(buildIntent(Intent.ACTION_PACKAGE_ADDED, packageName, userId, uid, isReplacing));
- }
-
- private void onPackageAdded(String packageName, int uid, boolean isReplacing) {
- onPackageAdded(packageName, UserHandle.USER_SYSTEM, uid, isReplacing);
- }
-
- private void onPackageRemoved(String packageName, int userId, int uid, boolean isReplacing) {
- sendIntent(buildIntent(Intent.ACTION_PACKAGE_REMOVED, packageName, userId, uid,
- isReplacing));
- }
-
- private void onPackageRemoved(String packageName, int uid, boolean isReplacing) {
- onPackageRemoved(packageName, UserHandle.USER_SYSTEM, uid, isReplacing);
- }
-
- @Test
- public void testReceiveIntentFromNonHandlerThread() {
- assertThrows(IllegalStateException.class, () ->
- mIntentReceiver.onReceive(mContext, buildIntent(Intent.ACTION_PACKAGE_REMOVED,
- PKGS[0], UserHandle.USER_SYSTEM, PKG_UIDS[0], true /* isReplacing */)));
-
- assertThrows(IllegalStateException.class, () ->
- mUserPresentReceiver.onReceive(mContext, new Intent(Intent.ACTION_USER_PRESENT)));
- }
-
- private void setupLockdownVpn(String packageName) {
- final byte[] profileTag = packageName.getBytes(StandardCharsets.UTF_8);
- doReturn(profileTag).when(mVpnProfileStore).get(Credentials.LOCKDOWN_VPN);
- }
-
- private void setupVpnProfile(String profileName) {
- final VpnProfile profile = new VpnProfile(profileName);
- profile.name = profileName;
- profile.server = "192.0.2.1";
- profile.dnsServers = "8.8.8.8";
- profile.type = VpnProfile.TYPE_IPSEC_XAUTH_PSK;
- final byte[] encodedProfile = profile.encode();
- doReturn(encodedProfile).when(mVpnProfileStore).get(Credentials.VPN + profileName);
- }
-
- @Test
- public void testUserPresent() {
- // Verify that LockDownVpnTracker is not created.
- verify(mLockdownVpnTracker, never()).init();
-
- setupLockdownVpn(TEST_VPN_PKG);
- setupVpnProfile(TEST_VPN_PKG);
-
- // mUserPresentReceiver only registers ACTION_USER_PRESENT intent and does no verification
- // on action, so an empty intent is enough.
- sendIntent(mUserPresentReceiver, mSystemContext, new Intent());
-
- verify(mLockdownVpnTracker).init();
- verify(mSystemContext).unregisterReceiver(mUserPresentReceiver);
- verify(mUserAllContext, never()).unregisterReceiver(any());
- }
-
- @Test
- public void testUpdateLockdownVpn() {
- setupLockdownVpn(TEST_VPN_PKG);
- onUserUnlocked(SYSTEM_USER_ID);
-
- // Will not create lockDownVpnTracker w/o valid profile configured in the keystore
- verify(mLockdownVpnTracker, never()).init();
-
- setupVpnProfile(TEST_VPN_PKG);
-
- // Remove the user from mVpns
- onUserStopped(SYSTEM_USER_ID);
- onUserUnlocked(SYSTEM_USER_ID);
- verify(mLockdownVpnTracker, never()).init();
-
- // Add user back
- onUserStarted(SYSTEM_USER_ID);
- verify(mLockdownVpnTracker).init();
-
- // Trigger another update. The existing LockDownVpnTracker should be shut down and
- // initialize another one.
- onUserUnlocked(SYSTEM_USER_ID);
- verify(mLockdownVpnTracker).shutdown();
- verify(mLockdownVpnTracker, times(2)).init();
- }
-
- @Test
- public void testLockdownReset() {
- // Init LockdownVpnTracker
- setupLockdownVpn(TEST_VPN_PKG);
- setupVpnProfile(TEST_VPN_PKG);
- onUserUnlocked(SYSTEM_USER_ID);
- verify(mLockdownVpnTracker).init();
-
- onLockDownReset();
- verify(mLockdownVpnTracker).reset();
- }
-
- @Test
- public void testLockdownResetWhenLockdownVpnTrackerIsNotInit() {
- setupLockdownVpn(TEST_VPN_PKG);
- setupVpnProfile(TEST_VPN_PKG);
-
- onLockDownReset();
-
- // LockDownVpnTracker is not created. Lockdown reset will not take effect.
- verify(mLockdownVpnTracker, never()).reset();
- }
-
- @Test
- public void testIsVpnLockdownEnabled() {
- // Vpn is created but the VPN lockdown is not enabled.
- assertFalse(mService.isVpnLockdownEnabled(SYSTEM_USER_ID));
-
- // Set lockdown for the SYSTEM_USER_ID VPN.
- doReturn(true).when(mVpn).getLockdown();
- assertTrue(mService.isVpnLockdownEnabled(SYSTEM_USER_ID));
-
- // Even lockdown is enabled but no Vpn is created for SECONDARY_USER.
- assertFalse(mService.isVpnLockdownEnabled(SECONDARY_USER.id));
- }
-
- @Test
- public void testGetVpnLockdownAllowlist() {
- doReturn(null).when(mVpn).getLockdownAllowlist();
- assertNull(mService.getVpnLockdownAllowlist(SYSTEM_USER_ID));
-
- final List<String> expected = List.of(PKGS);
- doReturn(expected).when(mVpn).getLockdownAllowlist();
- assertEquals(expected, mService.getVpnLockdownAllowlist(SYSTEM_USER_ID));
-
- // Even lockdown is enabled but no Vpn is created for SECONDARY_USER.
- assertNull(mService.getVpnLockdownAllowlist(SECONDARY_USER.id));
- }
-}
diff --git a/tests/unit/java/com/android/server/VpnTestBase.java b/tests/unit/java/com/android/server/VpnTestBase.java
deleted file mode 100644
index 6113872..0000000
--- a/tests/unit/java/com/android/server/VpnTestBase.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * 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 com.android.server;
-
-import static android.content.pm.UserInfo.FLAG_ADMIN;
-import static android.content.pm.UserInfo.FLAG_MANAGED_PROFILE;
-import static android.content.pm.UserInfo.FLAG_PRIMARY;
-import static android.content.pm.UserInfo.FLAG_RESTRICTED;
-
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.doAnswer;
-
-import android.content.pm.PackageManager;
-import android.content.pm.UserInfo;
-import android.os.Process;
-import android.os.UserHandle;
-import android.util.ArrayMap;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-/** Common variables or methods shared between VpnTest and VpnManagerServiceTest. */
-public class VpnTestBase {
- protected static final String TEST_VPN_PKG = "com.testvpn.vpn";
- /**
- * Names and UIDs for some fake packages. Important points:
- * - UID is ordered increasing.
- * - One pair of packages have consecutive UIDs.
- */
- protected static final String[] PKGS = {"com.example", "org.example", "net.example", "web.vpn"};
- protected static final int[] PKG_UIDS = {10066, 10077, 10078, 10400};
- // Mock packages
- protected static final Map<String, Integer> sPackages = new ArrayMap<>();
- static {
- for (int i = 0; i < PKGS.length; i++) {
- sPackages.put(PKGS[i], PKG_UIDS[i]);
- }
- sPackages.put(TEST_VPN_PKG, Process.myUid());
- }
-
- // Mock users
- protected static final int SYSTEM_USER_ID = 0;
- protected static final UserInfo SYSTEM_USER = new UserInfo(0, "system", UserInfo.FLAG_PRIMARY);
- protected static final UserInfo PRIMARY_USER = new UserInfo(27, "Primary",
- FLAG_ADMIN | FLAG_PRIMARY);
- protected static final UserInfo SECONDARY_USER = new UserInfo(15, "Secondary", FLAG_ADMIN);
- protected static final UserInfo RESTRICTED_PROFILE_A = new UserInfo(40, "RestrictedA",
- FLAG_RESTRICTED);
- protected static final UserInfo RESTRICTED_PROFILE_B = new UserInfo(42, "RestrictedB",
- FLAG_RESTRICTED);
- protected static final UserInfo MANAGED_PROFILE_A = new UserInfo(45, "ManagedA",
- FLAG_MANAGED_PROFILE);
- static {
- RESTRICTED_PROFILE_A.restrictedProfileParentId = PRIMARY_USER.id;
- RESTRICTED_PROFILE_B.restrictedProfileParentId = SECONDARY_USER.id;
- MANAGED_PROFILE_A.profileGroupId = PRIMARY_USER.id;
- }
-
- // Populate a fake packageName-to-UID mapping.
- protected void setMockedPackages(PackageManager mockPm, final Map<String, Integer> packages) {
- try {
- doAnswer(invocation -> {
- final String appName = (String) invocation.getArguments()[0];
- final int userId = (int) invocation.getArguments()[1];
-
- final Integer appId = packages.get(appName);
- if (appId == null) {
- throw new PackageManager.NameNotFoundException(appName);
- }
-
- return UserHandle.getUid(userId, appId);
- }).when(mockPm).getPackageUidAsUser(anyString(), anyInt());
- } catch (Exception e) {
- }
- }
-
- protected List<Integer> toList(int[] arr) {
- return Arrays.stream(arr).boxed().collect(Collectors.toList());
- }
-}
diff --git a/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
index 10a0982..c53feee 100644
--- a/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
@@ -20,10 +20,8 @@
import static android.net.ConnectivityManager.TYPE_MOBILE;
import static android.net.NetworkAgent.CMD_STOP_SOCKET_KEEPALIVE;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
-
import static com.android.server.connectivity.AutomaticOnOffKeepaliveTracker.METRICS_COLLECTION_DURATION_MS;
import static com.android.testutils.HandlerUtils.visibleOnHandlerThread;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -71,31 +69,16 @@
import android.os.Message;
import android.os.SystemClock;
import android.telephony.SubscriptionManager;
-import android.test.suitebuilder.annotation.SmallTest;
-import android.util.ArraySet;
import android.util.Log;
-import android.util.Range;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-
+import androidx.test.filters.SmallTest;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.connectivity.AutomaticOnOffKeepaliveTracker.AutomaticOnOffKeepalive;
import com.android.server.connectivity.KeepaliveTracker.KeepaliveInfo;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
import com.android.testutils.HandlerUtils;
-
-import libcore.util.HexEncoding;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
import java.io.FileDescriptor;
import java.io.StringWriter;
import java.net.Inet4Address;
@@ -104,9 +87,15 @@
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
-import java.util.Set;
+import libcore.util.HexEncoding;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
@@ -142,81 +131,81 @@
// Hexadecimal representation of a SOCK_DIAG response with tcp info.
private static final String SOCK_DIAG_TCP_INET_HEX =
// struct nlmsghdr.
- "14010000" + // length = 276
- "1400" + // type = SOCK_DIAG_BY_FAMILY
- "0301" + // flags = NLM_F_REQUEST | NLM_F_DUMP
- "00000000" + // seqno
- "00000000" + // pid (0 == kernel)
+ "14010000" // length = 276
+ + "1400" // type = SOCK_DIAG_BY_FAMILY
+ + "0301" // flags = NLM_F_REQUEST | NLM_F_DUMP
+ + "00000000" // seqno
+ + "00000000" // pid (0 == kernel)
// struct inet_diag_req_v2
- "02" + // family = AF_INET
- "06" + // state
- "00" + // timer
- "00" + // retrans
+ + "02" // family = AF_INET
+ + "06" // state
+ + "00" // timer
+ + "00" // retrans
// inet_diag_sockid
- "DEA5" + // idiag_sport = 42462
- "71B9" + // idiag_dport = 47473
- "0a006402000000000000000000000000" + // idiag_src = 10.0.100.2
- "08080808000000000000000000000000" + // idiag_dst = 8.8.8.8
- "00000000" + // idiag_if
- "34ED000076270000" + // idiag_cookie = 43387759684916
- "00000000" + // idiag_expires
- "00000000" + // idiag_rqueue
- "00000000" + // idiag_wqueue
- "00000000" + // idiag_uid
- "00000000" + // idiag_inode
+ + "DEA5" // idiag_sport = 42462
+ + "71B9" // idiag_dport = 47473
+ + "0a006402000000000000000000000000" // idiag_src = 10.0.100.2
+ + "08080808000000000000000000000000" // idiag_dst = 8.8.8.8
+ + "00000000" // idiag_if
+ + "34ED000076270000" // idiag_cookie = 43387759684916
+ + "00000000" // idiag_expires
+ + "00000000" // idiag_rqueue
+ + "00000000" // idiag_wqueue
+ + "39300000" // idiag_uid = 12345
+ + "00000000" // idiag_inode
// rtattr
- "0500" + // len = 5
- "0800" + // type = 8
- "00000000" + // data
- "0800" + // len = 8
- "0F00" + // type = 15(INET_DIAG_MARK)
- "850A0C00" + // data, socket mark=789125
- "AC00" + // len = 172
- "0200" + // type = 2(INET_DIAG_INFO)
+ + "0500" // len = 5
+ + "0800" // type = 8
+ + "00000000" // data
+ + "0800" // len = 8
+ + "0F00" // type = 15(INET_DIAG_MARK)
+ + "850A0C00" // data, socket mark=789125
+ + "AC00" // len = 172
+ + "0200" // type = 2(INET_DIAG_INFO)
// tcp_info
- "01" + // state = TCP_ESTABLISHED
- "00" + // ca_state = TCP_CA_OPEN
- "05" + // retransmits = 5
- "00" + // probes = 0
- "00" + // backoff = 0
- "07" + // option = TCPI_OPT_WSCALE|TCPI_OPT_SACK|TCPI_OPT_TIMESTAMPS
- "88" + // wscale = 8
- "00" + // delivery_rate_app_limited = 0
- "4A911B00" + // rto = 1806666
- "00000000" + // ato = 0
- "2E050000" + // sndMss = 1326
- "18020000" + // rcvMss = 536
- "00000000" + // unsacked = 0
- "00000000" + // acked = 0
- "00000000" + // lost = 0
- "00000000" + // retrans = 0
- "00000000" + // fackets = 0
- "BB000000" + // lastDataSent = 187
- "00000000" + // lastAckSent = 0
- "BB000000" + // lastDataRecv = 187
- "BB000000" + // lastDataAckRecv = 187
- "DC050000" + // pmtu = 1500
- "30560100" + // rcvSsthresh = 87600
- "3E2C0900" + // rttt = 601150
- "1F960400" + // rttvar = 300575
- "78050000" + // sndSsthresh = 1400
- "0A000000" + // sndCwnd = 10
- "A8050000" + // advmss = 1448
- "03000000" + // reordering = 3
- "00000000" + // rcvrtt = 0
- "30560100" + // rcvspace = 87600
- "00000000" + // totalRetrans = 0
- "53AC000000000000" + // pacingRate = 44115
- "FFFFFFFFFFFFFFFF" + // maxPacingRate = 18446744073709551615
- "0100000000000000" + // bytesAcked = 1
- "0000000000000000" + // bytesReceived = 0
- "0A000000" + // SegsOut = 10
- "00000000" + // SegsIn = 0
- "00000000" + // NotSentBytes = 0
- "3E2C0900" + // minRtt = 601150
- "00000000" + // DataSegsIn = 0
- "00000000" + // DataSegsOut = 0
- "0000000000000000"; // deliverRate = 0
+ + "01" // state = TCP_ESTABLISHED
+ + "00" // ca_state = TCP_CA_OPEN
+ + "05" // retransmits = 5
+ + "00" // probes = 0
+ + "00" // backoff = 0
+ + "07" // option = TCPI_OPT_WSCALE|TCPI_OPT_SACK|TCPI_OPT_TIMESTAMPS
+ + "88" // wscale = 8
+ + "00" // delivery_rate_app_limited = 0
+ + "4A911B00" // rto = 1806666
+ + "00000000" // ato = 0
+ + "2E050000" // sndMss = 1326
+ + "18020000" // rcvMss = 536
+ + "00000000" // unsacked = 0
+ + "00000000" // acked = 0
+ + "00000000" // lost = 0
+ + "00000000" // retrans = 0
+ + "00000000" // fackets = 0
+ + "BB000000" // lastDataSent = 187
+ + "00000000" // lastAckSent = 0
+ + "BB000000" // lastDataRecv = 187
+ + "BB000000" // lastDataAckRecv = 187
+ + "DC050000" // pmtu = 1500
+ + "30560100" // rcvSsthresh = 87600
+ + "3E2C0900" // rttt = 601150
+ + "1F960400" // rttvar = 300575
+ + "78050000" // sndSsthresh = 1400
+ + "0A000000" // sndCwnd = 10
+ + "A8050000" // advmss = 1448
+ + "03000000" // reordering = 3
+ + "00000000" // rcvrtt = 0
+ + "30560100" // rcvspace = 87600
+ + "00000000" // totalRetrans = 0
+ + "53AC000000000000" // pacingRate = 44115
+ + "FFFFFFFFFFFFFFFF" // maxPacingRate = 18446744073709551615
+ + "0100000000000000" // bytesAcked = 1
+ + "0000000000000000" // bytesReceived = 0
+ + "0A000000" // SegsOut = 10
+ + "00000000" // SegsIn = 0
+ + "00000000" // NotSentBytes = 0
+ + "3E2C0900" // minRtt = 601150
+ + "00000000" // DataSegsIn = 0
+ + "00000000" // DataSegsOut = 0
+ + "0000000000000000"; // deliverRate = 0
private static final String SOCK_DIAG_NO_TCP_INET_HEX =
// struct nlmsghdr
"14000000" // length = 20
@@ -236,9 +225,6 @@
private static final byte[] TEST_RESPONSE_BYTES =
HexEncoding.decode(TEST_RESPONSE_HEX.toCharArray(), false);
- private static final Set<Range<Integer>> TEST_UID_RANGES =
- new ArraySet<>(Arrays.asList(new Range<>(10000, 99999)));
-
private static class TestKeepaliveInfo {
private static List<Socket> sOpenSockets = new ArrayList<>();
@@ -416,28 +402,28 @@
public void testIsAnyTcpSocketConnected_runOnNonHandlerThread() throws Exception {
setupResponseWithSocketExisting();
assertThrows(IllegalStateException.class,
- () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID, TEST_UID_RANGES));
+ () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID));
}
@Test
public void testIsAnyTcpSocketConnected_withTargetNetId() throws Exception {
setupResponseWithSocketExisting();
assertTrue(visibleOnHandlerThread(mTestHandler,
- () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID, TEST_UID_RANGES)));
+ () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID)));
}
@Test
public void testIsAnyTcpSocketConnected_withIncorrectNetId() throws Exception {
setupResponseWithSocketExisting();
assertFalse(visibleOnHandlerThread(mTestHandler,
- () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(OTHER_NETID, TEST_UID_RANGES)));
+ () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(OTHER_NETID)));
}
@Test
public void testIsAnyTcpSocketConnected_noSocketExists() throws Exception {
setupResponseWithoutSocketExisting();
assertFalse(visibleOnHandlerThread(mTestHandler,
- () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID, TEST_UID_RANGES)));
+ () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID)));
}
private void triggerEventKeepalive(int slot, int reason) {
@@ -481,16 +467,14 @@
setupResponseWithoutSocketExisting();
visibleOnHandlerThread(
mTestHandler,
- () -> mAOOKeepaliveTracker.handleMonitorAutomaticKeepalive(
- autoKi, TEST_NETID, TEST_UID_RANGES));
+ () -> mAOOKeepaliveTracker.handleMonitorAutomaticKeepalive(autoKi, TEST_NETID));
}
private void doResumeKeepalive(AutomaticOnOffKeepalive autoKi) throws Exception {
setupResponseWithSocketExisting();
visibleOnHandlerThread(
mTestHandler,
- () -> mAOOKeepaliveTracker.handleMonitorAutomaticKeepalive(
- autoKi, TEST_NETID, TEST_UID_RANGES));
+ () -> mAOOKeepaliveTracker.handleMonitorAutomaticKeepalive(autoKi, TEST_NETID));
}
private void doStopKeepalive(AutomaticOnOffKeepalive autoKi) throws Exception {
diff --git a/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java b/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
index f07593e..7bd2b56 100644
--- a/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
@@ -46,7 +46,6 @@
import android.net.TelephonyNetworkSpecifier;
import android.os.Build;
import android.os.HandlerThread;
-import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import com.android.net.module.util.CollectionUtils;
@@ -54,10 +53,12 @@
import com.android.networkstack.apishim.common.TelephonyManagerShim.CarrierPrivilegesListenerShim;
import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
import com.android.server.connectivity.CarrierPrivilegeAuthenticator.Dependencies;
+import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.After;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@@ -67,6 +68,8 @@
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
+import java.util.Set;
+import java.util.function.BiConsumer;
/**
* Tests for CarrierPrivilegeAuthenticatorTest.
@@ -77,6 +80,9 @@
@RunWith(DevSdkIgnoreRunner.class)
@IgnoreUpTo(Build.VERSION_CODES.S_V2)
public class CarrierPrivilegeAuthenticatorTest {
+ @Rule
+ public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
private static final int SUBSCRIPTION_COUNT = 2;
private static final int TEST_SUBSCRIPTION_ID = 1;
@@ -85,7 +91,9 @@
@NonNull private final TelephonyManagerShimImpl mTelephonyManagerShim;
@NonNull private final PackageManager mPackageManager;
@NonNull private TestCarrierPrivilegeAuthenticator mCarrierPrivilegeAuthenticator;
+ @NonNull private final BiConsumer<Integer, Integer> mListener;
private final int mCarrierConfigPkgUid = 12345;
+ private final boolean mUseCallbacks;
private final String mTestPkg = "com.android.server.connectivity.test";
private final BroadcastReceiver mMultiSimBroadcastReceiver;
@NonNull private final HandlerThread mHandlerThread;
@@ -94,12 +102,12 @@
TestCarrierPrivilegeAuthenticator(@NonNull final Context c,
@NonNull final Dependencies deps,
@NonNull final TelephonyManager t) {
- super(c, deps, t, mTelephonyManagerShim);
+ super(c, deps, t, mTelephonyManagerShim, true /* requestRestrictedWifiEnabled */,
+ mListener);
}
@Override
- protected int getSlotIndex(int subId) {
- if (SubscriptionManager.DEFAULT_SUBSCRIPTION_ID == subId) return TEST_SUBSCRIPTION_ID;
- return subId;
+ protected int getSubId(int slotIndex) {
+ return TEST_SUBSCRIPTION_ID;
}
}
@@ -119,7 +127,9 @@
mTelephonyManager = mock(TelephonyManager.class);
mTelephonyManagerShim = mock(TelephonyManagerShimImpl.class);
mPackageManager = mock(PackageManager.class);
+ mListener = mock(BiConsumer.class);
mHandlerThread = new HandlerThread(CarrierPrivilegeAuthenticatorTest.class.getSimpleName());
+ mUseCallbacks = useCallbacks;
final Dependencies deps = mock(Dependencies.class);
doReturn(useCallbacks).when(deps).isFeatureEnabled(any() /* context */,
eq(CARRIER_SERVICE_CHANGED_USE_CALLBACK));
@@ -172,7 +182,7 @@
final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder()
.addTransportType(TRANSPORT_CELLULAR)
- .setNetworkSpecifier(new TelephonyNetworkSpecifier(0));
+ .setNetworkSpecifier(new TelephonyNetworkSpecifier(TEST_SUBSCRIPTION_ID));
assertTrue(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
mCarrierConfigPkgUid, ncBuilder.build()));
@@ -208,7 +218,8 @@
newListeners.get(0).onCarrierServiceChanged(null, mCarrierConfigPkgUid);
- final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(0);
+ final TelephonyNetworkSpecifier specifier =
+ new TelephonyNetworkSpecifier(TEST_SUBSCRIPTION_ID);
final NetworkCapabilities nc = new NetworkCapabilities.Builder()
.addTransportType(TRANSPORT_CELLULAR)
.setNetworkSpecifier(specifier)
@@ -220,10 +231,27 @@
}
@Test
+ @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+ public void testCarrierPrivilegesLostDueToCarrierServiceUpdate() throws Exception {
+ final CarrierPrivilegesListenerShim l = getCarrierPrivilegesListeners().get(0);
+
+ l.onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+ l.onCarrierServiceChanged(null, mCarrierConfigPkgUid + 1);
+ if (mUseCallbacks) {
+ verify(mListener).accept(eq(mCarrierConfigPkgUid), eq(TEST_SUBSCRIPTION_ID));
+ }
+ l.onCarrierServiceChanged(null, mCarrierConfigPkgUid + 2);
+ if (mUseCallbacks) {
+ verify(mListener).accept(eq(mCarrierConfigPkgUid + 1), eq(TEST_SUBSCRIPTION_ID));
+ }
+ }
+
+ @Test
public void testOnCarrierPrivilegesChanged() throws Exception {
final CarrierPrivilegesListenerShim listener = getCarrierPrivilegesListeners().get(0);
- final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(0);
+ final TelephonyNetworkSpecifier specifier =
+ new TelephonyNetworkSpecifier(TEST_SUBSCRIPTION_ID);
final NetworkCapabilities nc = new NetworkCapabilities.Builder()
.addTransportType(TRANSPORT_CELLULAR)
.setNetworkSpecifier(specifier)
@@ -251,7 +279,7 @@
assertFalse(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
mCarrierConfigPkgUid, ncBuilder.build()));
- ncBuilder.setNetworkSpecifier(new TelephonyNetworkSpecifier(0));
+ ncBuilder.setNetworkSpecifier(new TelephonyNetworkSpecifier(TEST_SUBSCRIPTION_ID));
assertTrue(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
mCarrierConfigPkgUid, ncBuilder.build()));
@@ -260,7 +288,35 @@
ncBuilder.setNetworkSpecifier(null);
ncBuilder.removeTransportType(TRANSPORT_CELLULAR);
ncBuilder.addTransportType(TRANSPORT_WIFI);
- ncBuilder.setNetworkSpecifier(new TelephonyNetworkSpecifier(0));
+ ncBuilder.setNetworkSpecifier(new TelephonyNetworkSpecifier(TEST_SUBSCRIPTION_ID));
+ assertFalse(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
+ mCarrierConfigPkgUid, ncBuilder.build()));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+ public void testNetworkCapabilitiesContainOneSubId() throws Exception {
+ final CarrierPrivilegesListenerShim listener = getCarrierPrivilegesListeners().get(0);
+ listener.onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+
+ final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder();
+ ncBuilder.addTransportType(TRANSPORT_WIFI);
+ ncBuilder.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
+ ncBuilder.setSubscriptionIds(Set.of(TEST_SUBSCRIPTION_ID));
+ assertTrue(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
+ mCarrierConfigPkgUid, ncBuilder.build()));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+ public void testNetworkCapabilitiesContainTwoSubIds() throws Exception {
+ final CarrierPrivilegesListenerShim listener = getCarrierPrivilegesListeners().get(0);
+ listener.onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+
+ final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder();
+ ncBuilder.addTransportType(TRANSPORT_WIFI);
+ ncBuilder.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
+ ncBuilder.setSubscriptionIds(Set.of(0, 1));
assertFalse(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
mCarrierConfigPkgUid, ncBuilder.build()));
}
diff --git a/tests/unit/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java b/tests/unit/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
index 52b05aa..ab1e467 100644
--- a/tests/unit/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
+++ b/tests/unit/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
@@ -26,7 +26,6 @@
import static com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityLog;
import static com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.MULTIPLE;
import static com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.WIFI;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
@@ -43,17 +42,14 @@
import android.net.metrics.ValidationProbeEvent;
import android.net.metrics.WakeupStats;
import android.os.Build;
-import android.test.suitebuilder.annotation.SmallTest;
-
+import androidx.test.filters.SmallTest;
import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityEvent;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
import java.util.Arrays;
import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
// TODO: instead of comparing textpb to textpb, parse textpb and compare proto to proto.
@RunWith(DevSdkIgnoreRunner.class)
diff --git a/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java b/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java
index 5881a8e..91626d2 100644
--- a/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java
+++ b/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java
@@ -18,7 +18,6 @@
import static android.net.metrics.INetdEventListener.EVENT_GETADDRINFO;
import static android.net.metrics.INetdEventListener.EVENT_GETHOSTBYNAME;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.fail;
@@ -50,14 +49,14 @@
import android.os.Parcelable;
import android.os.SystemClock;
import android.system.OsConstants;
-import android.test.suitebuilder.annotation.SmallTest;
import android.util.Base64;
-
+import androidx.test.filters.SmallTest;
import com.android.internal.util.BitUtils;
import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
-
+import java.io.PrintWriter;
+import java.io.StringWriter;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -65,9 +64,6 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import java.io.PrintWriter;
-import java.io.StringWriter;
-
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
diff --git a/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java b/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java
index 1b964e2..294dacb 100644
--- a/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java
@@ -1297,8 +1297,8 @@
assertTrue(mKeepaliveStatsTracker.allMetricsExpected(dailyKeepaliveInfoReported));
- // Write time after 26 hours.
- final int writeTime2 = 26 * 60 * 60 * 1000;
+ // Write time after 27 hours.
+ final int writeTime2 = 27 * 60 * 60 * 1000;
setElapsedRealtime(writeTime2);
visibleOnHandlerThread(mTestHandler, () -> mKeepaliveStatsTracker.writeAndResetMetrics());
diff --git a/tests/unit/java/com/android/server/connectivity/MulticastRoutingCoordinatorServiceTest.kt b/tests/unit/java/com/android/server/connectivity/MulticastRoutingCoordinatorServiceTest.kt
new file mode 100644
index 0000000..6c2c256
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/MulticastRoutingCoordinatorServiceTest.kt
@@ -0,0 +1,499 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity
+
+import android.net.MulticastRoutingConfig
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Looper
+import android.os.ParcelFileDescriptor
+import android.os.SystemClock
+import android.os.test.TestLooper
+import android.system.Os
+import android.system.OsConstants.AF_INET6
+import android.system.OsConstants.IPPROTO_UDP
+import android.system.OsConstants.SOCK_DGRAM
+import android.util.Log
+import androidx.test.filters.LargeTest
+import com.android.net.module.util.structs.StructMf6cctl
+import com.android.net.module.util.structs.StructMrt6Msg
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.tryTest
+import com.google.common.truth.Truth.assertThat
+import java.io.FileDescriptor
+import java.net.DatagramPacket
+import java.net.DatagramSocket
+import java.net.InetAddress
+import java.net.Inet6Address
+import java.net.InetSocketAddress
+import java.net.MulticastSocket
+import java.net.NetworkInterface
+import java.time.Clock
+import java.time.Instant
+import java.time.ZoneId
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.test.assertTrue
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+
+private const val TIMEOUT_MS = 2_000L
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class MulticastRoutingCoordinatorServiceTest {
+
+ // mocks are lateinit as they need to be setup between tests
+ @Mock private lateinit var mDeps: MulticastRoutingCoordinatorService.Dependencies
+ @Mock private lateinit var mMulticastSocket: MulticastSocket
+
+ val mSock = DatagramSocket()
+ val mPfd = ParcelFileDescriptor.fromDatagramSocket(mSock)
+ val mFd = mPfd.getFileDescriptor()
+ val mIfName1 = "interface1"
+ val mIfName2 = "interface2"
+ val mIfName3 = "interface3"
+ val mIfPhysicalIndex1 = 10
+ val mIfPhysicalIndex2 = 11
+ val mIfPhysicalIndex3 = 12
+ val mSourceAddress = Inet6Address.getByName("2000::8888") as Inet6Address
+ val mGroupAddressScope5 = Inet6Address.getByName("ff05::1234") as Inet6Address
+ val mGroupAddressScope4 = Inet6Address.getByName("ff04::1234") as Inet6Address
+ val mGroupAddressScope3 = Inet6Address.getByName("ff03::1234") as Inet6Address
+ val mSocketAddressScope5 = InetSocketAddress(mGroupAddressScope5, 0)
+ val mSocketAddressScope4 = InetSocketAddress(mGroupAddressScope4, 0)
+ val mEmptyOifs = setOf<Int>()
+ val mClock = FakeClock()
+ val mNetworkInterface1 = createEmptyNetworkInterface()
+ val mNetworkInterface2 = createEmptyNetworkInterface()
+ // MulticastRoutingCoordinatorService needs to be initialized after the dependencies
+ // are mocked.
+ lateinit var mService: MulticastRoutingCoordinatorService
+ lateinit var mLooper: TestLooper
+
+ class FakeClock() : Clock() {
+ private var offsetMs = 0L
+
+ fun fastForward(ms: Long) {
+ offsetMs += ms
+ }
+
+ override fun instant(): Instant {
+ return Instant.now().plusMillis(offsetMs)
+ }
+
+ override fun getZone(): ZoneId {
+ throw RuntimeException("Not implemented");
+ }
+
+ override fun withZone(zone: ZoneId): Clock {
+ throw RuntimeException("Not implemented");
+ }
+
+ }
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ doReturn(mClock).`when`(mDeps).getClock()
+ doReturn(mFd).`when`(mDeps).createMulticastRoutingSocket()
+ doReturn(mMulticastSocket).`when`(mDeps).createMulticastSocket()
+ doReturn(mIfPhysicalIndex1).`when`(mDeps).getInterfaceIndex(mIfName1)
+ doReturn(mIfPhysicalIndex2).`when`(mDeps).getInterfaceIndex(mIfName2)
+ doReturn(mIfPhysicalIndex3).`when`(mDeps).getInterfaceIndex(mIfName3)
+ doReturn(mNetworkInterface1).`when`(mDeps).getNetworkInterface(mIfPhysicalIndex1)
+ doReturn(mNetworkInterface2).`when`(mDeps).getNetworkInterface(mIfPhysicalIndex2)
+ }
+
+ @After
+ fun tearDown() {
+ mSock.close()
+ }
+
+ // Functions under @Before and @Test run in different threads,
+ // (i.e. androidx.test.runner.AndroidJUnitRunner vs Time-limited test)
+ // MulticastRoutingCoordinatorService requires the jobs are run on the thread looper,
+ // so TestLooper needs to be created inside each test case to install the
+ // correct looper.
+ fun prepareService() {
+ mLooper = TestLooper()
+ val handler = Handler(mLooper.getLooper())
+
+ mService = MulticastRoutingCoordinatorService(handler, mDeps)
+ }
+
+ private fun createEmptyNetworkInterface(): NetworkInterface {
+ val constructor = NetworkInterface::class.java.getDeclaredConstructor()
+ constructor.isAccessible = true
+ return constructor.newInstance()
+ }
+
+ private fun createStructMf6cctl(src: Inet6Address, dst: Inet6Address, iifIdx: Int,
+ oifSet: Set<Int>): StructMf6cctl {
+ return StructMf6cctl(src, dst, iifIdx, oifSet)
+ }
+
+ // Send a MRT6MSG_NOCACHE packet to sock, to indicate a packet has arrived without matching MulticastRoutingCache
+ private fun sendMrt6msgNocachePacket(interfaceVirtualIndex: Int,
+ source: Inet6Address, destination: Inet6Address) {
+ mLooper.dispatchAll() // let MulticastRoutingCoordinatorService handle all msgs first to
+ // apply any possible multicast routing config changes
+ val mrt6Msg = StructMrt6Msg(0 /* mbz must be 0 */, StructMrt6Msg.MRT6MSG_NOCACHE,
+ interfaceVirtualIndex, source, destination)
+ mLooper.getNewExecutor().execute({ mService.handleMulticastNocacheUpcall(mrt6Msg) })
+ mLooper.dispatchAll()
+ }
+
+ private fun applyMulticastForwardNone(fromIf: String, toIf: String) {
+ val configNone = MulticastRoutingConfig.CONFIG_FORWARD_NONE
+
+ mService.applyMulticastRoutingConfig(fromIf, toIf, configNone)
+ }
+
+ private fun applyMulticastForwardMinimumScope(fromIf: String, toIf: String, minScope: Int) {
+ val configMinimumScope = MulticastRoutingConfig.Builder(
+ MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE, minScope).build()
+
+ mService.applyMulticastRoutingConfig(fromIf, toIf, configMinimumScope)
+ }
+
+ private fun applyMulticastForwardSelected(fromIf: String, toIf: String) {
+ val configSelected = MulticastRoutingConfig.Builder(
+ MulticastRoutingConfig.FORWARD_SELECTED)
+ .addListeningAddress(mGroupAddressScope5).build()
+
+ mService.applyMulticastRoutingConfig(fromIf, toIf, configSelected)
+ }
+
+ @Test
+ fun testConstructor_multicastRoutingSocketIsCreated() {
+ prepareService()
+ verify(mDeps).createMulticastRoutingSocket()
+ }
+
+ @Test
+ fun testMulticastRouting_applyForwardNone() {
+ prepareService()
+
+ applyMulticastForwardNone(mIfName1, mIfName2)
+ mLooper.dispatchAll()
+
+ // Both interfaces are not added as multicast routing interfaces
+ verify(mDeps, never()).setsockoptMrt6AddMif(eq(mFd), any())
+ // No MFC should be added for FORWARD_NONE
+ verify(mDeps, never()).setsockoptMrt6AddMfc(eq(mFd), any())
+ assertEquals(MulticastRoutingConfig.CONFIG_FORWARD_NONE,
+ mService.getMulticastRoutingConfig(mIfName1, mIfName2));
+ }
+
+ @Test
+ fun testMulticastRouting_applyForwardMinimumScope() {
+ prepareService()
+
+ applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4 /* minScope */)
+ mLooper.dispatchAll()
+
+ // No MFC is added for FORWARD_WITH_MIN_SCOPE
+ verify(mDeps, never()).setsockoptMrt6AddMfc(eq(mFd), any())
+ assertEquals(MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE,
+ mService.getMulticastRoutingConfig(mIfName1, mIfName2).getForwardingMode())
+ assertEquals(4, mService.getMulticastRoutingConfig(mIfName1, mIfName2).getMinimumScope())
+ }
+
+ @Test
+ fun testMulticastRouting_addressScopelargerThanMinScope_allowMfcIsAdded() {
+ prepareService()
+ applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4 /* minScope */)
+ mLooper.dispatchAll()
+ val oifs = setOf(mService.getVirtualInterfaceIndex(mIfName2))
+ val mf6cctl = createStructMf6cctl(mSourceAddress, mGroupAddressScope5,
+ mService.getVirtualInterfaceIndex(mIfName1), oifs)
+
+ // simulate a MRT6MSG_NOCACHE upcall for a packet sent to group address of scope 5
+ sendMrt6msgNocachePacket(0, mSourceAddress, mGroupAddressScope5)
+
+ // an MFC is added for the packet
+ verify(mDeps).setsockoptMrt6AddMfc(eq(mFd), eq(mf6cctl))
+ }
+
+ @Test
+ fun testMulticastRouting_addressScopeSmallerThanMinScope_blockingMfcIsAdded() {
+ prepareService()
+ applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4)
+ val mf6cctl = createStructMf6cctl(mSourceAddress, mGroupAddressScope3,
+ mService.getVirtualInterfaceIndex(mIfName1), mEmptyOifs)
+
+ // simulate a MRT6MSG_NOCACHE upcall when a packet should not be forwarded
+ sendMrt6msgNocachePacket(0, mSourceAddress, mGroupAddressScope3)
+
+ // a blocking MFC is added
+ verify(mDeps).setsockoptMrt6AddMfc(eq(mFd), eq(mf6cctl))
+ }
+
+ @Test
+ fun testMulticastRouting_applyForwardSelected_joinsGroup() {
+ prepareService()
+
+ applyMulticastForwardSelected(mIfName1, mIfName2)
+ mLooper.dispatchAll()
+
+ verify(mMulticastSocket).joinGroup(eq(mSocketAddressScope5), eq(mNetworkInterface1))
+ assertEquals(MulticastRoutingConfig.FORWARD_SELECTED,
+ mService.getMulticastRoutingConfig(mIfName1, mIfName2).getForwardingMode())
+ }
+
+ @Test
+ fun testMulticastRouting_addListeningAddressInForwardSelected_joinsGroup() {
+ prepareService()
+
+ val configSelectedNoAddress = MulticastRoutingConfig.Builder(
+ MulticastRoutingConfig.FORWARD_SELECTED).build()
+ mService.applyMulticastRoutingConfig(mIfName1, mIfName2, configSelectedNoAddress)
+ mLooper.dispatchAll()
+
+ val configSelectedWithAddresses = MulticastRoutingConfig.Builder(
+ MulticastRoutingConfig.FORWARD_SELECTED)
+ .addListeningAddress(mGroupAddressScope5)
+ .addListeningAddress(mGroupAddressScope4)
+ .build()
+ mService.applyMulticastRoutingConfig(mIfName1, mIfName2, configSelectedWithAddresses)
+ mLooper.dispatchAll()
+
+ verify(mMulticastSocket).joinGroup(eq(mSocketAddressScope5), eq(mNetworkInterface1))
+ verify(mMulticastSocket).joinGroup(eq(mSocketAddressScope4), eq(mNetworkInterface1))
+ }
+
+ @Test
+ fun testMulticastRouting_removeListeningAddressInForwardSelected_leavesGroup() {
+ prepareService()
+ val configSelectedWith2Addresses = MulticastRoutingConfig.Builder(
+ MulticastRoutingConfig.FORWARD_SELECTED)
+ .addListeningAddress(mGroupAddressScope5)
+ .addListeningAddress(mGroupAddressScope4)
+ .build()
+ mService.applyMulticastRoutingConfig(mIfName1, mIfName2, configSelectedWith2Addresses)
+ mLooper.dispatchAll()
+
+ verify(mMulticastSocket).joinGroup(eq(mSocketAddressScope5), eq(mNetworkInterface1))
+ verify(mMulticastSocket).joinGroup(eq(mSocketAddressScope4), eq(mNetworkInterface1))
+
+ // remove the scope4 address
+ val configSelectedWith1Address = MulticastRoutingConfig.Builder(
+ MulticastRoutingConfig.FORWARD_SELECTED)
+ .addListeningAddress(mGroupAddressScope5)
+ .build()
+ mService.applyMulticastRoutingConfig(mIfName1, mIfName2, configSelectedWith1Address)
+ mLooper.dispatchAll()
+
+ verify(mMulticastSocket).leaveGroup(eq(mSocketAddressScope4), eq(mNetworkInterface1))
+ verify(mMulticastSocket, never())
+ .leaveGroup(eq(mSocketAddressScope5), eq(mNetworkInterface1))
+ }
+
+ @Test
+ fun testMulticastRouting_fromForwardSelectedToForwardNone_leavesGroup() {
+ prepareService()
+ applyMulticastForwardSelected(mIfName1, mIfName2)
+ mLooper.dispatchAll()
+
+ verify(mMulticastSocket).joinGroup(eq(mSocketAddressScope5), eq(mNetworkInterface1))
+
+ applyMulticastForwardNone(mIfName1, mIfName2)
+ mLooper.dispatchAll()
+
+ verify(mMulticastSocket).leaveGroup(eq(mSocketAddressScope5), eq(mNetworkInterface1))
+ assertEquals(MulticastRoutingConfig.CONFIG_FORWARD_NONE,
+ mService.getMulticastRoutingConfig(mIfName1, mIfName2));
+ }
+
+ @Test
+ fun testMulticastRouting_fromFowardSelectedToForwardNone_removesMulticastInterfaces() {
+ prepareService()
+
+ applyMulticastForwardSelected(mIfName1, mIfName2)
+ applyMulticastForwardSelected(mIfName1, mIfName3)
+ mLooper.dispatchAll()
+
+ assertNotNull(mService.getVirtualInterfaceIndex(mIfName1))
+ assertNotNull(mService.getVirtualInterfaceIndex(mIfName2))
+ assertNotNull(mService.getVirtualInterfaceIndex(mIfName3))
+
+ applyMulticastForwardNone(mIfName1, mIfName2)
+ mLooper.dispatchAll()
+
+ assertNotNull(mService.getVirtualInterfaceIndex(mIfName1))
+ assertNull(mService.getVirtualInterfaceIndex(mIfName2))
+ assertNotNull(mService.getVirtualInterfaceIndex(mIfName3))
+ }
+
+ @Test
+ fun testMulticastRouting_addMulticastRoutingInterfaces() {
+ prepareService()
+
+ applyMulticastForwardSelected(mIfName1, mIfName2)
+ mLooper.dispatchAll()
+
+ assertNotNull(mService.getVirtualInterfaceIndex(mIfName1))
+ assertNotNull(mService.getVirtualInterfaceIndex(mIfName2))
+ assertNotEquals(mService.getVirtualInterfaceIndex(mIfName1),
+ mService.getVirtualInterfaceIndex(mIfName2))
+ }
+
+ @Test
+ fun testMulticastRouting_removeMulticastRoutingInterfaces() {
+ prepareService()
+
+ applyMulticastForwardSelected(mIfName1, mIfName2)
+ mService.removeInterfaceFromMulticastRouting(mIfName1)
+ mLooper.dispatchAll()
+
+ assertNull(mService.getVirtualInterfaceIndex(mIfName1))
+ assertNotNull(mService.getVirtualInterfaceIndex(mIfName2))
+ }
+
+ @Test
+ fun testMulticastRouting_applyConfigNone_removesMfc() {
+ prepareService()
+
+ applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4 /* minScope */)
+ applyMulticastForwardSelected(mIfName1, mIfName3)
+
+ sendMrt6msgNocachePacket(0, mSourceAddress, mGroupAddressScope5)
+ val oifs = setOf(mService.getVirtualInterfaceIndex(mIfName2),
+ mService.getVirtualInterfaceIndex(mIfName3))
+ val oifsUpdate = setOf(mService.getVirtualInterfaceIndex(mIfName3))
+ val mf6cctlAdd = createStructMf6cctl(mSourceAddress, mGroupAddressScope5,
+ mService.getVirtualInterfaceIndex(mIfName1), oifs)
+ val mf6cctlUpdate = createStructMf6cctl(mSourceAddress, mGroupAddressScope5,
+ mService.getVirtualInterfaceIndex(mIfName1), oifsUpdate)
+ val mf6cctlDel = createStructMf6cctl(mSourceAddress, mGroupAddressScope5,
+ mService.getVirtualInterfaceIndex(mIfName1), mEmptyOifs)
+
+ verify(mDeps).setsockoptMrt6AddMfc(eq(mFd), eq(mf6cctlAdd))
+
+ applyMulticastForwardNone(mIfName1, mIfName2)
+ mLooper.dispatchAll()
+
+ verify(mDeps).setsockoptMrt6AddMfc(eq(mFd), eq(mf6cctlUpdate))
+
+ applyMulticastForwardNone(mIfName1, mIfName3)
+ mLooper.dispatchAll()
+
+ verify(mDeps, timeout(TIMEOUT_MS).times(1)).setsockoptMrt6DelMfc(eq(mFd), eq(mf6cctlDel))
+ }
+
+ @Test
+ @LargeTest
+ fun testMulticastRouting_maxNumberOfMfcs() {
+ prepareService()
+
+ // add MFC_MAX_NUMBER_OF_ENTRIES MFCs
+ applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4 /* minScope */)
+ for (i in 1..MulticastRoutingCoordinatorService.MFC_MAX_NUMBER_OF_ENTRIES) {
+ val groupAddress =
+ Inet6Address.getByName("ff05::" + Integer.toHexString(i)) as Inet6Address
+ sendMrt6msgNocachePacket(0, mSourceAddress, groupAddress)
+ }
+ val mf6cctlDel = createStructMf6cctl(mSourceAddress,
+ Inet6Address.getByName("ff05::1" ) as Inet6Address,
+ mService.getVirtualInterfaceIndex(mIfName1), mEmptyOifs)
+
+ verify(mDeps, times(MulticastRoutingCoordinatorService.MFC_MAX_NUMBER_OF_ENTRIES)).
+ setsockoptMrt6AddMfc(eq(mFd), any())
+ // when number of mfcs reaches the max value, one mfc should be removed
+ verify(mDeps).setsockoptMrt6DelMfc(eq(mFd), eq(mf6cctlDel))
+ }
+
+ @Test
+ fun testMulticastRouting_interfaceWithoutActiveConfig_isRemoved() {
+ prepareService()
+ applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4 /* minScope */)
+ mLooper.dispatchAll()
+ val virtualIndexIf1 = mService.getVirtualInterfaceIndex(mIfName1)
+ val virtualIndexIf2 = mService.getVirtualInterfaceIndex(mIfName2)
+
+ applyMulticastForwardNone(mIfName1, mIfName2)
+ mLooper.dispatchAll()
+
+ verify(mDeps).setsockoptMrt6DelMif(eq(mFd), eq(virtualIndexIf1))
+ verify(mDeps).setsockoptMrt6DelMif(eq(mFd), eq(virtualIndexIf2))
+ }
+
+ @Test
+ fun testMulticastRouting_interfaceWithActiveConfig_isNotRemoved() {
+ prepareService()
+ applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4 /* minScope */)
+ applyMulticastForwardMinimumScope(mIfName2, mIfName3, 4 /* minScope */)
+ mLooper.dispatchAll()
+ val virtualIndexIf1 = mService.getVirtualInterfaceIndex(mIfName1)
+ val virtualIndexIf2 = mService.getVirtualInterfaceIndex(mIfName2)
+ val virtualIndexIf3 = mService.getVirtualInterfaceIndex(mIfName3)
+
+ applyMulticastForwardNone(mIfName1, mIfName2)
+ mLooper.dispatchAll()
+
+ verify(mDeps).setsockoptMrt6DelMif(eq(mFd), eq(virtualIndexIf1))
+ verify(mDeps, never()).setsockoptMrt6DelMif(eq(mFd), eq(virtualIndexIf2))
+ verify(mDeps, never()).setsockoptMrt6DelMif(eq(mFd), eq(virtualIndexIf3))
+ }
+
+ @Test
+ fun testMulticastRouting_unusedMfc_isRemovedAfterTimeout() {
+ prepareService()
+ applyMulticastForwardMinimumScope(mIfName1, mIfName2, 4 /* minScope */)
+ sendMrt6msgNocachePacket(0, mSourceAddress, mGroupAddressScope5)
+ val oifs = setOf(mService.getVirtualInterfaceIndex(mIfName2))
+ val mf6cctlAdd = createStructMf6cctl(mSourceAddress, mGroupAddressScope5,
+ mService.getVirtualInterfaceIndex(mIfName1), oifs)
+ val mf6cctlDel = createStructMf6cctl(mSourceAddress, mGroupAddressScope5,
+ mService.getVirtualInterfaceIndex(mIfName1), mEmptyOifs)
+
+ // An MFC is added
+ verify(mDeps).setsockoptMrt6AddMfc(eq(mFd), eq(mf6cctlAdd))
+
+ repeat(MulticastRoutingCoordinatorService.MFC_INACTIVE_TIMEOUT_MS /
+ MulticastRoutingCoordinatorService.MFC_INACTIVE_CHECK_INTERVAL_MS + 1) {
+ mClock.fastForward(MulticastRoutingCoordinatorService
+ .MFC_INACTIVE_CHECK_INTERVAL_MS.toLong())
+ mLooper.moveTimeForward(MulticastRoutingCoordinatorService
+ .MFC_INACTIVE_CHECK_INTERVAL_MS.toLong())
+ mLooper.dispatchAll();
+ }
+
+ verify(mDeps).setsockoptMrt6DelMfc(eq(mFd), eq(mf6cctlDel))
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java b/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java
index d667662..89e2a51 100644
--- a/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java
+++ b/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java
@@ -18,9 +18,7 @@
import static android.net.metrics.INetdEventListener.EVENT_GETADDRINFO;
import static android.net.metrics.INetdEventListener.EVENT_GETHOSTBYNAME;
-
import static com.android.testutils.MiscAsserts.assertStringContains;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
@@ -34,27 +32,23 @@
import android.net.NetworkCapabilities;
import android.os.Build;
import android.system.OsConstants;
-import android.test.suitebuilder.annotation.SmallTest;
import android.util.Base64;
-
+import androidx.test.filters.SmallTest;
import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityEvent;
import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityLog;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
-
-import libcore.util.EmptyArray;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
+import libcore.util.EmptyArray;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkRequestStateInfoTest.java b/tests/unit/java/com/android/server/connectivity/NetworkRequestStateInfoTest.java
new file mode 100644
index 0000000..44a645a
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/NetworkRequestStateInfoTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED;
+
+import static org.junit.Assert.assertEquals;
+
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.Build;
+
+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.Mockito;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+public class NetworkRequestStateInfoTest {
+
+ @Mock
+ private NetworkRequestStateInfo.Dependencies mDependencies;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ }
+ @Test
+ public void testSetNetworkRequestRemoved() {
+ final long nrStartTime = 1L;
+ final long nrEndTime = 101L;
+
+ NetworkRequest notMeteredWifiNetworkRequest = new NetworkRequest(
+ new NetworkCapabilities()
+ .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ .setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, true),
+ 0, 1, NetworkRequest.Type.REQUEST
+ );
+
+ // This call will be used to calculate NR received time
+ Mockito.when(mDependencies.getElapsedRealtime()).thenReturn(nrStartTime);
+ NetworkRequestStateInfo networkRequestStateInfo = new NetworkRequestStateInfo(
+ notMeteredWifiNetworkRequest, mDependencies);
+
+ // This call will be used to calculate NR removed time
+ Mockito.when(mDependencies.getElapsedRealtime()).thenReturn(nrEndTime);
+ networkRequestStateInfo.setNetworkRequestRemoved();
+ assertEquals(
+ nrEndTime - nrStartTime,
+ networkRequestStateInfo.getNetworkRequestDurationMillis());
+ assertEquals(networkRequestStateInfo.getNetworkRequestStateStatsType(),
+ NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED);
+ }
+
+ @Test
+ public void testCheckInitialState() {
+ NetworkRequestStateInfo networkRequestStateInfo = new NetworkRequestStateInfo(
+ new NetworkRequest(new NetworkCapabilities(), 0, 1, NetworkRequest.Type.REQUEST),
+ mDependencies);
+ assertEquals(networkRequestStateInfo.getNetworkRequestStateStatsType(),
+ NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED);
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkRequestStateStatsMetricsTest.java b/tests/unit/java/com/android/server/connectivity/NetworkRequestStateStatsMetricsTest.java
new file mode 100644
index 0000000..8dc0528
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/NetworkRequestStateStatsMetricsTest.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.testutils.HandlerUtils;
+
+import org.junit.Before;
+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;
+
+@RunWith(AndroidJUnit4.class)
+public class NetworkRequestStateStatsMetricsTest {
+ @Mock
+ private NetworkRequestStateStatsMetrics.Dependencies mNRStateStatsDeps;
+ @Mock
+ private NetworkRequestStateInfo.Dependencies mNRStateInfoDeps;
+ @Captor
+ private ArgumentCaptor<Handler> mHandlerCaptor;
+ @Captor
+ private ArgumentCaptor<Integer> mMessageWhatCaptor;
+
+ private NetworkRequestStateStatsMetrics mNetworkRequestStateStatsMetrics;
+ private HandlerThread mHandlerThread;
+ private static final int TEST_REQUEST_ID = 10;
+ private static final int TEST_PACKAGE_UID = 20;
+ private static final int TIMEOUT_MS = 30_000;
+ private static final NetworkRequest NOT_METERED_WIFI_NETWORK_REQUEST = new NetworkRequest(
+ new NetworkCapabilities()
+ .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ .setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, true)
+ .setCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET, false)
+ .setRequestorUid(TEST_PACKAGE_UID),
+ 0, TEST_REQUEST_ID, NetworkRequest.Type.REQUEST
+ );
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mHandlerThread = new HandlerThread("NetworkRequestStateStatsMetrics");
+ Mockito.when(mNRStateStatsDeps.makeHandlerThread("NetworkRequestStateStatsMetrics"))
+ .thenReturn(mHandlerThread);
+ Mockito.when(mNRStateStatsDeps.getMillisSinceEvent(anyLong())).thenReturn(0L);
+ Mockito.doAnswer(invocation -> {
+ mHandlerCaptor.getValue().sendMessage(
+ Message.obtain(mHandlerCaptor.getValue(), mMessageWhatCaptor.getValue()));
+ return null;
+ }).when(mNRStateStatsDeps).sendMessageDelayed(
+ mHandlerCaptor.capture(), mMessageWhatCaptor.capture(), anyLong());
+ mNetworkRequestStateStatsMetrics = new NetworkRequestStateStatsMetrics(
+ mNRStateStatsDeps, mNRStateInfoDeps);
+ }
+
+ @Test
+ public void testNetworkRequestReceivedRemoved() {
+ final long nrStartTime = 1L;
+ final long nrEndTime = 101L;
+ // This call will be used to calculate NR received time
+ Mockito.when(mNRStateInfoDeps.getElapsedRealtime()).thenReturn(nrStartTime);
+ mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(NOT_METERED_WIFI_NETWORK_REQUEST);
+
+ ArgumentCaptor<NetworkRequestStateInfo> networkRequestStateInfoCaptor =
+ ArgumentCaptor.forClass(NetworkRequestStateInfo.class);
+ verify(mNRStateStatsDeps, timeout(TIMEOUT_MS))
+ .writeStats(networkRequestStateInfoCaptor.capture());
+
+ NetworkRequestStateInfo nrStateInfoSent = networkRequestStateInfoCaptor.getValue();
+ assertEquals(NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED,
+ nrStateInfoSent.getNetworkRequestStateStatsType());
+ assertEquals(NOT_METERED_WIFI_NETWORK_REQUEST.requestId, nrStateInfoSent.getRequestId());
+ assertEquals(TEST_PACKAGE_UID, nrStateInfoSent.getPackageUid());
+ assertEquals(1 << NetworkCapabilities.TRANSPORT_WIFI, nrStateInfoSent.getTransportTypes());
+ assertTrue(nrStateInfoSent.getNetCapabilityNotMetered());
+ assertFalse(nrStateInfoSent.getNetCapabilityInternet());
+ assertEquals(0, nrStateInfoSent.getNetworkRequestDurationMillis());
+
+ clearInvocations(mNRStateStatsDeps);
+ // This call will be used to calculate NR removed time
+ Mockito.when(mNRStateInfoDeps.getElapsedRealtime()).thenReturn(nrEndTime);
+ mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(NOT_METERED_WIFI_NETWORK_REQUEST);
+
+ verify(mNRStateStatsDeps, timeout(TIMEOUT_MS))
+ .writeStats(networkRequestStateInfoCaptor.capture());
+
+ nrStateInfoSent = networkRequestStateInfoCaptor.getValue();
+ assertEquals(NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED,
+ nrStateInfoSent.getNetworkRequestStateStatsType());
+ assertEquals(NOT_METERED_WIFI_NETWORK_REQUEST.requestId, nrStateInfoSent.getRequestId());
+ assertEquals(TEST_PACKAGE_UID, nrStateInfoSent.getPackageUid());
+ assertEquals(1 << NetworkCapabilities.TRANSPORT_WIFI, nrStateInfoSent.getTransportTypes());
+ assertTrue(nrStateInfoSent.getNetCapabilityNotMetered());
+ assertFalse(nrStateInfoSent.getNetCapabilityInternet());
+ assertEquals(nrEndTime - nrStartTime, nrStateInfoSent.getNetworkRequestDurationMillis());
+ }
+
+ @Test
+ public void testUnreceivedNetworkRequestRemoved() {
+ mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(NOT_METERED_WIFI_NETWORK_REQUEST);
+ HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
+ verify(mNRStateStatsDeps, never())
+ .writeStats(any(NetworkRequestStateInfo.class));
+ }
+
+ @Test
+ public void testNoMessagesWhenNetworkRequestReceived() {
+ mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(NOT_METERED_WIFI_NETWORK_REQUEST);
+ verify(mNRStateStatsDeps, timeout(TIMEOUT_MS))
+ .writeStats(any(NetworkRequestStateInfo.class));
+
+ clearInvocations(mNRStateStatsDeps);
+ mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(NOT_METERED_WIFI_NETWORK_REQUEST);
+ HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
+ verify(mNRStateStatsDeps, never())
+ .writeStats(any(NetworkRequestStateInfo.class));
+ }
+
+ @Test
+ public void testMessageQueueSizeLimitNotExceeded() {
+ // Imitate many events (MAX_QUEUED_REQUESTS) are coming together at once while
+ // the other event is being processed.
+ final ConditionVariable cv = new ConditionVariable();
+ mHandlerThread.getThreadHandler().post(() -> cv.block());
+ for (int i = 0; i < NetworkRequestStateStatsMetrics.MAX_QUEUED_REQUESTS / 2; i++) {
+ mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(new NetworkRequest(
+ new NetworkCapabilities().setRequestorUid(TEST_PACKAGE_UID),
+ 0, i + 1, NetworkRequest.Type.REQUEST));
+ mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(new NetworkRequest(
+ new NetworkCapabilities().setRequestorUid(TEST_PACKAGE_UID),
+ 0, i + 1, NetworkRequest.Type.REQUEST));
+ }
+
+ // When event queue is full, all other events should be dropped.
+ mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(new NetworkRequest(
+ new NetworkCapabilities().setRequestorUid(TEST_PACKAGE_UID),
+ 0, 2 * NetworkRequestStateStatsMetrics.MAX_QUEUED_REQUESTS + 1,
+ NetworkRequest.Type.REQUEST));
+
+ cv.open();
+
+ // Check only first MAX_QUEUED_REQUESTS events are logged.
+ ArgumentCaptor<NetworkRequestStateInfo> networkRequestStateInfoCaptor =
+ ArgumentCaptor.forClass(NetworkRequestStateInfo.class);
+ verify(mNRStateStatsDeps, timeout(TIMEOUT_MS).times(
+ NetworkRequestStateStatsMetrics.MAX_QUEUED_REQUESTS))
+ .writeStats(networkRequestStateInfoCaptor.capture());
+ for (int i = 0; i < NetworkRequestStateStatsMetrics.MAX_QUEUED_REQUESTS; i++) {
+ NetworkRequestStateInfo nrStateInfoSent =
+ networkRequestStateInfoCaptor.getAllValues().get(i);
+ assertEquals(i / 2 + 1, nrStateInfoSent.getRequestId());
+ assertEquals(
+ (i % 2 == 0)
+ ? NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED
+ : NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED,
+ nrStateInfoSent.getNetworkRequestStateStatsType());
+ }
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/RoutingCoordinatorServiceTest.kt b/tests/unit/java/com/android/server/connectivity/RoutingCoordinatorServiceTest.kt
index 12758c6..4e15d5f 100644
--- a/tests/unit/java/com/android/server/connectivity/RoutingCoordinatorServiceTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/RoutingCoordinatorServiceTest.kt
@@ -18,14 +18,17 @@
import android.net.INetd
import android.os.Build
+import android.util.Log
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.tryTest
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.test.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.mock
-import kotlin.test.assertFailsWith
@RunWith(DevSdkIgnoreRunner::class)
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
@@ -46,9 +49,15 @@
inOrder.verify(mNetd).tetherAddForward("from2", "to1")
inOrder.verify(mNetd).ipfwdAddInterfaceForward("from2", "to1")
- assertFailsWith<IllegalStateException> {
- // Can't add the same pair again
+ val hasFailed = AtomicBoolean(false)
+ val prevHandler = Log.setWtfHandler { tag, what, system ->
+ hasFailed.set(true)
+ }
+ tryTest {
mService.addInterfaceForward("from2", "to1")
+ assertTrue(hasFailed.get())
+ } cleanup {
+ Log.setWtfHandler(prevHandler)
}
mService.removeInterfaceForward("from1", "to1")
diff --git a/tests/unit/java/com/android/server/connectivity/SatelliteAccessControllerTest.kt b/tests/unit/java/com/android/server/connectivity/SatelliteAccessControllerTest.kt
new file mode 100644
index 0000000..193078b
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/SatelliteAccessControllerTest.kt
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.connectivity
+
+import android.Manifest
+import android.app.role.OnRoleHoldersChangedListener
+import android.app.role.RoleManager
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.UserInfo
+import android.os.Build
+import android.os.Handler
+import android.os.UserHandle
+import android.util.ArraySet
+import com.android.server.makeMockUserManager
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+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.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import java.util.concurrent.Executor
+import java.util.function.Consumer
+
+private const val USER = 0
+val USER_INFO = UserInfo(USER, "" /* name */, UserInfo.FLAG_PRIMARY)
+val USER_HANDLE = UserHandle(USER)
+private const val PRIMARY_USER = 0
+private const val SECONDARY_USER = 10
+private val PRIMARY_USER_HANDLE = UserHandle.of(PRIMARY_USER)
+private val SECONDARY_USER_HANDLE = UserHandle.of(SECONDARY_USER)
+// sms app names
+private const val SMS_APP1 = "sms_app_1"
+private const val SMS_APP2 = "sms_app_2"
+// sms app ids
+private const val SMS_APP_ID1 = 100
+private const val SMS_APP_ID2 = 101
+// UID for app1 and app2 on primary user
+// These app could become default sms app for user1
+private val PRIMARY_USER_SMS_APP_UID1 = UserHandle.getUid(PRIMARY_USER, SMS_APP_ID1)
+private val PRIMARY_USER_SMS_APP_UID2 = UserHandle.getUid(PRIMARY_USER, SMS_APP_ID2)
+// UID for app1 and app2 on secondary user
+// These app could become default sms app for user2
+private val SECONDARY_USER_SMS_APP_UID1 = UserHandle.getUid(SECONDARY_USER, SMS_APP_ID1)
+private val SECONDARY_USER_SMS_APP_UID2 = UserHandle.getUid(SECONDARY_USER, SMS_APP_ID2)
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class SatelliteAccessControllerTest {
+ private val context = mock(Context::class.java)
+ private val mPackageManager = mock(PackageManager::class.java)
+ private val mHandler = mock(Handler::class.java)
+ private val mRoleManager =
+ mock(SatelliteAccessController.Dependencies::class.java)
+ private val mCallback = mock(Consumer::class.java) as Consumer<Set<Int>>
+ private val mSatelliteAccessController =
+ SatelliteAccessController(context, mRoleManager, mCallback, mHandler)
+ private lateinit var mRoleHolderChangedListener: OnRoleHoldersChangedListener
+ @Before
+ @Throws(PackageManager.NameNotFoundException::class)
+ fun setup() {
+ makeMockUserManager(USER_INFO, USER_HANDLE)
+ doReturn(context).`when`(context).createContextAsUser(any(), anyInt())
+ doReturn(mPackageManager).`when`(context).packageManager
+
+ doReturn(PackageManager.PERMISSION_GRANTED)
+ .`when`(mPackageManager)
+ .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, SMS_APP1)
+ doReturn(PackageManager.PERMISSION_GRANTED)
+ .`when`(mPackageManager)
+ .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, SMS_APP2)
+
+ // Initialise default message application primary user package1
+ val applicationInfo1 = ApplicationInfo()
+ applicationInfo1.uid = PRIMARY_USER_SMS_APP_UID1
+ doReturn(applicationInfo1)
+ .`when`(mPackageManager)
+ .getApplicationInfo(eq(SMS_APP1), anyInt())
+
+ // Initialise default message application primary user package2
+ val applicationInfo2 = ApplicationInfo()
+ applicationInfo2.uid = PRIMARY_USER_SMS_APP_UID2
+ doReturn(applicationInfo2)
+ .`when`(mPackageManager)
+ .getApplicationInfo(eq(SMS_APP2), anyInt())
+
+ // Get registered listener using captor
+ val listenerCaptor = ArgumentCaptor.forClass(
+ OnRoleHoldersChangedListener::class.java
+ )
+ mSatelliteAccessController.start()
+ verify(mRoleManager).addOnRoleHoldersChangedListenerAsUser(
+ any(Executor::class.java), listenerCaptor.capture(), any(UserHandle::class.java))
+ mRoleHolderChangedListener = listenerCaptor.value
+ }
+
+ @Test
+ fun test_onRoleHoldersChanged_SatelliteFallbackUid_Changed_SingleUser() {
+ doReturn(listOf<String>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback, never()).accept(any())
+
+ // check DEFAULT_MESSAGING_APP1 is available as satellite network fallback uid
+ doReturn(listOf(SMS_APP1))
+ .`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID1))
+
+ // check SMS_APP2 is available as satellite network Fallback uid
+ doReturn(listOf(SMS_APP2)).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID2))
+
+ // check no uid is available as satellite network fallback uid
+ doReturn(listOf<String>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback).accept(ArraySet())
+ }
+
+ @Test
+ fun test_onRoleHoldersChanged_NoSatelliteCommunicationPermission() {
+ doReturn(listOf<Any>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback, never()).accept(any())
+
+ // check DEFAULT_MESSAGING_APP1 is not available as satellite network fallback uid
+ // since satellite communication permission not available.
+ doReturn(PackageManager.PERMISSION_DENIED)
+ .`when`(mPackageManager)
+ .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, SMS_APP1)
+ doReturn(listOf(SMS_APP1))
+ .`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback, never()).accept(any())
+ }
+
+ @Test
+ fun test_onRoleHoldersChanged_RoleSms_NotAvailable() {
+ doReturn(listOf(SMS_APP1))
+ .`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_BROWSER,
+ PRIMARY_USER_HANDLE)
+ verify(mCallback, never()).accept(any())
+ }
+
+ @Test
+ fun test_onRoleHoldersChanged_SatelliteNetworkFallbackUid_Changed_multiUser() {
+ doReturn(listOf<String>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback, never()).accept(any())
+
+ // check SMS_APP1 is available as satellite network fallback uid at primary user
+ doReturn(listOf(SMS_APP1))
+ .`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID1))
+
+ // check SMS_APP2 is available as satellite network fallback uid at primary user
+ doReturn(listOf(SMS_APP2)).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID2))
+
+ // check SMS_APP1 is available as satellite network fallback uid at secondary user
+ val applicationInfo1 = ApplicationInfo()
+ applicationInfo1.uid = SECONDARY_USER_SMS_APP_UID1
+ doReturn(applicationInfo1).`when`(mPackageManager)
+ .getApplicationInfo(eq(SMS_APP1), anyInt())
+ doReturn(listOf(SMS_APP1)).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+ SECONDARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, SECONDARY_USER_HANDLE)
+ verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID2, SECONDARY_USER_SMS_APP_UID1))
+
+ // check no uid is available as satellite network fallback uid at primary user
+ doReturn(listOf<String>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE)
+ verify(mCallback).accept(setOf(SECONDARY_USER_SMS_APP_UID1))
+
+ // check SMS_APP2 is available as satellite network fallback uid at secondary user
+ applicationInfo1.uid = SECONDARY_USER_SMS_APP_UID2
+ doReturn(applicationInfo1).`when`(mPackageManager)
+ .getApplicationInfo(eq(SMS_APP2), anyInt())
+ doReturn(listOf(SMS_APP2))
+ .`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS, SECONDARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, SECONDARY_USER_HANDLE)
+ verify(mCallback).accept(setOf(SECONDARY_USER_SMS_APP_UID2))
+
+ // check no uid is available as satellite network fallback uid at secondary user
+ doReturn(listOf<String>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
+ SECONDARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, SECONDARY_USER_HANDLE)
+ verify(mCallback).accept(ArraySet())
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
deleted file mode 100644
index c9cece0..0000000
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ /dev/null
@@ -1,3298 +0,0 @@
-/*
- * Copyright (C) 2016 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;
-
-import static android.Manifest.permission.BIND_VPN_SERVICE;
-import static android.Manifest.permission.CONTROL_VPN;
-import static android.content.pm.PackageManager.PERMISSION_DENIED;
-import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-import static android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback;
-import static android.net.ConnectivityDiagnosticsManager.DataStallReport;
-import static android.net.ConnectivityManager.NetworkCallback;
-import static android.net.INetd.IF_STATE_DOWN;
-import static android.net.INetd.IF_STATE_UP;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED;
-import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
-import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
-import static android.net.RouteInfo.RTN_UNREACHABLE;
-import static android.net.VpnManager.TYPE_VPN_PLATFORM;
-import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS;
-import static android.net.cts.util.IkeSessionTestUtils.TEST_IDENTITY;
-import static android.net.cts.util.IkeSessionTestUtils.TEST_KEEPALIVE_TIMEOUT_UNSET;
-import static android.net.cts.util.IkeSessionTestUtils.getTestIkeSessionParams;
-import static android.net.ipsec.ike.IkeSessionConfiguration.EXTENSION_TYPE_MOBIKE;
-import static android.net.ipsec.ike.IkeSessionParams.ESP_ENCAP_TYPE_AUTO;
-import static android.net.ipsec.ike.IkeSessionParams.ESP_ENCAP_TYPE_NONE;
-import static android.net.ipsec.ike.IkeSessionParams.ESP_ENCAP_TYPE_UDP;
-import static android.net.ipsec.ike.IkeSessionParams.ESP_IP_VERSION_AUTO;
-import static android.net.ipsec.ike.IkeSessionParams.ESP_IP_VERSION_IPV4;
-import static android.net.ipsec.ike.IkeSessionParams.ESP_IP_VERSION_IPV6;
-import static android.os.UserHandle.PER_USER_RANGE;
-import static android.telephony.CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL;
-import static android.telephony.CarrierConfigManager.KEY_MIN_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT;
-import static android.telephony.CarrierConfigManager.KEY_PREFERRED_IKE_PROTOCOL_INT;
-
-import static com.android.net.module.util.NetworkStackConstants.IPV6_MIN_MTU;
-import static com.android.server.connectivity.Vpn.AUTOMATIC_KEEPALIVE_DELAY_SECONDS;
-import static com.android.server.connectivity.Vpn.DEFAULT_LONG_LIVED_TCP_CONNS_EXPENSIVE_TIMEOUT_SEC;
-import static com.android.server.connectivity.Vpn.DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT;
-import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_AUTO;
-import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_IPV4_UDP;
-import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_IPV6_ESP;
-import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_IPV6_UDP;
-import static com.android.testutils.HandlerUtils.waitForIdleSerialExecutor;
-import static com.android.testutils.MiscAsserts.assertThrows;
-
-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.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.argThat;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.longThat;
-import static org.mockito.Mockito.after;
-import static org.mockito.Mockito.atLeast;
-import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doCallRealMethod;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.annotation.NonNull;
-import android.annotation.UserIdInt;
-import android.app.AppOpsManager;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.content.pm.ServiceInfo;
-import android.content.pm.UserInfo;
-import android.content.res.Resources;
-import android.net.ConnectivityDiagnosticsManager;
-import android.net.ConnectivityManager;
-import android.net.INetd;
-import android.net.Ikev2VpnProfile;
-import android.net.InetAddresses;
-import android.net.InterfaceConfigurationParcel;
-import android.net.IpPrefix;
-import android.net.IpSecConfig;
-import android.net.IpSecManager;
-import android.net.IpSecTransform;
-import android.net.IpSecTunnelInterfaceResponse;
-import android.net.LinkAddress;
-import android.net.LinkProperties;
-import android.net.Network;
-import android.net.NetworkAgent;
-import android.net.NetworkAgentConfig;
-import android.net.NetworkCapabilities;
-import android.net.NetworkInfo.DetailedState;
-import android.net.RouteInfo;
-import android.net.TelephonyNetworkSpecifier;
-import android.net.UidRangeParcel;
-import android.net.VpnManager;
-import android.net.VpnProfileState;
-import android.net.VpnService;
-import android.net.VpnTransportInfo;
-import android.net.ipsec.ike.ChildSessionCallback;
-import android.net.ipsec.ike.ChildSessionConfiguration;
-import android.net.ipsec.ike.IkeFqdnIdentification;
-import android.net.ipsec.ike.IkeSessionCallback;
-import android.net.ipsec.ike.IkeSessionConfiguration;
-import android.net.ipsec.ike.IkeSessionConnectionInfo;
-import android.net.ipsec.ike.IkeSessionParams;
-import android.net.ipsec.ike.IkeTrafficSelector;
-import android.net.ipsec.ike.IkeTunnelConnectionParams;
-import android.net.ipsec.ike.exceptions.IkeException;
-import android.net.ipsec.ike.exceptions.IkeNetworkLostException;
-import android.net.ipsec.ike.exceptions.IkeNonProtocolException;
-import android.net.ipsec.ike.exceptions.IkeProtocolException;
-import android.net.ipsec.ike.exceptions.IkeTimeoutException;
-import android.net.vcn.VcnTransportInfo;
-import android.net.wifi.WifiInfo;
-import android.os.Build.VERSION_CODES;
-import android.os.Bundle;
-import android.os.INetworkManagementService;
-import android.os.ParcelFileDescriptor;
-import android.os.PersistableBundle;
-import android.os.PowerWhitelistManager;
-import android.os.Process;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.os.test.TestLooper;
-import android.provider.Settings;
-import android.security.Credentials;
-import android.telephony.CarrierConfigManager;
-import android.telephony.SubscriptionInfo;
-import android.telephony.SubscriptionManager;
-import android.telephony.TelephonyManager;
-import android.util.ArrayMap;
-import android.util.ArraySet;
-import android.util.Pair;
-import android.util.Range;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.internal.R;
-import com.android.internal.net.LegacyVpnInfo;
-import com.android.internal.net.VpnConfig;
-import com.android.internal.net.VpnProfile;
-import com.android.internal.util.HexDump;
-import com.android.internal.util.IndentingPrintWriter;
-import com.android.server.DeviceIdleInternal;
-import com.android.server.IpSecService;
-import com.android.server.VpnTestBase;
-import com.android.server.vcn.util.PersistableBundleUtils;
-import com.android.testutils.DevSdkIgnoreRule;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.AdditionalAnswers;
-import org.mockito.Answers;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.InOrder;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.io.FileDescriptor;
-import java.io.IOException;
-import java.io.StringWriter;
-import java.net.Inet4Address;
-import java.net.Inet6Address;
-import java.net.InetAddress;
-import java.net.UnknownHostException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.ScheduledThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Tests for {@link Vpn}.
- *
- * Build, install and run with:
- * runtest frameworks-net -c com.android.server.connectivity.VpnTest
- */
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class VpnTest extends VpnTestBase {
- private static final String TAG = "VpnTest";
-
- @Rule
- public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
-
- static final Network EGRESS_NETWORK = new Network(101);
- static final String EGRESS_IFACE = "wlan0";
- private static final String TEST_VPN_CLIENT = "2.4.6.8";
- private static final String TEST_VPN_SERVER = "1.2.3.4";
- private static final String TEST_VPN_IDENTITY = "identity";
- private static final byte[] TEST_VPN_PSK = "psk".getBytes();
-
- private static final int IP4_PREFIX_LEN = 32;
- private static final int IP6_PREFIX_LEN = 64;
- private static final int MIN_PORT = 0;
- private static final int MAX_PORT = 65535;
-
- private static final InetAddress TEST_VPN_CLIENT_IP =
- InetAddresses.parseNumericAddress(TEST_VPN_CLIENT);
- private static final InetAddress TEST_VPN_SERVER_IP =
- InetAddresses.parseNumericAddress(TEST_VPN_SERVER);
- private static final InetAddress TEST_VPN_CLIENT_IP_2 =
- InetAddresses.parseNumericAddress("192.0.2.200");
- private static final InetAddress TEST_VPN_SERVER_IP_2 =
- InetAddresses.parseNumericAddress("192.0.2.201");
- private static final InetAddress TEST_VPN_INTERNAL_IP =
- InetAddresses.parseNumericAddress("198.51.100.10");
- private static final InetAddress TEST_VPN_INTERNAL_IP6 =
- InetAddresses.parseNumericAddress("2001:db8::1");
- private static final InetAddress TEST_VPN_INTERNAL_DNS =
- InetAddresses.parseNumericAddress("8.8.8.8");
- private static final InetAddress TEST_VPN_INTERNAL_DNS6 =
- InetAddresses.parseNumericAddress("2001:4860:4860::8888");
-
- private static final IkeTrafficSelector IN_TS =
- new IkeTrafficSelector(MIN_PORT, MAX_PORT, TEST_VPN_INTERNAL_IP, TEST_VPN_INTERNAL_IP);
- private static final IkeTrafficSelector IN_TS6 =
- new IkeTrafficSelector(
- MIN_PORT, MAX_PORT, TEST_VPN_INTERNAL_IP6, TEST_VPN_INTERNAL_IP6);
- private static final IkeTrafficSelector OUT_TS =
- new IkeTrafficSelector(MIN_PORT, MAX_PORT,
- InetAddresses.parseNumericAddress("0.0.0.0"),
- InetAddresses.parseNumericAddress("255.255.255.255"));
- private static final IkeTrafficSelector OUT_TS6 =
- new IkeTrafficSelector(
- MIN_PORT,
- MAX_PORT,
- InetAddresses.parseNumericAddress("::"),
- InetAddresses.parseNumericAddress("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
-
- private static final Network TEST_NETWORK = new Network(Integer.MAX_VALUE);
- private static final Network TEST_NETWORK_2 = new Network(Integer.MAX_VALUE - 1);
- private static final String TEST_IFACE_NAME = "TEST_IFACE";
- private static final int TEST_TUNNEL_RESOURCE_ID = 0x2345;
- private static final long TEST_TIMEOUT_MS = 500L;
- private static final long TIMEOUT_CROSSTHREAD_MS = 20_000L;
- private static final String PRIMARY_USER_APP_EXCLUDE_KEY =
- "VPNAPPEXCLUDED_27_com.testvpn.vpn";
- static final String PKGS_BYTES = getPackageByteString(List.of(PKGS));
- private static final Range<Integer> PRIMARY_USER_RANGE = uidRangeForUser(PRIMARY_USER.id);
- private static final int TEST_KEEPALIVE_TIMER = 800;
- private static final int TEST_SUB_ID = 1234;
- private static final String TEST_MCCMNC = "12345";
-
- @Mock(answer = Answers.RETURNS_DEEP_STUBS) private Context mContext;
- @Mock private UserManager mUserManager;
- @Mock private PackageManager mPackageManager;
- @Mock private INetworkManagementService mNetService;
- @Mock private INetd mNetd;
- @Mock private AppOpsManager mAppOps;
- @Mock private NotificationManager mNotificationManager;
- @Mock private Vpn.SystemServices mSystemServices;
- @Mock private Vpn.IkeSessionWrapper mIkeSessionWrapper;
- @Mock private Vpn.Ikev2SessionCreator mIkev2SessionCreator;
- @Mock private Vpn.VpnNetworkAgentWrapper mMockNetworkAgent;
- @Mock private ConnectivityManager mConnectivityManager;
- @Mock private ConnectivityDiagnosticsManager mCdm;
- @Mock private TelephonyManager mTelephonyManager;
- @Mock private TelephonyManager mTmPerSub;
- @Mock private CarrierConfigManager mConfigManager;
- @Mock private SubscriptionManager mSubscriptionManager;
- @Mock private IpSecService mIpSecService;
- @Mock private VpnProfileStore mVpnProfileStore;
- private final TestExecutor mExecutor;
- @Mock DeviceIdleInternal mDeviceIdleInternal;
- private final VpnProfile mVpnProfile;
-
- @Captor private ArgumentCaptor<Collection<Range<Integer>>> mUidRangesCaptor;
-
- private IpSecManager mIpSecManager;
- private TestDeps mTestDeps;
-
- public static class TestExecutor extends ScheduledThreadPoolExecutor {
- public static final long REAL_DELAY = -1;
-
- // For the purposes of the test, run all scheduled tasks after 10ms to save
- // execution time, unless overridden by the specific test. Set to REAL_DELAY
- // to actually wait for the delay specified by the real call to schedule().
- public long delayMs = 10;
- // If this is true, execute() will call the runnable inline. This is useful because
- // super.execute() calls schedule(), which messes with checks that scheduled() is
- // called a given number of times.
- public boolean executeDirect = false;
-
- public TestExecutor() {
- super(1);
- }
-
- @Override
- public void execute(final Runnable command) {
- // See |executeDirect| for why this is necessary.
- if (executeDirect) {
- command.run();
- } else {
- super.execute(command);
- }
- }
-
- @Override
- public ScheduledFuture<?> schedule(final Runnable command, final long delay,
- TimeUnit unit) {
- if (0 == delay || delayMs == REAL_DELAY) {
- // super.execute() calls schedule() with 0, so use the real delay if it's 0.
- return super.schedule(command, delay, unit);
- } else {
- return super.schedule(command, delayMs, TimeUnit.MILLISECONDS);
- }
- }
- }
-
- public VpnTest() throws Exception {
- // Build an actual VPN profile that is capable of being converted to and from an
- // Ikev2VpnProfile
- final Ikev2VpnProfile.Builder builder =
- new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY);
- builder.setAuthPsk(TEST_VPN_PSK);
- builder.setBypassable(true /* isBypassable */);
- mExecutor = spy(new TestExecutor());
- mVpnProfile = builder.build().toVpnProfile();
- }
-
- @Before
- public void setUp() throws Exception {
- MockitoAnnotations.initMocks(this);
-
- mIpSecManager = new IpSecManager(mContext, mIpSecService);
- mTestDeps = spy(new TestDeps());
- doReturn(IPV6_MIN_MTU)
- .when(mTestDeps)
- .calculateVpnMtu(any(), anyInt(), anyInt(), anyBoolean());
- doReturn(1500).when(mTestDeps).getJavaNetworkInterfaceMtu(any(), anyInt());
-
- when(mContext.getPackageManager()).thenReturn(mPackageManager);
- setMockedPackages(sPackages);
-
- when(mContext.getPackageName()).thenReturn(TEST_VPN_PKG);
- when(mContext.getOpPackageName()).thenReturn(TEST_VPN_PKG);
- mockService(UserManager.class, Context.USER_SERVICE, mUserManager);
- mockService(AppOpsManager.class, Context.APP_OPS_SERVICE, mAppOps);
- mockService(NotificationManager.class, Context.NOTIFICATION_SERVICE, mNotificationManager);
- mockService(ConnectivityManager.class, Context.CONNECTIVITY_SERVICE, mConnectivityManager);
- mockService(IpSecManager.class, Context.IPSEC_SERVICE, mIpSecManager);
- mockService(ConnectivityDiagnosticsManager.class, Context.CONNECTIVITY_DIAGNOSTICS_SERVICE,
- mCdm);
- mockService(TelephonyManager.class, Context.TELEPHONY_SERVICE, mTelephonyManager);
- mockService(CarrierConfigManager.class, Context.CARRIER_CONFIG_SERVICE, mConfigManager);
- mockService(SubscriptionManager.class, Context.TELEPHONY_SUBSCRIPTION_SERVICE,
- mSubscriptionManager);
- doReturn(mTmPerSub).when(mTelephonyManager).createForSubscriptionId(anyInt());
- when(mContext.getString(R.string.config_customVpnAlwaysOnDisconnectedDialogComponent))
- .thenReturn(Resources.getSystem().getString(
- R.string.config_customVpnAlwaysOnDisconnectedDialogComponent));
- when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS))
- .thenReturn(true);
-
- // Used by {@link Notification.Builder}
- ApplicationInfo applicationInfo = new ApplicationInfo();
- applicationInfo.targetSdkVersion = VERSION_CODES.CUR_DEVELOPMENT;
- when(mContext.getApplicationInfo()).thenReturn(applicationInfo);
- when(mPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
- .thenReturn(applicationInfo);
-
- doNothing().when(mNetService).registerObserver(any());
-
- // Deny all appops by default.
- when(mAppOps.noteOpNoThrow(anyString(), anyInt(), anyString(), any(), any()))
- .thenReturn(AppOpsManager.MODE_IGNORED);
-
- // Setup IpSecService
- final IpSecTunnelInterfaceResponse tunnelResp =
- new IpSecTunnelInterfaceResponse(
- IpSecManager.Status.OK, TEST_TUNNEL_RESOURCE_ID, TEST_IFACE_NAME);
- when(mIpSecService.createTunnelInterface(any(), any(), any(), any(), any()))
- .thenReturn(tunnelResp);
- doReturn(new LinkProperties()).when(mConnectivityManager).getLinkProperties(any());
-
- // The unit test should know what kind of permission it needs and set the permission by
- // itself, so set the default value of Context#checkCallingOrSelfPermission to
- // PERMISSION_DENIED.
- doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(any());
-
- // Set up mIkev2SessionCreator and mExecutor
- resetIkev2SessionCreator(mIkeSessionWrapper);
- }
-
- private void resetIkev2SessionCreator(Vpn.IkeSessionWrapper ikeSession) {
- reset(mIkev2SessionCreator);
- when(mIkev2SessionCreator.createIkeSession(any(), any(), any(), any(), any(), any()))
- .thenReturn(ikeSession);
- }
-
- private <T> void mockService(Class<T> clazz, String name, T service) {
- doReturn(service).when(mContext).getSystemService(name);
- doReturn(name).when(mContext).getSystemServiceName(clazz);
- if (mContext.getSystemService(clazz).getClass().equals(Object.class)) {
- // Test is using mockito-extended (mContext uses Answers.RETURNS_DEEP_STUBS and returned
- // a mock object on a final method)
- doCallRealMethod().when(mContext).getSystemService(clazz);
- }
- }
-
- private Set<Range<Integer>> rangeSet(Range<Integer> ... ranges) {
- final Set<Range<Integer>> range = new ArraySet<>();
- for (Range<Integer> r : ranges) range.add(r);
-
- return range;
- }
-
- private static Range<Integer> uidRangeForUser(int userId) {
- return new Range<Integer>(userId * PER_USER_RANGE, (userId + 1) * PER_USER_RANGE - 1);
- }
-
- private Range<Integer> uidRange(int start, int stop) {
- return new Range<Integer>(start, stop);
- }
-
- private static String getPackageByteString(List<String> packages) {
- try {
- return HexDump.toHexString(
- PersistableBundleUtils.toDiskStableBytes(PersistableBundleUtils.fromList(
- packages, PersistableBundleUtils.STRING_SERIALIZER)),
- true /* upperCase */);
- } catch (IOException e) {
- return null;
- }
- }
-
- @Test
- public void testRestrictedProfilesAreAddedToVpn() {
- setMockedUsers(PRIMARY_USER, SECONDARY_USER, RESTRICTED_PROFILE_A, RESTRICTED_PROFILE_B);
-
- final Vpn vpn = createVpn(PRIMARY_USER.id);
-
- // Assume the user can have restricted profiles.
- doReturn(true).when(mUserManager).canHaveRestrictedProfile();
- final Set<Range<Integer>> ranges =
- vpn.createUserAndRestrictedProfilesRanges(PRIMARY_USER.id, null, null);
-
- assertEquals(rangeSet(PRIMARY_USER_RANGE, uidRangeForUser(RESTRICTED_PROFILE_A.id)),
- ranges);
- }
-
- @Test
- public void testManagedProfilesAreNotAddedToVpn() {
- setMockedUsers(PRIMARY_USER, MANAGED_PROFILE_A);
-
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- final Set<Range<Integer>> ranges = vpn.createUserAndRestrictedProfilesRanges(
- PRIMARY_USER.id, null, null);
-
- assertEquals(rangeSet(PRIMARY_USER_RANGE), ranges);
- }
-
- @Test
- public void testAddUserToVpnOnlyAddsOneUser() {
- setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A, MANAGED_PROFILE_A);
-
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- final Set<Range<Integer>> ranges = new ArraySet<>();
- vpn.addUserToRanges(ranges, PRIMARY_USER.id, null, null);
-
- assertEquals(rangeSet(PRIMARY_USER_RANGE), ranges);
- }
-
- @Test
- public void testUidAllowAndDenylist() throws Exception {
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- final Range<Integer> user = PRIMARY_USER_RANGE;
- final int userStart = user.getLower();
- final int userStop = user.getUpper();
- final String[] packages = {PKGS[0], PKGS[1], PKGS[2]};
-
- // Allowed list
- final Set<Range<Integer>> allow = vpn.createUserAndRestrictedProfilesRanges(PRIMARY_USER.id,
- Arrays.asList(packages), null /* disallowedApplications */);
- assertEquals(rangeSet(
- uidRange(userStart + PKG_UIDS[0], userStart + PKG_UIDS[0]),
- uidRange(userStart + PKG_UIDS[1], userStart + PKG_UIDS[2]),
- uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[0]),
- Process.toSdkSandboxUid(userStart + PKG_UIDS[0])),
- uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[1]),
- Process.toSdkSandboxUid(userStart + PKG_UIDS[2]))),
- allow);
-
- // Denied list
- final Set<Range<Integer>> disallow =
- vpn.createUserAndRestrictedProfilesRanges(PRIMARY_USER.id,
- null /* allowedApplications */, Arrays.asList(packages));
- assertEquals(rangeSet(
- uidRange(userStart, userStart + PKG_UIDS[0] - 1),
- uidRange(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1),
- /* Empty range between UIDS[1] and UIDS[2], should be excluded, */
- uidRange(userStart + PKG_UIDS[2] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
- uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
- Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
- uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)),
- disallow);
- }
-
- private void verifyPowerSaveTempWhitelistApp(String packageName) {
- verify(mDeviceIdleInternal, timeout(TEST_TIMEOUT_MS)).addPowerSaveTempWhitelistApp(
- anyInt(), eq(packageName), anyLong(), anyInt(), eq(false),
- eq(PowerWhitelistManager.REASON_VPN), eq("VpnManager event"));
- }
-
- @Test
- public void testGetAlwaysAndOnGetLockDown() throws Exception {
- final Vpn vpn = createVpn(PRIMARY_USER.id);
-
- // Default state.
- assertFalse(vpn.getAlwaysOn());
- assertFalse(vpn.getLockdown());
-
- // Set always-on without lockdown.
- assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false, Collections.emptyList()));
- assertTrue(vpn.getAlwaysOn());
- assertFalse(vpn.getLockdown());
-
- // Set always-on with lockdown.
- assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, Collections.emptyList()));
- assertTrue(vpn.getAlwaysOn());
- assertTrue(vpn.getLockdown());
-
- // Remove always-on configuration.
- assertTrue(vpn.setAlwaysOnPackage(null, false, Collections.emptyList()));
- assertFalse(vpn.getAlwaysOn());
- assertFalse(vpn.getLockdown());
- }
-
- @Test
- public void testAlwaysOnWithoutLockdown() throws Exception {
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- assertTrue(vpn.setAlwaysOnPackage(
- PKGS[1], false /* lockdown */, null /* lockdownAllowlist */));
- verify(mConnectivityManager, never()).setRequireVpnForUids(anyBoolean(), any());
-
- assertTrue(vpn.setAlwaysOnPackage(
- null /* packageName */, false /* lockdown */, null /* lockdownAllowlist */));
- verify(mConnectivityManager, never()).setRequireVpnForUids(anyBoolean(), any());
- }
-
- @Test
- public void testLockdownChangingPackage() throws Exception {
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- final Range<Integer> user = PRIMARY_USER_RANGE;
- final int userStart = user.getLower();
- final int userStop = user.getUpper();
- // Set always-on without lockdown.
- assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false, null));
-
- // Set always-on with lockdown.
- assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, null));
- verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
- }));
-
- // Switch to another app.
- assertTrue(vpn.setAlwaysOnPackage(PKGS[3], true, null));
- verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
- }));
- verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart, userStart + PKG_UIDS[3] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[3] + 1), userStop)
- }));
- }
-
- @Test
- public void testLockdownAllowlist() throws Exception {
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- final Range<Integer> user = PRIMARY_USER_RANGE;
- final int userStart = user.getLower();
- final int userStop = user.getUpper();
- // Set always-on with lockdown and allow app PKGS[2] from lockdown.
- assertTrue(vpn.setAlwaysOnPackage(
- PKGS[1], true, Collections.singletonList(PKGS[2])));
- verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[2] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[1]) - 1),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)
- }));
- // Change allowed app list to PKGS[3].
- assertTrue(vpn.setAlwaysOnPackage(
- PKGS[1], true, Collections.singletonList(PKGS[3])));
- verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart + PKG_UIDS[2] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)
- }));
- verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStart + PKG_UIDS[3] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1),
- Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[3] + 1), userStop)
- }));
-
- // Change the VPN app.
- assertTrue(vpn.setAlwaysOnPackage(
- PKGS[0], true, Collections.singletonList(PKGS[3])));
- verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStart + PKG_UIDS[3] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1),
- Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1))
- }));
- verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart, userStart + PKG_UIDS[0] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[3] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
- Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1))
- }));
-
- // Remove the list of allowed packages.
- assertTrue(vpn.setAlwaysOnPackage(PKGS[0], true, null));
- verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[3] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
- Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[3] + 1), userStop)
- }));
- verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart + PKG_UIDS[0] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1), userStop),
- }));
-
- // Add the list of allowed packages.
- assertTrue(vpn.setAlwaysOnPackage(
- PKGS[0], true, Collections.singletonList(PKGS[1])));
- verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart + PKG_UIDS[0] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1), userStop),
- }));
- verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
- Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
- }));
-
- // Try allowing a package with a comma, should be rejected.
- assertFalse(vpn.setAlwaysOnPackage(
- PKGS[0], true, Collections.singletonList("a.b,c.d")));
-
- // Pass a non-existent packages in the allowlist, they (and only they) should be ignored.
- // allowed package should change from PGKS[1] to PKGS[2].
- assertTrue(vpn.setAlwaysOnPackage(
- PKGS[0], true, Arrays.asList("com.foo.app", PKGS[2], "com.bar.app")));
- verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
- Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
- }));
- verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[2] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[2] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
- Process.toSdkSandboxUid(userStart + PKG_UIDS[2] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)
- }));
- }
-
- @Test
- public void testLockdownSystemUser() throws Exception {
- final Vpn vpn = createVpn(SYSTEM_USER_ID);
-
- // Uid 0 is always excluded and PKG_UIDS[1] is the uid of the VPN.
- final List<Integer> excludedUids = new ArrayList<>(List.of(0, PKG_UIDS[1]));
- final List<Range<Integer>> ranges = makeVpnUidRange(SYSTEM_USER_ID, excludedUids);
-
- // Set always-on with lockdown.
- assertTrue(vpn.setAlwaysOnPackage(
- PKGS[1], true /* lockdown */, null /* lockdownAllowlist */));
- verify(mConnectivityManager).setRequireVpnForUids(true, ranges);
-
- // Disable always-on with lockdown.
- assertTrue(vpn.setAlwaysOnPackage(
- null /* packageName */, false /* lockdown */, null /* lockdownAllowlist */));
- verify(mConnectivityManager).setRequireVpnForUids(false, ranges);
-
- // Set always-on with lockdown and allow the app PKGS[2].
- excludedUids.add(PKG_UIDS[2]);
- final List<Range<Integer>> ranges2 = makeVpnUidRange(SYSTEM_USER_ID, excludedUids);
- assertTrue(vpn.setAlwaysOnPackage(
- PKGS[1], true /* lockdown */, Collections.singletonList(PKGS[2])));
- verify(mConnectivityManager).setRequireVpnForUids(true, ranges2);
-
- // Disable always-on with lockdown.
- assertTrue(vpn.setAlwaysOnPackage(
- null /* packageName */, false /* lockdown */, null /* lockdownAllowlist */));
- verify(mConnectivityManager).setRequireVpnForUids(false, ranges2);
- }
-
- @Test
- public void testLockdownRuleRepeatability() throws Exception {
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- final UidRangeParcel[] primaryUserRangeParcel = new UidRangeParcel[] {
- new UidRangeParcel(PRIMARY_USER_RANGE.getLower(), PRIMARY_USER_RANGE.getUpper())};
- // Given legacy lockdown is already enabled,
- vpn.setLockdown(true);
- verify(mConnectivityManager, times(1)).setRequireVpnForUids(true,
- toRanges(primaryUserRangeParcel));
-
- // Enabling legacy lockdown twice should do nothing.
- vpn.setLockdown(true);
- verify(mConnectivityManager, times(1)).setRequireVpnForUids(anyBoolean(), any());
-
- // And disabling should remove the rules exactly once.
- vpn.setLockdown(false);
- verify(mConnectivityManager, times(1)).setRequireVpnForUids(false,
- toRanges(primaryUserRangeParcel));
-
- // Removing the lockdown again should have no effect.
- vpn.setLockdown(false);
- verify(mConnectivityManager, times(2)).setRequireVpnForUids(anyBoolean(), any());
- }
-
- private ArrayList<Range<Integer>> toRanges(UidRangeParcel[] ranges) {
- ArrayList<Range<Integer>> rangesArray = new ArrayList<>(ranges.length);
- for (int i = 0; i < ranges.length; i++) {
- rangesArray.add(new Range<>(ranges[i].start, ranges[i].stop));
- }
- return rangesArray;
- }
-
- @Test
- public void testLockdownRuleReversibility() throws Exception {
- doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- final UidRangeParcel[] entireUser = {
- new UidRangeParcel(PRIMARY_USER_RANGE.getLower(), PRIMARY_USER_RANGE.getUpper())
- };
- final UidRangeParcel[] exceptPkg0 = {
- new UidRangeParcel(entireUser[0].start, entireUser[0].start + PKG_UIDS[0] - 1),
- new UidRangeParcel(entireUser[0].start + PKG_UIDS[0] + 1,
- Process.toSdkSandboxUid(entireUser[0].start + PKG_UIDS[0] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(entireUser[0].start + PKG_UIDS[0] + 1),
- entireUser[0].stop),
- };
-
- final InOrder order = inOrder(mConnectivityManager);
-
- // Given lockdown is enabled with no package (legacy VPN),
- vpn.setLockdown(true);
- order.verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(entireUser));
-
- // When a new VPN package is set the rules should change to cover that package.
- vpn.prepare(null, PKGS[0], VpnManager.TYPE_VPN_SERVICE);
- order.verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(entireUser));
- order.verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(exceptPkg0));
-
- // When that VPN package is unset, everything should be undone again in reverse.
- vpn.prepare(null, VpnConfig.LEGACY_VPN, VpnManager.TYPE_VPN_SERVICE);
- order.verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(exceptPkg0));
- order.verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(entireUser));
- }
-
- @Test
- public void testOnUserAddedAndRemoved_restrictedUser() throws Exception {
- final InOrder order = inOrder(mMockNetworkAgent);
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- final Set<Range<Integer>> initialRange = rangeSet(PRIMARY_USER_RANGE);
- // Note since mVpnProfile is a Ikev2VpnProfile, this starts an IkeV2VpnRunner.
- startLegacyVpn(vpn, mVpnProfile);
- // Set an initial Uid range and mock the network agent
- vpn.mNetworkCapabilities.setUids(initialRange);
- vpn.mNetworkAgent = mMockNetworkAgent;
-
- // Add the restricted user
- setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A);
- vpn.onUserAdded(RESTRICTED_PROFILE_A.id);
- // Expect restricted user range to be added to the NetworkCapabilities.
- final Set<Range<Integer>> expectRestrictedRange =
- rangeSet(PRIMARY_USER_RANGE, uidRangeForUser(RESTRICTED_PROFILE_A.id));
- assertEquals(expectRestrictedRange, vpn.mNetworkCapabilities.getUids());
- order.verify(mMockNetworkAgent).doSendNetworkCapabilities(
- argThat(nc -> expectRestrictedRange.equals(nc.getUids())));
-
- // Remove the restricted user
- vpn.onUserRemoved(RESTRICTED_PROFILE_A.id);
- // Expect restricted user range to be removed from the NetworkCapabilities.
- assertEquals(initialRange, vpn.mNetworkCapabilities.getUids());
- order.verify(mMockNetworkAgent).doSendNetworkCapabilities(
- argThat(nc -> initialRange.equals(nc.getUids())));
- }
-
- @Test
- public void testOnUserAddedAndRemoved_restrictedUserLockdown() throws Exception {
- final UidRangeParcel[] primaryUserRangeParcel = new UidRangeParcel[] {
- new UidRangeParcel(PRIMARY_USER_RANGE.getLower(), PRIMARY_USER_RANGE.getUpper())};
- final Range<Integer> restrictedUserRange = uidRangeForUser(RESTRICTED_PROFILE_A.id);
- final UidRangeParcel[] restrictedUserRangeParcel = new UidRangeParcel[] {
- new UidRangeParcel(restrictedUserRange.getLower(), restrictedUserRange.getUpper())};
- final Vpn vpn = createVpn(PRIMARY_USER.id);
-
- // Set lockdown calls setRequireVpnForUids
- vpn.setLockdown(true);
- verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(primaryUserRangeParcel));
-
- // Add the restricted user
- doReturn(true).when(mUserManager).canHaveRestrictedProfile();
- setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A);
- vpn.onUserAdded(RESTRICTED_PROFILE_A.id);
-
- // Expect restricted user range to be added.
- verify(mConnectivityManager).setRequireVpnForUids(true,
- toRanges(restrictedUserRangeParcel));
-
- // Mark as partial indicates that the user is removed, mUserManager.getAliveUsers() does not
- // return the restricted user but it is still returned in mUserManager.getUserInfo().
- RESTRICTED_PROFILE_A.partial = true;
- // Remove the restricted user
- vpn.onUserRemoved(RESTRICTED_PROFILE_A.id);
- verify(mConnectivityManager).setRequireVpnForUids(false,
- toRanges(restrictedUserRangeParcel));
- // reset to avoid affecting other tests since RESTRICTED_PROFILE_A is static.
- RESTRICTED_PROFILE_A.partial = false;
- }
-
- @Test
- public void testOnUserAddedAndRemoved_restrictedUserAlwaysOn() throws Exception {
- final Vpn vpn = createVpn(PRIMARY_USER.id);
-
- // setAlwaysOnPackage() calls setRequireVpnForUids()
- assertTrue(vpn.setAlwaysOnPackage(
- PKGS[0], true /* lockdown */, null /* lockdownAllowlist */));
- final List<Integer> excludedUids = List.of(PKG_UIDS[0]);
- final List<Range<Integer>> primaryRanges =
- makeVpnUidRange(PRIMARY_USER.id, excludedUids);
- verify(mConnectivityManager).setRequireVpnForUids(true, primaryRanges);
-
- // Add the restricted user
- doReturn(true).when(mUserManager).canHaveRestrictedProfile();
- setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A);
- vpn.onUserAdded(RESTRICTED_PROFILE_A.id);
-
- final List<Range<Integer>> restrictedRanges =
- makeVpnUidRange(RESTRICTED_PROFILE_A.id, excludedUids);
- // Expect restricted user range to be added.
- verify(mConnectivityManager).setRequireVpnForUids(true, restrictedRanges);
-
- // Mark as partial indicates that the user is removed, mUserManager.getAliveUsers() does not
- // return the restricted user but it is still returned in mUserManager.getUserInfo().
- RESTRICTED_PROFILE_A.partial = true;
- // Remove the restricted user
- vpn.onUserRemoved(RESTRICTED_PROFILE_A.id);
- verify(mConnectivityManager).setRequireVpnForUids(false, restrictedRanges);
-
- // reset to avoid affecting other tests since RESTRICTED_PROFILE_A is static.
- RESTRICTED_PROFILE_A.partial = false;
- }
-
- @Test
- public void testPrepare_throwSecurityExceptionWhenGivenPackageDoesNotBelongToTheCaller()
- throws Exception {
- mTestDeps.mIgnoreCallingUidChecks = false;
- final Vpn vpn = createVpn();
- assertThrows(SecurityException.class,
- () -> vpn.prepare("com.not.vpn.owner", null, VpnManager.TYPE_VPN_SERVICE));
- assertThrows(SecurityException.class,
- () -> vpn.prepare(null, "com.not.vpn.owner", VpnManager.TYPE_VPN_SERVICE));
- assertThrows(SecurityException.class,
- () -> vpn.prepare("com.not.vpn.owner1", "com.not.vpn.owner2",
- VpnManager.TYPE_VPN_SERVICE));
- }
-
- @Test
- public void testPrepare_bothOldPackageAndNewPackageAreNull() throws Exception {
- final Vpn vpn = createVpn();
- assertTrue(vpn.prepare(null, null, VpnManager.TYPE_VPN_SERVICE));
-
- }
-
- @Test
- public void testPrepare_legacyVpnWithoutControlVpn()
- throws Exception {
- doThrow(new SecurityException("no CONTROL_VPN")).when(mContext)
- .enforceCallingOrSelfPermission(eq(CONTROL_VPN), any());
- final Vpn vpn = createVpn();
- assertThrows(SecurityException.class,
- () -> vpn.prepare(null, VpnConfig.LEGACY_VPN, VpnManager.TYPE_VPN_SERVICE));
-
- // CONTROL_VPN can be held by the caller or another system server process - both are
- // allowed. Just checking for `enforceCallingPermission` may not be sufficient.
- verify(mContext, never()).enforceCallingPermission(eq(CONTROL_VPN), any());
- }
-
- @Test
- public void testPrepare_legacyVpnWithControlVpn()
- throws Exception {
- doNothing().when(mContext).enforceCallingOrSelfPermission(eq(CONTROL_VPN), any());
- final Vpn vpn = createVpn();
- assertTrue(vpn.prepare(null, VpnConfig.LEGACY_VPN, VpnManager.TYPE_VPN_SERVICE));
-
- // CONTROL_VPN can be held by the caller or another system server process - both are
- // allowed. Just checking for `enforceCallingPermission` may not be sufficient.
- verify(mContext, never()).enforceCallingPermission(eq(CONTROL_VPN), any());
- }
-
- @Test
- public void testIsAlwaysOnPackageSupported() throws Exception {
- final Vpn vpn = createVpn(PRIMARY_USER.id);
-
- ApplicationInfo appInfo = new ApplicationInfo();
- when(mPackageManager.getApplicationInfoAsUser(eq(PKGS[0]), anyInt(), eq(PRIMARY_USER.id)))
- .thenReturn(appInfo);
-
- ServiceInfo svcInfo = new ServiceInfo();
- ResolveInfo resInfo = new ResolveInfo();
- resInfo.serviceInfo = svcInfo;
- when(mPackageManager.queryIntentServicesAsUser(any(), eq(PackageManager.GET_META_DATA),
- eq(PRIMARY_USER.id)))
- .thenReturn(Collections.singletonList(resInfo));
-
- // null package name should return false
- assertFalse(vpn.isAlwaysOnPackageSupported(null));
-
- // Pre-N apps are not supported
- appInfo.targetSdkVersion = VERSION_CODES.M;
- assertFalse(vpn.isAlwaysOnPackageSupported(PKGS[0]));
-
- // N+ apps are supported by default
- appInfo.targetSdkVersion = VERSION_CODES.N;
- assertTrue(vpn.isAlwaysOnPackageSupported(PKGS[0]));
-
- // Apps that opt out explicitly are not supported
- appInfo.targetSdkVersion = VERSION_CODES.CUR_DEVELOPMENT;
- Bundle metaData = new Bundle();
- metaData.putBoolean(VpnService.SERVICE_META_DATA_SUPPORTS_ALWAYS_ON, false);
- svcInfo.metaData = metaData;
- assertFalse(vpn.isAlwaysOnPackageSupported(PKGS[0]));
- }
-
- @Test
- public void testNotificationShownForAlwaysOnApp() throws Exception {
- final UserHandle userHandle = UserHandle.of(PRIMARY_USER.id);
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- setMockedUsers(PRIMARY_USER);
-
- final InOrder order = inOrder(mNotificationManager);
-
- // Don't show a notification for regular disconnected states.
- vpn.updateState(DetailedState.DISCONNECTED, TAG);
- order.verify(mNotificationManager, atLeastOnce()).cancel(anyString(), anyInt());
-
- // Start showing a notification for disconnected once always-on.
- vpn.setAlwaysOnPackage(PKGS[0], false, null);
- order.verify(mNotificationManager).notify(anyString(), anyInt(), any());
-
- // Stop showing the notification once connected.
- vpn.updateState(DetailedState.CONNECTED, TAG);
- order.verify(mNotificationManager).cancel(anyString(), anyInt());
-
- // Show the notification if we disconnect again.
- vpn.updateState(DetailedState.DISCONNECTED, TAG);
- order.verify(mNotificationManager).notify(anyString(), anyInt(), any());
-
- // Notification should be cleared after unsetting always-on package.
- vpn.setAlwaysOnPackage(null, false, null);
- order.verify(mNotificationManager).cancel(anyString(), anyInt());
- }
-
- /**
- * The profile name should NOT change between releases for backwards compatibility
- *
- * <p>If this is changed between releases, the {@link Vpn#getVpnProfilePrivileged()} method MUST
- * be updated to ensure backward compatibility.
- */
- @Test
- public void testGetProfileNameForPackage() throws Exception {
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- setMockedUsers(PRIMARY_USER);
-
- final String expected = Credentials.PLATFORM_VPN + PRIMARY_USER.id + "_" + TEST_VPN_PKG;
- assertEquals(expected, vpn.getProfileNameForPackage(TEST_VPN_PKG));
- }
-
- private Vpn createVpn(String... grantedOps) throws Exception {
- return createVpn(PRIMARY_USER, grantedOps);
- }
-
- private Vpn createVpn(UserInfo user, String... grantedOps) throws Exception {
- final Vpn vpn = createVpn(user.id);
- setMockedUsers(user);
-
- for (final String opStr : grantedOps) {
- when(mAppOps.noteOpNoThrow(opStr, Process.myUid(), TEST_VPN_PKG,
- null /* attributionTag */, null /* message */))
- .thenReturn(AppOpsManager.MODE_ALLOWED);
- }
-
- return vpn;
- }
-
- private void checkProvisionVpnProfile(Vpn vpn, boolean expectedResult, String... checkedOps) {
- assertEquals(expectedResult, vpn.provisionVpnProfile(TEST_VPN_PKG, mVpnProfile));
-
- // The profile should always be stored, whether or not consent has been previously granted.
- verify(mVpnProfileStore)
- .put(
- eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)),
- eq(mVpnProfile.encode()));
-
- for (final String checkedOpStr : checkedOps) {
- verify(mAppOps).noteOpNoThrow(checkedOpStr, Process.myUid(), TEST_VPN_PKG,
- null /* attributionTag */, null /* message */);
- }
- }
-
- @Test
- public void testProvisionVpnProfileNoIpsecTunnels() throws Exception {
- when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS))
- .thenReturn(false);
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
- try {
- checkProvisionVpnProfile(
- vpn, true /* expectedResult */, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
- fail("Expected exception due to missing feature");
- } catch (UnsupportedOperationException expected) {
- }
- }
-
- private String startVpnForVerifyAppExclusionList(Vpn vpn) throws Exception {
- when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
- .thenReturn(mVpnProfile.encode());
- when(mVpnProfileStore.get(PRIMARY_USER_APP_EXCLUDE_KEY))
- .thenReturn(HexDump.hexStringToByteArray(PKGS_BYTES));
- final String sessionKey = vpn.startVpnProfile(TEST_VPN_PKG);
- final Set<Range<Integer>> uidRanges = vpn.createUserAndRestrictedProfilesRanges(
- PRIMARY_USER.id, null /* allowedApplications */, Arrays.asList(PKGS));
- verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
- clearInvocations(mConnectivityManager);
- verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
- vpn.mNetworkAgent = mMockNetworkAgent;
-
- return sessionKey;
- }
-
- private Vpn prepareVpnForVerifyAppExclusionList() throws Exception {
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
- startVpnForVerifyAppExclusionList(vpn);
-
- return vpn;
- }
-
- @Test
- public void testSetAndGetAppExclusionList() throws Exception {
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
- final String sessionKey = startVpnForVerifyAppExclusionList(vpn);
- verify(mVpnProfileStore, never()).put(eq(PRIMARY_USER_APP_EXCLUDE_KEY), any());
- vpn.setAppExclusionList(TEST_VPN_PKG, Arrays.asList(PKGS));
- verify(mVpnProfileStore)
- .put(eq(PRIMARY_USER_APP_EXCLUDE_KEY),
- eq(HexDump.hexStringToByteArray(PKGS_BYTES)));
- final Set<Range<Integer>> uidRanges = vpn.createUserAndRestrictedProfilesRanges(
- PRIMARY_USER.id, null /* allowedApplications */, Arrays.asList(PKGS));
- verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
- assertEquals(uidRanges, vpn.mNetworkCapabilities.getUids());
- assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
- }
-
- @Test
- public void testRefreshPlatformVpnAppExclusionList_updatesExcludedUids() throws Exception {
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
- final String sessionKey = startVpnForVerifyAppExclusionList(vpn);
- vpn.setAppExclusionList(TEST_VPN_PKG, Arrays.asList(PKGS));
- final Set<Range<Integer>> uidRanges = vpn.createUserAndRestrictedProfilesRanges(
- PRIMARY_USER.id, null /* allowedApplications */, Arrays.asList(PKGS));
- verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
- verify(mMockNetworkAgent).doSendNetworkCapabilities(any());
- assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
-
- reset(mMockNetworkAgent);
-
- // Remove one of the package
- List<Integer> newExcludedUids = toList(PKG_UIDS);
- newExcludedUids.remove((Integer) PKG_UIDS[0]);
- Set<Range<Integer>> newUidRanges = makeVpnUidRangeSet(PRIMARY_USER.id, newExcludedUids);
- sPackages.remove(PKGS[0]);
- vpn.refreshPlatformVpnAppExclusionList();
-
- // List in keystore is not changed, but UID for the removed packages is no longer exempted.
- assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
- assertEquals(newUidRanges, vpn.mNetworkCapabilities.getUids());
- ArgumentCaptor<NetworkCapabilities> ncCaptor =
- ArgumentCaptor.forClass(NetworkCapabilities.class);
- verify(mMockNetworkAgent).doSendNetworkCapabilities(ncCaptor.capture());
- assertEquals(newUidRanges, ncCaptor.getValue().getUids());
- verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(newUidRanges));
-
- reset(mMockNetworkAgent);
-
- // Add the package back
- newExcludedUids.add(PKG_UIDS[0]);
- newUidRanges = makeVpnUidRangeSet(PRIMARY_USER.id, newExcludedUids);
- sPackages.put(PKGS[0], PKG_UIDS[0]);
- vpn.refreshPlatformVpnAppExclusionList();
-
- // List in keystore is not changed and the uid list should be updated in the net cap.
- assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
- assertEquals(newUidRanges, vpn.mNetworkCapabilities.getUids());
- verify(mMockNetworkAgent).doSendNetworkCapabilities(ncCaptor.capture());
- assertEquals(newUidRanges, ncCaptor.getValue().getUids());
-
- // The uidRange is the same as the original setAppExclusionList so this is the second call
- verify(mConnectivityManager, times(2))
- .setVpnDefaultForUids(eq(sessionKey), eq(newUidRanges));
- }
-
- private List<Range<Integer>> makeVpnUidRange(int userId, List<Integer> excludedAppIdList) {
- final SortedSet<Integer> list = new TreeSet<>();
-
- final int userBase = userId * UserHandle.PER_USER_RANGE;
- for (int appId : excludedAppIdList) {
- final int uid = UserHandle.getUid(userId, appId);
- list.add(uid);
- if (Process.isApplicationUid(uid)) {
- list.add(Process.toSdkSandboxUid(uid)); // Add Sdk Sandbox UID
- }
- }
-
- final int minUid = userBase;
- final int maxUid = userBase + UserHandle.PER_USER_RANGE - 1;
- final List<Range<Integer>> ranges = new ArrayList<>();
-
- // Iterate the list to create the ranges between each uid.
- int start = minUid;
- for (int uid : list) {
- if (uid == start) {
- start++;
- } else {
- ranges.add(new Range<>(start, uid - 1));
- start = uid + 1;
- }
- }
-
- // Create the range between last uid and max uid.
- if (start <= maxUid) {
- ranges.add(new Range<>(start, maxUid));
- }
-
- return ranges;
- }
-
- private Set<Range<Integer>> makeVpnUidRangeSet(int userId, List<Integer> excludedAppIdList) {
- return new ArraySet<>(makeVpnUidRange(userId, excludedAppIdList));
- }
-
- @Test
- public void testSetAndGetAppExclusionListRestrictedUser() throws Exception {
- final Vpn vpn = prepareVpnForVerifyAppExclusionList();
-
- // Mock it to restricted profile
- when(mUserManager.getUserInfo(anyInt())).thenReturn(RESTRICTED_PROFILE_A);
-
- // Restricted users cannot configure VPNs
- assertThrows(SecurityException.class,
- () -> vpn.setAppExclusionList(TEST_VPN_PKG, new ArrayList<>()));
-
- assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
- }
-
- @Test
- public void testProvisionVpnProfilePreconsented() throws Exception {
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
- checkProvisionVpnProfile(
- vpn, true /* expectedResult */, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
- }
-
- @Test
- public void testProvisionVpnProfileNotPreconsented() throws Exception {
- final Vpn vpn = createVpn();
-
- // Expect that both the ACTIVATE_VPN and ACTIVATE_PLATFORM_VPN were tried, but the caller
- // had neither.
- checkProvisionVpnProfile(vpn, false /* expectedResult */,
- AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN, AppOpsManager.OPSTR_ACTIVATE_VPN);
- }
-
- @Test
- public void testProvisionVpnProfileVpnServicePreconsented() throws Exception {
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_VPN);
-
- checkProvisionVpnProfile(vpn, true /* expectedResult */, AppOpsManager.OPSTR_ACTIVATE_VPN);
- }
-
- @Test
- public void testProvisionVpnProfileTooLarge() throws Exception {
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
- final VpnProfile bigProfile = new VpnProfile("");
- bigProfile.name = new String(new byte[Vpn.MAX_VPN_PROFILE_SIZE_BYTES + 1]);
-
- try {
- vpn.provisionVpnProfile(TEST_VPN_PKG, bigProfile);
- fail("Expected IAE due to profile size");
- } catch (IllegalArgumentException expected) {
- }
- }
-
- @Test
- public void testProvisionVpnProfileRestrictedUser() throws Exception {
- final Vpn vpn =
- createVpn(
- RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
- try {
- vpn.provisionVpnProfile(TEST_VPN_PKG, mVpnProfile);
- fail("Expected SecurityException due to restricted user");
- } catch (SecurityException expected) {
- }
- }
-
- @Test
- public void testDeleteVpnProfile() throws Exception {
- final Vpn vpn = createVpn();
-
- vpn.deleteVpnProfile(TEST_VPN_PKG);
-
- verify(mVpnProfileStore)
- .remove(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
- }
-
- @Test
- public void testDeleteVpnProfileRestrictedUser() throws Exception {
- final Vpn vpn =
- createVpn(
- RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
- try {
- vpn.deleteVpnProfile(TEST_VPN_PKG);
- fail("Expected SecurityException due to restricted user");
- } catch (SecurityException expected) {
- }
- }
-
- @Test
- public void testGetVpnProfilePrivileged() throws Exception {
- final Vpn vpn = createVpn();
-
- when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
- .thenReturn(new VpnProfile("").encode());
-
- vpn.getVpnProfilePrivileged(TEST_VPN_PKG);
-
- verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
- }
-
- private void verifyPlatformVpnIsActivated(String packageName) {
- verify(mAppOps).noteOpNoThrow(
- eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
- eq(Process.myUid()),
- eq(packageName),
- eq(null) /* attributionTag */,
- eq(null) /* message */);
- verify(mAppOps).startOp(
- eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
- eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
- eq(packageName),
- eq(null) /* attributionTag */,
- eq(null) /* message */);
- }
-
- private void verifyPlatformVpnIsDeactivated(String packageName) {
- // Add a small delay to double confirm that finishOp is only called once.
- verify(mAppOps, after(100)).finishOp(
- eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
- eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
- eq(packageName),
- eq(null) /* attributionTag */);
- }
-
- @Test
- public void testStartVpnProfile() throws Exception {
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
- when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
- .thenReturn(mVpnProfile.encode());
-
- vpn.startVpnProfile(TEST_VPN_PKG);
-
- verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
- verifyPlatformVpnIsActivated(TEST_VPN_PKG);
- }
-
- @Test
- public void testStartVpnProfileVpnServicePreconsented() throws Exception {
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_VPN);
-
- when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
- .thenReturn(mVpnProfile.encode());
-
- vpn.startVpnProfile(TEST_VPN_PKG);
-
- // Verify that the ACTIVATE_VPN appop was checked, but no error was thrown.
- verify(mAppOps).noteOpNoThrow(AppOpsManager.OPSTR_ACTIVATE_VPN, Process.myUid(),
- TEST_VPN_PKG, null /* attributionTag */, null /* message */);
- }
-
- @Test
- public void testStartVpnProfileNotConsented() throws Exception {
- final Vpn vpn = createVpn();
-
- try {
- vpn.startVpnProfile(TEST_VPN_PKG);
- fail("Expected failure due to no user consent");
- } catch (SecurityException expected) {
- }
-
- // Verify both appops were checked.
- verify(mAppOps)
- .noteOpNoThrow(
- eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
- eq(Process.myUid()),
- eq(TEST_VPN_PKG),
- eq(null) /* attributionTag */,
- eq(null) /* message */);
- verify(mAppOps).noteOpNoThrow(AppOpsManager.OPSTR_ACTIVATE_VPN, Process.myUid(),
- TEST_VPN_PKG, null /* attributionTag */, null /* message */);
-
- // Keystore should never have been accessed.
- verify(mVpnProfileStore, never()).get(any());
- }
-
- @Test
- public void testStartVpnProfileMissingProfile() throws Exception {
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
- when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))).thenReturn(null);
-
- try {
- vpn.startVpnProfile(TEST_VPN_PKG);
- fail("Expected failure due to missing profile");
- } catch (IllegalArgumentException expected) {
- }
-
- verify(mVpnProfileStore).get(vpn.getProfileNameForPackage(TEST_VPN_PKG));
- verify(mAppOps)
- .noteOpNoThrow(
- eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
- eq(Process.myUid()),
- eq(TEST_VPN_PKG),
- eq(null) /* attributionTag */,
- eq(null) /* message */);
- }
-
- @Test
- public void testStartVpnProfileRestrictedUser() throws Exception {
- final Vpn vpn = createVpn(RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
- try {
- vpn.startVpnProfile(TEST_VPN_PKG);
- fail("Expected SecurityException due to restricted user");
- } catch (SecurityException expected) {
- }
- }
-
- @Test
- public void testStopVpnProfileRestrictedUser() throws Exception {
- final Vpn vpn = createVpn(RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
- try {
- vpn.stopVpnProfile(TEST_VPN_PKG);
- fail("Expected SecurityException due to restricted user");
- } catch (SecurityException expected) {
- }
- }
-
- @Test
- public void testStartOpAndFinishOpWillBeCalledWhenPlatformVpnIsOnAndOff() throws Exception {
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
- when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
- .thenReturn(mVpnProfile.encode());
- vpn.startVpnProfile(TEST_VPN_PKG);
- verifyPlatformVpnIsActivated(TEST_VPN_PKG);
- // Add a small delay to make sure that startOp is only called once.
- verify(mAppOps, after(100).times(1)).startOp(
- eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
- eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
- eq(TEST_VPN_PKG),
- eq(null) /* attributionTag */,
- eq(null) /* message */);
- // Check that the startOp is not called with OPSTR_ESTABLISH_VPN_SERVICE.
- verify(mAppOps, never()).startOp(
- eq(AppOpsManager.OPSTR_ESTABLISH_VPN_SERVICE),
- eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
- eq(TEST_VPN_PKG),
- eq(null) /* attributionTag */,
- eq(null) /* message */);
- vpn.stopVpnProfile(TEST_VPN_PKG);
- verifyPlatformVpnIsDeactivated(TEST_VPN_PKG);
- }
-
- @Test
- public void testStartOpWithSeamlessHandover() throws Exception {
- // Create with SYSTEM_USER so that establish() will match the user ID when checking
- // against Binder.getCallerUid
- final Vpn vpn = createVpn(SYSTEM_USER, AppOpsManager.OPSTR_ACTIVATE_VPN);
- assertTrue(vpn.prepare(TEST_VPN_PKG, null, VpnManager.TYPE_VPN_SERVICE));
- final VpnConfig config = new VpnConfig();
- config.user = "VpnTest";
- config.addresses.add(new LinkAddress("192.0.2.2/32"));
- config.mtu = 1450;
- final ResolveInfo resolveInfo = new ResolveInfo();
- final ServiceInfo serviceInfo = new ServiceInfo();
- serviceInfo.permission = BIND_VPN_SERVICE;
- resolveInfo.serviceInfo = serviceInfo;
- when(mPackageManager.resolveService(any(), anyInt())).thenReturn(resolveInfo);
- when(mContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(true);
- vpn.establish(config);
- verify(mAppOps, times(1)).startOp(
- eq(AppOpsManager.OPSTR_ESTABLISH_VPN_SERVICE),
- eq(Process.myUid()),
- eq(TEST_VPN_PKG),
- eq(null) /* attributionTag */,
- eq(null) /* message */);
- // Call establish() twice with the same config, it should match seamless handover case and
- // startOp() shouldn't be called again.
- vpn.establish(config);
- verify(mAppOps, times(1)).startOp(
- eq(AppOpsManager.OPSTR_ESTABLISH_VPN_SERVICE),
- eq(Process.myUid()),
- eq(TEST_VPN_PKG),
- eq(null) /* attributionTag */,
- eq(null) /* message */);
- }
-
- private void verifyVpnManagerEvent(String sessionKey, String category, int errorClass,
- int errorCode, String[] packageName, @NonNull VpnProfileState... profileState) {
- final Context userContext =
- mContext.createContextAsUser(UserHandle.of(PRIMARY_USER.id), 0 /* flags */);
- final ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
-
- final int verifyTimes = profileState.length;
- verify(userContext, timeout(TEST_TIMEOUT_MS).times(verifyTimes))
- .startService(intentArgumentCaptor.capture());
-
- for (int i = 0; i < verifyTimes; i++) {
- final Intent intent = intentArgumentCaptor.getAllValues().get(i);
- assertEquals(packageName[i], intent.getPackage());
- assertEquals(sessionKey, intent.getStringExtra(VpnManager.EXTRA_SESSION_KEY));
- final Set<String> categories = intent.getCategories();
- assertTrue(categories.contains(category));
- assertEquals(1, categories.size());
- assertEquals(errorClass,
- intent.getIntExtra(VpnManager.EXTRA_ERROR_CLASS, -1 /* defaultValue */));
- assertEquals(errorCode,
- intent.getIntExtra(VpnManager.EXTRA_ERROR_CODE, -1 /* defaultValue */));
- // CATEGORY_EVENT_DEACTIVATED_BY_USER & CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED won't
- // send NetworkCapabilities & LinkProperties to VPN app.
- // For ERROR_CODE_NETWORK_LOST, the NetworkCapabilities & LinkProperties of underlying
- // network will be cleared. So the VPN app will receive null for those 2 extra values.
- if (category.equals(VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER)
- || category.equals(VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED)
- || errorCode == VpnManager.ERROR_CODE_NETWORK_LOST) {
- assertNull(intent.getParcelableExtra(
- VpnManager.EXTRA_UNDERLYING_NETWORK_CAPABILITIES));
- assertNull(intent.getParcelableExtra(VpnManager.EXTRA_UNDERLYING_LINK_PROPERTIES));
- } else {
- assertNotNull(intent.getParcelableExtra(
- VpnManager.EXTRA_UNDERLYING_NETWORK_CAPABILITIES));
- assertNotNull(intent.getParcelableExtra(
- VpnManager.EXTRA_UNDERLYING_LINK_PROPERTIES));
- }
-
- assertEquals(profileState[i], intent.getParcelableExtra(
- VpnManager.EXTRA_VPN_PROFILE_STATE, VpnProfileState.class));
- }
- reset(userContext);
- }
-
- private void verifyDeactivatedByUser(String sessionKey, String[] packageName) {
- // CATEGORY_EVENT_DEACTIVATED_BY_USER is not an error event, so both of errorClass and
- // errorCode won't be set.
- verifyVpnManagerEvent(sessionKey, VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER,
- -1 /* errorClass */, -1 /* errorCode */, packageName,
- // VPN NetworkAgnet does not switch to CONNECTED in the test, and the state is not
- // important here. Verify that the state as it is, i.e. CONNECTING state.
- new VpnProfileState(VpnProfileState.STATE_CONNECTING,
- sessionKey, false /* alwaysOn */, false /* lockdown */));
- }
-
- private void verifyAlwaysOnStateChanged(String[] packageName, VpnProfileState... profileState) {
- verifyVpnManagerEvent(null /* sessionKey */,
- VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */,
- -1 /* errorCode */, packageName, profileState);
- }
-
- @Test
- public void testVpnManagerEventForUserDeactivated() throws Exception {
- // For security reasons, Vpn#prepare() will check that oldPackage and newPackage are either
- // null or the package of the caller. This test will call Vpn#prepare() to pretend the old
- // VPN is replaced by a new one. But only Settings can change to some other packages, and
- // this is checked with CONTROL_VPN so simulate holding CONTROL_VPN in order to pass the
- // security checks.
- doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
- when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
- .thenReturn(mVpnProfile.encode());
-
- // Test the case that the user deactivates the vpn in vpn app.
- final String sessionKey1 = vpn.startVpnProfile(TEST_VPN_PKG);
- verifyPlatformVpnIsActivated(TEST_VPN_PKG);
- vpn.stopVpnProfile(TEST_VPN_PKG);
- verifyPlatformVpnIsDeactivated(TEST_VPN_PKG);
- verifyPowerSaveTempWhitelistApp(TEST_VPN_PKG);
- reset(mDeviceIdleInternal);
- verifyDeactivatedByUser(sessionKey1, new String[] {TEST_VPN_PKG});
- reset(mAppOps);
-
- // Test the case that the user chooses another vpn and the original one is replaced.
- final String sessionKey2 = vpn.startVpnProfile(TEST_VPN_PKG);
- verifyPlatformVpnIsActivated(TEST_VPN_PKG);
- vpn.prepare(TEST_VPN_PKG, "com.new.vpn" /* newPackage */, TYPE_VPN_PLATFORM);
- verifyPlatformVpnIsDeactivated(TEST_VPN_PKG);
- verifyPowerSaveTempWhitelistApp(TEST_VPN_PKG);
- reset(mDeviceIdleInternal);
- verifyDeactivatedByUser(sessionKey2, new String[] {TEST_VPN_PKG});
- }
-
- @Test
- public void testVpnManagerEventForAlwaysOnChanged() throws Exception {
- // Calling setAlwaysOnPackage() needs to hold CONTROL_VPN.
- doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- // Enable VPN always-on for PKGS[1].
- assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false /* lockdown */,
- null /* lockdownAllowlist */));
- verifyPowerSaveTempWhitelistApp(PKGS[1]);
- reset(mDeviceIdleInternal);
- verifyAlwaysOnStateChanged(new String[] {PKGS[1]},
- new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
- null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
-
- // Enable VPN lockdown for PKGS[1].
- assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true /* lockdown */,
- null /* lockdownAllowlist */));
- verifyPowerSaveTempWhitelistApp(PKGS[1]);
- reset(mDeviceIdleInternal);
- verifyAlwaysOnStateChanged(new String[] {PKGS[1]},
- new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
- null /* sessionKey */, true /* alwaysOn */, true /* lockdown */));
-
- // Disable VPN lockdown for PKGS[1].
- assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false /* lockdown */,
- null /* lockdownAllowlist */));
- verifyPowerSaveTempWhitelistApp(PKGS[1]);
- reset(mDeviceIdleInternal);
- verifyAlwaysOnStateChanged(new String[] {PKGS[1]},
- new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
- null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
-
- // Disable VPN always-on.
- assertTrue(vpn.setAlwaysOnPackage(null, false /* lockdown */,
- null /* lockdownAllowlist */));
- verifyPowerSaveTempWhitelistApp(PKGS[1]);
- reset(mDeviceIdleInternal);
- verifyAlwaysOnStateChanged(new String[] {PKGS[1]},
- new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
- null /* sessionKey */, false /* alwaysOn */, false /* lockdown */));
-
- // Enable VPN always-on for PKGS[1] again.
- assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false /* lockdown */,
- null /* lockdownAllowlist */));
- verifyPowerSaveTempWhitelistApp(PKGS[1]);
- reset(mDeviceIdleInternal);
- verifyAlwaysOnStateChanged(new String[] {PKGS[1]},
- new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
- null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
-
- // Enable VPN always-on for PKGS[2].
- assertTrue(vpn.setAlwaysOnPackage(PKGS[2], false /* lockdown */,
- null /* lockdownAllowlist */));
- verifyPowerSaveTempWhitelistApp(PKGS[2]);
- reset(mDeviceIdleInternal);
- // PKGS[1] is replaced with PKGS[2].
- // Pass 2 VpnProfileState objects to verifyVpnManagerEvent(), the first one is sent to
- // PKGS[1] to notify PKGS[1] that the VPN always-on is disabled, the second one is sent to
- // PKGS[2] to notify PKGS[2] that the VPN always-on is enabled.
- verifyAlwaysOnStateChanged(new String[] {PKGS[1], PKGS[2]},
- new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
- null /* sessionKey */, false /* alwaysOn */, false /* lockdown */),
- new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
- null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
- }
-
- @Test
- public void testReconnectVpnManagerVpnWithAlwaysOnEnabled() throws Exception {
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
- when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
- .thenReturn(mVpnProfile.encode());
- vpn.startVpnProfile(TEST_VPN_PKG);
- verifyPlatformVpnIsActivated(TEST_VPN_PKG);
-
- // Enable VPN always-on for TEST_VPN_PKG.
- assertTrue(vpn.setAlwaysOnPackage(TEST_VPN_PKG, false /* lockdown */,
- null /* lockdownAllowlist */));
-
- // Reset to verify next startVpnProfile.
- reset(mAppOps);
-
- vpn.stopVpnProfile(TEST_VPN_PKG);
-
- // Reconnect the vpn with different package will cause exception.
- assertThrows(SecurityException.class, () -> vpn.startVpnProfile(PKGS[0]));
-
- // Reconnect the vpn again with the vpn always on package w/o exception.
- vpn.startVpnProfile(TEST_VPN_PKG);
- verifyPlatformVpnIsActivated(TEST_VPN_PKG);
- }
-
- @Test
- public void testLockdown_enableDisableWhileConnected() throws Exception {
- final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
- createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
-
- final InOrder order = inOrder(mTestDeps);
- order.verify(mTestDeps, timeout(TIMEOUT_CROSSTHREAD_MS))
- .newNetworkAgent(any(), any(), any(), any(), any(), any(),
- argThat(config -> config.allowBypass), any(), any());
-
- // Make VPN lockdown.
- assertTrue(vpnSnapShot.vpn.setAlwaysOnPackage(TEST_VPN_PKG, true /* lockdown */,
- null /* lockdownAllowlist */));
-
- order.verify(mTestDeps, timeout(TIMEOUT_CROSSTHREAD_MS))
- .newNetworkAgent(any(), any(), any(), any(), any(), any(),
- argThat(config -> !config.allowBypass), any(), any());
-
- // Disable lockdown.
- assertTrue(vpnSnapShot.vpn.setAlwaysOnPackage(TEST_VPN_PKG, false /* lockdown */,
- null /* lockdownAllowlist */));
-
- order.verify(mTestDeps, timeout(TIMEOUT_CROSSTHREAD_MS))
- .newNetworkAgent(any(), any(), any(), any(), any(), any(),
- argThat(config -> config.allowBypass), any(), any());
- }
-
- @Test
- public void testSetPackageAuthorizationVpnService() throws Exception {
- final Vpn vpn = createVpn();
-
- assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, VpnManager.TYPE_VPN_SERVICE));
- verify(mAppOps)
- .setMode(
- eq(AppOpsManager.OPSTR_ACTIVATE_VPN),
- eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
- eq(TEST_VPN_PKG),
- eq(AppOpsManager.MODE_ALLOWED));
- }
-
- @Test
- public void testSetPackageAuthorizationPlatformVpn() throws Exception {
- final Vpn vpn = createVpn();
-
- assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, TYPE_VPN_PLATFORM));
- verify(mAppOps)
- .setMode(
- eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
- eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
- eq(TEST_VPN_PKG),
- eq(AppOpsManager.MODE_ALLOWED));
- }
-
- @Test
- public void testSetPackageAuthorizationRevokeAuthorization() throws Exception {
- final Vpn vpn = createVpn();
-
- assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, VpnManager.TYPE_VPN_NONE));
- verify(mAppOps)
- .setMode(
- eq(AppOpsManager.OPSTR_ACTIVATE_VPN),
- eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
- eq(TEST_VPN_PKG),
- eq(AppOpsManager.MODE_IGNORED));
- verify(mAppOps)
- .setMode(
- eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
- eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
- eq(TEST_VPN_PKG),
- eq(AppOpsManager.MODE_IGNORED));
- }
-
- private NetworkCallback triggerOnAvailableAndGetCallback() throws Exception {
- return triggerOnAvailableAndGetCallback(new NetworkCapabilities.Builder().build());
- }
-
- private NetworkCallback triggerOnAvailableAndGetCallback(
- @NonNull final NetworkCapabilities caps) throws Exception {
- final ArgumentCaptor<NetworkCallback> networkCallbackCaptor =
- ArgumentCaptor.forClass(NetworkCallback.class);
- verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS))
- .registerSystemDefaultNetworkCallback(networkCallbackCaptor.capture(), any());
-
- // onAvailable() will trigger onDefaultNetworkChanged(), so NetdUtils#setInterfaceUp will be
- // invoked. Set the return value of INetd#interfaceGetCfg to prevent NullPointerException.
- final InterfaceConfigurationParcel config = new InterfaceConfigurationParcel();
- config.flags = new String[] {IF_STATE_DOWN};
- when(mNetd.interfaceGetCfg(anyString())).thenReturn(config);
- final NetworkCallback cb = networkCallbackCaptor.getValue();
- cb.onAvailable(TEST_NETWORK);
- // Trigger onCapabilitiesChanged() and onLinkPropertiesChanged() so the test can verify that
- // if NetworkCapabilities and LinkProperties of underlying network will be sent/cleared or
- // not.
- // See verifyVpnManagerEvent().
- cb.onCapabilitiesChanged(TEST_NETWORK, caps);
- cb.onLinkPropertiesChanged(TEST_NETWORK, new LinkProperties());
- return cb;
- }
-
- private void verifyInterfaceSetCfgWithFlags(String flag) throws Exception {
- // Add a timeout for waiting for interfaceSetCfg to be called.
- verify(mNetd, timeout(TEST_TIMEOUT_MS)).interfaceSetCfg(argThat(
- config -> Arrays.asList(config.flags).contains(flag)));
- }
-
- private void doTestPlatformVpnWithException(IkeException exception,
- String category, int errorType, int errorCode) throws Exception {
- final ArgumentCaptor<IkeSessionCallback> captor =
- ArgumentCaptor.forClass(IkeSessionCallback.class);
-
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
- when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
- .thenReturn(mVpnProfile.encode());
-
- doReturn(new NetworkCapabilities()).when(mConnectivityManager)
- .getRedactedNetworkCapabilitiesForPackage(any(), anyInt(), anyString());
- doReturn(new LinkProperties()).when(mConnectivityManager)
- .getRedactedLinkPropertiesForPackage(any(), anyInt(), anyString());
-
- final String sessionKey = vpn.startVpnProfile(TEST_VPN_PKG);
- final Set<Range<Integer>> uidRanges = rangeSet(PRIMARY_USER_RANGE);
- // This is triggered by Ikev2VpnRunner constructor.
- verify(mConnectivityManager, times(1)).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
- final NetworkCallback cb = triggerOnAvailableAndGetCallback();
-
- verifyInterfaceSetCfgWithFlags(IF_STATE_UP);
-
- // Wait for createIkeSession() to be called before proceeding in order to ensure consistent
- // state
- verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS))
- .createIkeSession(any(), any(), any(), any(), captor.capture(), any());
- // This is triggered by Vpn#startOrMigrateIkeSession().
- verify(mConnectivityManager, times(2)).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
- reset(mIkev2SessionCreator);
- // For network lost case, the process should be triggered by calling onLost(), which is the
- // same process with the real case.
- if (errorCode == VpnManager.ERROR_CODE_NETWORK_LOST) {
- cb.onLost(TEST_NETWORK);
- verify(mExecutor, atLeastOnce()).schedule(any(Runnable.class), anyLong(), any());
- } else {
- final IkeSessionCallback ikeCb = captor.getValue();
- mExecutor.execute(() -> ikeCb.onClosedWithException(exception));
- }
-
- verifyPowerSaveTempWhitelistApp(TEST_VPN_PKG);
- reset(mDeviceIdleInternal);
- verifyVpnManagerEvent(sessionKey, category, errorType, errorCode,
- // VPN NetworkAgnet does not switch to CONNECTED in the test, and the state is not
- // important here. Verify that the state as it is, i.e. CONNECTING state.
- new String[] {TEST_VPN_PKG}, new VpnProfileState(VpnProfileState.STATE_CONNECTING,
- sessionKey, false /* alwaysOn */, false /* lockdown */));
- if (errorType == VpnManager.ERROR_CLASS_NOT_RECOVERABLE) {
- verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey),
- eq(Collections.EMPTY_LIST));
- verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS))
- .unregisterNetworkCallback(eq(cb));
- } else if (errorType == VpnManager.ERROR_CLASS_RECOVERABLE
- // Vpn won't retry when there is no usable underlying network.
- && errorCode != VpnManager.ERROR_CODE_NETWORK_LOST) {
- int retryIndex = 0;
- // First failure occurred above.
- final IkeSessionCallback retryCb = verifyRetryAndGetNewIkeCb(retryIndex++);
- // Trigger 2 more failures to let the retry delay increase to 5s.
- mExecutor.execute(() -> retryCb.onClosedWithException(exception));
- final IkeSessionCallback retryCb2 = verifyRetryAndGetNewIkeCb(retryIndex++);
- mExecutor.execute(() -> retryCb2.onClosedWithException(exception));
- final IkeSessionCallback retryCb3 = verifyRetryAndGetNewIkeCb(retryIndex++);
-
- // setVpnDefaultForUids may be called again but the uidRanges should not change.
- verify(mConnectivityManager, atLeast(2)).setVpnDefaultForUids(eq(sessionKey),
- mUidRangesCaptor.capture());
- final List<Collection<Range<Integer>>> capturedUidRanges =
- mUidRangesCaptor.getAllValues();
- for (int i = 2; i < capturedUidRanges.size(); i++) {
- // Assert equals no order.
- assertTrue(
- "uid ranges should not be modified. Expected: " + uidRanges
- + ", actual: " + capturedUidRanges.get(i),
- capturedUidRanges.get(i).containsAll(uidRanges)
- && capturedUidRanges.get(i).size() == uidRanges.size());
- }
-
- // A fourth failure will cause the retry delay to be greater than 5s.
- mExecutor.execute(() -> retryCb3.onClosedWithException(exception));
- verifyRetryAndGetNewIkeCb(retryIndex++);
-
- // The VPN network preference will be cleared when the retry delay is greater than 5s.
- verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey),
- eq(Collections.EMPTY_LIST));
- }
- }
-
- private IkeSessionCallback verifyRetryAndGetNewIkeCb(int retryIndex) {
- final ArgumentCaptor<IkeSessionCallback> ikeCbCaptor =
- ArgumentCaptor.forClass(IkeSessionCallback.class);
-
- // Verify retry is scheduled
- final long expectedDelayMs = mTestDeps.getNextRetryDelayMs(retryIndex);
- verify(mExecutor, timeout(TEST_TIMEOUT_MS)).schedule(any(Runnable.class),
- eq(expectedDelayMs), eq(TimeUnit.MILLISECONDS));
-
- verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS + expectedDelayMs))
- .createIkeSession(any(), any(), any(), any(), ikeCbCaptor.capture(), any());
-
- // Forget the mIkev2SessionCreator#createIkeSession call and mExecutor#schedule call
- // for the next retry verification
- resetIkev2SessionCreator(mIkeSessionWrapper);
-
- return ikeCbCaptor.getValue();
- }
-
- @Test
- public void testStartPlatformVpnAuthenticationFailed() throws Exception {
- final IkeProtocolException exception = mock(IkeProtocolException.class);
- final int errorCode = IkeProtocolException.ERROR_TYPE_AUTHENTICATION_FAILED;
- when(exception.getErrorType()).thenReturn(errorCode);
- doTestPlatformVpnWithException(exception,
- VpnManager.CATEGORY_EVENT_IKE_ERROR, VpnManager.ERROR_CLASS_NOT_RECOVERABLE,
- errorCode);
- }
-
- @Test
- public void testStartPlatformVpnFailedWithRecoverableError() throws Exception {
- final IkeProtocolException exception = mock(IkeProtocolException.class);
- final int errorCode = IkeProtocolException.ERROR_TYPE_TEMPORARY_FAILURE;
- when(exception.getErrorType()).thenReturn(errorCode);
- doTestPlatformVpnWithException(exception,
- VpnManager.CATEGORY_EVENT_IKE_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE, errorCode);
- }
-
- @Test
- public void testStartPlatformVpnFailedWithUnknownHostException() throws Exception {
- final IkeNonProtocolException exception = mock(IkeNonProtocolException.class);
- final UnknownHostException unknownHostException = new UnknownHostException();
- final int errorCode = VpnManager.ERROR_CODE_NETWORK_UNKNOWN_HOST;
- when(exception.getCause()).thenReturn(unknownHostException);
- doTestPlatformVpnWithException(exception,
- VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
- errorCode);
- }
-
- @Test
- public void testStartPlatformVpnFailedWithIkeTimeoutException() throws Exception {
- final IkeNonProtocolException exception = mock(IkeNonProtocolException.class);
- final IkeTimeoutException ikeTimeoutException =
- new IkeTimeoutException("IkeTimeoutException");
- final int errorCode = VpnManager.ERROR_CODE_NETWORK_PROTOCOL_TIMEOUT;
- when(exception.getCause()).thenReturn(ikeTimeoutException);
- doTestPlatformVpnWithException(exception,
- VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
- errorCode);
- }
-
- @Test
- public void testStartPlatformVpnFailedWithIkeNetworkLostException() throws Exception {
- final IkeNetworkLostException exception = new IkeNetworkLostException(
- new Network(100));
- doTestPlatformVpnWithException(exception,
- VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
- VpnManager.ERROR_CODE_NETWORK_LOST);
- }
-
- @Test
- public void testStartPlatformVpnFailedWithIOException() throws Exception {
- final IkeNonProtocolException exception = mock(IkeNonProtocolException.class);
- final IOException ioException = new IOException();
- final int errorCode = VpnManager.ERROR_CODE_NETWORK_IO;
- when(exception.getCause()).thenReturn(ioException);
- doTestPlatformVpnWithException(exception,
- VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
- errorCode);
- }
-
- @Test
- public void testStartPlatformVpnIllegalArgumentExceptionInSetup() throws Exception {
- when(mIkev2SessionCreator.createIkeSession(any(), any(), any(), any(), any(), any()))
- .thenThrow(new IllegalArgumentException());
- final Vpn vpn = startLegacyVpn(createVpn(PRIMARY_USER.id), mVpnProfile);
- final NetworkCallback cb = triggerOnAvailableAndGetCallback();
-
- verifyInterfaceSetCfgWithFlags(IF_STATE_UP);
-
- // Wait for createIkeSession() to be called before proceeding in order to ensure consistent
- // state
- verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS)).unregisterNetworkCallback(eq(cb));
- assertEquals(LegacyVpnInfo.STATE_FAILED, vpn.getLegacyVpnInfo().state);
- }
-
- @Test
- public void testVpnManagerEventWillNotBeSentToSettingsVpn() throws Exception {
- startLegacyVpn(createVpn(PRIMARY_USER.id), mVpnProfile);
- triggerOnAvailableAndGetCallback();
-
- verifyInterfaceSetCfgWithFlags(IF_STATE_UP);
-
- final IkeNonProtocolException exception = mock(IkeNonProtocolException.class);
- final IkeTimeoutException ikeTimeoutException =
- new IkeTimeoutException("IkeTimeoutException");
- when(exception.getCause()).thenReturn(ikeTimeoutException);
-
- final ArgumentCaptor<IkeSessionCallback> captor =
- ArgumentCaptor.forClass(IkeSessionCallback.class);
- verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS))
- .createIkeSession(any(), any(), any(), any(), captor.capture(), any());
- final IkeSessionCallback ikeCb = captor.getValue();
- ikeCb.onClosedWithException(exception);
-
- final Context userContext =
- mContext.createContextAsUser(UserHandle.of(PRIMARY_USER.id), 0 /* flags */);
- verify(userContext, never()).startService(any());
- }
-
- private void setAndVerifyAlwaysOnPackage(Vpn vpn, int uid, boolean lockdownEnabled) {
- assertTrue(vpn.setAlwaysOnPackage(TEST_VPN_PKG, lockdownEnabled, null));
-
- verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
- verify(mAppOps).setMode(
- eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), eq(uid), eq(TEST_VPN_PKG),
- eq(AppOpsManager.MODE_ALLOWED));
-
- verify(mSystemServices).settingsSecurePutStringForUser(
- eq(Settings.Secure.ALWAYS_ON_VPN_APP), eq(TEST_VPN_PKG), eq(PRIMARY_USER.id));
- verify(mSystemServices).settingsSecurePutIntForUser(
- eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN), eq(lockdownEnabled ? 1 : 0),
- eq(PRIMARY_USER.id));
- verify(mSystemServices).settingsSecurePutStringForUser(
- eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN_WHITELIST), eq(""), eq(PRIMARY_USER.id));
- }
-
- @Test
- public void testSetAndStartAlwaysOnVpn() throws Exception {
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- setMockedUsers(PRIMARY_USER);
-
- // UID checks must return a different UID; otherwise it'll be treated as already prepared.
- final int uid = Process.myUid() + 1;
- when(mPackageManager.getPackageUidAsUser(eq(TEST_VPN_PKG), anyInt()))
- .thenReturn(uid);
- when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
- .thenReturn(mVpnProfile.encode());
-
- setAndVerifyAlwaysOnPackage(vpn, uid, false);
- assertTrue(vpn.startAlwaysOnVpn());
-
- // TODO: Test the Ikev2VpnRunner started up properly. Relies on utility methods added in
- // a subsequent CL.
- }
-
- private Vpn startLegacyVpn(final Vpn vpn, final VpnProfile vpnProfile) throws Exception {
- setMockedUsers(PRIMARY_USER);
- vpn.startLegacyVpn(vpnProfile);
- return vpn;
- }
-
- private IkeSessionConnectionInfo createIkeConnectInfo() {
- return new IkeSessionConnectionInfo(TEST_VPN_CLIENT_IP, TEST_VPN_SERVER_IP, TEST_NETWORK);
- }
-
- private IkeSessionConnectionInfo createIkeConnectInfo_2() {
- return new IkeSessionConnectionInfo(
- TEST_VPN_CLIENT_IP_2, TEST_VPN_SERVER_IP_2, TEST_NETWORK_2);
- }
-
- private IkeSessionConfiguration createIkeConfig(
- IkeSessionConnectionInfo ikeConnectInfo, boolean isMobikeEnabled) {
- final IkeSessionConfiguration.Builder builder =
- new IkeSessionConfiguration.Builder(ikeConnectInfo);
-
- if (isMobikeEnabled) {
- builder.addIkeExtension(EXTENSION_TYPE_MOBIKE);
- }
-
- return builder.build();
- }
-
- private ChildSessionConfiguration createChildConfig() {
- return new ChildSessionConfiguration.Builder(
- Arrays.asList(IN_TS, IN_TS6), Arrays.asList(OUT_TS, OUT_TS6))
- .addInternalAddress(new LinkAddress(TEST_VPN_INTERNAL_IP, IP4_PREFIX_LEN))
- .addInternalAddress(new LinkAddress(TEST_VPN_INTERNAL_IP6, IP6_PREFIX_LEN))
- .addInternalDnsServer(TEST_VPN_INTERNAL_DNS)
- .addInternalDnsServer(TEST_VPN_INTERNAL_DNS6)
- .build();
- }
-
- private IpSecTransform createIpSecTransform() {
- return new IpSecTransform(mContext, new IpSecConfig());
- }
-
- private void verifyApplyTunnelModeTransforms(int expectedTimes) throws Exception {
- verify(mIpSecService, times(expectedTimes)).applyTunnelModeTransform(
- eq(TEST_TUNNEL_RESOURCE_ID), eq(IpSecManager.DIRECTION_IN),
- anyInt(), anyString());
- verify(mIpSecService, times(expectedTimes)).applyTunnelModeTransform(
- eq(TEST_TUNNEL_RESOURCE_ID), eq(IpSecManager.DIRECTION_OUT),
- anyInt(), anyString());
- }
-
- private Pair<IkeSessionCallback, ChildSessionCallback> verifyCreateIkeAndCaptureCbs()
- throws Exception {
- final ArgumentCaptor<IkeSessionCallback> ikeCbCaptor =
- ArgumentCaptor.forClass(IkeSessionCallback.class);
- final ArgumentCaptor<ChildSessionCallback> childCbCaptor =
- ArgumentCaptor.forClass(ChildSessionCallback.class);
-
- verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS)).createIkeSession(
- any(), any(), any(), any(), ikeCbCaptor.capture(), childCbCaptor.capture());
-
- return new Pair<>(ikeCbCaptor.getValue(), childCbCaptor.getValue());
- }
-
- private static class PlatformVpnSnapshot {
- public final Vpn vpn;
- public final NetworkCallback nwCb;
- public final IkeSessionCallback ikeCb;
- public final ChildSessionCallback childCb;
-
- PlatformVpnSnapshot(Vpn vpn, NetworkCallback nwCb,
- IkeSessionCallback ikeCb, ChildSessionCallback childCb) {
- this.vpn = vpn;
- this.nwCb = nwCb;
- this.ikeCb = ikeCb;
- this.childCb = childCb;
- }
- }
-
- private PlatformVpnSnapshot verifySetupPlatformVpn(IkeSessionConfiguration ikeConfig)
- throws Exception {
- return verifySetupPlatformVpn(ikeConfig, true);
- }
-
- private PlatformVpnSnapshot verifySetupPlatformVpn(
- IkeSessionConfiguration ikeConfig, boolean mtuSupportsIpv6) throws Exception {
- return verifySetupPlatformVpn(mVpnProfile, ikeConfig, mtuSupportsIpv6);
- }
-
- private PlatformVpnSnapshot verifySetupPlatformVpn(VpnProfile vpnProfile,
- IkeSessionConfiguration ikeConfig, boolean mtuSupportsIpv6) throws Exception {
- return verifySetupPlatformVpn(vpnProfile, ikeConfig,
- new NetworkCapabilities.Builder().build() /* underlying network caps */,
- mtuSupportsIpv6, false /* areLongLivedTcpConnectionsExpensive */);
- }
-
- private PlatformVpnSnapshot verifySetupPlatformVpn(VpnProfile vpnProfile,
- IkeSessionConfiguration ikeConfig,
- @NonNull final NetworkCapabilities underlyingNetworkCaps,
- boolean mtuSupportsIpv6,
- boolean areLongLivedTcpConnectionsExpensive) throws Exception {
- if (!mtuSupportsIpv6) {
- doReturn(IPV6_MIN_MTU - 1).when(mTestDeps).calculateVpnMtu(any(), anyInt(), anyInt(),
- anyBoolean());
- }
-
- doReturn(mMockNetworkAgent).when(mTestDeps)
- .newNetworkAgent(
- any(), any(), anyString(), any(), any(), any(), any(), any(), any());
- doReturn(TEST_NETWORK).when(mMockNetworkAgent).getNetwork();
-
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
- when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
- .thenReturn(vpnProfile.encode());
-
- final String sessionKey = vpn.startVpnProfile(TEST_VPN_PKG);
- final Set<Range<Integer>> uidRanges = Collections.singleton(PRIMARY_USER_RANGE);
- verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
- final NetworkCallback nwCb = triggerOnAvailableAndGetCallback(underlyingNetworkCaps);
- // There are 4 interactions with the executor.
- // - Network available
- // - LP change
- // - NC change
- // - schedule() calls in scheduleStartIkeSession()
- // The first 3 calls are triggered from Executor.execute(). The execute() will also call to
- // schedule() with 0 delay. Verify the exact interaction here so that it won't cause flakes
- // in the follow-up flow.
- verify(mExecutor, timeout(TEST_TIMEOUT_MS).times(4))
- .schedule(any(Runnable.class), anyLong(), any());
- reset(mExecutor);
-
- // Mock the setup procedure by firing callbacks
- final Pair<IkeSessionCallback, ChildSessionCallback> cbPair =
- verifyCreateIkeAndCaptureCbs();
- final IkeSessionCallback ikeCb = cbPair.first;
- final ChildSessionCallback childCb = cbPair.second;
-
- ikeCb.onOpened(ikeConfig);
- childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_IN);
- childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_OUT);
- childCb.onOpened(createChildConfig());
-
- // Verification VPN setup
- verifyApplyTunnelModeTransforms(1);
-
- ArgumentCaptor<LinkProperties> lpCaptor = ArgumentCaptor.forClass(LinkProperties.class);
- ArgumentCaptor<NetworkCapabilities> ncCaptor =
- ArgumentCaptor.forClass(NetworkCapabilities.class);
- ArgumentCaptor<NetworkAgentConfig> nacCaptor =
- ArgumentCaptor.forClass(NetworkAgentConfig.class);
- verify(mTestDeps).newNetworkAgent(
- any(), any(), anyString(), ncCaptor.capture(), lpCaptor.capture(),
- any(), nacCaptor.capture(), any(), any());
- verify(mIkeSessionWrapper).setUnderpinnedNetwork(TEST_NETWORK);
- // Check LinkProperties
- final LinkProperties lp = lpCaptor.getValue();
- final List<RouteInfo> expectedRoutes =
- new ArrayList<>(
- Arrays.asList(
- new RouteInfo(
- new IpPrefix(Inet4Address.ANY, 0),
- null /* gateway */,
- TEST_IFACE_NAME,
- RouteInfo.RTN_UNICAST)));
- final List<LinkAddress> expectedAddresses =
- new ArrayList<>(
- Arrays.asList(new LinkAddress(TEST_VPN_INTERNAL_IP, IP4_PREFIX_LEN)));
- final List<InetAddress> expectedDns = new ArrayList<>(Arrays.asList(TEST_VPN_INTERNAL_DNS));
-
- if (mtuSupportsIpv6) {
- expectedRoutes.add(
- new RouteInfo(
- new IpPrefix(Inet6Address.ANY, 0),
- null /* gateway */,
- TEST_IFACE_NAME,
- RouteInfo.RTN_UNICAST));
- expectedAddresses.add(new LinkAddress(TEST_VPN_INTERNAL_IP6, IP6_PREFIX_LEN));
- expectedDns.add(TEST_VPN_INTERNAL_DNS6);
- } else {
- expectedRoutes.add(
- new RouteInfo(
- new IpPrefix(Inet6Address.ANY, 0),
- null /* gateway */,
- TEST_IFACE_NAME,
- RTN_UNREACHABLE));
- }
-
- assertEquals(expectedRoutes, lp.getRoutes());
- assertEquals(expectedAddresses, lp.getLinkAddresses());
- assertEquals(expectedDns, lp.getDnsServers());
-
- // Check NetworkCapabilities
- assertEquals(Arrays.asList(TEST_NETWORK), ncCaptor.getValue().getUnderlyingNetworks());
-
- // Check if allowBypass is set or not.
- assertTrue(nacCaptor.getValue().isBypassableVpn());
- // Check if extra info for VPN is set.
- assertTrue(nacCaptor.getValue().getLegacyExtraInfo().contains(TEST_VPN_PKG));
- final VpnTransportInfo info = (VpnTransportInfo) ncCaptor.getValue().getTransportInfo();
- assertTrue(info.isBypassable());
- assertEquals(areLongLivedTcpConnectionsExpensive,
- info.areLongLivedTcpConnectionsExpensive());
- return new PlatformVpnSnapshot(vpn, nwCb, ikeCb, childCb);
- }
-
- @Test
- public void testStartPlatformVpn() throws Exception {
- final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
- createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
- vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
- verify(mConnectivityManager).setVpnDefaultForUids(anyString(), eq(Collections.EMPTY_LIST));
- }
-
- @Test
- public void testMigrateIkeSession_FromIkeTunnConnParams_AutoTimerNoTimer() throws Exception {
- doTestMigrateIkeSession_FromIkeTunnConnParams(
- false /* isAutomaticIpVersionSelectionEnabled */,
- true /* isAutomaticNattKeepaliveTimerEnabled */,
- TEST_KEEPALIVE_TIMEOUT_UNSET /* keepaliveInProfile */,
- ESP_IP_VERSION_AUTO /* ipVersionInProfile */,
- ESP_ENCAP_TYPE_AUTO /* encapTypeInProfile */);
- }
-
- @Test
- public void testMigrateIkeSession_FromIkeTunnConnParams_AutoTimerTimerSet() throws Exception {
- doTestMigrateIkeSession_FromIkeTunnConnParams(
- false /* isAutomaticIpVersionSelectionEnabled */,
- true /* isAutomaticNattKeepaliveTimerEnabled */,
- TEST_KEEPALIVE_TIMER /* keepaliveInProfile */,
- ESP_IP_VERSION_AUTO /* ipVersionInProfile */,
- ESP_ENCAP_TYPE_AUTO /* encapTypeInProfile */);
- }
-
- @Test
- public void testMigrateIkeSession_FromIkeTunnConnParams_AutoIp() throws Exception {
- doTestMigrateIkeSession_FromIkeTunnConnParams(
- true /* isAutomaticIpVersionSelectionEnabled */,
- false /* isAutomaticNattKeepaliveTimerEnabled */,
- TEST_KEEPALIVE_TIMEOUT_UNSET /* keepaliveInProfile */,
- ESP_IP_VERSION_AUTO /* ipVersionInProfile */,
- ESP_ENCAP_TYPE_AUTO /* encapTypeInProfile */);
- }
-
- @Test
- public void testMigrateIkeSession_FromIkeTunnConnParams_AssignedIpProtocol() throws Exception {
- doTestMigrateIkeSession_FromIkeTunnConnParams(
- false /* isAutomaticIpVersionSelectionEnabled */,
- false /* isAutomaticNattKeepaliveTimerEnabled */,
- TEST_KEEPALIVE_TIMEOUT_UNSET /* keepaliveInProfile */,
- ESP_IP_VERSION_IPV4 /* ipVersionInProfile */,
- ESP_ENCAP_TYPE_UDP /* encapTypeInProfile */);
- }
-
- @Test
- public void testMigrateIkeSession_FromNotIkeTunnConnParams_AutoTimer() throws Exception {
- doTestMigrateIkeSession_FromNotIkeTunnConnParams(
- false /* isAutomaticIpVersionSelectionEnabled */,
- true /* isAutomaticNattKeepaliveTimerEnabled */);
- }
-
- @Test
- public void testMigrateIkeSession_FromNotIkeTunnConnParams_AutoIp() throws Exception {
- doTestMigrateIkeSession_FromNotIkeTunnConnParams(
- true /* isAutomaticIpVersionSelectionEnabled */,
- false /* isAutomaticNattKeepaliveTimerEnabled */);
- }
-
- private void doTestMigrateIkeSession_FromNotIkeTunnConnParams(
- boolean isAutomaticIpVersionSelectionEnabled,
- boolean isAutomaticNattKeepaliveTimerEnabled) throws Exception {
- final Ikev2VpnProfile ikeProfile =
- new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY)
- .setAuthPsk(TEST_VPN_PSK)
- .setBypassable(true /* isBypassable */)
- .setAutomaticNattKeepaliveTimerEnabled(isAutomaticNattKeepaliveTimerEnabled)
- .setAutomaticIpVersionSelectionEnabled(isAutomaticIpVersionSelectionEnabled)
- .build();
-
- final int expectedKeepalive = isAutomaticNattKeepaliveTimerEnabled
- ? AUTOMATIC_KEEPALIVE_DELAY_SECONDS
- : DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT;
- doTestMigrateIkeSession(ikeProfile.toVpnProfile(),
- expectedKeepalive,
- ESP_IP_VERSION_AUTO /* expectedIpVersion */,
- ESP_ENCAP_TYPE_AUTO /* expectedEncapType */,
- new NetworkCapabilities.Builder().build());
- }
-
- private Ikev2VpnProfile makeIkeV2VpnProfile(
- boolean isAutomaticIpVersionSelectionEnabled,
- boolean isAutomaticNattKeepaliveTimerEnabled,
- int keepaliveInProfile,
- int ipVersionInProfile,
- int encapTypeInProfile) {
- // TODO: Update helper function in IkeSessionTestUtils to support building IkeSessionParams
- // with IP version and encap type when mainline-prod branch support these two APIs.
- final IkeSessionParams params = getTestIkeSessionParams(true /* testIpv6 */,
- new IkeFqdnIdentification(TEST_IDENTITY), keepaliveInProfile);
- final IkeSessionParams ikeSessionParams = new IkeSessionParams.Builder(params)
- .setIpVersion(ipVersionInProfile)
- .setEncapType(encapTypeInProfile)
- .build();
-
- final IkeTunnelConnectionParams tunnelParams =
- new IkeTunnelConnectionParams(ikeSessionParams, CHILD_PARAMS);
- return new Ikev2VpnProfile.Builder(tunnelParams)
- .setBypassable(true)
- .setAutomaticNattKeepaliveTimerEnabled(isAutomaticNattKeepaliveTimerEnabled)
- .setAutomaticIpVersionSelectionEnabled(isAutomaticIpVersionSelectionEnabled)
- .build();
- }
-
- private void doTestMigrateIkeSession_FromIkeTunnConnParams(
- boolean isAutomaticIpVersionSelectionEnabled,
- boolean isAutomaticNattKeepaliveTimerEnabled,
- int keepaliveInProfile,
- int ipVersionInProfile,
- int encapTypeInProfile) throws Exception {
- doTestMigrateIkeSession_FromIkeTunnConnParams(isAutomaticIpVersionSelectionEnabled,
- isAutomaticNattKeepaliveTimerEnabled, keepaliveInProfile, ipVersionInProfile,
- encapTypeInProfile, new NetworkCapabilities.Builder().build());
- }
-
- private void doTestMigrateIkeSession_FromIkeTunnConnParams(
- boolean isAutomaticIpVersionSelectionEnabled,
- boolean isAutomaticNattKeepaliveTimerEnabled,
- int keepaliveInProfile,
- int ipVersionInProfile,
- int encapTypeInProfile,
- @NonNull final NetworkCapabilities nc) throws Exception {
- final Ikev2VpnProfile ikeProfile = makeIkeV2VpnProfile(
- isAutomaticIpVersionSelectionEnabled,
- isAutomaticNattKeepaliveTimerEnabled,
- keepaliveInProfile,
- ipVersionInProfile,
- encapTypeInProfile);
-
- final IkeSessionParams ikeSessionParams =
- ikeProfile.getIkeTunnelConnectionParams().getIkeSessionParams();
- final int expectedKeepalive = isAutomaticNattKeepaliveTimerEnabled
- ? AUTOMATIC_KEEPALIVE_DELAY_SECONDS
- : ikeSessionParams.getNattKeepAliveDelaySeconds();
- final int expectedIpVersion = isAutomaticIpVersionSelectionEnabled
- ? ESP_IP_VERSION_AUTO
- : ikeSessionParams.getIpVersion();
- final int expectedEncapType = isAutomaticIpVersionSelectionEnabled
- ? ESP_ENCAP_TYPE_AUTO
- : ikeSessionParams.getEncapType();
- doTestMigrateIkeSession(ikeProfile.toVpnProfile(), expectedKeepalive,
- expectedIpVersion, expectedEncapType, nc);
- }
-
- @Test
- public void doTestMigrateIkeSession_Vcn() throws Exception {
- final int expectedKeepalive = 2097; // Any unlikely number will do
- final NetworkCapabilities vcnNc = new NetworkCapabilities.Builder()
- .addTransportType(TRANSPORT_CELLULAR)
- .setTransportInfo(new VcnTransportInfo(TEST_SUB_ID, expectedKeepalive))
- .build();
- final Ikev2VpnProfile ikev2VpnProfile = makeIkeV2VpnProfile(
- true /* isAutomaticIpVersionSelectionEnabled */,
- true /* isAutomaticNattKeepaliveTimerEnabled */,
- 234 /* keepaliveInProfile */, // Should be ignored, any value will do
- ESP_IP_VERSION_IPV4, // Should be ignored
- ESP_ENCAP_TYPE_UDP // Should be ignored
- );
- doTestMigrateIkeSession(
- ikev2VpnProfile.toVpnProfile(),
- expectedKeepalive,
- ESP_IP_VERSION_AUTO /* expectedIpVersion */,
- ESP_ENCAP_TYPE_AUTO /* expectedEncapType */,
- vcnNc);
- }
-
- private void doTestMigrateIkeSession(
- @NonNull final VpnProfile profile,
- final int expectedKeepalive,
- final int expectedIpVersion,
- final int expectedEncapType,
- @NonNull final NetworkCapabilities caps) throws Exception {
- final PlatformVpnSnapshot vpnSnapShot =
- verifySetupPlatformVpn(profile,
- createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */),
- caps /* underlying network capabilities */,
- false /* mtuSupportsIpv6 */,
- expectedKeepalive < DEFAULT_LONG_LIVED_TCP_CONNS_EXPENSIVE_TIMEOUT_SEC);
- // Simulate a new network coming up
- vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
- verify(mIkeSessionWrapper, never()).setNetwork(any(), anyInt(), anyInt(), anyInt());
-
- vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK_2, caps);
- // Verify MOBIKE is triggered
- verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).setNetwork(TEST_NETWORK_2,
- expectedIpVersion, expectedEncapType, expectedKeepalive);
-
- vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
- }
-
- @Test
- public void testLinkPropertiesUpdateTriggerReevaluation() throws Exception {
- final boolean hasV6 = true;
-
- mockCarrierConfig(TEST_SUB_ID, TelephonyManager.SIM_STATE_LOADED, TEST_KEEPALIVE_TIMER,
- PREFERRED_IKE_PROTOCOL_IPV6_ESP);
- final IkeSessionParams params = getTestIkeSessionParams(hasV6,
- new IkeFqdnIdentification(TEST_IDENTITY), TEST_KEEPALIVE_TIMER);
- final IkeTunnelConnectionParams tunnelParams =
- new IkeTunnelConnectionParams(params, CHILD_PARAMS);
- final Ikev2VpnProfile ikeProfile = new Ikev2VpnProfile.Builder(tunnelParams)
- .setBypassable(true)
- .setAutomaticNattKeepaliveTimerEnabled(false)
- .setAutomaticIpVersionSelectionEnabled(true)
- .build();
- final PlatformVpnSnapshot vpnSnapShot =
- verifySetupPlatformVpn(ikeProfile.toVpnProfile(),
- createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */),
- new NetworkCapabilities.Builder().build() /* underlying network caps */,
- hasV6 /* mtuSupportsIpv6 */,
- false /* areLongLivedTcpConnectionsExpensive */);
- reset(mExecutor);
-
- // Simulate a new network coming up
- final LinkProperties lp = new LinkProperties();
- lp.addLinkAddress(new LinkAddress("192.0.2.2/32"));
-
- // Have the executor use the real delay to make sure schedule() was called only
- // once for all calls. Also, arrange for execute() not to call schedule() to avoid
- // messing with the checks for schedule().
- mExecutor.delayMs = TestExecutor.REAL_DELAY;
- mExecutor.executeDirect = true;
- vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
- vpnSnapShot.nwCb.onCapabilitiesChanged(
- TEST_NETWORK_2, new NetworkCapabilities.Builder().build());
- vpnSnapShot.nwCb.onLinkPropertiesChanged(TEST_NETWORK_2, new LinkProperties(lp));
- verify(mExecutor).schedule(any(Runnable.class), longThat(it -> it > 0), any());
- reset(mExecutor);
-
- final InOrder order = inOrder(mIkeSessionWrapper);
-
- // Verify the network is started
- order.verify(mIkeSessionWrapper, timeout(TIMEOUT_CROSSTHREAD_MS)).setNetwork(TEST_NETWORK_2,
- ESP_IP_VERSION_AUTO, ESP_ENCAP_TYPE_AUTO, TEST_KEEPALIVE_TIMER);
-
- // Send the same properties, check that no migration is scheduled
- vpnSnapShot.nwCb.onLinkPropertiesChanged(TEST_NETWORK_2, new LinkProperties(lp));
- verify(mExecutor, never()).schedule(any(Runnable.class), anyLong(), any());
-
- // Add v6 address, verify MOBIKE is triggered
- lp.addLinkAddress(new LinkAddress("2001:db8::1/64"));
- vpnSnapShot.nwCb.onLinkPropertiesChanged(TEST_NETWORK_2, new LinkProperties(lp));
- order.verify(mIkeSessionWrapper, timeout(TIMEOUT_CROSSTHREAD_MS)).setNetwork(TEST_NETWORK_2,
- ESP_IP_VERSION_AUTO, ESP_ENCAP_TYPE_AUTO, TEST_KEEPALIVE_TIMER);
-
- // Add another v4 address, verify MOBIKE is triggered
- final LinkProperties stacked = new LinkProperties();
- stacked.setInterfaceName("v4-" + lp.getInterfaceName());
- stacked.addLinkAddress(new LinkAddress("192.168.0.1/32"));
- lp.addStackedLink(stacked);
- vpnSnapShot.nwCb.onLinkPropertiesChanged(TEST_NETWORK_2, new LinkProperties(lp));
- order.verify(mIkeSessionWrapper, timeout(TIMEOUT_CROSSTHREAD_MS)).setNetwork(TEST_NETWORK_2,
- ESP_IP_VERSION_AUTO, ESP_ENCAP_TYPE_AUTO, TEST_KEEPALIVE_TIMER);
-
- vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
- }
-
- private void mockCarrierConfig(int subId, int simStatus, int keepaliveTimer, int ikeProtocol) {
- final SubscriptionInfo subscriptionInfo = mock(SubscriptionInfo.class);
- doReturn(subId).when(subscriptionInfo).getSubscriptionId();
- doReturn(List.of(subscriptionInfo)).when(mSubscriptionManager)
- .getActiveSubscriptionInfoList();
-
- doReturn(simStatus).when(mTmPerSub).getSimApplicationState();
- doReturn(TEST_MCCMNC).when(mTmPerSub).getSimOperator(subId);
-
- final PersistableBundle persistableBundle = new PersistableBundle();
- persistableBundle.putInt(KEY_MIN_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT, keepaliveTimer);
- persistableBundle.putInt(KEY_PREFERRED_IKE_PROTOCOL_INT, ikeProtocol);
- // For CarrierConfigManager.isConfigForIdentifiedCarrier check
- persistableBundle.putBoolean(KEY_CARRIER_CONFIG_APPLIED_BOOL, true);
- doReturn(persistableBundle).when(mConfigManager).getConfigForSubId(subId);
- }
-
- private CarrierConfigManager.CarrierConfigChangeListener getCarrierConfigListener() {
- final ArgumentCaptor<CarrierConfigManager.CarrierConfigChangeListener> listenerCaptor =
- ArgumentCaptor.forClass(CarrierConfigManager.CarrierConfigChangeListener.class);
-
- verify(mConfigManager).registerCarrierConfigChangeListener(any(), listenerCaptor.capture());
-
- return listenerCaptor.getValue();
- }
-
- @Test
- public void testNattKeepaliveTimerFromCarrierConfig_noSubId() throws Exception {
- doTestReadCarrierConfig(new NetworkCapabilities(),
- TelephonyManager.SIM_STATE_LOADED,
- PREFERRED_IKE_PROTOCOL_IPV4_UDP,
- AUTOMATIC_KEEPALIVE_DELAY_SECONDS /* expectedKeepaliveTimer */,
- ESP_IP_VERSION_AUTO /* expectedIpVersion */,
- ESP_ENCAP_TYPE_AUTO /* expectedEncapType */,
- false /* expectedReadFromCarrierConfig*/,
- true /* areLongLivedTcpConnectionsExpensive */);
- }
-
- @Test
- public void testNattKeepaliveTimerFromCarrierConfig_simAbsent() throws Exception {
- doTestReadCarrierConfig(new NetworkCapabilities.Builder().build(),
- TelephonyManager.SIM_STATE_ABSENT,
- PREFERRED_IKE_PROTOCOL_IPV4_UDP,
- AUTOMATIC_KEEPALIVE_DELAY_SECONDS /* expectedKeepaliveTimer */,
- ESP_IP_VERSION_AUTO /* expectedIpVersion */,
- ESP_ENCAP_TYPE_AUTO /* expectedEncapType */,
- false /* expectedReadFromCarrierConfig*/,
- true /* areLongLivedTcpConnectionsExpensive */);
- }
-
- @Test
- public void testNattKeepaliveTimerFromCarrierConfig() throws Exception {
- doTestReadCarrierConfig(createTestCellNc(),
- TelephonyManager.SIM_STATE_LOADED,
- PREFERRED_IKE_PROTOCOL_AUTO,
- TEST_KEEPALIVE_TIMER /* expectedKeepaliveTimer */,
- ESP_IP_VERSION_AUTO /* expectedIpVersion */,
- ESP_ENCAP_TYPE_AUTO /* expectedEncapType */,
- true /* expectedReadFromCarrierConfig*/,
- false /* areLongLivedTcpConnectionsExpensive */);
- }
-
- @Test
- public void testNattKeepaliveTimerFromCarrierConfig_NotCell() throws Exception {
- final NetworkCapabilities nc = new NetworkCapabilities.Builder()
- .addTransportType(TRANSPORT_WIFI)
- .setTransportInfo(new WifiInfo.Builder().build())
- .build();
- doTestReadCarrierConfig(nc,
- TelephonyManager.SIM_STATE_LOADED,
- PREFERRED_IKE_PROTOCOL_IPV4_UDP,
- AUTOMATIC_KEEPALIVE_DELAY_SECONDS /* expectedKeepaliveTimer */,
- ESP_IP_VERSION_AUTO /* expectedIpVersion */,
- ESP_ENCAP_TYPE_AUTO /* expectedEncapType */,
- false /* expectedReadFromCarrierConfig*/,
- true /* areLongLivedTcpConnectionsExpensive */);
- }
-
- @Test
- public void testPreferredIpProtocolFromCarrierConfig_v4UDP() throws Exception {
- doTestReadCarrierConfig(createTestCellNc(),
- TelephonyManager.SIM_STATE_LOADED,
- PREFERRED_IKE_PROTOCOL_IPV4_UDP,
- TEST_KEEPALIVE_TIMER /* expectedKeepaliveTimer */,
- ESP_IP_VERSION_IPV4 /* expectedIpVersion */,
- ESP_ENCAP_TYPE_UDP /* expectedEncapType */,
- true /* expectedReadFromCarrierConfig*/,
- false /* areLongLivedTcpConnectionsExpensive */);
- }
-
- @Test
- public void testPreferredIpProtocolFromCarrierConfig_v6ESP() throws Exception {
- doTestReadCarrierConfig(createTestCellNc(),
- TelephonyManager.SIM_STATE_LOADED,
- PREFERRED_IKE_PROTOCOL_IPV6_ESP,
- TEST_KEEPALIVE_TIMER /* expectedKeepaliveTimer */,
- ESP_IP_VERSION_IPV6 /* expectedIpVersion */,
- ESP_ENCAP_TYPE_NONE /* expectedEncapType */,
- true /* expectedReadFromCarrierConfig*/,
- false /* areLongLivedTcpConnectionsExpensive */);
- }
-
- @Test
- public void testPreferredIpProtocolFromCarrierConfig_v6UDP() throws Exception {
- doTestReadCarrierConfig(createTestCellNc(),
- TelephonyManager.SIM_STATE_LOADED,
- PREFERRED_IKE_PROTOCOL_IPV6_UDP,
- TEST_KEEPALIVE_TIMER /* expectedKeepaliveTimer */,
- ESP_IP_VERSION_IPV6 /* expectedIpVersion */,
- ESP_ENCAP_TYPE_UDP /* expectedEncapType */,
- true /* expectedReadFromCarrierConfig*/,
- false /* areLongLivedTcpConnectionsExpensive */);
- }
-
- private NetworkCapabilities createTestCellNc() {
- return new NetworkCapabilities.Builder()
- .addTransportType(TRANSPORT_CELLULAR)
- .setNetworkSpecifier(new TelephonyNetworkSpecifier.Builder()
- .setSubscriptionId(TEST_SUB_ID)
- .build())
- .build();
- }
-
- private void doTestReadCarrierConfig(NetworkCapabilities nc, int simState, int preferredIpProto,
- int expectedKeepaliveTimer, int expectedIpVersion, int expectedEncapType,
- boolean expectedReadFromCarrierConfig,
- boolean areLongLivedTcpConnectionsExpensive)
- throws Exception {
- final Ikev2VpnProfile ikeProfile =
- new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY)
- .setAuthPsk(TEST_VPN_PSK)
- .setBypassable(true /* isBypassable */)
- .setAutomaticNattKeepaliveTimerEnabled(true)
- .setAutomaticIpVersionSelectionEnabled(true)
- .build();
-
- final PlatformVpnSnapshot vpnSnapShot =
- verifySetupPlatformVpn(ikeProfile.toVpnProfile(),
- createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */),
- new NetworkCapabilities.Builder().build() /* underlying network caps */,
- false /* mtuSupportsIpv6 */,
- true /* areLongLivedTcpConnectionsExpensive */);
-
- final CarrierConfigManager.CarrierConfigChangeListener listener =
- getCarrierConfigListener();
-
- // Simulate a new network coming up
- vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
- // Migration will not be started until receiving network capabilities change.
- verify(mIkeSessionWrapper, never()).setNetwork(any(), anyInt(), anyInt(), anyInt());
-
- reset(mIkeSessionWrapper);
- mockCarrierConfig(TEST_SUB_ID, simState, TEST_KEEPALIVE_TIMER, preferredIpProto);
- vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK_2, nc);
- verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).setNetwork(TEST_NETWORK_2,
- expectedIpVersion, expectedEncapType, expectedKeepaliveTimer);
- if (expectedReadFromCarrierConfig) {
- final ArgumentCaptor<NetworkCapabilities> ncCaptor =
- ArgumentCaptor.forClass(NetworkCapabilities.class);
- verify(mMockNetworkAgent, timeout(TEST_TIMEOUT_MS))
- .doSendNetworkCapabilities(ncCaptor.capture());
-
- final VpnTransportInfo info =
- (VpnTransportInfo) ncCaptor.getValue().getTransportInfo();
- assertEquals(areLongLivedTcpConnectionsExpensive,
- info.areLongLivedTcpConnectionsExpensive());
- } else {
- verify(mMockNetworkAgent, never()).doSendNetworkCapabilities(any());
- }
-
- reset(mExecutor);
- reset(mIkeSessionWrapper);
- reset(mMockNetworkAgent);
-
- // Trigger carrier config change
- listener.onCarrierConfigChanged(1 /* logicalSlotIndex */, TEST_SUB_ID,
- -1 /* carrierId */, -1 /* specificCarrierId */);
- verify(mIkeSessionWrapper).setNetwork(TEST_NETWORK_2,
- expectedIpVersion, expectedEncapType, expectedKeepaliveTimer);
- // Expect no NetworkCapabilities change.
- // Call to doSendNetworkCapabilities() will not be triggered.
- verify(mMockNetworkAgent, never()).doSendNetworkCapabilities(any());
- }
-
- @Test
- public void testStartPlatformVpn_mtuDoesNotSupportIpv6() throws Exception {
- final PlatformVpnSnapshot vpnSnapShot =
- verifySetupPlatformVpn(
- createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */),
- false /* mtuSupportsIpv6 */);
- vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
- }
-
- @Test
- public void testStartPlatformVpn_underlyingNetworkNotChange() throws Exception {
- final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
- createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
- // Trigger update on the same network should not cause underlying network change in NC of
- // the VPN network
- vpnSnapShot.nwCb.onAvailable(TEST_NETWORK);
- vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK,
- new NetworkCapabilities.Builder()
- .setSubscriptionIds(Set.of(TEST_SUB_ID))
- .build());
- // Verify setNetwork() called but no underlying network update
- verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).setNetwork(eq(TEST_NETWORK),
- eq(ESP_IP_VERSION_AUTO) /* ipVersion */,
- eq(ESP_ENCAP_TYPE_AUTO) /* encapType */,
- eq(DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT) /* keepaliveDelay */);
- verify(mMockNetworkAgent, never())
- .doSetUnderlyingNetworks(any());
-
- vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
- vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK_2,
- new NetworkCapabilities.Builder().build());
-
- // A new network should trigger both setNetwork() and a underlying network update.
- verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).setNetwork(eq(TEST_NETWORK_2),
- eq(ESP_IP_VERSION_AUTO) /* ipVersion */,
- eq(ESP_ENCAP_TYPE_AUTO) /* encapType */,
- eq(DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT) /* keepaliveDelay */);
- verify(mMockNetworkAgent).doSetUnderlyingNetworks(
- Collections.singletonList(TEST_NETWORK_2));
-
- vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
- }
-
- @Test
- public void testStartPlatformVpnMobility_mobikeEnabled() throws Exception {
- final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
- createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
-
- // Set new MTU on a different network
- final int newMtu = IPV6_MIN_MTU + 1;
- doReturn(newMtu).when(mTestDeps).calculateVpnMtu(any(), anyInt(), anyInt(), anyBoolean());
-
- // Mock network loss and verify a cleanup task is scheduled
- vpnSnapShot.nwCb.onLost(TEST_NETWORK);
- verify(mExecutor, atLeastOnce()).schedule(any(Runnable.class), anyLong(), any());
-
- // Mock new network comes up and the cleanup task is cancelled
- vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
- verify(mIkeSessionWrapper, never()).setNetwork(any(), anyInt(), anyInt(), anyInt());
-
- vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK_2,
- new NetworkCapabilities.Builder().build());
- // Verify MOBIKE is triggered
- verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).setNetwork(eq(TEST_NETWORK_2),
- eq(ESP_IP_VERSION_AUTO) /* ipVersion */,
- eq(ESP_ENCAP_TYPE_AUTO) /* encapType */,
- eq(DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT) /* keepaliveDelay */);
- // Verify mNetworkCapabilities is updated
- assertEquals(
- Collections.singletonList(TEST_NETWORK_2),
- vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks());
- verify(mMockNetworkAgent)
- .doSetUnderlyingNetworks(Collections.singletonList(TEST_NETWORK_2));
-
- // Mock the MOBIKE procedure
- vpnSnapShot.ikeCb.onIkeSessionConnectionInfoChanged(createIkeConnectInfo_2());
- vpnSnapShot.childCb.onIpSecTransformsMigrated(
- createIpSecTransform(), createIpSecTransform());
-
- verify(mIpSecService).setNetworkForTunnelInterface(
- eq(TEST_TUNNEL_RESOURCE_ID), eq(TEST_NETWORK_2), anyString());
-
- // Expect 2 times: one for initial setup and one for MOBIKE
- verifyApplyTunnelModeTransforms(2);
-
- // Verify mNetworkAgent is updated
- verify(mMockNetworkAgent).doSendLinkProperties(argThat(lp -> lp.getMtu() == newMtu));
- verify(mMockNetworkAgent, never()).unregister();
- // No further doSetUnderlyingNetworks interaction. The interaction count should stay one.
- verify(mMockNetworkAgent, times(1)).doSetUnderlyingNetworks(any());
- vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
- }
-
- @Test
- public void testStartPlatformVpnMobility_mobikeEnabledMtuDoesNotSupportIpv6() throws Exception {
- final PlatformVpnSnapshot vpnSnapShot =
- verifySetupPlatformVpn(
- createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
-
- // Set MTU below 1280
- final int newMtu = IPV6_MIN_MTU - 1;
- doReturn(newMtu).when(mTestDeps).calculateVpnMtu(any(), anyInt(), anyInt(), anyBoolean());
-
- // Mock new network available & MOBIKE procedures
- vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
- vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK_2,
- new NetworkCapabilities.Builder().build());
- // Verify mNetworkCapabilities is updated
- verify(mMockNetworkAgent, timeout(TEST_TIMEOUT_MS))
- .doSetUnderlyingNetworks(Collections.singletonList(TEST_NETWORK_2));
- assertEquals(
- Collections.singletonList(TEST_NETWORK_2),
- vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks());
-
- vpnSnapShot.ikeCb.onIkeSessionConnectionInfoChanged(createIkeConnectInfo_2());
- vpnSnapShot.childCb.onIpSecTransformsMigrated(
- createIpSecTransform(), createIpSecTransform());
-
- // Verify removal of IPv6 addresses and routes triggers a network agent restart
- final ArgumentCaptor<LinkProperties> lpCaptor =
- ArgumentCaptor.forClass(LinkProperties.class);
- verify(mTestDeps, times(2))
- .newNetworkAgent(any(), any(), anyString(), any(), lpCaptor.capture(), any(), any(),
- any(), any());
- verify(mMockNetworkAgent).unregister();
- // mMockNetworkAgent is an old NetworkAgent, so it won't update LinkProperties after
- // unregistering.
- verify(mMockNetworkAgent, never()).doSendLinkProperties(any());
-
- final LinkProperties lp = lpCaptor.getValue();
-
- for (LinkAddress addr : lp.getLinkAddresses()) {
- if (addr.isIpv6()) {
- fail("IPv6 address found on VPN with MTU < IPv6 minimum MTU");
- }
- }
-
- for (InetAddress dnsAddr : lp.getDnsServers()) {
- if (dnsAddr instanceof Inet6Address) {
- fail("IPv6 DNS server found on VPN with MTU < IPv6 minimum MTU");
- }
- }
-
- for (RouteInfo routeInfo : lp.getRoutes()) {
- if (routeInfo.getDestinationLinkAddress().isIpv6()
- && !routeInfo.isIPv6UnreachableDefault()) {
- fail("IPv6 route found on VPN with MTU < IPv6 minimum MTU");
- }
- }
-
- assertEquals(newMtu, lp.getMtu());
-
- vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
- }
-
- @Test
- public void testStartPlatformVpnReestablishes_mobikeDisabled() throws Exception {
- final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
- createIkeConfig(createIkeConnectInfo(), false /* isMobikeEnabled */));
-
- // Forget the first IKE creation to be prepared to capture callbacks of the second
- // IKE session
- resetIkev2SessionCreator(mock(Vpn.IkeSessionWrapper.class));
-
- // Mock network switch
- vpnSnapShot.nwCb.onLost(TEST_NETWORK);
- vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
- // The old IKE Session will not be killed until receiving network capabilities change.
- verify(mIkeSessionWrapper, never()).kill();
-
- vpnSnapShot.nwCb.onCapabilitiesChanged(
- TEST_NETWORK_2, new NetworkCapabilities.Builder().build());
- // Verify the old IKE Session is killed
- verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).kill();
-
- // Capture callbacks of the new IKE Session
- final Pair<IkeSessionCallback, ChildSessionCallback> cbPair =
- verifyCreateIkeAndCaptureCbs();
- final IkeSessionCallback ikeCb = cbPair.first;
- final ChildSessionCallback childCb = cbPair.second;
-
- // Mock the IKE Session setup
- ikeCb.onOpened(createIkeConfig(createIkeConnectInfo_2(), false /* isMobikeEnabled */));
-
- childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_IN);
- childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_OUT);
- childCb.onOpened(createChildConfig());
-
- // Expect 2 times since there have been two Session setups
- verifyApplyTunnelModeTransforms(2);
-
- // Verify mNetworkCapabilities and mNetworkAgent are updated
- assertEquals(
- Collections.singletonList(TEST_NETWORK_2),
- vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks());
- verify(mMockNetworkAgent)
- .doSetUnderlyingNetworks(Collections.singletonList(TEST_NETWORK_2));
-
- vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
- }
-
- private String getDump(@NonNull final Vpn vpn) {
- final StringWriter sw = new StringWriter();
- final IndentingPrintWriter writer = new IndentingPrintWriter(sw, "");
- vpn.dump(writer);
- writer.flush();
- return sw.toString();
- }
-
- private int countMatches(@NonNull final Pattern regexp, @NonNull final String string) {
- final Matcher m = regexp.matcher(string);
- int i = 0;
- while (m.find()) ++i;
- return i;
- }
-
- @Test
- public void testNCEventChanges() throws Exception {
- final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder()
- .addTransportType(TRANSPORT_CELLULAR)
- .addCapability(NET_CAPABILITY_INTERNET)
- .addCapability(NET_CAPABILITY_NOT_RESTRICTED)
- .setLinkDownstreamBandwidthKbps(1000)
- .setLinkUpstreamBandwidthKbps(500);
-
- final Ikev2VpnProfile ikeProfile =
- new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY)
- .setAuthPsk(TEST_VPN_PSK)
- .setBypassable(true /* isBypassable */)
- .setAutomaticNattKeepaliveTimerEnabled(true)
- .setAutomaticIpVersionSelectionEnabled(true)
- .build();
-
- final PlatformVpnSnapshot vpnSnapShot =
- verifySetupPlatformVpn(ikeProfile.toVpnProfile(),
- createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */),
- ncBuilder.build(), false /* mtuSupportsIpv6 */,
- true /* areLongLivedTcpConnectionsExpensive */);
-
- // Calls to onCapabilitiesChanged will be thrown to the executor for execution ; by
- // default this will incur a 10ms delay before it's executed, messing with the timing
- // of the log and having the checks for counts in equals() below flake.
- mExecutor.executeDirect = true;
-
- // First nc changed triggered by verifySetupPlatformVpn
- final Pattern pattern = Pattern.compile("Cap changed from", Pattern.MULTILINE);
- final String stage1 = getDump(vpnSnapShot.vpn);
- assertEquals(1, countMatches(pattern, stage1));
-
- vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK, ncBuilder.build());
- final String stage2 = getDump(vpnSnapShot.vpn);
- // Was the same caps, there should still be only 1 match
- assertEquals(1, countMatches(pattern, stage2));
-
- ncBuilder.setLinkDownstreamBandwidthKbps(1200)
- .setLinkUpstreamBandwidthKbps(300);
- vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK, ncBuilder.build());
- final String stage3 = getDump(vpnSnapShot.vpn);
- // Was not an important change, should not be logged, still only 1 match
- assertEquals(1, countMatches(pattern, stage3));
-
- ncBuilder.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED);
- vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK, ncBuilder.build());
- final String stage4 = getDump(vpnSnapShot.vpn);
- // Change to caps is important, should cause a new match
- assertEquals(2, countMatches(pattern, stage4));
-
- ncBuilder.removeCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED);
- ncBuilder.setLinkDownstreamBandwidthKbps(600);
- vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK, ncBuilder.build());
- final String stage5 = getDump(vpnSnapShot.vpn);
- // Change to caps is important, should cause a new match even with the unimportant change
- assertEquals(3, countMatches(pattern, stage5));
- }
- // TODO : beef up event logs tests
-
- private void verifyHandlingNetworkLoss(PlatformVpnSnapshot vpnSnapShot) throws Exception {
- // Forget the #sendLinkProperties during first setup.
- reset(mMockNetworkAgent);
-
- // Mock network loss
- vpnSnapShot.nwCb.onLost(TEST_NETWORK);
-
- // Mock the grace period expires
- verify(mExecutor, atLeastOnce()).schedule(any(Runnable.class), anyLong(), any());
-
- final ArgumentCaptor<LinkProperties> lpCaptor =
- ArgumentCaptor.forClass(LinkProperties.class);
- verify(mMockNetworkAgent, timeout(TEST_TIMEOUT_MS))
- .doSendLinkProperties(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());
-
- verify(mMockNetworkAgent, timeout(TEST_TIMEOUT_MS)).unregister();
- }
-
- @Test
- public void testStartPlatformVpnHandlesNetworkLoss_mobikeEnabled() throws Exception {
- final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
- createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
- verifyHandlingNetworkLoss(vpnSnapShot);
- }
-
- @Test
- public void testStartPlatformVpnHandlesNetworkLoss_mobikeDisabled() throws Exception {
- final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
- createIkeConfig(createIkeConnectInfo(), false /* isMobikeEnabled */));
- verifyHandlingNetworkLoss(vpnSnapShot);
- }
-
- private ConnectivityDiagnosticsCallback getConnectivityDiagCallback() {
- final ArgumentCaptor<ConnectivityDiagnosticsCallback> cdcCaptor =
- ArgumentCaptor.forClass(ConnectivityDiagnosticsCallback.class);
- verify(mCdm).registerConnectivityDiagnosticsCallback(
- any(), any(), cdcCaptor.capture());
- return cdcCaptor.getValue();
- }
-
- private DataStallReport createDataStallReport() {
- return new DataStallReport(TEST_NETWORK, 1234 /* reportTimestamp */,
- 1 /* detectionMethod */, new LinkProperties(), new NetworkCapabilities(),
- new PersistableBundle());
- }
-
- private void verifyMobikeTriggered(List<Network> expected, int retryIndex) {
- // Verify retry is scheduled
- final long expectedDelayMs = mTestDeps.getValidationFailRecoveryMs(retryIndex);
- final ArgumentCaptor<Long> delayCaptor = ArgumentCaptor.forClass(Long.class);
- verify(mExecutor, times(retryIndex + 1)).schedule(
- any(Runnable.class), delayCaptor.capture(), eq(TimeUnit.MILLISECONDS));
- final List<Long> delays = delayCaptor.getAllValues();
- assertEquals(expectedDelayMs, (long) delays.get(delays.size() - 1));
-
- final ArgumentCaptor<Network> networkCaptor = ArgumentCaptor.forClass(Network.class);
- verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS + expectedDelayMs))
- .setNetwork(networkCaptor.capture(), anyInt() /* ipVersion */,
- anyInt() /* encapType */, anyInt() /* keepaliveDelay */);
- assertEquals(expected, Collections.singletonList(networkCaptor.getValue()));
- }
-
- @Test
- public void testDataStallInIkev2VpnMobikeDisabled() throws Exception {
- final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
- createIkeConfig(createIkeConnectInfo(), false /* isMobikeEnabled */));
-
- doReturn(TEST_NETWORK).when(mMockNetworkAgent).getNetwork();
- ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
- NetworkAgent.VALIDATION_STATUS_NOT_VALID);
-
- // Should not trigger MOBIKE if MOBIKE is not enabled
- verify(mIkeSessionWrapper, never()).setNetwork(any() /* network */,
- anyInt() /* ipVersion */, anyInt() /* encapType */, anyInt() /* keepaliveDelay */);
- }
-
- @Test
- public void testDataStallInIkev2VpnRecoveredByMobike() throws Exception {
- final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
- createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
-
- doReturn(TEST_NETWORK).when(mMockNetworkAgent).getNetwork();
- ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
- NetworkAgent.VALIDATION_STATUS_NOT_VALID);
- // Verify MOBIKE is triggered
- verifyMobikeTriggered(vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks(),
- 0 /* retryIndex */);
- // Validation failure on VPN network should trigger a re-evaluation request for the
- // underlying network.
- verify(mConnectivityManager).reportNetworkConnectivity(TEST_NETWORK, false);
-
- reset(mIkev2SessionCreator);
- reset(mExecutor);
-
- // Send validation status update.
- // Recovered and get network validated. It should not trigger the ike session reset.
- ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
- NetworkAgent.VALIDATION_STATUS_VALID);
- // Verify that the retry count is reset. The mValidationFailRetryCount will not be reset
- // until the executor finishes the execute() call, so wait until the all tasks are executed.
- waitForIdleSerialExecutor(mExecutor, TEST_TIMEOUT_MS);
- assertEquals(0,
- ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).mValidationFailRetryCount);
- verify(mIkev2SessionCreator, never()).createIkeSession(
- any(), any(), any(), any(), any(), any());
-
- reset(mIkeSessionWrapper);
- reset(mExecutor);
-
- // Another validation fail should trigger another reportNetworkConnectivity
- ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
- NetworkAgent.VALIDATION_STATUS_NOT_VALID);
- verifyMobikeTriggered(vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks(),
- 0 /* retryIndex */);
- verify(mConnectivityManager, times(2)).reportNetworkConnectivity(TEST_NETWORK, false);
- }
-
- @Test
- public void testDataStallInIkev2VpnNotRecoveredByMobike() throws Exception {
- final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
- createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
-
- int retry = 0;
- doReturn(TEST_NETWORK).when(mMockNetworkAgent).getNetwork();
- ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
- NetworkAgent.VALIDATION_STATUS_NOT_VALID);
- verifyMobikeTriggered(vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks(),
- retry++);
- // Validation failure on VPN network should trigger a re-evaluation request for the
- // underlying network.
- verify(mConnectivityManager).reportNetworkConnectivity(TEST_NETWORK, false);
- reset(mIkev2SessionCreator);
-
- // Second validation status update.
- ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
- NetworkAgent.VALIDATION_STATUS_NOT_VALID);
- verifyMobikeTriggered(vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks(),
- retry++);
- // Call to reportNetworkConnectivity should only happen once. No further interaction.
- verify(mConnectivityManager, times(1)).reportNetworkConnectivity(TEST_NETWORK, false);
-
- // Use real delay to verify reset session will not be performed if there is an existing
- // recovery for resetting the session.
- mExecutor.delayMs = TestExecutor.REAL_DELAY;
- mExecutor.executeDirect = true;
- // Send validation status update should result in ike session reset.
- ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
- NetworkAgent.VALIDATION_STATUS_NOT_VALID);
-
- // Verify session reset is scheduled
- long expectedDelay = mTestDeps.getValidationFailRecoveryMs(retry++);
- final ArgumentCaptor<Long> delayCaptor = ArgumentCaptor.forClass(Long.class);
- verify(mExecutor, times(retry)).schedule(any(Runnable.class), delayCaptor.capture(),
- eq(TimeUnit.MILLISECONDS));
- final List<Long> delays = delayCaptor.getAllValues();
- assertEquals(expectedDelay, (long) delays.get(delays.size() - 1));
- // Call to reportNetworkConnectivity should only happen once. No further interaction.
- verify(mConnectivityManager, times(1)).reportNetworkConnectivity(TEST_NETWORK, false);
-
- // Another invalid status reported should not trigger other scheduled recovery.
- expectedDelay = mTestDeps.getValidationFailRecoveryMs(retry++);
- ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
- NetworkAgent.VALIDATION_STATUS_NOT_VALID);
- verify(mExecutor, never()).schedule(
- any(Runnable.class), eq(expectedDelay), eq(TimeUnit.MILLISECONDS));
-
- // Verify that session being reset
- verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS + expectedDelay))
- .createIkeSession(any(), any(), any(), any(), any(), any());
- // Call to reportNetworkConnectivity should only happen once. No further interaction.
- verify(mConnectivityManager, times(1)).reportNetworkConnectivity(TEST_NETWORK, false);
- }
-
- @Test
- public void testStartLegacyVpnType() throws Exception {
- setMockedUsers(PRIMARY_USER);
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- final VpnProfile profile = new VpnProfile("testProfile" /* key */);
-
- profile.type = VpnProfile.TYPE_PPTP;
- assertThrows(UnsupportedOperationException.class, () -> startLegacyVpn(vpn, profile));
- profile.type = VpnProfile.TYPE_L2TP_IPSEC_PSK;
- assertThrows(UnsupportedOperationException.class, () -> startLegacyVpn(vpn, profile));
- }
-
- @Test
- public void testStartLegacyVpnModifyProfile_TypePSK() throws Exception {
- setMockedUsers(PRIMARY_USER);
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- final Ikev2VpnProfile ikev2VpnProfile =
- new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY)
- .setAuthPsk(TEST_VPN_PSK)
- .build();
- final VpnProfile profile = ikev2VpnProfile.toVpnProfile();
-
- startLegacyVpn(vpn, profile);
- assertEquals(profile, ikev2VpnProfile.toVpnProfile());
- }
-
- private void assertTransportInfoMatches(NetworkCapabilities nc, int type) {
- assertNotNull(nc);
- VpnTransportInfo ti = (VpnTransportInfo) nc.getTransportInfo();
- assertNotNull(ti);
- assertEquals(type, ti.getType());
- }
-
- // Make it public and un-final so as to spy it
- public class TestDeps extends Vpn.Dependencies {
- TestDeps() {}
-
- @Override
- public boolean isCallerSystem() {
- return true;
- }
-
- @Override
- public PendingIntent getIntentForStatusPanel(Context context) {
- return null;
- }
-
- @Override
- public ParcelFileDescriptor adoptFd(Vpn vpn, int mtu) {
- return new ParcelFileDescriptor(new FileDescriptor());
- }
-
- @Override
- public int jniCreate(Vpn vpn, int mtu) {
- // Pick a random positive number as fd to return.
- return 345;
- }
-
- @Override
- public String jniGetName(Vpn vpn, int fd) {
- return TEST_IFACE_NAME;
- }
-
- @Override
- public int jniSetAddresses(Vpn vpn, String interfaze, String addresses) {
- if (addresses == null) return 0;
- // Return the number of addresses.
- return addresses.split(" ").length;
- }
-
- @Override
- public void setBlocking(FileDescriptor fd, boolean blocking) {}
-
- @Override
- public DeviceIdleInternal getDeviceIdleInternal() {
- return mDeviceIdleInternal;
- }
-
- @Override
- public long getValidationFailRecoveryMs(int retryCount) {
- // Simply return retryCount as the delay seconds for retrying.
- return retryCount * 100L;
- }
-
- @Override
- public ScheduledThreadPoolExecutor newScheduledThreadPoolExecutor() {
- return mExecutor;
- }
-
- public boolean mIgnoreCallingUidChecks = true;
- @Override
- public void verifyCallingUidAndPackage(Context context, String packageName, int userId) {
- if (!mIgnoreCallingUidChecks) {
- super.verifyCallingUidAndPackage(context, packageName, userId);
- }
- }
- }
-
- /**
- * Mock some methods of vpn object.
- */
- private Vpn createVpn(@UserIdInt int userId) {
- final Context asUserContext = mock(Context.class, AdditionalAnswers.delegatesTo(mContext));
- doReturn(UserHandle.of(userId)).when(asUserContext).getUser();
- when(mContext.createContextAsUser(eq(UserHandle.of(userId)), anyInt()))
- .thenReturn(asUserContext);
- final TestLooper testLooper = new TestLooper();
- final Vpn vpn = new Vpn(testLooper.getLooper(), mContext, mTestDeps, mNetService,
- mNetd, userId, mVpnProfileStore, mSystemServices, mIkev2SessionCreator);
- verify(mConnectivityManager, times(1)).registerNetworkProvider(argThat(
- provider -> provider.getName().contains("VpnNetworkProvider")
- ));
- return vpn;
- }
-
- /**
- * Populate {@link #mUserManager} with a list of fake users.
- */
- private void setMockedUsers(UserInfo... users) {
- final Map<Integer, UserInfo> userMap = new ArrayMap<>();
- for (UserInfo user : users) {
- userMap.put(user.id, user);
- }
-
- /**
- * @see UserManagerService#getUsers(boolean)
- */
- doAnswer(invocation -> {
- final ArrayList<UserInfo> result = new ArrayList<>(users.length);
- for (UserInfo ui : users) {
- if (ui.isEnabled() && !ui.partial) {
- result.add(ui);
- }
- }
- return result;
- }).when(mUserManager).getAliveUsers();
-
- doAnswer(invocation -> {
- final int id = (int) invocation.getArguments()[0];
- return userMap.get(id);
- }).when(mUserManager).getUserInfo(anyInt());
- }
-
- /**
- * Populate {@link #mPackageManager} with a fake packageName-to-UID mapping.
- */
- private void setMockedPackages(final Map<String, Integer> packages) {
- try {
- doAnswer(invocation -> {
- final String appName = (String) invocation.getArguments()[0];
- final int userId = (int) invocation.getArguments()[1];
- Integer appId = packages.get(appName);
- if (appId == null) throw new PackageManager.NameNotFoundException(appName);
- return UserHandle.getUid(userId, appId);
- }).when(mPackageManager).getPackageUidAsUser(anyString(), anyInt());
- } catch (Exception e) {
- }
- }
-}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
index f0cb6df..b8ebf0f 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
@@ -16,6 +16,8 @@
package com.android.server.connectivity.mdns
+import android.content.Context
+import android.content.res.Resources
import android.net.InetAddresses.parseNumericAddress
import android.net.LinkAddress
import android.net.Network
@@ -26,13 +28,17 @@
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
+import com.android.connectivity.resources.R
import com.android.net.module.util.SharedLog
+import com.android.server.connectivity.ConnectivityResources
import com.android.server.connectivity.mdns.MdnsAdvertiser.AdvertiserCallback
+import com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_SERVICE
import com.android.server.connectivity.mdns.MdnsSocketProvider.SocketCallback
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.waitForIdle
import java.net.NetworkInterface
+import java.time.Duration
import java.util.Objects
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
@@ -55,9 +61,10 @@
private const val SERVICE_ID_1 = 1
private const val SERVICE_ID_2 = 2
-private const val LONG_SERVICE_ID_1 = 3
-private const val LONG_SERVICE_ID_2 = 4
-private const val CASE_INSENSITIVE_TEST_SERVICE_ID = 5
+private const val SERVICE_ID_3 = 3
+private const val LONG_SERVICE_ID_1 = 4
+private const val LONG_SERVICE_ID_2 = 5
+private const val CASE_INSENSITIVE_TEST_SERVICE_ID = 6
private const val TIMEOUT_MS = 10_000L
private val TEST_ADDR = parseNumericAddress("2001:db8::123")
private val TEST_ADDR2 = parseNumericAddress("2001:db8::124")
@@ -68,8 +75,10 @@
private val TEST_SOCKETKEY_2 = SocketKey(1002 /* interfaceIndex */)
private val TEST_HOSTNAME = arrayOf("Android_test", "local")
private const val TEST_SUBTYPE = "_subtype"
+private const val TEST_SUBTYPE2 = "_subtype2"
private val TEST_INTERFACE1 = "test_iface1"
private val TEST_INTERFACE2 = "test_iface2"
+private val TEST_CLIENT_UID_1 = 10010
private val TEST_OFFLOAD_PACKET1 = byteArrayOf(0x01, 0x02, 0x03)
private val TEST_OFFLOAD_PACKET2 = byteArrayOf(0x02, 0x03, 0x04)
private val DEFAULT_ADVERTISING_OPTION = MdnsAdvertisingOptions.getDefaultOptions()
@@ -80,6 +89,13 @@
network = TEST_NETWORK_1
}
+private val SERVICE_1_SUBTYPE = NsdServiceInfo("TestServiceName", "_advertisertest._tcp").apply {
+ subtypes = setOf(TEST_SUBTYPE)
+ port = 12345
+ hostAddresses = listOf(TEST_ADDR)
+ network = TEST_NETWORK_1
+}
+
private val LONG_SERVICE_1 =
NsdServiceInfo("a".repeat(48) + "TestServiceName", "_longadvertisertest._tcp").apply {
port = 12345
@@ -93,6 +109,14 @@
network = null
}
+private val ALL_NETWORKS_SERVICE_SUBTYPE =
+ NsdServiceInfo("TestServiceName", "_advertisertest._tcp").apply {
+ subtypes = setOf(TEST_SUBTYPE)
+ port = 12345
+ hostAddresses = listOf(TEST_ADDR)
+ network = null
+}
+
private val ALL_NETWORKS_SERVICE_2 =
NsdServiceInfo("TESTSERVICENAME", "_ADVERTISERTEST._tcp").apply {
port = 12345
@@ -134,6 +158,12 @@
OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
)
+private val SERVICES_PRIORITY_LIST = arrayOf(
+ "0:_advertisertest._tcp",
+ "5:_prioritytest._udp",
+ "5:_otherprioritytest._tcp"
+)
+
@RunWith(DevSdkIgnoreRunner::class)
@IgnoreUpTo(Build.VERSION_CODES.S_V2)
class MdnsAdvertiserTest {
@@ -148,6 +178,8 @@
private val mockInterfaceAdvertiser1 = mock(MdnsInterfaceAdvertiser::class.java)
private val mockInterfaceAdvertiser2 = mock(MdnsInterfaceAdvertiser::class.java)
private val mockDeps = mock(MdnsAdvertiser.Dependencies::class.java)
+ private val context = mock(Context::class.java)
+ private val resources = mock(Resources::class.java)
private val flags = MdnsFeatureFlags.newBuilder().setIsMdnsOffloadFeatureEnabled(true).build()
@Before
@@ -168,12 +200,21 @@
doReturn(TEST_INTERFACE2).`when`(mockInterfaceAdvertiser2).socketInterfaceName
doReturn(TEST_OFFLOAD_PACKET1).`when`(mockInterfaceAdvertiser1).getRawOffloadPayload(
SERVICE_ID_1)
+ doReturn(TEST_OFFLOAD_PACKET1).`when`(mockInterfaceAdvertiser1).getRawOffloadPayload(
+ SERVICE_ID_2)
+ doReturn(TEST_OFFLOAD_PACKET1).`when`(mockInterfaceAdvertiser1).getRawOffloadPayload(
+ SERVICE_ID_3)
doReturn(TEST_OFFLOAD_PACKET1).`when`(mockInterfaceAdvertiser2).getRawOffloadPayload(
SERVICE_ID_1)
+ doReturn(resources).`when`(context).getResources()
+ doReturn(SERVICES_PRIORITY_LIST).`when`(resources).getStringArray(
+ R.array.config_nsdOffloadServicesPriority)
+ ConnectivityResources.setResourcesContextForTest(context)
}
@After
fun tearDown() {
+ ConnectivityResources.setResourcesContextForTest(null)
thread.quitSafely()
thread.join()
}
@@ -187,9 +228,9 @@
@Test
fun testAddService_OneNetwork() {
val advertiser =
- MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
+ MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
- null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+ DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), socketCbCaptor.capture())
@@ -216,7 +257,10 @@
verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO_NO_SUBTYPE))
// Service is conflicted.
- postSync { intAdvCbCaptor.value.onServiceConflict(mockInterfaceAdvertiser1, SERVICE_ID_1) }
+ postSync {
+ intAdvCbCaptor.value
+ .onServiceConflict(mockInterfaceAdvertiser1, SERVICE_ID_1, CONFLICT_SERVICE)
+ }
// Verify the metrics data
doReturn(25).`when`(mockInterfaceAdvertiser1).getServiceRepliedRequestsCount(SERVICE_ID_1)
@@ -247,14 +291,14 @@
}
@Test
- fun testAddService_AllNetworks() {
+ fun testAddService_AllNetworksWithSubType() {
val advertiser =
- MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
- postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE,
- TEST_SUBTYPE, DEFAULT_ADVERTISING_OPTION) }
+ MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
+ postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE_SUBTYPE,
+ DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
- verify(socketProvider).requestSocket(eq(ALL_NETWORKS_SERVICE.network),
+ verify(socketProvider).requestSocket(eq(ALL_NETWORKS_SERVICE_SUBTYPE.network),
socketCbCaptor.capture())
val socketCb = socketCbCaptor.value
@@ -270,9 +314,9 @@
eq(thread.looper), any(), intAdvCbCaptor2.capture(), eq(TEST_HOSTNAME), any(), any()
)
verify(mockInterfaceAdvertiser1).addService(
- anyInt(), eq(ALL_NETWORKS_SERVICE), eq(TEST_SUBTYPE))
+ anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE), any())
verify(mockInterfaceAdvertiser2).addService(
- anyInt(), eq(ALL_NETWORKS_SERVICE), eq(TEST_SUBTYPE))
+ anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE), any())
doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
postSync { intAdvCbCaptor1.value.onServiceProbingSucceeded(
@@ -286,12 +330,21 @@
mockInterfaceAdvertiser2, SERVICE_ID_1) }
verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE2), eq(OFFLOAD_SERVICEINFO))
verify(cb).onRegisterServiceSucceeded(eq(SERVICE_ID_1),
- argThat { it.matches(ALL_NETWORKS_SERVICE) })
+ argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) })
// Services are conflicted.
- postSync { intAdvCbCaptor1.value.onServiceConflict(mockInterfaceAdvertiser1, SERVICE_ID_1) }
- postSync { intAdvCbCaptor1.value.onServiceConflict(mockInterfaceAdvertiser1, SERVICE_ID_1) }
- postSync { intAdvCbCaptor2.value.onServiceConflict(mockInterfaceAdvertiser2, SERVICE_ID_1) }
+ postSync {
+ intAdvCbCaptor1.value
+ .onServiceConflict(mockInterfaceAdvertiser1, SERVICE_ID_1, CONFLICT_SERVICE)
+ }
+ postSync {
+ intAdvCbCaptor1.value
+ .onServiceConflict(mockInterfaceAdvertiser1, SERVICE_ID_1, CONFLICT_SERVICE)
+ }
+ postSync {
+ intAdvCbCaptor2.value
+ .onServiceConflict(mockInterfaceAdvertiser2, SERVICE_ID_1, CONFLICT_SERVICE)
+ }
// Verify the metrics data
doReturn(10).`when`(mockInterfaceAdvertiser1).getServiceRepliedRequestsCount(SERVICE_ID_1)
@@ -319,11 +372,70 @@
}
@Test
+ fun testAddService_OffloadPriority() {
+ val advertiser =
+ MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
+ postSync {
+ advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1, DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1)
+ advertiser.addOrUpdateService(SERVICE_ID_2,
+ NsdServiceInfo("TestService2", "_PRIORITYTEST._udp").apply {
+ port = 12345
+ hostAddresses = listOf(TEST_ADDR)
+ }, DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1)
+ advertiser.addOrUpdateService(
+ SERVICE_ID_3,
+ NsdServiceInfo("TestService3", "_notprioritized._tcp").apply {
+ port = 12345
+ hostAddresses = listOf(TEST_ADDR)
+ }, DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1)
+ }
+
+ val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
+ verify(socketProvider).requestSocket(eq(SERVICE_1.network), socketCbCaptor.capture())
+
+ val socketCb = socketCbCaptor.value
+ postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
+
+ val intAdvCbCaptor1 = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
+ verify(mockDeps).makeAdvertiser(eq(mockSocket1), eq(listOf(TEST_LINKADDR)),
+ eq(thread.looper), any(), intAdvCbCaptor1.capture(), eq(TEST_HOSTNAME), any(), any()
+ )
+
+ doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
+ doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_2)
+ doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_3)
+ postSync {
+ intAdvCbCaptor1.value.onServiceProbingSucceeded(mockInterfaceAdvertiser1, SERVICE_ID_1)
+ intAdvCbCaptor1.value.onServiceProbingSucceeded(mockInterfaceAdvertiser1, SERVICE_ID_2)
+ intAdvCbCaptor1.value.onServiceProbingSucceeded(mockInterfaceAdvertiser1, SERVICE_ID_3)
+ }
+
+ verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO_NO_SUBTYPE))
+ verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), eq(OffloadServiceInfo(
+ OffloadServiceInfo.Key("TestService2", "_PRIORITYTEST._udp"),
+ emptyList() /* subtypes */,
+ "Android_test.local",
+ TEST_OFFLOAD_PACKET1,
+ 5, /* priority */
+ OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
+ )))
+ verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), eq(OffloadServiceInfo(
+ OffloadServiceInfo.Key("TestService3", "_notprioritized._tcp"),
+ emptyList() /* subtypes */,
+ "Android_test.local",
+ TEST_OFFLOAD_PACKET1,
+ Integer.MAX_VALUE, /* priority */
+ OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
+ )))
+ }
+
+ @Test
fun testAddService_Conflicts() {
val advertiser =
- MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
+ MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
- null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+ DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
val oneNetSocketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), oneNetSocketCbCaptor.capture())
@@ -331,18 +443,18 @@
// Register a service with the same name on all networks (name conflict)
postSync { advertiser.addOrUpdateService(SERVICE_ID_2, ALL_NETWORKS_SERVICE,
- null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+ DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
val allNetSocketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(null), allNetSocketCbCaptor.capture())
val allNetSocketCb = allNetSocketCbCaptor.value
postSync { advertiser.addOrUpdateService(LONG_SERVICE_ID_1, LONG_SERVICE_1,
- null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+ DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
postSync { advertiser.addOrUpdateService(LONG_SERVICE_ID_2, LONG_ALL_NETWORKS_SERVICE,
- null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+ DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
postSync { advertiser.addOrUpdateService(CASE_INSENSITIVE_TEST_SERVICE_ID,
- ALL_NETWORKS_SERVICE_2, null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+ ALL_NETWORKS_SERVICE_2, DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
// Callbacks for matching network and all networks both get the socket
postSync {
@@ -378,15 +490,15 @@
eq(thread.looper), any(), intAdvCbCaptor.capture(), eq(TEST_HOSTNAME), any(), any()
)
verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
- argThat { it.matches(SERVICE_1) }, eq(null))
+ argThat { it.matches(SERVICE_1) }, any())
verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_2),
- argThat { it.matches(expectedRenamed) }, eq(null))
+ argThat { it.matches(expectedRenamed) }, any())
verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_1),
- argThat { it.matches(LONG_SERVICE_1) }, eq(null))
+ argThat { it.matches(LONG_SERVICE_1) }, any())
verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_2),
- argThat { it.matches(expectedLongRenamed) }, eq(null))
+ argThat { it.matches(expectedLongRenamed) }, any())
verify(mockInterfaceAdvertiser1).addService(eq(CASE_INSENSITIVE_TEST_SERVICE_ID),
- argThat { it.matches(expectedCaseInsensitiveRenamed) }, eq(null))
+ argThat { it.matches(expectedCaseInsensitiveRenamed) }, any())
doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
postSync { intAdvCbCaptor.value.onServiceProbingSucceeded(
@@ -409,9 +521,10 @@
@Test
fun testAddOrUpdateService_Updates() {
val advertiser =
- MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
+ MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags,
+ context)
postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE,
- null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+ DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(null), socketCbCaptor.capture())
@@ -420,38 +533,55 @@
postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
- argThat { it.matches(ALL_NETWORKS_SERVICE) }, eq(null))
+ argThat { it.matches(ALL_NETWORKS_SERVICE) }, any())
val updateOptions = MdnsAdvertisingOptions.newBuilder().setIsOnlyUpdate(true).build()
// Update with serviceId that is not registered yet should fail
- postSync { advertiser.addOrUpdateService(SERVICE_ID_2, ALL_NETWORKS_SERVICE, TEST_SUBTYPE,
- updateOptions) }
+ postSync { advertiser.addOrUpdateService(SERVICE_ID_2, ALL_NETWORKS_SERVICE_SUBTYPE,
+ updateOptions, TEST_CLIENT_UID_1) }
verify(cb).onRegisterServiceFailed(SERVICE_ID_2, NsdManager.FAILURE_INTERNAL_ERROR)
// Update service with different NsdServiceInfo should fail
- postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1, TEST_SUBTYPE,
- updateOptions) }
+ postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1_SUBTYPE, updateOptions,
+ TEST_CLIENT_UID_1) }
verify(cb).onRegisterServiceFailed(SERVICE_ID_1, NsdManager.FAILURE_INTERNAL_ERROR)
// Update service with same NsdServiceInfo but different subType should succeed
- postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE, TEST_SUBTYPE,
- updateOptions) }
- verify(mockInterfaceAdvertiser1).updateService(eq(SERVICE_ID_1), eq(TEST_SUBTYPE))
+ postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE_SUBTYPE,
+ updateOptions, TEST_CLIENT_UID_1) }
+ verify(mockInterfaceAdvertiser1).updateService(eq(SERVICE_ID_1), eq(setOf(TEST_SUBTYPE)))
// Newly created MdnsInterfaceAdvertiser will get addService() call.
postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_2, mockSocket2, listOf(TEST_LINKADDR2)) }
verify(mockInterfaceAdvertiser2).addService(eq(SERVICE_ID_1),
- argThat { it.matches(ALL_NETWORKS_SERVICE) }, eq(TEST_SUBTYPE))
+ argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) }, any())
+ }
+
+ @Test
+ fun testAddOrUpdateService_customTtl_registeredSuccess() {
+ val advertiser = MdnsAdvertiser(
+ thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
+ val updateOptions =
+ MdnsAdvertisingOptions.newBuilder().setTtl(Duration.ofSeconds(30)).build()
+
+ postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE,
+ updateOptions, TEST_CLIENT_UID_1) }
+
+ val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
+ verify(socketProvider).requestSocket(eq(null), socketCbCaptor.capture())
+ val socketCb = socketCbCaptor.value
+ postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
+ verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1), any(), eq(updateOptions))
}
@Test
fun testRemoveService_whenAllServiceRemoved_thenUpdateHostName() {
val advertiser =
- MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
+ MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
verify(mockDeps, times(1)).generateHostname()
postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
- null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+ DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
postSync { advertiser.removeService(SERVICE_ID_1) }
verify(mockDeps, times(2)).generateHostname()
}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt
index 2797462..27242f1 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt
@@ -55,6 +55,7 @@
private val socket = mock(MdnsInterfaceSocket::class.java)
private val sharedLog = mock(SharedLog::class.java)
private val buffer = ByteArray(1500)
+ private val flags = MdnsFeatureFlags.newBuilder().build()
@Before
fun setUp() {
@@ -83,7 +84,7 @@
@Test
fun testAnnounce() {
val replySender = MdnsReplySender(
- thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */)
+ thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */, flags)
@Suppress("UNCHECKED_CAST")
val cb = mock(MdnsPacketRepeater.PacketRepeaterCallback::class.java)
as MdnsPacketRepeater.PacketRepeaterCallback<BaseAnnouncementInfo>
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
index f85d71d..28608bb 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
@@ -18,6 +18,7 @@
import android.net.InetAddresses.parseNumericAddress
import android.net.LinkAddress
+import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import android.os.Build
import android.os.HandlerThread
@@ -26,6 +27,7 @@
import com.android.server.connectivity.mdns.MdnsAnnouncer.AnnouncementInfo
import com.android.server.connectivity.mdns.MdnsAnnouncer.BaseAnnouncementInfo
import com.android.server.connectivity.mdns.MdnsAnnouncer.ExitAnnouncementInfo
+import com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_SERVICE
import com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.EXIT_ANNOUNCEMENT_DELAY_MS
import com.android.server.connectivity.mdns.MdnsPacketRepeater.PacketRepeaterCallback
import com.android.server.connectivity.mdns.MdnsProber.ProbingInfo
@@ -35,6 +37,7 @@
import java.net.InetSocketAddress
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
+import kotlin.test.assertNotSame
import kotlin.test.assertTrue
import org.junit.After
import org.junit.Before
@@ -44,6 +47,7 @@
import org.mockito.Mockito.any
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.anyString
+import org.mockito.Mockito.argThat
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.eq
@@ -67,6 +71,13 @@
port = 12345
}
+private val TEST_SERVICE_1_SUBTYPE = NsdServiceInfo().apply {
+ subtypes = setOf("_sub")
+ serviceType = "_testservice._tcp"
+ serviceName = "MyTestService"
+ port = 12345
+}
+
@RunWith(DevSdkIgnoreRunner::class)
@IgnoreUpTo(Build.VERSION_CODES.S_V2)
class MdnsInterfaceAdvertiserTest {
@@ -79,7 +90,8 @@
private val announcer = mock(MdnsAnnouncer::class.java)
private val prober = mock(MdnsProber::class.java)
private val sharedlog = SharedLog("MdnsInterfaceAdvertiserTest")
- private val flags = MdnsFeatureFlags.newBuilder().build()
+ private val flags = MdnsFeatureFlags.newBuilder()
+ .setIsKnownAnswerSuppressionEnabled(true).build()
@Suppress("UNCHECKED_CAST")
private val probeCbCaptor = ArgumentCaptor.forClass(PacketRepeaterCallback::class.java)
as ArgumentCaptor<PacketRepeaterCallback<ProbingInfo>>
@@ -110,7 +122,8 @@
@Before
fun setUp() {
doReturn(repository).`when`(deps).makeRecordRepository(any(), eq(TEST_HOSTNAME), any())
- doReturn(replySender).`when`(deps).makeReplySender(anyString(), any(), any(), any(), any())
+ doReturn(replySender).`when`(deps).makeReplySender(
+ anyString(), any(), any(), any(), any(), any())
doReturn(announcer).`when`(deps).makeMdnsAnnouncer(anyString(), any(), any(), any(), any())
doReturn(prober).`when`(deps).makeMdnsProber(anyString(), any(), any(), any(), any())
@@ -192,7 +205,8 @@
fun testReplyToQuery() {
addServiceAndFinishProbing(TEST_SERVICE_ID_1, TEST_SERVICE_1)
- val testReply = MdnsReplyInfo(emptyList(), emptyList(), 0, InetSocketAddress(0))
+ val testReply = MdnsReplyInfo(emptyList(), emptyList(), 0, InetSocketAddress(0),
+ InetSocketAddress(0), emptyList())
doReturn(testReply).`when`(repository).getReply(any(), any())
// Query obtained with:
@@ -206,7 +220,12 @@
packetHandler.handlePacket(query, query.size, src)
val packetCaptor = ArgumentCaptor.forClass(MdnsPacket::class.java)
- verify(repository).getReply(packetCaptor.capture(), eq(src))
+ val srcCaptor = ArgumentCaptor.forClass(InetSocketAddress::class.java)
+ verify(repository).getReply(packetCaptor.capture(), srcCaptor.capture())
+
+ assertEquals(src, srcCaptor.value)
+ assertNotSame(src, srcCaptor.value, "src will be reused by the packetHandler, references " +
+ "to it should not be used outside of handlePacket.")
packetCaptor.value.let {
assertEquals(1, it.questions.size)
@@ -222,9 +241,116 @@
}
@Test
+ fun testReplyToQuery_TruncatedBitSet() {
+ addServiceAndFinishProbing(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ val src = InetSocketAddress(parseNumericAddress("2001:db8::456"), MdnsConstants.MDNS_PORT)
+ val testReply = MdnsReplyInfo(emptyList(), emptyList(), 400L, InetSocketAddress(0), src,
+ emptyList())
+ val knownAnswersReply = MdnsReplyInfo(emptyList(), emptyList(), 400L, InetSocketAddress(0),
+ src, emptyList())
+ val knownAnswersReply2 = MdnsReplyInfo(emptyList(), emptyList(), 0L, InetSocketAddress(0),
+ src, emptyList())
+ doReturn(testReply).`when`(repository).getReply(
+ argThat { pkg -> pkg.questions.size != 0 && pkg.answers.size == 0 &&
+ (pkg.flags and MdnsConstants.FLAG_TRUNCATED) != 0},
+ eq(src))
+ doReturn(knownAnswersReply).`when`(repository).getReply(
+ argThat { pkg -> pkg.questions.size == 0 && pkg.answers.size != 0 &&
+ (pkg.flags and MdnsConstants.FLAG_TRUNCATED) != 0},
+ eq(src))
+ doReturn(knownAnswersReply2).`when`(repository).getReply(
+ argThat { pkg -> pkg.questions.size == 0 && pkg.answers.size != 0 &&
+ (pkg.flags and MdnsConstants.FLAG_TRUNCATED) == 0},
+ eq(src))
+
+ // Query obtained with:
+ // scapy.raw(scapy.DNS(
+ // tc = 1, qd = scapy.DNSQR(qtype='PTR', qname='_testservice._tcp.local'))
+ // ).hex().upper()
+ val query = HexDump.hexStringToByteArray(
+ "0000030000010000000000000C5F7465737473657276696365045F746370056C6F63616C00000C0001"
+ )
+
+ packetHandler.handlePacket(query, query.size, src)
+
+ val packetCaptor = ArgumentCaptor.forClass(MdnsPacket::class.java)
+ verify(repository).getReply(packetCaptor.capture(), eq(src))
+
+ packetCaptor.value.let {
+ assertTrue((it.flags and MdnsConstants.FLAG_TRUNCATED) != 0)
+ assertEquals(1, it.questions.size)
+ assertEquals(0, it.answers.size)
+ assertEquals(0, it.authorityRecords.size)
+ assertEquals(0, it.additionalRecords.size)
+
+ assertTrue(it.questions[0] is MdnsPointerRecord)
+ assertContentEquals(arrayOf("_testservice", "_tcp", "local"), it.questions[0].name)
+ }
+
+ verify(replySender).queueReply(testReply)
+
+ // Known-Answer packet with truncated bit set obtained with:
+ // scapy.raw(scapy.DNS(
+ // tc = 1, qd = None, an = scapy.DNSRR(type='PTR', rrname='_testtype._tcp.local',
+ // rdata='othertestservice._testtype._tcp.local', rclass='IN', ttl=4500))
+ // ).hex().upper()
+ val knownAnswers = HexDump.hexStringToByteArray(
+ "000003000000000100000000095F7465737474797065045F746370056C6F63616C00000C0001000" +
+ "011940027106F746865727465737473657276696365095F7465737474797065045F7463" +
+ "70056C6F63616C00"
+ )
+
+ packetHandler.handlePacket(knownAnswers, knownAnswers.size, src)
+
+ verify(repository, times(2)).getReply(packetCaptor.capture(), eq(src))
+
+ packetCaptor.value.let {
+ assertTrue((it.flags and MdnsConstants.FLAG_TRUNCATED) != 0)
+ assertEquals(0, it.questions.size)
+ assertEquals(1, it.answers.size)
+ assertEquals(0, it.authorityRecords.size)
+ assertEquals(0, it.additionalRecords.size)
+
+ assertTrue(it.answers[0] is MdnsPointerRecord)
+ assertContentEquals(arrayOf("_testtype", "_tcp", "local"), it.answers[0].name)
+ }
+
+ verify(replySender).queueReply(knownAnswersReply)
+
+ // Known-Answer packet obtained with:
+ // scapy.raw(scapy.DNS(
+ // qd = None, an = scapy.DNSRR(type='PTR', rrname='_testtype._tcp.local',
+ // rdata='testservice._testtype._tcp.local', rclass='IN', ttl=4500))
+ // ).hex().upper()
+ val knownAnswers2 = HexDump.hexStringToByteArray(
+ "000001000000000100000000095F7465737474797065045F746370056C6F63616C00000C0001000" +
+ "0119400220B7465737473657276696365095F7465737474797065045F746370056C6F63" +
+ "616C00"
+ )
+
+ packetHandler.handlePacket(knownAnswers2, knownAnswers2.size, src)
+
+ verify(repository, times(3)).getReply(packetCaptor.capture(), eq(src))
+
+ packetCaptor.value.let {
+ assertTrue((it.flags and MdnsConstants.FLAG_TRUNCATED) == 0)
+ assertEquals(0, it.questions.size)
+ assertEquals(1, it.answers.size)
+ assertEquals(0, it.authorityRecords.size)
+ assertEquals(0, it.additionalRecords.size)
+
+ assertTrue(it.answers[0] is MdnsPointerRecord)
+ assertContentEquals(arrayOf("_testtype", "_tcp", "local"), it.answers[0].name)
+ }
+
+ verify(replySender).queueReply(knownAnswersReply2)
+ }
+
+ @Test
fun testConflict() {
addServiceAndFinishProbing(TEST_SERVICE_ID_1, TEST_SERVICE_1)
- doReturn(setOf(TEST_SERVICE_ID_1)).`when`(repository).getConflictingServices(any())
+ doReturn(mapOf(TEST_SERVICE_ID_1 to CONFLICT_SERVICE))
+ .`when`(repository).getConflictingServices(any())
// Reply obtained with:
// scapy.raw(scapy.DNS(
@@ -250,7 +376,7 @@
}
thread.waitForIdle(TIMEOUT_MS)
- verify(cb).onServiceConflict(advertiser, TEST_SERVICE_ID_1)
+ verify(cb).onServiceConflict(advertiser, TEST_SERVICE_ID_1, CONFLICT_SERVICE)
}
@Test
@@ -278,8 +404,8 @@
fun testReplaceExitingService() {
doReturn(TEST_SERVICE_ID_DUPLICATE).`when`(repository)
.addService(eq(TEST_SERVICE_ID_DUPLICATE), any(), any())
- val subType = "_sub"
- advertiser.addService(TEST_SERVICE_ID_DUPLICATE, TEST_SERVICE_1, subType)
+ advertiser.addService(TEST_SERVICE_ID_DUPLICATE, TEST_SERVICE_1_SUBTYPE,
+ MdnsAdvertisingOptions.getDefaultOptions())
verify(repository).addService(eq(TEST_SERVICE_ID_DUPLICATE), any(), any())
verify(announcer).stop(TEST_SERVICE_ID_DUPLICATE)
verify(prober).startProbing(any())
@@ -289,8 +415,8 @@
fun testUpdateExistingService() {
doReturn(TEST_SERVICE_ID_DUPLICATE).`when`(repository)
.addService(eq(TEST_SERVICE_ID_DUPLICATE), any(), any())
- val subType = "_sub"
- advertiser.updateService(TEST_SERVICE_ID_DUPLICATE, subType)
+ val subTypes = setOf("_sub")
+ advertiser.updateService(TEST_SERVICE_ID_DUPLICATE, subTypes)
verify(repository).updateService(eq(TEST_SERVICE_ID_DUPLICATE), any())
verify(announcer, never()).stop(TEST_SERVICE_ID_DUPLICATE)
verify(prober, never()).startProbing(any())
@@ -302,8 +428,8 @@
doReturn(serviceId).`when`(testProbingInfo).serviceId
doReturn(testProbingInfo).`when`(repository).setServiceProbing(serviceId)
- advertiser.addService(serviceId, serviceInfo, null /* subtype */)
- verify(repository).addService(serviceId, serviceInfo, null /* subtype */)
+ advertiser.addService(serviceId, serviceInfo, MdnsAdvertisingOptions.getDefaultOptions())
+ verify(repository).addService(serviceId, serviceInfo, null /* ttl */)
verify(prober).startProbing(testProbingInfo)
// Simulate probing success: continues to announcing
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
index ad30ce0..9474464 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
@@ -101,11 +101,17 @@
private SocketCallback expectSocketCallback(MdnsServiceBrowserListener listener,
Network requestedNetwork) {
+ return expectSocketCallback(listener, requestedNetwork, mSocketCreationCallback,
+ 1 /* requestSocketCount */);
+ }
+
+ private SocketCallback expectSocketCallback(MdnsServiceBrowserListener listener,
+ Network requestedNetwork, SocketCreationCallback callback, int requestSocketCount) {
final ArgumentCaptor<SocketCallback> callbackCaptor =
ArgumentCaptor.forClass(SocketCallback.class);
mHandler.post(() -> mSocketClient.notifyNetworkRequested(
- listener, requestedNetwork, mSocketCreationCallback));
- verify(mProvider, timeout(DEFAULT_TIMEOUT))
+ listener, requestedNetwork, callback));
+ verify(mProvider, timeout(DEFAULT_TIMEOUT).times(requestSocketCount))
.requestSocket(eq(requestedNetwork), callbackCaptor.capture());
return callbackCaptor.getValue();
}
@@ -365,4 +371,40 @@
callback.onInterfaceDestroyed(otherSocketKey, otherSocket);
verify(mSocketCreationCallback).onSocketDestroyed(otherSocketKey);
}
+
+ @Test
+ public void testSocketDestroyed_MultipleCallbacks() {
+ final MdnsInterfaceSocket socket2 = mock(MdnsInterfaceSocket.class);
+ final SocketKey socketKey2 = new SocketKey(1001 /* interfaceIndex */);
+ final SocketCreationCallback creationCallback1 = mock(SocketCreationCallback.class);
+ final SocketCreationCallback creationCallback2 = mock(SocketCreationCallback.class);
+ final SocketCreationCallback creationCallback3 = mock(SocketCreationCallback.class);
+ final SocketCallback callback1 = expectSocketCallback(
+ mock(MdnsServiceBrowserListener.class), mNetwork, creationCallback1,
+ 1 /* requestSocketCount */);
+ final SocketCallback callback2 = expectSocketCallback(
+ mock(MdnsServiceBrowserListener.class), mNetwork, creationCallback2,
+ 2 /* requestSocketCount */);
+ final SocketCallback callback3 = expectSocketCallback(
+ mock(MdnsServiceBrowserListener.class), null /* requestedNetwork */,
+ creationCallback3, 1 /* requestSocketCount */);
+
+ doReturn(createEmptyNetworkInterface()).when(mSocket).getInterface();
+ callback1.onSocketCreated(mSocketKey, mSocket, List.of());
+ callback2.onSocketCreated(mSocketKey, mSocket, List.of());
+ callback3.onSocketCreated(mSocketKey, mSocket, List.of());
+ callback3.onSocketCreated(socketKey2, socket2, List.of());
+ verify(creationCallback1).onSocketCreated(mSocketKey);
+ verify(creationCallback2).onSocketCreated(mSocketKey);
+ verify(creationCallback3).onSocketCreated(mSocketKey);
+ verify(creationCallback3).onSocketCreated(socketKey2);
+
+ callback1.onInterfaceDestroyed(mSocketKey, mSocket);
+ callback2.onInterfaceDestroyed(mSocketKey, mSocket);
+ callback3.onInterfaceDestroyed(mSocketKey, mSocket);
+ verify(creationCallback1).onSocketDestroyed(mSocketKey);
+ verify(creationCallback2).onSocketDestroyed(mSocketKey);
+ verify(creationCallback3).onSocketDestroyed(mSocketKey);
+ verify(creationCallback3, never()).onSocketDestroyed(socketKey2);
+ }
}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
index 5b7c0ba..9befbc1 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
@@ -61,6 +61,7 @@
private val cb = mock(MdnsPacketRepeater.PacketRepeaterCallback::class.java)
as MdnsPacketRepeater.PacketRepeaterCallback<ProbingInfo>
private val buffer = ByteArray(1500)
+ private val flags = MdnsFeatureFlags.newBuilder().build()
@Before
fun setUp() {
@@ -120,7 +121,7 @@
@Test
fun testProbe() {
val replySender = MdnsReplySender(
- thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */)
+ thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */, flags)
val prober = TestProber(thread.looper, replySender, cb, sharedLog)
val probeInfo = TestProbeInfo(
listOf(makeServiceRecord(TEST_SERVICE_NAME_1, 37890)))
@@ -145,7 +146,7 @@
@Test
fun testProbeMultipleRecords() {
val replySender = MdnsReplySender(
- thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */)
+ thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */, flags)
val prober = TestProber(thread.looper, replySender, cb, sharedLog)
val probeInfo = TestProbeInfo(listOf(
makeServiceRecord(TEST_SERVICE_NAME_1, 37890),
@@ -184,7 +185,7 @@
@Test
fun testStopProbing() {
val replySender = MdnsReplySender(
- thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */)
+ thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */, flags)
val prober = TestProber(thread.looper, replySender, cb, sharedLog)
val probeInfo = TestProbeInfo(
listOf(makeServiceRecord(TEST_SERVICE_NAME_1, 37890)),
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
index c74e330..8d1dff6 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -22,11 +22,19 @@
import android.os.Build
import android.os.HandlerThread
import com.android.server.connectivity.mdns.MdnsAnnouncer.AnnouncementInfo
+import com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_HOST
+import com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_SERVICE
+import com.android.server.connectivity.mdns.MdnsRecord.TYPE_A
+import com.android.server.connectivity.mdns.MdnsRecord.TYPE_AAAA
+import com.android.server.connectivity.mdns.MdnsRecord.TYPE_PTR
+import com.android.server.connectivity.mdns.MdnsRecord.TYPE_SRV
+import com.android.server.connectivity.mdns.MdnsRecord.TYPE_TXT
import com.android.server.connectivity.mdns.MdnsRecordRepository.Dependencies
import com.android.server.connectivity.mdns.MdnsRecordRepository.getReverseDnsAddress
import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
+import com.google.common.truth.Truth.assertThat
import java.net.InetSocketAddress
import java.net.NetworkInterface
import java.util.Collections
@@ -35,7 +43,9 @@
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
+import kotlin.test.assertNull
import kotlin.test.assertTrue
+import kotlin.test.fail
import org.junit.After
import org.junit.Before
import org.junit.Test
@@ -44,8 +54,19 @@
private const val TEST_SERVICE_ID_1 = 42
private const val TEST_SERVICE_ID_2 = 43
private const val TEST_SERVICE_ID_3 = 44
+private const val TEST_CUSTOM_HOST_ID_1 = 45
+private const val TEST_CUSTOM_HOST_ID_2 = 46
+private const val TEST_SERVICE_CUSTOM_HOST_ID_1 = 48
private const val TEST_PORT = 12345
private const val TEST_SUBTYPE = "_subtype"
+private const val TEST_SUBTYPE2 = "_subtype2"
+// RFC6762 10. Resource Record TTL Values and Cache Coherency
+// The recommended TTL value for Multicast DNS resource records with a host name as the resource
+// record's name (e.g., A, AAAA, HINFO) or a host name contained within the resource record's rdata
+// (e.g., SRV, reverse mapping PTR record) SHOULD be 120 seconds. The recommended TTL value for
+// other Multicast DNS resource records is 75 minutes.
+private const val LONG_TTL = 4_500_000L
+private const val SHORT_TTL = 120_000L
private val TEST_HOSTNAME = arrayOf("Android_000102030405060708090A0B0C0D0E0F", "local")
private val TEST_ADDRESSES = listOf(
LinkAddress(parseNumericAddress("192.0.2.111"), 24),
@@ -70,6 +91,26 @@
port = TEST_PORT
}
+private val TEST_CUSTOM_HOST_1 = NsdServiceInfo().apply {
+ hostname = "TestHost"
+ hostAddresses = listOf(parseNumericAddress("2001:db8::1"), parseNumericAddress("2001:db8::2"))
+}
+
+private val TEST_CUSTOM_HOST_1_NAME = arrayOf("TestHost", "local")
+
+private val TEST_CUSTOM_HOST_2 = NsdServiceInfo().apply {
+ hostname = "OtherTestHost"
+ hostAddresses = listOf(parseNumericAddress("2001:db8::3"), parseNumericAddress("2001:db8::4"))
+}
+
+private val TEST_SERVICE_CUSTOM_HOST_1 = NsdServiceInfo().apply {
+ hostname = "TestHost"
+ hostAddresses = listOf(parseNumericAddress("2001:db8::1"))
+ serviceType = "_testservice._tcp"
+ serviceName = "TestService"
+ port = TEST_PORT
+}
+
@RunWith(DevSdkIgnoreRunner::class)
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
class MdnsRecordRepositoryTest {
@@ -78,7 +119,6 @@
override fun getInterfaceInetAddresses(iface: NetworkInterface) =
Collections.enumeration(TEST_ADDRESSES.map { it.address })
}
- private val flags = MdnsFeatureFlags.newBuilder().build()
@Before
fun setUp() {
@@ -91,12 +131,22 @@
thread.join()
}
+ private fun makeFlags(
+ includeInetAddressesInProbing: Boolean = false,
+ isKnownAnswerSuppressionEnabled: Boolean = false,
+ unicastReplyEnabled: Boolean = true
+ ) = MdnsFeatureFlags.Builder()
+ .setIncludeInetAddressRecordsInProbing(includeInetAddressesInProbing)
+ .setIsKnownAnswerSuppressionEnabled(isKnownAnswerSuppressionEnabled)
+ .setIsUnicastReplyEnabled(unicastReplyEnabled)
+ .build()
+
@Test
fun testAddServiceAndProbe() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
assertEquals(0, repository.servicesCount)
- assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
- null /* subtype */))
+ assertEquals(-1,
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */))
assertEquals(1, repository.servicesCount)
val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -119,7 +169,7 @@
assertEquals(MdnsServiceRecord(expectedName,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
- 120_000L /* ttlMillis */,
+ SHORT_TTL /* ttlMillis */,
0 /* servicePriority */, 0 /* serviceWeight */,
TEST_PORT, TEST_HOSTNAME), packet.authorityRecords[0])
@@ -128,31 +178,31 @@
@Test
fun testAddAndConflicts() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
assertFailsWith(NameConflictException::class) {
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* subtype */)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* ttl */)
}
assertFailsWith(NameConflictException::class) {
- repository.addService(TEST_SERVICE_ID_3, TEST_SERVICE_3, null /* subtype */)
+ repository.addService(TEST_SERVICE_ID_3, TEST_SERVICE_3, null /* ttl */)
}
}
@Test
fun testAddAndUpdates() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
assertFailsWith(IllegalArgumentException::class) {
- repository.updateService(TEST_SERVICE_ID_2, null /* subtype */)
+ repository.updateService(TEST_SERVICE_ID_2, emptySet() /* subtype */)
}
- repository.updateService(TEST_SERVICE_ID_1, TEST_SUBTYPE)
+ repository.updateService(TEST_SERVICE_ID_1, setOf(TEST_SUBTYPE))
val queriedName = arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local")
val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */))
- val query = MdnsPacket(0 /* flags */, questions, listOf() /* answers */,
- listOf() /* authorityRecords */, listOf() /* additionalRecords */)
+ val query = MdnsPacket(0 /* flags */, questions, emptyList() /* answers */,
+ emptyList() /* authorityRecords */, emptyList() /* additionalRecords */)
val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
val reply = repository.getReply(query, src)
@@ -174,19 +224,19 @@
@Test
fun testInvalidReuseOfServiceId() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
assertFailsWith(IllegalArgumentException::class) {
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2, null /* subtype */)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2, null /* ttl */)
}
}
@Test
fun testHasActiveService() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
assertFalse(repository.hasActiveService(TEST_SERVICE_ID_1))
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
assertTrue(repository.hasActiveService(TEST_SERVICE_ID_1))
val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -200,7 +250,7 @@
@Test
fun testExitAnnouncements() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
@@ -229,9 +279,10 @@
}
@Test
- fun testExitAnnouncements_WithSubtype() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
- repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, TEST_SUBTYPE)
+ fun testExitAnnouncements_WithSubtypes() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
+ setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
val exitAnnouncement = repository.exitService(TEST_SERVICE_ID_1)
@@ -245,7 +296,7 @@
assertEquals(0, packet.authorityRecords.size)
assertEquals(0, packet.additionalRecords.size)
- assertContentEquals(listOf(
+ assertThat(packet.answers).containsExactly(
MdnsPointerRecord(
arrayOf("_testservice", "_tcp", "local"),
0L /* receiptTimeMillis */,
@@ -258,7 +309,12 @@
false /* cacheFlush */,
0L /* ttlMillis */,
arrayOf("MyTestService", "_testservice", "_tcp", "local")),
- ), packet.answers)
+ MdnsPointerRecord(
+ arrayOf("_subtype2", "_sub", "_testservice", "_tcp", "local"),
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ 0L /* ttlMillis */,
+ arrayOf("MyTestService", "_testservice", "_tcp", "local")))
repository.removeService(TEST_SERVICE_ID_1)
assertEquals(0, repository.servicesCount)
@@ -266,13 +322,13 @@
@Test
fun testExitingServiceReAdded() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
repository.exitService(TEST_SERVICE_ID_1)
assertEquals(TEST_SERVICE_ID_1,
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* subtype */))
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* ttl */))
assertEquals(1, repository.servicesCount)
repository.removeService(TEST_SERVICE_ID_2)
@@ -281,9 +337,9 @@
@Test
fun testOnProbingSucceeded() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
val announcementInfo = repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
- TEST_SUBTYPE)
+ setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
val packet = announcementInfo.getPacket(0)
@@ -294,12 +350,13 @@
val serviceType = arrayOf("_testservice", "_tcp", "local")
val serviceSubtype = arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local")
+ val serviceSubtype2 = arrayOf(TEST_SUBTYPE2, "_sub", "_testservice", "_tcp", "local")
val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
val v4AddrRev = getReverseDnsAddress(TEST_ADDRESSES[0].address)
val v6Addr1Rev = getReverseDnsAddress(TEST_ADDRESSES[1].address)
val v6Addr2Rev = getReverseDnsAddress(TEST_ADDRESSES[2].address)
- assertContentEquals(listOf(
+ assertThat(packet.answers).containsExactly(
// Reverse address and address records for the hostname
MdnsPointerRecord(v4AddrRev,
0L /* receiptTimeMillis */,
@@ -346,6 +403,13 @@
false /* cacheFlush */,
4500000L /* ttlMillis */,
serviceName),
+ MdnsPointerRecord(
+ serviceSubtype2,
+ 0L /* receiptTimeMillis */,
+ // Not a unique name owned by the announcer, so cacheFlush=false
+ false /* cacheFlush */,
+ 4500000L /* ttlMillis */,
+ serviceName),
MdnsServiceRecord(
serviceName,
0L /* receiptTimeMillis */,
@@ -367,8 +431,7 @@
0L /* receiptTimeMillis */,
false /* cacheFlush */,
4500000L /* ttlMillis */,
- serviceType)
- ), packet.answers)
+ serviceType))
assertContentEquals(listOf(
MdnsNsecRecord(v4AddrRev,
@@ -376,37 +439,37 @@
true /* cacheFlush */,
120000L /* ttlMillis */,
v4AddrRev,
- intArrayOf(MdnsRecord.TYPE_PTR)),
+ intArrayOf(TYPE_PTR)),
MdnsNsecRecord(TEST_HOSTNAME,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
120000L /* ttlMillis */,
TEST_HOSTNAME,
- intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)),
+ intArrayOf(TYPE_A, TYPE_AAAA)),
MdnsNsecRecord(v6Addr1Rev,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
120000L /* ttlMillis */,
v6Addr1Rev,
- intArrayOf(MdnsRecord.TYPE_PTR)),
+ intArrayOf(TYPE_PTR)),
MdnsNsecRecord(v6Addr2Rev,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
120000L /* ttlMillis */,
v6Addr2Rev,
- intArrayOf(MdnsRecord.TYPE_PTR)),
+ intArrayOf(TYPE_PTR)),
MdnsNsecRecord(serviceName,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
4500000L /* ttlMillis */,
serviceName,
- intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV))
+ intArrayOf(TYPE_TXT, TYPE_SRV))
), packet.additionalRecords)
}
@Test
fun testGetOffloadPacket() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
val serviceType = arrayOf("_testservice", "_tcp", "local")
@@ -468,13 +531,13 @@
@Test
fun testGetReplyCaseInsensitive() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
val questionsCaseInSensitive = listOf(
MdnsPointerRecord(arrayOf("_TESTSERVICE", "_TCP", "local"), false /* isUnicast */))
val queryCaseInsensitive = MdnsPacket(0 /* flags */, questionsCaseInSensitive,
- listOf() /* answers */, listOf() /* authorityRecords */,
- listOf() /* additionalRecords */)
+ emptyList() /* answers */, emptyList() /* authorityRecords */,
+ emptyList() /* additionalRecords */)
val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
val replyCaseInsensitive = repository.getReply(queryCaseInsensitive, src)
assertNotNull(replyCaseInsensitive)
@@ -482,103 +545,454 @@
assertEquals(7, replyCaseInsensitive.additionalAnswers.size)
}
- @Test
- fun testGetReply() {
- doGetReplyTest(subtype = null)
+ /**
+ * Creates mDNS query packet with given query names and types.
+ */
+ private fun makeQuery(vararg queries: Pair<Int, Array<String>>): MdnsPacket {
+ val questions = queries.map { (type, name) -> makeQuestionRecord(name, type) }
+ return MdnsPacket(0 /* flags */, questions, emptyList() /* answers */,
+ emptyList() /* authorityRecords */, emptyList() /* additionalRecords */)
+ }
+
+ private fun makeQuestionRecord(name: Array<String>, type: Int): MdnsRecord {
+ when (type) {
+ TYPE_PTR -> return MdnsPointerRecord(name, false /* isUnicast */)
+ TYPE_SRV -> return MdnsServiceRecord(name, false /* isUnicast */)
+ TYPE_TXT -> return MdnsTextRecord(name, false /* isUnicast */)
+ TYPE_A, TYPE_AAAA -> return MdnsInetAddressRecord(name, type, false /* isUnicast */)
+ else -> fail("Unexpected question type: $type")
+ }
}
@Test
- fun testGetReply_WithSubtype() {
- doGetReplyTest(TEST_SUBTYPE)
- }
-
- private fun doGetReplyTest(subtype: String?) {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
- repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, subtype)
- val queriedName = if (subtype == null) arrayOf("_testservice", "_tcp", "local")
- else arrayOf(subtype, "_sub", "_testservice", "_tcp", "local")
-
- val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */))
- val query = MdnsPacket(0 /* flags */, questions, listOf() /* answers */,
- listOf() /* authorityRecords */, listOf() /* additionalRecords */)
+ fun testGetReply_singlePtrQuestion_returnsSrvTxtAddressNsecRecords() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
val reply = repository.getReply(query, src)
assertNotNull(reply)
- // Source address is IPv4
- assertEquals(MdnsConstants.getMdnsIPv4Address(), reply.destination.address)
- assertEquals(MdnsConstants.MDNS_PORT, reply.destination.port)
-
- // TTLs as per RFC6762 10.
- val longTtl = 4_500_000L
- val shortTtl = 120_000L
- val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
-
assertEquals(listOf(
MdnsPointerRecord(
- queriedName,
- 0L /* receiptTimeMillis */,
- false /* cacheFlush */,
- longTtl,
- serviceName),
- ), reply.answers)
-
+ arrayOf("_testservice", "_tcp", "local"), 0L, false, LONG_TTL, serviceName)),
+ reply.answers)
assertEquals(listOf(
- MdnsTextRecord(
- serviceName,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- longTtl,
- listOf() /* entries */),
- MdnsServiceRecord(
- serviceName,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- shortTtl,
- 0 /* servicePriority */,
- 0 /* serviceWeight */,
- TEST_PORT,
- TEST_HOSTNAME),
- MdnsInetAddressRecord(
- TEST_HOSTNAME,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- shortTtl,
- TEST_ADDRESSES[0].address),
- MdnsInetAddressRecord(
- TEST_HOSTNAME,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- shortTtl,
- TEST_ADDRESSES[1].address),
- MdnsInetAddressRecord(
- TEST_HOSTNAME,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- shortTtl,
- TEST_ADDRESSES[2].address),
- MdnsNsecRecord(
- serviceName,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- longTtl,
- serviceName /* nextDomain */,
- intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV)),
- MdnsNsecRecord(
- TEST_HOSTNAME,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- shortTtl,
- TEST_HOSTNAME /* nextDomain */,
- intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)),
+ MdnsTextRecord(serviceName, 0L, true, LONG_TTL, emptyList()),
+ MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[2].address),
+ MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+ intArrayOf(TYPE_TXT, TYPE_SRV)),
+ MdnsNsecRecord(TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(TYPE_A, TYPE_AAAA)),
+ ), reply.additionalAnswers)
+ }
+
+
+ @Test
+ fun testGetReply_ptrQuestionForServiceWithCustomHost_customHostUsedInAdditionalAnswers() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(TEST_SERVICE_CUSTOM_HOST_ID_1, TEST_SERVICE_CUSTOM_HOST_1,
+ setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
+ val src = InetSocketAddress(parseNumericAddress("fe80::1234"), 5353)
+ val serviceName = arrayOf("TestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(
+ MdnsPointerRecord(
+ arrayOf("_testservice", "_tcp", "local"),
+ 0L, false, LONG_TTL, serviceName)),
+ reply.answers)
+ assertEquals(listOf(
+ MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf()),
+ MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL,
+ 0, 0, TEST_PORT, TEST_CUSTOM_HOST_1_NAME),
+ MdnsInetAddressRecord(
+ TEST_CUSTOM_HOST_1_NAME, 0L, true, SHORT_TTL,
+ parseNumericAddress("2001:db8::1")),
+ MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+ intArrayOf(TYPE_TXT, TYPE_SRV)),
+ MdnsNsecRecord(TEST_CUSTOM_HOST_1_NAME, 0L, true, SHORT_TTL,
+ TEST_CUSTOM_HOST_1_NAME /* nextDomain */,
+ intArrayOf(TYPE_AAAA)),
), reply.additionalAnswers)
}
@Test
+ fun testGetReply_ptrQuestionForServicesWithSameCustomHost_customHostUsedInAdditionalAnswers() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ val serviceWithCustomHost1 = NsdServiceInfo().apply {
+ hostname = "TestHost"
+ hostAddresses = listOf(
+ parseNumericAddress("2001:db8::1"),
+ parseNumericAddress("192.0.2.1"))
+ serviceType = "_testservice._tcp"
+ serviceName = "TestService1"
+ port = TEST_PORT
+ }
+ val serviceWithCustomHost2 = NsdServiceInfo().apply {
+ hostname = "TestHost"
+ hostAddresses = listOf(
+ parseNumericAddress("2001:db8::1"),
+ parseNumericAddress("2001:db8::3"))
+ }
+ repository.addServiceAndFinishProbing(TEST_SERVICE_ID_1, serviceWithCustomHost1)
+ repository.addServiceAndFinishProbing(TEST_SERVICE_ID_2, serviceWithCustomHost2)
+ val src = InetSocketAddress(parseNumericAddress("fe80::1234"), 5353)
+ val serviceName = arrayOf("TestService1", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(
+ MdnsPointerRecord(
+ arrayOf("_testservice", "_tcp", "local"),
+ 0L, false, LONG_TTL, serviceName)),
+ reply.answers)
+ assertEquals(listOf(
+ MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf()),
+ MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL,
+ 0, 0, TEST_PORT, TEST_CUSTOM_HOST_1_NAME),
+ MdnsInetAddressRecord(
+ TEST_CUSTOM_HOST_1_NAME, 0L, true, SHORT_TTL,
+ parseNumericAddress("2001:db8::1")),
+ MdnsInetAddressRecord(
+ TEST_CUSTOM_HOST_1_NAME, 0L, true, SHORT_TTL,
+ parseNumericAddress("192.0.2.1")),
+ MdnsInetAddressRecord(
+ TEST_CUSTOM_HOST_1_NAME, 0L, true, SHORT_TTL,
+ parseNumericAddress("2001:db8::3")),
+ MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+ intArrayOf(TYPE_TXT, TYPE_SRV)),
+ MdnsNsecRecord(TEST_CUSTOM_HOST_1_NAME, 0L, true, SHORT_TTL,
+ TEST_CUSTOM_HOST_1_NAME /* nextDomain */,
+ intArrayOf(TYPE_A, TYPE_AAAA)),
+ ), reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_singleSubtypePtrQuestion_returnsSrvTxtAddressNsecRecords() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(
+ TYPE_PTR to arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local"))
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(
+ MdnsPointerRecord(
+ arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local"), 0L, false,
+ LONG_TTL, serviceName)),
+ reply.answers)
+ assertEquals(listOf(
+ MdnsTextRecord(serviceName, 0L, true, LONG_TTL, emptyList()),
+ MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[2].address),
+ MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+ intArrayOf(TYPE_TXT, TYPE_SRV)),
+ MdnsNsecRecord(TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(TYPE_A, TYPE_AAAA)),
+ ), reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_duplicatePtrQuestions_doesNotReturnDuplicateRecords() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(
+ TYPE_PTR to arrayOf("_testservice", "_tcp", "local"),
+ TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(
+ MdnsPointerRecord(
+ arrayOf("_testservice", "_tcp", "local"), 0L, false, LONG_TTL, serviceName)),
+ reply.answers)
+ assertEquals(listOf(
+ MdnsTextRecord(serviceName, 0L, true, LONG_TTL, emptyList()),
+ MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[2].address),
+ MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+ intArrayOf(TYPE_TXT, TYPE_SRV)),
+ MdnsNsecRecord(TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(TYPE_A, TYPE_AAAA)),
+ ), reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_multiplePtrQuestionsWithSubtype_doesNotReturnDuplicateRecords() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(
+ TYPE_PTR to arrayOf("_testservice", "_tcp", "local"),
+ TYPE_PTR to arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local"))
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(
+ MdnsPointerRecord(
+ arrayOf("_testservice", "_tcp", "local"), 0L, false, LONG_TTL, serviceName),
+ MdnsPointerRecord(
+ arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local"),
+ 0L, false, LONG_TTL, serviceName)),
+ reply.answers)
+ assertEquals(listOf(
+ MdnsTextRecord(serviceName, 0L, true, LONG_TTL, emptyList()),
+ MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[2].address),
+ MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+ intArrayOf(TYPE_TXT, TYPE_SRV)),
+ MdnsNsecRecord(TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(TYPE_A, TYPE_AAAA)),
+ ), reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_txtQuestion_returnsNoNsecRecord() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(TYPE_TXT to serviceName)
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(MdnsTextRecord(serviceName, 0L, true, LONG_TTL, emptyList())),
+ reply.answers)
+ // No NSEC records because the reply doesn't include the SRV record
+ assertTrue(reply.additionalAnswers.isEmpty())
+ }
+
+ @Test
+ fun testGetReply_AAAAQuestionButNoIpv6Address_returnsNsecRecord() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(
+ TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE),
+ listOf(LinkAddress(parseNumericAddress("192.0.2.111"), 24)))
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+
+ val query = makeQuery(TYPE_AAAA to TEST_HOSTNAME)
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertTrue(reply.answers.isEmpty())
+ assertEquals(listOf(
+ MdnsNsecRecord(TEST_HOSTNAME, 0L, true, LONG_TTL, TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(TYPE_AAAA))),
+ reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_AAAAQuestionForCustomHost_returnsAAAARecords() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(
+ TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, subtypes = setOf(),
+ listOf(LinkAddress(parseNumericAddress("192.0.2.111"), 24)))
+ repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */)
+ val src = InetSocketAddress(parseNumericAddress("fe80::123"), 5353)
+
+ val query = makeQuery(TYPE_AAAA to TEST_CUSTOM_HOST_1_NAME)
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(
+ MdnsInetAddressRecord(TEST_CUSTOM_HOST_1_NAME,
+ 0, false, LONG_TTL, parseNumericAddress("2001:db8::1")),
+ MdnsInetAddressRecord(TEST_CUSTOM_HOST_1_NAME,
+ 0, false, LONG_TTL, parseNumericAddress("2001:db8::2"))),
+ reply.answers)
+ assertEquals(
+ listOf(MdnsNsecRecord(TEST_CUSTOM_HOST_1_NAME,
+ 0L, true, SHORT_TTL,
+ TEST_CUSTOM_HOST_1_NAME /* nextDomain */,
+ intArrayOf(TYPE_AAAA))),
+ reply.additionalAnswers)
+ }
+
+
+ @Test
+ fun testGetReply_AAAAQuestionForCustomHostInMultipleRegistrations_returnsAAAARecords() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+
+ repository.addServiceAndFinishProbing(TEST_CUSTOM_HOST_ID_1, NsdServiceInfo().apply {
+ hostname = "TestHost"
+ hostAddresses = listOf(
+ parseNumericAddress("2001:db8::1"),
+ parseNumericAddress("2001:db8::2"))
+ })
+ repository.addServiceAndFinishProbing(TEST_CUSTOM_HOST_ID_2, NsdServiceInfo().apply {
+ hostname = "TestHost"
+ hostAddresses = listOf(
+ parseNumericAddress("2001:db8::1"),
+ parseNumericAddress("2001:db8::3"))
+ })
+ val src = InetSocketAddress(parseNumericAddress("fe80::123"), 5353)
+
+ val query = makeQuery(TYPE_AAAA to TEST_CUSTOM_HOST_1_NAME)
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(
+ MdnsInetAddressRecord(TEST_CUSTOM_HOST_1_NAME,
+ 0, false, LONG_TTL, parseNumericAddress("2001:db8::1")),
+ MdnsInetAddressRecord(TEST_CUSTOM_HOST_1_NAME,
+ 0, false, LONG_TTL, parseNumericAddress("2001:db8::2")),
+ MdnsInetAddressRecord(TEST_CUSTOM_HOST_1_NAME,
+ 0, false, LONG_TTL, parseNumericAddress("2001:db8::3"))),
+ reply.answers)
+ assertEquals(
+ listOf(MdnsNsecRecord(TEST_CUSTOM_HOST_1_NAME,
+ 0L, true, SHORT_TTL,
+ TEST_CUSTOM_HOST_1_NAME /* nextDomain */,
+ intArrayOf(TYPE_AAAA))),
+ reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_customHostRemoved_noAnswerToAAAAQuestion() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(
+ TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, subtypes = setOf(),
+ listOf(LinkAddress(parseNumericAddress("192.0.2.111"), 24)))
+ repository.addService(
+ TEST_SERVICE_CUSTOM_HOST_ID_1, TEST_SERVICE_CUSTOM_HOST_1, null /* ttl */)
+ repository.removeService(TEST_CUSTOM_HOST_ID_1)
+ repository.removeService(TEST_SERVICE_CUSTOM_HOST_ID_1)
+
+ val src = InetSocketAddress(parseNumericAddress("fe80::123"), 5353)
+
+ val query = makeQuery(TYPE_AAAA to TEST_CUSTOM_HOST_1_NAME)
+ val reply = repository.getReply(query, src)
+
+ assertNull(reply)
+ }
+
+ @Test
+ fun testGetReply_ptrAndSrvQuestions_doesNotReturnSrvRecordInAdditionalAnswerSection() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(
+ TYPE_PTR to arrayOf("_testservice", "_tcp", "local"),
+ TYPE_SRV to serviceName)
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(
+ MdnsPointerRecord(
+ arrayOf("_testservice", "_tcp", "local"), 0L, false, LONG_TTL, serviceName),
+ MdnsServiceRecord(
+ serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME)),
+ reply.answers)
+ assertFalse(reply.additionalAnswers.any { it -> it is MdnsServiceRecord })
+ }
+
+ @Test
+ fun testGetReply_srvTxtAddressQuestions_returnsAllRecordsInAnswerSectionExceptNsec() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(
+ TYPE_SRV to serviceName,
+ TYPE_TXT to serviceName,
+ TYPE_SRV to serviceName,
+ TYPE_A to TEST_HOSTNAME,
+ TYPE_AAAA to TEST_HOSTNAME)
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(
+ MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
+ MdnsTextRecord(serviceName, 0L, true, LONG_TTL, emptyList()),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[2].address)),
+ reply.answers)
+ assertEquals(listOf(
+ MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+ intArrayOf(TYPE_TXT, TYPE_SRV)),
+ MdnsNsecRecord(TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(TYPE_A, TYPE_AAAA))),
+ reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_queryWithIpv4Address_replyWithIpv4Address() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+
+ val srcIpv4 = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val replyIpv4 = repository.getReply(query, srcIpv4)
+
+ assertNotNull(replyIpv4)
+ assertEquals(MdnsConstants.getMdnsIPv4Address(), replyIpv4.destination.address)
+ assertEquals(MdnsConstants.MDNS_PORT, replyIpv4.destination.port)
+ }
+
+ @Test
+ fun testGetReply_queryWithIpv6Address_replyWithIpv6Address() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+
+ val srcIpv6 = InetSocketAddress(parseNumericAddress("2001:db8::123"), 5353)
+ val replyIpv6 = repository.getReply(query, srcIpv6)
+
+ assertNotNull(replyIpv6)
+ assertEquals(MdnsConstants.getMdnsIPv6Address(), replyIpv6.destination.address)
+ assertEquals(MdnsConstants.MDNS_PORT, replyIpv6.destination.port)
+ }
+
+ @Test
fun testGetConflictingServices() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* ttl */)
val packet = MdnsPacket(
0 /* flags */,
@@ -598,15 +1012,18 @@
emptyList() /* authorityRecords */,
emptyList() /* additionalRecords */)
- assertEquals(setOf(TEST_SERVICE_ID_1, TEST_SERVICE_ID_2),
+ assertEquals(
+ mapOf(
+ TEST_SERVICE_ID_1 to CONFLICT_SERVICE,
+ TEST_SERVICE_ID_2 to CONFLICT_SERVICE),
repository.getConflictingServices(packet))
}
@Test
fun testGetConflictingServicesCaseInsensitive() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* ttl */)
val packet = MdnsPacket(
0 /* flags */,
@@ -626,15 +1043,138 @@
emptyList() /* authorityRecords */,
emptyList() /* additionalRecords */)
- assertEquals(setOf(TEST_SERVICE_ID_1, TEST_SERVICE_ID_2),
- repository.getConflictingServices(packet))
+ assertEquals(
+ mapOf(TEST_SERVICE_ID_1 to CONFLICT_SERVICE,
+ TEST_SERVICE_ID_2 to CONFLICT_SERVICE),
+ repository.getConflictingServices(packet))
+ }
+
+ @Test
+ fun testGetConflictingServices_customHosts_differentAddresses() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */)
+ repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */)
+
+ val packet = MdnsPacket(
+ 0, /* flags */
+ emptyList(), /* questions */
+ listOf(
+ MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+ 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ 0L /* ttlMillis */, parseNumericAddress("2001:db8::5")),
+ MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+ 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ 0L /* ttlMillis */, parseNumericAddress("2001:db8::6")),
+ ) /* answers */,
+ emptyList() /* authorityRecords */,
+ emptyList() /* additionalRecords */)
+
+ assertEquals(mapOf(TEST_CUSTOM_HOST_ID_1 to CONFLICT_HOST),
+ repository.getConflictingServices(packet))
+ }
+
+ @Test
+ fun testGetConflictingServices_customHosts_moreAddressesThanUs_conflict() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */)
+ repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */)
+
+ val packet = MdnsPacket(
+ 0, /* flags */
+ emptyList(), /* questions */
+ listOf(
+ MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+ 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ 0L /* ttlMillis */, parseNumericAddress("2001:db8::1")),
+ MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+ 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ 0L /* ttlMillis */, parseNumericAddress("2001:db8::2")),
+ MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+ 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ 0L /* ttlMillis */, parseNumericAddress("2001:db8::3")),
+ ) /* answers */,
+ emptyList() /* authorityRecords */,
+ emptyList() /* additionalRecords */)
+
+ assertEquals(mapOf(TEST_CUSTOM_HOST_ID_1 to CONFLICT_HOST),
+ repository.getConflictingServices(packet))
+ }
+
+ @Test
+ fun testGetConflictingServices_customHostsReplyHasFewerAddressesThanUs_noConflict() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */)
+ repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */)
+
+ val packet = MdnsPacket(
+ 0, /* flags */
+ emptyList(), /* questions */
+ listOf(
+ MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+ 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ 0L /* ttlMillis */, parseNumericAddress("2001:db8::2")),
+ ) /* answers */,
+ emptyList() /* authorityRecords */,
+ emptyList() /* additionalRecords */)
+
+ assertEquals(emptyMap(),
+ repository.getConflictingServices(packet))
+ }
+
+ @Test
+ fun testGetConflictingServices_customHostsReplyHasIdenticalHosts_noConflict() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */)
+ repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */)
+
+ val packet = MdnsPacket(
+ 0, /* flags */
+ emptyList(), /* questions */
+ listOf(
+ MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+ 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ 0L /* ttlMillis */, parseNumericAddress("2001:db8::1")),
+ MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+ 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ 0L /* ttlMillis */, parseNumericAddress("2001:db8::2")),
+ ) /* answers */,
+ emptyList() /* authorityRecords */,
+ emptyList() /* additionalRecords */)
+
+ assertEquals(emptyMap(),
+ repository.getConflictingServices(packet))
+ }
+
+
+ @Test
+ fun testGetConflictingServices_customHostsCaseInsensitiveReplyHasIdenticalHosts_noConflict() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */)
+ repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */)
+
+ val packet = MdnsPacket(
+ 0, /* flags */
+ emptyList(), /* questions */
+ listOf(
+ MdnsInetAddressRecord(arrayOf("TESTHOST", "local"),
+ 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ 0L /* ttlMillis */, parseNumericAddress("2001:db8::1")),
+ MdnsInetAddressRecord(arrayOf("testhost", "local"),
+ 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ 0L /* ttlMillis */, parseNumericAddress("2001:db8::2")),
+ ) /* answers */,
+ emptyList() /* authorityRecords */,
+ emptyList() /* additionalRecords */)
+
+ assertEquals(emptyMap(),
+ repository.getConflictingServices(packet))
}
@Test
fun testGetConflictingServices_IdenticalService() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* ttl */)
val otherTtlMillis = 1234L
val packet = MdnsPacket(
@@ -656,14 +1196,14 @@
emptyList() /* additionalRecords */)
// Above records are identical to the actual registrations: no conflict
- assertEquals(emptySet(), repository.getConflictingServices(packet))
+ assertEquals(emptyMap(), repository.getConflictingServices(packet))
}
@Test
fun testGetConflictingServicesCaseInsensitive_IdenticalService() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* ttl */)
val otherTtlMillis = 1234L
val packet = MdnsPacket(
@@ -685,12 +1225,12 @@
emptyList() /* additionalRecords */)
// Above records are identical to the actual registrations: no conflict
- assertEquals(emptySet(), repository.getConflictingServices(packet))
+ assertEquals(emptyMap(), repository.getConflictingServices(packet))
}
@Test
fun testGetServiceRepliedRequestsCount() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
// Verify that there is no packet replied.
assertEquals(MdnsConstants.NO_PACKET,
@@ -698,8 +1238,8 @@
val questions = listOf(
MdnsPointerRecord(arrayOf("_testservice", "_tcp", "local"), false /* isUnicast */))
- val query = MdnsPacket(0 /* flags */, questions, listOf() /* answers */,
- listOf() /* authorityRecords */, listOf() /* additionalRecords */)
+ val query = MdnsPacket(0 /* flags */, questions, emptyList() /* answers */,
+ emptyList() /* authorityRecords */, emptyList() /* additionalRecords */)
val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
// Reply to the question and verify there is one packet replied.
@@ -715,11 +1255,11 @@
@Test
fun testIncludeInetAddressRecordsInProbing() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME,
- MdnsFeatureFlags.newBuilder().setIncludeInetAddressRecordsInProbing(true).build())
+ makeFlags(includeInetAddressesInProbing = true))
repository.updateAddresses(TEST_ADDRESSES)
assertEquals(0, repository.servicesCount)
- assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
- null /* subtype */))
+ assertEquals(-1,
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */))
assertEquals(1, repository.servicesCount)
val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -746,7 +1286,7 @@
expectedName,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
- 120_000L /* ttlMillis */,
+ SHORT_TTL /* ttlMillis */,
0 /* servicePriority */,
0 /* serviceWeight */,
TEST_PORT,
@@ -755,33 +1295,404 @@
TEST_HOSTNAME,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
- 120_000L /* ttlMillis */,
+ SHORT_TTL /* ttlMillis */,
TEST_ADDRESSES[0].address),
MdnsInetAddressRecord(
TEST_HOSTNAME,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
- 120_000L /* ttlMillis */,
+ SHORT_TTL /* ttlMillis */,
TEST_ADDRESSES[1].address),
MdnsInetAddressRecord(
TEST_HOSTNAME,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
- 120_000L /* ttlMillis */,
+ SHORT_TTL /* ttlMillis */,
TEST_ADDRESSES[2].address)
), packet.authorityRecords)
assertContentEquals(intArrayOf(TEST_SERVICE_ID_1), repository.clearServices())
}
+
+ private fun doGetReplyWithAnswersTest(
+ questions: List<MdnsRecord>,
+ knownAnswers: List<MdnsRecord>,
+ replyAnswers: List<MdnsRecord>,
+ additionalAnswers: List<MdnsRecord>
+ ) {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME,
+ makeFlags(isKnownAnswerSuppressionEnabled = true))
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ val query = MdnsPacket(0 /* flags */, questions, knownAnswers,
+ emptyList() /* authorityRecords */, emptyList() /* additionalRecords */)
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val reply = repository.getReply(query, src)
+
+ if (replyAnswers.isEmpty() || additionalAnswers.isEmpty()) {
+ assertNull(reply)
+ return
+ }
+
+ assertNotNull(reply)
+ // Source address is IPv4
+ assertEquals(MdnsConstants.getMdnsIPv4Address(), reply.destination.address)
+ assertEquals(MdnsConstants.MDNS_PORT, reply.destination.port)
+ assertEquals(replyAnswers, reply.answers)
+ assertEquals(additionalAnswers, reply.additionalAnswers)
+ assertEquals(knownAnswers, reply.knownAnswers)
+ }
+
+ @Test
+ fun testGetReply_HasAnswers() {
+ val queriedName = arrayOf("_testservice", "_tcp", "local")
+ val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */))
+ val knownAnswers = listOf(MdnsPointerRecord(
+ arrayOf("_testservice", "_tcp", "local"),
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ LONG_TTL,
+ arrayOf("MyTestService", "_testservice", "_tcp", "local")))
+ doGetReplyWithAnswersTest(questions, knownAnswers, emptyList() /* replyAnswers */,
+ emptyList() /* additionalAnswers */)
+ }
+
+ @Test
+ fun testGetReply_HasAnswers_TtlLessThanHalf() {
+ val queriedName = arrayOf("_testservice", "_tcp", "local")
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+ val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */))
+ val knownAnswers = listOf(MdnsPointerRecord(
+ arrayOf("_testservice", "_tcp", "local"),
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ (LONG_TTL / 2 - 1000L),
+ arrayOf("MyTestService", "_testservice", "_tcp", "local")))
+ val replyAnswers = listOf(MdnsPointerRecord(
+ queriedName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ LONG_TTL,
+ serviceName))
+ val additionalAnswers = listOf(
+ MdnsTextRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ LONG_TTL,
+ emptyList() /* entries */),
+ MdnsServiceRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ 0 /* servicePriority */,
+ 0 /* serviceWeight */,
+ TEST_PORT,
+ TEST_HOSTNAME),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[2].address),
+ MdnsNsecRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ LONG_TTL,
+ serviceName /* nextDomain */,
+ intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV)),
+ MdnsNsecRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)))
+ doGetReplyWithAnswersTest(questions, knownAnswers, replyAnswers, additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_HasAnotherAnswer() {
+ val queriedName = arrayOf("_testservice", "_tcp", "local")
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+ val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */))
+ val knownAnswers = listOf(MdnsPointerRecord(
+ queriedName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ LONG_TTL,
+ arrayOf("MyOtherTestService", "_testservice", "_tcp", "local")))
+ val replyAnswers = listOf(MdnsPointerRecord(
+ queriedName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ LONG_TTL,
+ serviceName))
+ val additionalAnswers = listOf(
+ MdnsTextRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ LONG_TTL,
+ emptyList() /* entries */),
+ MdnsServiceRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ 0 /* servicePriority */,
+ 0 /* serviceWeight */,
+ TEST_PORT,
+ TEST_HOSTNAME),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[2].address),
+ MdnsNsecRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ LONG_TTL,
+ serviceName /* nextDomain */,
+ intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV)),
+ MdnsNsecRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)))
+ doGetReplyWithAnswersTest(questions, knownAnswers, replyAnswers, additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_HasAnswers_MultiQuestions() {
+ val queriedName = arrayOf("_testservice", "_tcp", "local")
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+ val questions = listOf(
+ MdnsPointerRecord(queriedName, false /* isUnicast */),
+ MdnsServiceRecord(serviceName, false /* isUnicast */))
+ val knownAnswers = listOf(MdnsPointerRecord(
+ queriedName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ LONG_TTL - 1000L,
+ serviceName))
+ val replyAnswers = listOf(MdnsServiceRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ SHORT_TTL /* ttlMillis */,
+ 0 /* servicePriority */,
+ 0 /* serviceWeight */,
+ TEST_PORT,
+ TEST_HOSTNAME))
+ val additionalAnswers = listOf(
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[2].address),
+ MdnsNsecRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)))
+ doGetReplyWithAnswersTest(questions, knownAnswers, replyAnswers, additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_HasAnswers_MultiQuestions_NoReply() {
+ val queriedName = arrayOf("_testservice", "_tcp", "local")
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+ val questions = listOf(
+ MdnsPointerRecord(queriedName, false /* isUnicast */),
+ MdnsServiceRecord(serviceName, false /* isUnicast */))
+ val knownAnswers = listOf(
+ MdnsPointerRecord(
+ queriedName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ LONG_TTL - 1000L,
+ serviceName
+ ),
+ MdnsServiceRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ SHORT_TTL - 15_000L,
+ 0 /* servicePriority */,
+ 0 /* serviceWeight */,
+ TEST_PORT,
+ TEST_HOSTNAME
+ )
+ )
+ doGetReplyWithAnswersTest(questions, knownAnswers, emptyList() /* replyAnswers */,
+ emptyList() /* additionalAnswers */)
+ }
+
+ @Test
+ fun testReplyUnicastToQueryUnicastQuestions() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+
+ // Ask for 2 services, only the first one is known and requests unicast reply
+ val questions = listOf(
+ MdnsPointerRecord(arrayOf("_testservice", "_tcp", "local"), true /* isUnicast */),
+ MdnsPointerRecord(arrayOf("_otherservice", "_tcp", "local"), true /* isUnicast */))
+ val query = MdnsPacket(0 /* flags */, questions, emptyList() /* answers */,
+ emptyList() /* authorityRecords */, emptyList() /* additionalRecords */)
+ val src = InetSocketAddress(parseNumericAddress("2001:db8::123"), 5353)
+
+ // Reply to the question and verify it is sent to the source.
+ val reply = repository.getReply(query, src)
+ assertNotNull(reply)
+ assertEquals(src, reply.destination)
+ }
+
+ @Test
+ fun testReplyMulticastToQueryUnicastAndMulticastMixedQuestions() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ repository.addServiceAndFinishProbing(TEST_SERVICE_ID_2, NsdServiceInfo().apply {
+ serviceType = "_otherservice._tcp"
+ serviceName = "OtherTestService"
+ port = TEST_PORT
+ })
+
+ // Ask for 2 services, both are known and only the first one requests unicast reply
+ val questions = listOf(
+ MdnsPointerRecord(arrayOf("_testservice", "_tcp", "local"), true /* isUnicast */),
+ MdnsPointerRecord(arrayOf("_otherservice", "_tcp", "local"), false /* isUnicast */))
+ val query = MdnsPacket(0 /* flags */, questions, emptyList() /* answers */,
+ emptyList() /* authorityRecords */, emptyList() /* additionalRecords */)
+ val src = InetSocketAddress(parseNumericAddress("2001:db8::123"), 5353)
+
+ // Reply to the question and verify it is sent multicast.
+ val reply = repository.getReply(query, src)
+ assertNotNull(reply)
+ assertEquals(MdnsConstants.getMdnsIPv6Address(), reply.destination.address)
+ }
+
+ @Test
+ fun testReplyMulticastWhenNoUnicastQueryMatches() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+
+ // Ask for 2 services, the first one requests a unicast reply but is unknown
+ val questions = listOf(
+ MdnsPointerRecord(arrayOf("_otherservice", "_tcp", "local"), true /* isUnicast */),
+ MdnsPointerRecord(arrayOf("_testservice", "_tcp", "local"), false /* isUnicast */))
+ val query = MdnsPacket(0 /* flags */, questions, emptyList() /* answers */,
+ emptyList() /* authorityRecords */, emptyList() /* additionalRecords */)
+ val src = InetSocketAddress(parseNumericAddress("2001:db8::123"), 5353)
+
+ // Reply to the question and verify it is sent multicast.
+ val reply = repository.getReply(query, src)
+ assertNotNull(reply)
+ assertEquals(MdnsConstants.getMdnsIPv6Address(), reply.destination.address)
+ }
+
+ @Test
+ fun testReplyMulticastWhenUnicastFeatureDisabled() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME,
+ makeFlags(unicastReplyEnabled = false))
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+
+ // The service is known and requests unicast reply, but the feature is disabled
+ val questions = listOf(
+ MdnsPointerRecord(arrayOf("_testservice", "_tcp", "local"), true /* isUnicast */))
+ val query = MdnsPacket(0 /* flags */, questions, emptyList() /* answers */,
+ emptyList() /* authorityRecords */, emptyList() /* additionalRecords */)
+ val src = InetSocketAddress(parseNumericAddress("2001:db8::123"), 5353)
+
+ // Reply to the question and verify it is sent multicast.
+ val reply = repository.getReply(query, src)
+ assertNotNull(reply)
+ assertEquals(MdnsConstants.getMdnsIPv6Address(), reply.destination.address)
+ }
+
+ @Test
+ fun testGetReply_OnlyKnownAnswers() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME,
+ makeFlags(isKnownAnswerSuppressionEnabled = true))
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ val knownAnswers = listOf(MdnsPointerRecord(
+ arrayOf("_testservice", "_tcp", "local"),
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ LONG_TTL - 1000L,
+ arrayOf("MyTestService", "_testservice", "_tcp", "local")))
+ val query = MdnsPacket(MdnsConstants.FLAG_TRUNCATED /* flags */, emptyList(),
+ knownAnswers, emptyList() /* authorityRecords */,
+ emptyList() /* additionalRecords */)
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val reply = repository.getReply(query, src)
+ assertNotNull(reply)
+ assertEquals(0, reply.answers.size)
+ assertEquals(0, reply.additionalAnswers.size)
+ assertEquals(knownAnswers, reply.knownAnswers)
+ }
}
private fun MdnsRecordRepository.initWithService(
serviceId: Int,
serviceInfo: NsdServiceInfo,
- subtype: String? = null,
+ subtypes: Set<String> = setOf(),
+ addresses: List<LinkAddress> = TEST_ADDRESSES
): AnnouncementInfo {
- updateAddresses(TEST_ADDRESSES)
- addService(serviceId, serviceInfo, subtype)
+ updateAddresses(addresses)
+ serviceInfo.setSubtypes(subtypes)
+ return addServiceAndFinishProbing(serviceId, serviceInfo)
+}
+
+private fun MdnsRecordRepository.addServiceAndFinishProbing(
+ serviceId: Int,
+ serviceInfo: NsdServiceInfo
+): AnnouncementInfo {
+ addService(serviceId, serviceInfo, null /* ttl */)
val probingInfo = setServiceProbing(serviceId)
assertNotNull(probingInfo)
return onProbingSucceeded(probingInfo)
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsReplySenderTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsReplySenderTest.kt
new file mode 100644
index 0000000..9bd0530
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsReplySenderTest.kt
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns
+
+import android.net.InetAddresses
+import android.net.LinkAddress
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Message
+import com.android.net.module.util.SharedLog
+import com.android.server.connectivity.mdns.MdnsConstants.IPV4_SOCKET_ADDR
+import com.android.server.connectivity.mdns.MdnsConstants.IPV6_SOCKET_ADDR
+import com.android.server.connectivity.mdns.MdnsReplySender.getReplyDestination
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import java.net.DatagramPacket
+import java.net.InetSocketAddress
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import kotlin.test.assertEquals
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyLong
+import org.mockito.Mockito.argThat
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+private const val TEST_PORT = 12345
+private const val DEFAULT_TIMEOUT_MS = 2000L
+private const val LONG_TTL = 4_500_000L
+private const val SHORT_TTL = 120_000L
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.S_V2)
+class MdnsReplySenderTest {
+ private val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+ private val otherServiceName = arrayOf("OtherTestService", "_testservice", "_tcp", "local")
+ private val serviceType = arrayOf("_testservice", "_tcp", "local")
+ private val source = InetSocketAddress(
+ InetAddresses.parseNumericAddress("192.0.2.1"), TEST_PORT)
+ private val hostname = arrayOf("Android_000102030405060708090A0B0C0D0E0F", "local")
+ private val otherHostname = arrayOf("Android_0F0E0D0C0B0A09080706050403020100", "local")
+ private val hostAddresses = listOf(
+ LinkAddress(InetAddresses.parseNumericAddress("192.0.2.111"), 24),
+ LinkAddress(InetAddresses.parseNumericAddress("2001:db8::111"), 64),
+ LinkAddress(InetAddresses.parseNumericAddress("2001:db8::222"), 64))
+ private val answers = listOf(
+ MdnsPointerRecord(serviceType, 0L /* receiptTimeMillis */, false /* cacheFlush */,
+ LONG_TTL, serviceName))
+ private val otherAnswers = listOf(
+ MdnsPointerRecord(serviceType, 0L /* receiptTimeMillis */, false /* cacheFlush */,
+ LONG_TTL, otherServiceName))
+ private val additionalAnswers = listOf(
+ MdnsTextRecord(serviceName, 0L /* receiptTimeMillis */, true /* cacheFlush */, LONG_TTL,
+ emptyList() /* entries */),
+ MdnsServiceRecord(serviceName, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ SHORT_TTL, 0 /* servicePriority */, 0 /* serviceWeight */, TEST_PORT, hostname),
+ MdnsInetAddressRecord(hostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ SHORT_TTL, hostAddresses[0].address),
+ MdnsInetAddressRecord(hostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ SHORT_TTL, hostAddresses[1].address),
+ MdnsInetAddressRecord(hostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ SHORT_TTL, hostAddresses[2].address),
+ MdnsNsecRecord(serviceName, 0L /* receiptTimeMillis */, true /* cacheFlush */, LONG_TTL,
+ serviceName /* nextDomain */,
+ intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV)),
+ MdnsNsecRecord(hostname, 0L /* receiptTimeMillis */, true /* cacheFlush */, SHORT_TTL,
+ hostname /* nextDomain */, intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)))
+ private val otherAdditionalAnswers = listOf(
+ MdnsTextRecord(otherServiceName, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ LONG_TTL, emptyList() /* entries */),
+ MdnsServiceRecord(otherServiceName, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ SHORT_TTL, 0 /* servicePriority */, 0 /* serviceWeight */, TEST_PORT,
+ otherHostname),
+ MdnsInetAddressRecord(otherHostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ SHORT_TTL, hostAddresses[0].address),
+ MdnsInetAddressRecord(otherHostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ SHORT_TTL, hostAddresses[1].address),
+ MdnsInetAddressRecord(otherHostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ SHORT_TTL, hostAddresses[2].address),
+ MdnsNsecRecord(otherServiceName, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ LONG_TTL, otherServiceName /* nextDomain */,
+ intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV)),
+ MdnsNsecRecord(otherHostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ SHORT_TTL, otherHostname /* nextDomain */,
+ intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)))
+ private val thread = HandlerThread(MdnsReplySenderTest::class.simpleName)
+ private val socket = mock(MdnsInterfaceSocket::class.java)
+ private val buffer = ByteArray(1500)
+ private val sharedLog = SharedLog(MdnsReplySenderTest::class.simpleName)
+ private val deps = mock(MdnsReplySender.Dependencies::class.java)
+ private val handler by lazy { Handler(thread.looper) }
+
+ @Before
+ fun setUp() {
+ thread.start()
+ doReturn(true).`when`(socket).hasJoinedIpv4()
+ doReturn(true).`when`(socket).hasJoinedIpv6()
+ }
+
+ @After
+ fun tearDown() {
+ thread.quitSafely()
+ thread.join()
+ }
+
+ private fun <T> runningOnHandlerAndReturn(functor: (() -> T)): T {
+ val future = CompletableFuture<T>()
+ handler.post {
+ future.complete(functor())
+ }
+ return future.get(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ }
+
+ private fun sendNow(sender: MdnsReplySender, packet: MdnsPacket, dest: InetSocketAddress):
+ Unit = runningOnHandlerAndReturn { sender.sendNow(packet, dest) }
+
+ private fun queueReply(sender: MdnsReplySender, reply: MdnsReplyInfo):
+ Unit = runningOnHandlerAndReturn { sender.queueReply(reply) }
+
+ private fun buildFlags(enableKAS: Boolean): MdnsFeatureFlags {
+ return MdnsFeatureFlags.newBuilder()
+ .setIsKnownAnswerSuppressionEnabled(enableKAS).build()
+ }
+
+ private fun createSender(enableKAS: Boolean): MdnsReplySender =
+ MdnsReplySender(thread.looper, socket, buffer, sharedLog, false /* enableDebugLog */,
+ deps, buildFlags(enableKAS))
+
+ @Test
+ fun testSendNow() {
+ val replySender = createSender(enableKAS = false)
+ val packet = MdnsPacket(0x8400,
+ emptyList() /* questions */,
+ answers,
+ emptyList() /* authorityRecords */,
+ additionalAnswers)
+ sendNow(replySender, packet, IPV4_SOCKET_ADDR)
+ verify(socket).send(argThat{ it.socketAddress.equals(IPV4_SOCKET_ADDR) })
+ }
+
+ private fun verifyMessageQueued(
+ sender: MdnsReplySender,
+ replies: List<MdnsReplyInfo>
+ ): Pair<Handler, Message> {
+ val handlerCaptor = ArgumentCaptor.forClass(Handler::class.java)
+ val messageCaptor = ArgumentCaptor.forClass(Message::class.java)
+ for (reply in replies) {
+ queueReply(sender, reply)
+ verify(deps).sendMessageDelayed(
+ handlerCaptor.capture(), messageCaptor.capture(), eq(reply.sendDelayMs))
+ }
+ return Pair(handlerCaptor.value, messageCaptor.value)
+ }
+
+ private fun verifyReplySent(
+ realHandler: Handler,
+ delayMessage: Message,
+ remainingAnswers: List<MdnsRecord>
+ ) {
+ val datagramPacketCaptor = ArgumentCaptor.forClass(DatagramPacket::class.java)
+ realHandler.sendMessage(delayMessage)
+ verify(socket, timeout(DEFAULT_TIMEOUT_MS)).send(datagramPacketCaptor.capture())
+
+ val dPacket = datagramPacketCaptor.value
+ val mdnsPacket = MdnsPacket.parse(MdnsPacketReader(
+ dPacket.data, dPacket.length, buildFlags(enableKAS = false)))
+ assertEquals(mdnsPacket.answers.toSet(), remainingAnswers.toSet())
+ }
+
+ @Test
+ fun testQueueReply() {
+ val replySender = createSender(enableKAS = false)
+ val reply = MdnsReplyInfo(answers, additionalAnswers, 20L /* sendDelayMs */,
+ IPV4_SOCKET_ADDR, source, emptyList())
+ val (handler, message) = verifyMessageQueued(replySender, listOf(reply))
+ verifyReplySent(handler, message, answers)
+ }
+
+ @Test
+ fun testQueueReply_KnownAnswerSuppressionEnabled() {
+ val replySender = createSender(enableKAS = true)
+ val reply = MdnsReplyInfo(answers, additionalAnswers, 20L /* sendDelayMs */,
+ IPV4_SOCKET_ADDR, source, emptyList())
+ val (handler, message) = verifyMessageQueued(replySender, listOf(reply))
+ verifyReplySent(handler, message, answers)
+ }
+
+ @Test
+ fun testQueueReply_MultiplePacket() {
+ val replySender = createSender(enableKAS = true)
+ val reply = MdnsReplyInfo(answers, additionalAnswers, 400L /* sendDelayMs */,
+ IPV4_SOCKET_ADDR, source, emptyList())
+ verifyMessageQueued(replySender, listOf(reply))
+
+ // Receive a known-answer packet and verify no message queued.
+ val knownAnswersReply = MdnsReplyInfo(emptyList(), emptyList(), 0L /* sendDelayMs */,
+ IPV4_SOCKET_ADDR, source, answers)
+ queueReply(replySender, knownAnswersReply)
+ verify(deps, times(1)).sendMessageDelayed(any(), any(), anyLong())
+ }
+
+ @Test
+ fun testQueueReply_MultiplePacket_LostSubsequentPacket() {
+ val replySender = createSender(enableKAS = true)
+ val reply = MdnsReplyInfo(answers, additionalAnswers, 400L /* sendDelayMs */,
+ IPV4_SOCKET_ADDR, source, emptyList())
+ val (handler, message) = verifyMessageQueued(replySender, listOf(reply))
+
+ // No subsequent packets
+ verifyReplySent(handler, message, answers)
+ }
+
+ @Test
+ fun testQueueReply_MultiplePacket_OtherKnownAnswer() {
+ val replySender = createSender(enableKAS = true)
+ val reply = MdnsReplyInfo(answers, additionalAnswers, 400L /* sendDelayMs */,
+ IPV4_SOCKET_ADDR, source, emptyList())
+ // Other known-answer service
+ val otherKnownAnswersReply = MdnsReplyInfo(emptyList(), emptyList(), 0L /* sendDelayMs */,
+ IPV4_SOCKET_ADDR, source, otherAnswers)
+ val (handler, message) = verifyMessageQueued(
+ replySender, listOf(reply, otherKnownAnswersReply))
+ verifyReplySent(handler, message, answers)
+ }
+
+ @Test
+ fun testQueueReply_MultiplePacket_TwoKnownAnswerPackets() {
+ val replySender = createSender(enableKAS = true)
+ val reply = MdnsReplyInfo(answers, additionalAnswers, 400L /* sendDelayMs */,
+ IPV4_SOCKET_ADDR, source, emptyList())
+ val firstKnownAnswerReply = MdnsReplyInfo(emptyList(), emptyList(), 401L /* sendDelayMs */,
+ IPV4_SOCKET_ADDR, source, otherAnswers)
+ verifyMessageQueued(replySender, listOf(reply, firstKnownAnswerReply))
+
+ // Second known-answer service
+ val secondKnownAnswerReply = MdnsReplyInfo(emptyList(), emptyList(), 0L /* sendDelayMs */,
+ IPV4_SOCKET_ADDR, source, answers)
+ queueReply(replySender, secondKnownAnswerReply)
+
+ // Verify that no reply is queued, as all answers are known.
+ verify(deps, times(2)).sendMessageDelayed(any(), any(), anyLong())
+ }
+
+ @Test
+ fun testQueueReply_MultiplePacket_LostSecondaryPacket() {
+ val replySender = createSender(enableKAS = true)
+ val reply = MdnsReplyInfo(answers, additionalAnswers, 400L /* sendDelayMs */,
+ IPV4_SOCKET_ADDR, source, emptyList())
+ val firstKnownAnswerReply = MdnsReplyInfo(emptyList(), emptyList(), 401L /* sendDelayMs */,
+ IPV4_SOCKET_ADDR, source, otherAnswers)
+ val (handler, message) = verifyMessageQueued(
+ replySender, listOf(reply, firstKnownAnswerReply))
+
+ // Second known-answer service lost
+ verifyReplySent(handler, message, answers)
+ }
+
+ @Test
+ fun testQueueReply_MultiplePacket_WithMultipleQuestions() {
+ val replySender = createSender(enableKAS = true)
+ val twoAnswers = listOf(
+ MdnsPointerRecord(serviceType, 0L /* receiptTimeMillis */, false /* cacheFlush */,
+ LONG_TTL, serviceName),
+ MdnsServiceRecord(otherServiceName, 0L /* receiptTimeMillis */,
+ true /* cacheFlush */, SHORT_TTL, 0 /* servicePriority */,
+ 0 /* serviceWeight */, TEST_PORT, otherHostname))
+ val reply = MdnsReplyInfo(twoAnswers, additionalAnswers, 400L /* sendDelayMs */,
+ IPV4_SOCKET_ADDR, source, emptyList())
+ val knownAnswersReply = MdnsReplyInfo(otherAnswers, otherAdditionalAnswers,
+ 20L /* sendDelayMs */, IPV4_SOCKET_ADDR, source, answers)
+ val (handler, message) = verifyMessageQueued(replySender, listOf(reply, knownAnswersReply))
+
+ val remainingAnswers = listOf(
+ MdnsPointerRecord(serviceType, 0L /* receiptTimeMillis */, false /* cacheFlush */,
+ LONG_TTL, otherServiceName),
+ MdnsServiceRecord(otherServiceName, 0L /* receiptTimeMillis */,
+ true /* cacheFlush */, SHORT_TTL, 0 /* servicePriority */,
+ 0 /* serviceWeight */, TEST_PORT, otherHostname))
+ verifyReplySent(handler, message, remainingAnswers)
+ }
+
+ @Test
+ fun testGetReplyDestination() {
+ assertEquals(IPV4_SOCKET_ADDR, getReplyDestination(IPV4_SOCKET_ADDR, IPV4_SOCKET_ADDR))
+ assertEquals(IPV6_SOCKET_ADDR, getReplyDestination(IPV6_SOCKET_ADDR, IPV6_SOCKET_ADDR))
+ assertEquals(IPV4_SOCKET_ADDR, getReplyDestination(source, IPV4_SOCKET_ADDR))
+ assertEquals(IPV6_SOCKET_ADDR, getReplyDestination(source, IPV6_SOCKET_ADDR))
+ assertEquals(source, getReplyDestination(source, source))
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
index e7d7a98..8740e80 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
@@ -35,6 +35,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.time.Instant;
import java.util.List;
import java.util.Map;
@@ -202,7 +203,8 @@
List.of(),
/* textEntries= */ null,
/* interfaceIndex= */ 20,
- network);
+ network,
+ Instant.MAX /* expirationTime */);
assertEquals(network, info2.getNetwork());
}
@@ -225,7 +227,8 @@
MdnsServiceInfo.TextEntry.fromString("mn=Google Nest Hub Max"),
MdnsServiceInfo.TextEntry.fromString("test=")),
20 /* interfaceIndex */,
- new Network(123));
+ new Network(123),
+ Instant.MAX /* expirationTime */);
beforeParcel.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
index 7a2e4bf..09236b1 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -16,7 +16,13 @@
package com.android.server.connectivity.mdns;
+import static com.android.server.connectivity.mdns.MdnsSearchOptions.ACTIVE_QUERY_MODE;
+import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE;
+import static com.android.server.connectivity.mdns.MdnsSearchOptions.PASSIVE_QUERY_MODE;
import static com.android.server.connectivity.mdns.MdnsServiceTypeClient.EVENT_START_QUERYTASK;
+import static com.android.server.connectivity.mdns.QueryTaskConfig.INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS;
+import static com.android.server.connectivity.mdns.QueryTaskConfig.MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS;
+import static com.android.server.connectivity.mdns.QueryTaskConfig.TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.junit.Assert.assertArrayEquals;
@@ -37,6 +43,7 @@
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -145,8 +152,8 @@
MockitoAnnotations.initMocks(this);
doReturn(TEST_ELAPSED_REALTIME).when(mockDecoderClock).elapsedRealtime();
- expectedIPv4Packets = new DatagramPacket[16];
- expectedIPv6Packets = new DatagramPacket[16];
+ expectedIPv4Packets = new DatagramPacket[24];
+ expectedIPv6Packets = new DatagramPacket[24];
socketKey = new SocketKey(mockNetwork, INTERFACE_INDEX);
for (int i = 0; i < expectedIPv4Packets.length; ++i) {
@@ -171,7 +178,15 @@
.thenReturn(expectedIPv4Packets[12])
.thenReturn(expectedIPv4Packets[13])
.thenReturn(expectedIPv4Packets[14])
- .thenReturn(expectedIPv4Packets[15]);
+ .thenReturn(expectedIPv4Packets[15])
+ .thenReturn(expectedIPv4Packets[16])
+ .thenReturn(expectedIPv4Packets[17])
+ .thenReturn(expectedIPv4Packets[18])
+ .thenReturn(expectedIPv4Packets[19])
+ .thenReturn(expectedIPv4Packets[20])
+ .thenReturn(expectedIPv4Packets[21])
+ .thenReturn(expectedIPv4Packets[22])
+ .thenReturn(expectedIPv4Packets[23]);
when(mockPacketWriter.getPacket(IPV6_ADDRESS))
.thenReturn(expectedIPv6Packets[0])
@@ -189,7 +204,15 @@
.thenReturn(expectedIPv6Packets[12])
.thenReturn(expectedIPv6Packets[13])
.thenReturn(expectedIPv6Packets[14])
- .thenReturn(expectedIPv6Packets[15]);
+ .thenReturn(expectedIPv6Packets[15])
+ .thenReturn(expectedIPv6Packets[16])
+ .thenReturn(expectedIPv6Packets[17])
+ .thenReturn(expectedIPv6Packets[18])
+ .thenReturn(expectedIPv6Packets[19])
+ .thenReturn(expectedIPv6Packets[20])
+ .thenReturn(expectedIPv6Packets[21])
+ .thenReturn(expectedIPv6Packets[22])
+ .thenReturn(expectedIPv6Packets[23]);
thread = new HandlerThread("MdnsServiceTypeClientTests");
thread.start();
@@ -219,15 +242,22 @@
return true;
}).when(mockDeps).sendMessage(any(Handler.class), any(Message.class));
- client =
- new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
- mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
- serviceCache) {
- @Override
- MdnsPacketWriter createMdnsPacketWriter() {
- return mockPacketWriter;
- }
- };
+ client = makeMdnsServiceTypeClient(mockPacketWriter);
+ }
+
+ private MdnsServiceTypeClient makeMdnsServiceTypeClient(
+ @Nullable MdnsPacketWriter packetWriter) {
+ return new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
+ mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
+ serviceCache) {
+ @Override
+ MdnsPacketWriter createMdnsPacketWriter() {
+ if (packetWriter == null) {
+ return super.createMdnsPacketWriter();
+ }
+ return packetWriter;
+ }
+ };
}
@After
@@ -267,8 +297,8 @@
@Test
public void sendQueries_activeScanMode() {
- MdnsSearchOptions searchOptions =
- MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE).setIsPassiveMode(false).build();
+ MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+ .addSubtype(SUBTYPE).setQueryMode(ACTIVE_QUERY_MODE).build();
startSendAndReceive(mockListenerOne, searchOptions);
// Always try to remove the task.
verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
@@ -319,8 +349,8 @@
@Test
public void sendQueries_reentry_activeScanMode() {
- MdnsSearchOptions searchOptions =
- MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE).setIsPassiveMode(false).build();
+ MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+ .addSubtype(SUBTYPE).setQueryMode(ACTIVE_QUERY_MODE).build();
startSendAndReceive(mockListenerOne, searchOptions);
// Always try to remove the task.
verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
@@ -333,7 +363,7 @@
MdnsSearchOptions.newBuilder()
.addSubtype(SUBTYPE)
.addSubtype("_subtype2")
- .setIsPassiveMode(false)
+ .setQueryMode(ACTIVE_QUERY_MODE)
.build();
startSendAndReceive(mockListenerOne, searchOptions);
// The previous scheduled task should be canceled.
@@ -353,8 +383,8 @@
@Test
public void sendQueries_passiveScanMode() {
- MdnsSearchOptions searchOptions =
- MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE).setIsPassiveMode(true).build();
+ MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+ .addSubtype(SUBTYPE).setQueryMode(PASSIVE_QUERY_MODE).build();
startSendAndReceive(mockListenerOne, searchOptions);
// Always try to remove the task.
verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
@@ -380,8 +410,10 @@
@Test
public void sendQueries_activeScanWithQueryBackoff() {
MdnsSearchOptions searchOptions =
- MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE).setIsPassiveMode(
- false).setNumOfQueriesBeforeBackoff(11).build();
+ MdnsSearchOptions.newBuilder()
+ .addSubtype(SUBTYPE)
+ .setQueryMode(ACTIVE_QUERY_MODE)
+ .setNumOfQueriesBeforeBackoff(11).build();
startSendAndReceive(mockListenerOne, searchOptions);
// Always try to remove the task.
verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
@@ -439,8 +471,10 @@
@Test
public void sendQueries_passiveScanWithQueryBackoff() {
MdnsSearchOptions searchOptions =
- MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE).setIsPassiveMode(
- true).setNumOfQueriesBeforeBackoff(3).build();
+ MdnsSearchOptions.newBuilder()
+ .addSubtype(SUBTYPE)
+ .setQueryMode(PASSIVE_QUERY_MODE)
+ .setNumOfQueriesBeforeBackoff(3).build();
startSendAndReceive(mockListenerOne, searchOptions);
// Always try to remove the task.
verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
@@ -497,8 +531,8 @@
@Test
public void sendQueries_reentry_passiveScanMode() {
- MdnsSearchOptions searchOptions =
- MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE).setIsPassiveMode(true).build();
+ MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+ .addSubtype(SUBTYPE).setQueryMode(PASSIVE_QUERY_MODE).build();
startSendAndReceive(mockListenerOne, searchOptions);
// Always try to remove the task.
verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
@@ -511,7 +545,7 @@
MdnsSearchOptions.newBuilder()
.addSubtype(SUBTYPE)
.addSubtype("_subtype2")
- .setIsPassiveMode(true)
+ .setQueryMode(PASSIVE_QUERY_MODE)
.build();
startSendAndReceive(mockListenerOne, searchOptions);
// The previous scheduled task should be canceled.
@@ -533,16 +567,15 @@
@Ignore("MdnsConfigs is not configurable currently.")
public void testQueryTaskConfig_alwaysAskForUnicastResponse() {
//MdnsConfigsFlagsImpl.alwaysAskForUnicastResponseInEachBurst.override(true);
- MdnsSearchOptions searchOptions =
- MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE).setIsPassiveMode(false).build();
+ MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+ .addSubtype(SUBTYPE).setQueryMode(ACTIVE_QUERY_MODE).build();
QueryTaskConfig config = new QueryTaskConfig(
- searchOptions.getSubtypes(), searchOptions.isPassiveMode(),
+ searchOptions.getQueryMode(),
false /* onlyUseIpv6OnIpv6OnlyNetworks */, 3 /* numOfQueriesBeforeBackoff */,
socketKey);
// 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.
@@ -550,7 +583,6 @@
int oldTransactionId = config.transactionId;
config = config.getConfigForNextRun();
assertFalse(config.expectUnicastResponse);
- assertEquals(config.subtypes, searchOptions.getSubtypes());
assertEquals(config.transactionId, oldTransactionId + 1);
}
@@ -558,22 +590,20 @@
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(SUBTYPE).setIsPassiveMode(false).build();
+ MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+ .addSubtype(SUBTYPE).setQueryMode(ACTIVE_QUERY_MODE).build();
QueryTaskConfig config = new QueryTaskConfig(
- searchOptions.getSubtypes(), searchOptions.isPassiveMode(),
+ searchOptions.getQueryMode(),
false /* onlyUseIpv6OnIpv6OnlyNetworks */, 3 /* numOfQueriesBeforeBackoff */,
socketKey);
// 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.
@@ -581,7 +611,6 @@
int oldTransactionId = config.transactionId;
config = config.getConfigForNextRun();
assertFalse(config.expectUnicastResponse);
- assertEquals(config.subtypes, searchOptions.getSubtypes());
assertEquals(config.transactionId, oldTransactionId + 1);
}
@@ -589,14 +618,13 @@
int oldTransactionId = config.transactionId;
config = config.getConfigForNextRun();
assertFalse(config.expectUnicastResponse);
- assertEquals(config.subtypes, searchOptions.getSubtypes());
assertEquals(config.transactionId, oldTransactionId + 1);
}
@Test
public void testIfPreviousTaskIsCanceledWhenNewSessionStarts() {
- MdnsSearchOptions searchOptions =
- MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE).setIsPassiveMode(true).build();
+ MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+ .addSubtype(SUBTYPE).setQueryMode(PASSIVE_QUERY_MODE).build();
startSendAndReceive(mockListenerOne, searchOptions);
Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
@@ -605,7 +633,7 @@
MdnsSearchOptions.newBuilder()
.addSubtype(SUBTYPE)
.addSubtype("_subtype2")
- .setIsPassiveMode(true)
+ .setQueryMode(PASSIVE_QUERY_MODE)
.build();
startSendAndReceive(mockListenerOne, searchOptions);
@@ -624,8 +652,8 @@
@Ignore("MdnsConfigs is not configurable currently.")
public void testIfPreviousTaskIsCanceledWhenSessionStops() {
//MdnsConfigsFlagsImpl.shouldCancelScanTaskWhenFutureIsNull.override(true);
- MdnsSearchOptions searchOptions =
- MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE).setIsPassiveMode(true).build();
+ MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+ .addSubtype(SUBTYPE).setQueryMode(PASSIVE_QUERY_MODE).build();
startSendAndReceive(mockListenerOne, searchOptions);
// Change the sutypes and start a new session.
stopSendAndReceive(mockListenerOne);
@@ -667,6 +695,81 @@
any(), any(), eq(MdnsConfigs.timeBetweenQueriesInBurstMs()));
}
+ @Test
+ public void testCombinedSubtypesQueriedWithMultipleListeners() throws Exception {
+ client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
+ final MdnsSearchOptions searchOptions1 = MdnsSearchOptions.newBuilder()
+ .addSubtype("subtype1").build();
+ final MdnsSearchOptions searchOptions2 = MdnsSearchOptions.newBuilder()
+ .addSubtype("subtype2").build();
+ startSendAndReceive(mockListenerOne, searchOptions1);
+ currentThreadExecutor.getAndClearSubmittedRunnable().run();
+
+ InOrder inOrder = inOrder(mockListenerOne, mockSocketClient, mockDeps);
+
+ // Verify the query asks for subtype1
+ final ArgumentCaptor<DatagramPacket> subtype1QueryCaptor =
+ ArgumentCaptor.forClass(DatagramPacket.class);
+ currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+ // Send twice for IPv4 and IPv6
+ inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingUnicastResponse(
+ subtype1QueryCaptor.capture(),
+ eq(socketKey), eq(false));
+
+ final MdnsPacket subtype1Query = MdnsPacket.parse(
+ new MdnsPacketReader(subtype1QueryCaptor.getValue()));
+
+ assertEquals(2, subtype1Query.questions.size());
+ assertTrue(hasQuestion(subtype1Query, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
+ assertTrue(hasQuestion(subtype1Query, MdnsRecord.TYPE_PTR,
+ getServiceTypeWithSubtype("_subtype1")));
+
+ // Add subtype2
+ startSendAndReceive(mockListenerTwo, searchOptions2);
+ inOrder.verify(mockDeps).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+ currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+
+ final ArgumentCaptor<DatagramPacket> combinedSubtypesQueryCaptor =
+ ArgumentCaptor.forClass(DatagramPacket.class);
+ inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingUnicastResponse(
+ combinedSubtypesQueryCaptor.capture(),
+ eq(socketKey), eq(false));
+ // The next query must have been scheduled
+ inOrder.verify(mockDeps).sendMessageDelayed(any(), any(), anyLong());
+
+ final MdnsPacket combinedSubtypesQuery = MdnsPacket.parse(
+ new MdnsPacketReader(combinedSubtypesQueryCaptor.getValue()));
+
+ assertEquals(3, combinedSubtypesQuery.questions.size());
+ assertTrue(hasQuestion(combinedSubtypesQuery, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
+ assertTrue(hasQuestion(combinedSubtypesQuery, MdnsRecord.TYPE_PTR,
+ getServiceTypeWithSubtype("_subtype1")));
+ assertTrue(hasQuestion(combinedSubtypesQuery, MdnsRecord.TYPE_PTR,
+ getServiceTypeWithSubtype("_subtype2")));
+
+ // Remove subtype1
+ stopSendAndReceive(mockListenerOne);
+
+ // Queries are not rescheduled, but the next query is affected
+ dispatchMessage();
+ currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+
+ final ArgumentCaptor<DatagramPacket> subtype2QueryCaptor =
+ ArgumentCaptor.forClass(DatagramPacket.class);
+ // Send twice for IPv4 and IPv6
+ inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingMulticastResponse(
+ subtype2QueryCaptor.capture(),
+ eq(socketKey), eq(false));
+
+ final MdnsPacket subtype2Query = MdnsPacket.parse(
+ new MdnsPacketReader(subtype2QueryCaptor.getValue()));
+
+ assertEquals(2, subtype2Query.questions.size());
+ assertTrue(hasQuestion(subtype2Query, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
+ assertTrue(hasQuestion(subtype2Query, MdnsRecord.TYPE_PTR,
+ getServiceTypeWithSubtype("_subtype2")));
+ }
+
private static void verifyServiceInfo(MdnsServiceInfo serviceInfo, String serviceName,
String[] serviceType, List<String> ipv4Addresses, List<String> ipv6Addresses, int port,
List<String> subTypes, Map<String, String> attributes, SocketKey socketKey) {
@@ -919,15 +1022,7 @@
public void processResponse_searchOptionsEnableServiceRemoval_shouldRemove()
throws Exception {
final String serviceInstanceName = "service-instance-1";
- client =
- new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
- mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
- serviceCache) {
- @Override
- MdnsPacketWriter createMdnsPacketWriter() {
- return mockPacketWriter;
- }
- };
+ client = makeMdnsServiceTypeClient(mockPacketWriter);
MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
.setRemoveExpiredService(true)
.setNumOfQueriesBeforeBackoff(Integer.MAX_VALUE)
@@ -965,15 +1060,7 @@
public void processResponse_searchOptionsNotEnableServiceRemoval_shouldNotRemove()
throws Exception {
final String serviceInstanceName = "service-instance-1";
- client =
- new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
- mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
- serviceCache) {
- @Override
- MdnsPacketWriter createMdnsPacketWriter() {
- return mockPacketWriter;
- }
- };
+ client = makeMdnsServiceTypeClient(mockPacketWriter);
startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
@@ -999,15 +1086,7 @@
throws Exception {
//MdnsConfigsFlagsImpl.removeServiceAfterTtlExpires.override(true);
final String serviceInstanceName = "service-instance-1";
- client =
- new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
- mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
- serviceCache) {
- @Override
- MdnsPacketWriter createMdnsPacketWriter() {
- return mockPacketWriter;
- }
- };
+ client = makeMdnsServiceTypeClient(mockPacketWriter);
startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
@@ -1122,19 +1201,21 @@
@Test
public void testProcessResponse_Resolve() throws Exception {
- client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
- mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
- serviceCache);
+ client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
final String instanceName = "service-instance";
final String[] hostname = new String[] { "testhost "};
final String ipV4Address = "192.0.2.0";
final String ipV6Address = "2001:db8::";
- final MdnsSearchOptions resolveOptions = MdnsSearchOptions.newBuilder()
+ final MdnsSearchOptions resolveOptions1 = MdnsSearchOptions.newBuilder()
+ .setResolveInstanceName(instanceName).build();
+ final MdnsSearchOptions resolveOptions2 = MdnsSearchOptions.newBuilder()
.setResolveInstanceName(instanceName).build();
- startSendAndReceive(mockListenerOne, resolveOptions);
+ startSendAndReceive(mockListenerOne, resolveOptions1);
+ startSendAndReceive(mockListenerTwo, resolveOptions2);
+ // No need to verify order for both listeners; and order is not guaranteed between them
InOrder inOrder = inOrder(mockListenerOne, mockSocketClient);
// Verify a query for SRV/TXT was sent, but no PTR query
@@ -1147,13 +1228,19 @@
eq(socketKey), eq(false));
verify(mockDeps, times(1)).sendMessage(any(), any(Message.class));
assertNotNull(delayMessage);
+ inOrder.verify(mockListenerOne).onDiscoveryQuerySent(any(), anyInt());
+ verify(mockListenerTwo).onDiscoveryQuerySent(any(), anyInt());
final MdnsPacket srvTxtQueryPacket = MdnsPacket.parse(
new MdnsPacketReader(srvTxtQueryCaptor.getValue()));
final String[] serviceName = getTestServiceName(instanceName);
+ assertEquals(1, srvTxtQueryPacket.questions.size());
assertFalse(hasQuestion(srvTxtQueryPacket, MdnsRecord.TYPE_PTR));
assertTrue(hasQuestion(srvTxtQueryPacket, MdnsRecord.TYPE_ANY, serviceName));
+ assertEquals(0, srvTxtQueryPacket.answers.size());
+ assertEquals(0, srvTxtQueryPacket.authorityRecords.size());
+ assertEquals(0, srvTxtQueryPacket.additionalRecords.size());
// Process a response with SRV+TXT
final MdnsPacket srvTxtResponse = new MdnsPacket(
@@ -1170,6 +1257,10 @@
Collections.emptyList() /* additionalRecords */);
processResponse(srvTxtResponse, socketKey);
+ inOrder.verify(mockListenerOne).onServiceNameDiscovered(
+ matchServiceName(instanceName), eq(false) /* isServiceFromCache */);
+ verify(mockListenerTwo).onServiceNameDiscovered(
+ matchServiceName(instanceName), eq(false) /* isServiceFromCache */);
// Expect a query for A/AAAA
dispatchMessage();
@@ -1179,11 +1270,18 @@
inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingMulticastResponse(
addressQueryCaptor.capture(),
eq(socketKey), eq(false));
+ inOrder.verify(mockListenerOne).onDiscoveryQuerySent(any(), anyInt());
+ // onDiscoveryQuerySent was called 2 times in total
+ verify(mockListenerTwo, times(2)).onDiscoveryQuerySent(any(), anyInt());
final MdnsPacket addressQueryPacket = MdnsPacket.parse(
new MdnsPacketReader(addressQueryCaptor.getValue()));
+ assertEquals(2, addressQueryPacket.questions.size());
assertTrue(hasQuestion(addressQueryPacket, MdnsRecord.TYPE_A, hostname));
assertTrue(hasQuestion(addressQueryPacket, MdnsRecord.TYPE_AAAA, hostname));
+ assertEquals(0, addressQueryPacket.answers.size());
+ assertEquals(0, addressQueryPacket.authorityRecords.size());
+ assertEquals(0, addressQueryPacket.additionalRecords.size());
// Process a response with address records
final MdnsPacket addressResponse = new MdnsPacket(
@@ -1200,10 +1298,12 @@
Collections.emptyList() /* additionalRecords */);
inOrder.verify(mockListenerOne, never()).onServiceNameDiscovered(any(), anyBoolean());
+ verifyNoMoreInteractions(mockListenerTwo);
processResponse(addressResponse, socketKey);
inOrder.verify(mockListenerOne).onServiceFound(
serviceInfoCaptor.capture(), eq(false) /* isServiceFromCache */);
+ verify(mockListenerTwo).onServiceFound(any(), anyBoolean());
verifyServiceInfo(serviceInfoCaptor.getValue(),
instanceName,
SERVICE_TYPE_LABELS,
@@ -1217,9 +1317,7 @@
@Test
public void testRenewTxtSrvInResolve() throws Exception {
- client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
- mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
- serviceCache);
+ client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
final String instanceName = "service-instance";
final String[] hostname = new String[] { "testhost "};
@@ -1333,9 +1431,7 @@
@Test
public void testProcessResponse_ResolveExcludesOtherServices() {
- client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
- mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
- serviceCache);
+ client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
final String requestedInstance = "instance1";
final String otherInstance = "instance2";
@@ -1403,9 +1499,7 @@
@Test
public void testProcessResponse_SubtypeDiscoveryLimitedToSubtype() {
- client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
- mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
- serviceCache);
+ client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
final String matchingInstance = "instance1";
final String subtype = "_subtype";
@@ -1492,10 +1586,91 @@
}
@Test
+ public void testProcessResponse_SubtypeChange() {
+ client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
+
+ final String matchingInstance = "instance1";
+ final String subtype = "_subtype";
+ final String ipV4Address = "192.0.2.0";
+ final String ipV6Address = "2001:db8::";
+
+ final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
+ .addSubtype("othersub").build();
+
+ startSendAndReceive(mockListenerOne, options);
+
+ // Complete response from instanceName
+ final MdnsPacket packetWithoutSubtype = createResponse(
+ matchingInstance, ipV4Address, 5353, SERVICE_TYPE_LABELS,
+ Collections.emptyMap() /* textAttributes */, TEST_TTL);
+ final MdnsPointerRecord originalPtr = (MdnsPointerRecord) CollectionUtils.findFirst(
+ packetWithoutSubtype.answers, r -> r instanceof MdnsPointerRecord);
+
+ // Add a subtype PTR record
+ final ArrayList<MdnsRecord> newAnswers = new ArrayList<>(packetWithoutSubtype.answers);
+ newAnswers.add(new MdnsPointerRecord(
+ // PTR should be _subtype._sub._type._tcp.local -> instance1._type._tcp.local
+ Stream.concat(Stream.of(subtype, "_sub"), Arrays.stream(SERVICE_TYPE_LABELS))
+ .toArray(String[]::new),
+ originalPtr.getReceiptTime(), originalPtr.getCacheFlush(), originalPtr.getTtl(),
+ originalPtr.getPointer()));
+ processResponse(new MdnsPacket(
+ packetWithoutSubtype.flags,
+ packetWithoutSubtype.questions,
+ newAnswers,
+ packetWithoutSubtype.authorityRecords,
+ packetWithoutSubtype.additionalRecords), socketKey);
+
+ // The subtype does not match
+ final InOrder inOrder = inOrder(mockListenerOne);
+ inOrder.verify(mockListenerOne, never()).onServiceNameDiscovered(any(), anyBoolean());
+
+ // Add another matching subtype
+ newAnswers.add(new MdnsPointerRecord(
+ // PTR should be _subtype._sub._type._tcp.local -> instance1._type._tcp.local
+ Stream.concat(Stream.of("_othersub", "_sub"), Arrays.stream(SERVICE_TYPE_LABELS))
+ .toArray(String[]::new),
+ originalPtr.getReceiptTime(), originalPtr.getCacheFlush(), originalPtr.getTtl(),
+ originalPtr.getPointer()));
+ processResponse(new MdnsPacket(
+ packetWithoutSubtype.flags,
+ packetWithoutSubtype.questions,
+ newAnswers,
+ packetWithoutSubtype.authorityRecords,
+ packetWithoutSubtype.additionalRecords), socketKey);
+
+ final ArgumentMatcher<MdnsServiceInfo> subtypeInstanceMatcher = info ->
+ info.getServiceInstanceName().equals(matchingInstance)
+ && info.getSubtypes().equals(List.of("_subtype", "_othersub"));
+
+ // Service found callbacks are sent now
+ inOrder.verify(mockListenerOne).onServiceNameDiscovered(
+ argThat(subtypeInstanceMatcher), eq(false) /* isServiceFromCache */);
+ inOrder.verify(mockListenerOne).onServiceFound(
+ argThat(subtypeInstanceMatcher), eq(false) /* isServiceFromCache */);
+
+ // Address update: update callbacks are sent
+ processResponse(createResponse(
+ matchingInstance, ipV6Address, 5353, SERVICE_TYPE_LABELS,
+ Collections.emptyMap(), TEST_TTL), socketKey);
+
+ inOrder.verify(mockListenerOne).onServiceUpdated(argThat(info ->
+ subtypeInstanceMatcher.matches(info)
+ && info.getIpv4Addresses().equals(List.of(ipV4Address))
+ && info.getIpv6Addresses().equals(List.of(ipV6Address))));
+
+ // Goodbye: service removed callbacks are sent
+ processResponse(createResponse(
+ matchingInstance, ipV6Address, 5353, SERVICE_TYPE_LABELS,
+ Collections.emptyMap(), 0L /* ttl */), socketKey);
+
+ inOrder.verify(mockListenerOne).onServiceRemoved(matchServiceName(matchingInstance));
+ inOrder.verify(mockListenerOne).onServiceNameRemoved(matchServiceName(matchingInstance));
+ }
+
+ @Test
public void testNotifySocketDestroyed() throws Exception {
- client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
- mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
- serviceCache);
+ client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
final String requestedInstance = "instance1";
final String otherInstance = "instance2";
@@ -1666,6 +1841,111 @@
socketKey);
}
+ @Test
+ public void sendQueries_aggressiveScanMode() {
+ final MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+ .addSubtype(SUBTYPE).setQueryMode(AGGRESSIVE_QUERY_MODE).build();
+ startSendAndReceive(mockListenerOne, searchOptions);
+ // Always try to remove the task.
+ verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+
+ int burstCounter = 0;
+ int betweenBurstTime = 0;
+ for (int i = 0; i < expectedIPv4Packets.length; i += 3) {
+ verifyAndSendQuery(i, betweenBurstTime, /* expectsUnicastResponse= */ true);
+ verifyAndSendQuery(i + 1, /* timeInMs= */ 0, /* expectsUnicastResponse= */ false);
+ verifyAndSendQuery(i + 2, TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS,
+ /* expectsUnicastResponse= */ false);
+ betweenBurstTime = Math.min(
+ INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS * (int) Math.pow(2, burstCounter),
+ MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS);
+ burstCounter++;
+ }
+ // Verify that Task is not removed before stopSendAndReceive was called.
+ verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+
+ // Stop sending packets.
+ stopSendAndReceive(mockListenerOne);
+ verify(mockDeps, times(2)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+ }
+
+ @Test
+ public void sendQueries_reentry_aggressiveScanMode() {
+ final MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+ .addSubtype(SUBTYPE).setQueryMode(AGGRESSIVE_QUERY_MODE).build();
+ startSendAndReceive(mockListenerOne, searchOptions);
+ // Always try to remove the task.
+ verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+
+ // First burst, first query is sent.
+ verifyAndSendQuery(0, /* timeInMs= */ 0, /* expectsUnicastResponse= */ true);
+
+ // After the first query is sent, change the subtypes, and restart.
+ final MdnsSearchOptions searchOptions2 = MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE)
+ .addSubtype("_subtype2").setQueryMode(AGGRESSIVE_QUERY_MODE).build();
+ startSendAndReceive(mockListenerOne, searchOptions2);
+ // The previous scheduled task should be canceled.
+ verify(mockDeps, times(2)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+
+ // Queries should continue to be sent.
+ verifyAndSendQuery(1, /* timeInMs= */ 0, /* expectsUnicastResponse= */ true);
+ verifyAndSendQuery(2, /* timeInMs= */ 0, /* expectsUnicastResponse= */ false);
+ verifyAndSendQuery(3, TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS,
+ /* expectsUnicastResponse= */ false);
+
+ // Stop sending packets.
+ stopSendAndReceive(mockListenerOne);
+ verify(mockDeps, times(3)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+ }
+
+ @Test
+ public void sendQueries_blendScanWithQueryBackoff() {
+ final int numOfQueriesBeforeBackoff = 11;
+ final MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+ .addSubtype(SUBTYPE)
+ .setQueryMode(AGGRESSIVE_QUERY_MODE)
+ .setNumOfQueriesBeforeBackoff(numOfQueriesBeforeBackoff)
+ .build();
+ startSendAndReceive(mockListenerOne, searchOptions);
+ // Always try to remove the task.
+ verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+
+ int burstCounter = 0;
+ int betweenBurstTime = 0;
+ for (int i = 0; i < numOfQueriesBeforeBackoff; i += 3) {
+ verifyAndSendQuery(i, betweenBurstTime, /* expectsUnicastResponse= */ true);
+ verifyAndSendQuery(i + 1, /* timeInMs= */ 0, /* expectsUnicastResponse= */ false);
+ verifyAndSendQuery(i + 2, TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS,
+ /* expectsUnicastResponse= */ false);
+ betweenBurstTime = Math.min(
+ INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS * (int) Math.pow(2, burstCounter),
+ MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS);
+ burstCounter++;
+ }
+ // In backoff mode, the current scheduled task will be canceled and reschedule if the
+ // 0.8 * smallestRemainingTtl is larger than time to next run.
+ long currentTime = TEST_TTL / 2 + TEST_ELAPSED_REALTIME;
+ doReturn(currentTime).when(mockDecoderClock).elapsedRealtime();
+ doReturn(true).when(mockDeps).hasMessages(any(), eq(EVENT_START_QUERYTASK));
+ processResponse(createResponse(
+ "service-instance-1", "192.0.2.123", 5353,
+ SERVICE_TYPE_LABELS,
+ Collections.emptyMap(), TEST_TTL), socketKey);
+ verify(mockDeps, times(2)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+ assertNotNull(delayMessage);
+ verifyAndSendQuery(12 /* index */, (long) (TEST_TTL / 2 * 0.8) /* timeInMs */,
+ true /* expectsUnicastResponse */, true /* multipleSocketDiscovery */,
+ 14 /* scheduledCount */);
+ currentTime += (long) (TEST_TTL / 2 * 0.8);
+ doReturn(currentTime).when(mockDecoderClock).elapsedRealtime();
+ verifyAndSendQuery(13 /* index */, 0 /* timeInMs */,
+ false /* expectsUnicastResponse */, true /* multipleSocketDiscovery */,
+ 15 /* scheduledCount */);
+ verifyAndSendQuery(14 /* index */, TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS,
+ false /* expectsUnicastResponse */, true /* multipleSocketDiscovery */,
+ 16 /* scheduledCount */);
+ }
+
private static MdnsServiceInfo matchServiceName(String name) {
return argThat(info -> info.getServiceInstanceName().equals(name));
}
@@ -1712,6 +1992,11 @@
Arrays.stream(SERVICE_TYPE_LABELS)).toArray(String[]::new);
}
+ private static String[] getServiceTypeWithSubtype(String subtype) {
+ return Stream.concat(Stream.of(subtype, "_sub"),
+ Arrays.stream(SERVICE_TYPE_LABELS)).toArray(String[]::new);
+ }
+
private static boolean hasQuestion(MdnsPacket packet, int type) {
return hasQuestion(packet, type, null);
}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt
index 58f20a9..a5d5297 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt
@@ -23,11 +23,12 @@
import androidx.test.filters.SmallTest
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
import com.android.testutils.DevSdkIgnoreRunner
-import org.junit.Test
-import org.junit.runner.RunWith
import kotlin.test.assertFalse
import kotlin.test.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner::class)
@SmallTest
@IgnoreUpTo(Build.VERSION_CODES.R)
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
index c26ec53..8155fd0 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
@@ -38,6 +38,7 @@
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
+@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner::class)
@SmallTest
@IgnoreUpTo(Build.VERSION_CODES.S_V2) // Bpf only supports in T+.
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSCaptivePortalAppTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSCaptivePortalAppTest.kt
new file mode 100644
index 0000000..be2b29c
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSCaptivePortalAppTest.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.Manifest.permission.NETWORK_STACK
+import android.content.Intent
+import android.content.pm.PackageManager.PERMISSION_DENIED
+import android.content.pm.PackageManager.PERMISSION_GRANTED
+import android.net.ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN
+import android.net.ConnectivityManager.EXTRA_CAPTIVE_PORTAL
+import android.net.IpPrefix
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkStack
+import android.net.CaptivePortal
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
+import android.net.RouteInfo
+import android.os.Build
+import android.os.Bundle
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.assertThrows
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import kotlin.test.assertEquals
+
+// This allows keeping all the networks connected without having to file individual requests
+// for them.
+private fun keepScore() = FromS(
+ NetworkScore.Builder().setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST).build()
+)
+
+private fun nc(transport: Int, vararg caps: Int) = NetworkCapabilities.Builder().apply {
+ addTransportType(transport)
+ caps.forEach {
+ addCapability(it)
+ }
+ // Useful capabilities for everybody
+ addCapability(NET_CAPABILITY_NOT_RESTRICTED)
+ addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ addCapability(NET_CAPABILITY_NOT_ROAMING)
+ addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+}.build()
+
+private fun lp(iface: String) = LinkProperties().apply {
+ interfaceName = iface
+ addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
+ addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
+}
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.R)
+class CSCaptivePortalAppTest : CSTest() {
+ private val WIFI_IFACE = "wifi0"
+ private val TEST_REDIRECT_URL = "http://example.com/firstPath"
+ private val TIMEOUT_MS = 2_000L
+
+ @Test
+ fun testCaptivePortalApp_Reevaluate_Nopermission() {
+ val captivePortalCallback = TestableNetworkCallback()
+ val captivePortalRequest = NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build()
+ cm.registerNetworkCallback(captivePortalRequest, captivePortalCallback)
+ val wifiAgent = createWifiAgent()
+ wifiAgent.connectWithCaptivePortal(TEST_REDIRECT_URL)
+ captivePortalCallback.expectAvailableCallbacksUnvalidated(wifiAgent)
+ val signInIntent = startCaptivePortalApp(wifiAgent)
+ // Remove the granted permissions
+ context.setPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+ PERMISSION_DENIED)
+ context.setPermission(NETWORK_STACK, PERMISSION_DENIED)
+ val captivePortal: CaptivePortal? = signInIntent.getParcelableExtra(EXTRA_CAPTIVE_PORTAL)
+ assertThrows(SecurityException::class.java, { captivePortal?.reevaluateNetwork() })
+ }
+
+ private fun createWifiAgent(): CSAgentWrapper {
+ return Agent(score = keepScore(), lp = lp(WIFI_IFACE),
+ nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
+ }
+
+ private fun startCaptivePortalApp(networkAgent: CSAgentWrapper): Intent {
+ val network = networkAgent.network
+ cm.startCaptivePortalApp(network)
+ waitForIdle()
+ verify(networkAgent.networkMonitor).launchCaptivePortalApp()
+
+ val testBundle = Bundle()
+ val testKey = "testkey"
+ val testValue = "testvalue"
+ testBundle.putString(testKey, testValue)
+ context.setPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, PERMISSION_GRANTED)
+ cm.startCaptivePortalApp(network, testBundle)
+ val signInIntent: Intent = context.expectStartActivityIntent(TIMEOUT_MS)
+ assertEquals(ACTION_CAPTIVE_PORTAL_SIGN_IN, signInIntent.getAction())
+ assertEquals(testValue, signInIntent.getStringExtra(testKey))
+ return signInIntent
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSDestroyedNetworkTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSDestroyedNetworkTests.kt
index 572c7bb..5c29e3a 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSDestroyedNetworkTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSDestroyedNetworkTests.kt
@@ -30,6 +30,7 @@
private const val LONG_TIMEOUT_MS = 5_000
+@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner::class)
@SmallTest
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt
index a753922..94c68c0 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt
@@ -22,8 +22,8 @@
import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.net.NetworkRequest
import android.net.NetworkScore
-import android.net.NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK
import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
+import android.net.NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK
import android.os.Build
import androidx.test.filters.SmallTest
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
@@ -33,6 +33,7 @@
import org.junit.Test
import org.junit.runner.RunWith
+@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner::class)
@SmallTest
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
index 6add6b9..cb98454 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
@@ -33,6 +33,7 @@
import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.RecorderCallback.CallbackEntry.Available
import com.android.testutils.TestableNetworkCallback
+import kotlin.test.assertFailsWith
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@@ -41,7 +42,6 @@
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.never
import org.mockito.Mockito.timeout
-import kotlin.test.assertFailsWith
private const val TIMEOUT_MS = 2_000L
private const val NO_CALLBACK_TIMEOUT_MS = 200L
@@ -51,6 +51,7 @@
private fun defaultLnc() = FromS(LocalNetworkConfig.Builder().build())
+@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner::class)
@SmallTest
@IgnoreUpTo(Build.VERSION_CODES.R)
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
index dd0706b..c1730a4 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
@@ -20,6 +20,8 @@
import android.net.LinkAddress
import android.net.LinkProperties
import android.net.LocalNetworkConfig
+import android.net.MulticastRoutingConfig
+import android.net.MulticastRoutingConfig.CONFIG_FORWARD_NONE
import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.NET_CAPABILITY_DUN
import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
@@ -42,14 +44,15 @@
import com.android.testutils.RecorderCallback.CallbackEntry.LocalInfoChanged
import com.android.testutils.RecorderCallback.CallbackEntry.Lost
import com.android.testutils.TestableNetworkCallback
+import kotlin.test.assertFailsWith
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.eq
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.never
import org.mockito.Mockito.timeout
import org.mockito.Mockito.verify
-import kotlin.test.assertFailsWith
private const val TIMEOUT_MS = 200L
private const val MEDIUM_TIMEOUT_MS = 1_000L
@@ -79,9 +82,28 @@
NetworkScore.Builder().setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST).build()
)
+@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner::class)
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
class CSLocalAgentTests : CSTest() {
+ val multicastRoutingConfigMinScope =
+ MulticastRoutingConfig.Builder(MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE, 4)
+ .build();
+ val multicastRoutingConfigSelected =
+ MulticastRoutingConfig.Builder(MulticastRoutingConfig.FORWARD_SELECTED)
+ .build();
+ val upstreamSelectorAny = NetworkRequest.Builder()
+ .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ .build()
+ val upstreamSelectorWifi = NetworkRequest.Builder()
+ .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ .addTransportType(TRANSPORT_WIFI)
+ .build()
+ val upstreamSelectorCell = NetworkRequest.Builder()
+ .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ .addTransportType(TRANSPORT_CELLULAR)
+ .build()
+
@Test
fun testBadAgents() {
deps.setBuildSdk(VERSION_V)
@@ -177,6 +199,266 @@
localAgent.disconnect()
}
+ private fun createLocalAgent(name: String, localNetworkConfig: FromS<LocalNetworkConfig>):
+ CSAgentWrapper {
+ val localAgent = Agent(
+ nc = nc(TRANSPORT_THREAD, NET_CAPABILITY_LOCAL_NETWORK),
+ lp = lp(name),
+ lnc = localNetworkConfig,
+ )
+ return localAgent
+ }
+
+ private fun createWifiAgent(name: String): CSAgentWrapper {
+ return Agent(score = keepScore(), lp = lp(name),
+ nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
+ }
+
+ private fun createCellAgent(name: String): CSAgentWrapper {
+ return Agent(score = keepScore(), lp = lp(name),
+ nc = nc(TRANSPORT_CELLULAR, NET_CAPABILITY_INTERNET))
+ }
+
+ private fun sendLocalNetworkConfig(localAgent: CSAgentWrapper,
+ upstreamSelector: NetworkRequest?, upstreamConfig: MulticastRoutingConfig,
+ downstreamConfig: MulticastRoutingConfig) {
+ val newLnc = LocalNetworkConfig.Builder()
+ .setUpstreamSelector(upstreamSelector)
+ .setUpstreamMulticastRoutingConfig(upstreamConfig)
+ .setDownstreamMulticastRoutingConfig(downstreamConfig)
+ .build()
+ localAgent.sendLocalNetworkConfig(newLnc)
+ }
+
+ @Test
+ fun testMulticastRoutingConfig() {
+ deps.setBuildSdk(VERSION_V)
+ val cb = TestableNetworkCallback()
+ cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities().build(), cb)
+ val inOrder = inOrder(multicastRoutingCoordinatorService)
+
+ val lnc = FromS(LocalNetworkConfig.Builder()
+ .setUpstreamSelector(upstreamSelectorWifi)
+ .setUpstreamMulticastRoutingConfig(multicastRoutingConfigMinScope)
+ .setDownstreamMulticastRoutingConfig(multicastRoutingConfigSelected)
+ .build()
+ )
+ val localAgent = createLocalAgent("local0", lnc)
+ localAgent.connect()
+
+ cb.expectAvailableCallbacks(localAgent.network, validated = false)
+
+ val wifiAgent = createWifiAgent("wifi0")
+ wifiAgent.connect()
+ cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+ cb.expect<LocalInfoChanged>(localAgent.network) {
+ it.info.upstreamNetwork == wifiAgent.network
+ }
+
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "local0", "wifi0", multicastRoutingConfigMinScope)
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "wifi0", "local0", multicastRoutingConfigSelected)
+
+ wifiAgent.disconnect()
+
+ inOrder.verify(multicastRoutingCoordinatorService)
+ .applyMulticastRoutingConfig("local0", "wifi0", CONFIG_FORWARD_NONE)
+ inOrder.verify(multicastRoutingCoordinatorService)
+ .applyMulticastRoutingConfig("wifi0", "local0", CONFIG_FORWARD_NONE)
+
+ localAgent.disconnect()
+ }
+
+ @Test
+ fun testMulticastRoutingConfig_2LocalNetworks() {
+ deps.setBuildSdk(VERSION_V)
+ val inOrder = inOrder(multicastRoutingCoordinatorService)
+ val lnc = FromS(LocalNetworkConfig.Builder()
+ .setUpstreamSelector(upstreamSelectorWifi)
+ .setUpstreamMulticastRoutingConfig(multicastRoutingConfigMinScope)
+ .setDownstreamMulticastRoutingConfig(multicastRoutingConfigSelected)
+ .build()
+ )
+ val localAgent0 = createLocalAgent("local0", lnc)
+ localAgent0.connect()
+
+ val wifiAgent = createWifiAgent("wifi0")
+ wifiAgent.connect()
+ waitForIdle()
+
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "local0", "wifi0", multicastRoutingConfigMinScope)
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "wifi0", "local0", multicastRoutingConfigSelected)
+
+ val localAgent1 = createLocalAgent("local1", lnc)
+ localAgent1.connect()
+ waitForIdle()
+
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "local1", "wifi0", multicastRoutingConfigMinScope)
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "wifi0", "local1", multicastRoutingConfigSelected)
+
+ localAgent0.disconnect()
+ localAgent1.disconnect()
+ wifiAgent.disconnect()
+ }
+
+ @Test
+ fun testMulticastRoutingConfig_UpstreamNetworkCellToWifi() {
+ deps.setBuildSdk(VERSION_V)
+ val cb = TestableNetworkCallback()
+ cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities()
+ .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ .build(), cb)
+ val inOrder = inOrder(multicastRoutingCoordinatorService)
+ val lnc = FromS(LocalNetworkConfig.Builder()
+ .setUpstreamSelector(upstreamSelectorAny)
+ .setUpstreamMulticastRoutingConfig(multicastRoutingConfigMinScope)
+ .setDownstreamMulticastRoutingConfig(multicastRoutingConfigSelected)
+ .build()
+ )
+ val localAgent = createLocalAgent("local0", lnc)
+ val wifiAgent = createWifiAgent("wifi0")
+ val cellAgent = createCellAgent("cell0")
+
+ localAgent.connect()
+ cb.expectAvailableCallbacks(localAgent.network, validated = false)
+
+ cellAgent.connect()
+ cb.expect<LocalInfoChanged>(localAgent.network) {
+ it.info.upstreamNetwork == cellAgent.network
+ }
+
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "local0", "cell0", multicastRoutingConfigMinScope)
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "cell0", "local0", multicastRoutingConfigSelected)
+
+ wifiAgent.connect()
+
+ cb.expect<LocalInfoChanged>(localAgent.network) {
+ it.info.upstreamNetwork == wifiAgent.network
+ }
+
+ // upstream should have been switched to wifi
+ inOrder.verify(multicastRoutingCoordinatorService)
+ .applyMulticastRoutingConfig("local0", "cell0", CONFIG_FORWARD_NONE)
+ inOrder.verify(multicastRoutingCoordinatorService)
+ .applyMulticastRoutingConfig("cell0", "local0", CONFIG_FORWARD_NONE)
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "local0", "wifi0", multicastRoutingConfigMinScope)
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "wifi0", "local0", multicastRoutingConfigSelected)
+
+ localAgent.disconnect()
+ cellAgent.disconnect()
+ wifiAgent.disconnect()
+ }
+
+ @Test
+ fun testMulticastRoutingConfig_UpstreamSelectorCellToWifi() {
+ deps.setBuildSdk(VERSION_V)
+ val cb = TestableNetworkCallback()
+ cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities()
+ .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ .build(), cb)
+ val inOrder = inOrder(multicastRoutingCoordinatorService)
+ val lnc = FromS(LocalNetworkConfig.Builder()
+ .setUpstreamSelector(upstreamSelectorCell)
+ .setUpstreamMulticastRoutingConfig(multicastRoutingConfigMinScope)
+ .setDownstreamMulticastRoutingConfig(multicastRoutingConfigSelected)
+ .build()
+ )
+ val localAgent = createLocalAgent("local0", lnc)
+ val wifiAgent = createWifiAgent("wifi0")
+ val cellAgent = createCellAgent("cell0")
+
+ localAgent.connect()
+ cellAgent.connect()
+ wifiAgent.connect()
+ cb.expectAvailableCallbacks(localAgent.network, validated = false)
+ cb.expect<LocalInfoChanged>(localAgent.network) {
+ it.info.upstreamNetwork == cellAgent.network
+ }
+
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "local0", "cell0", multicastRoutingConfigMinScope)
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "cell0", "local0", multicastRoutingConfigSelected)
+
+ sendLocalNetworkConfig(localAgent, upstreamSelectorWifi, multicastRoutingConfigMinScope,
+ multicastRoutingConfigSelected)
+ cb.expect<LocalInfoChanged>(localAgent.network) {
+ it.info.upstreamNetwork == wifiAgent.network
+ }
+
+ // upstream should have been switched to wifi
+ inOrder.verify(multicastRoutingCoordinatorService)
+ .applyMulticastRoutingConfig("local0", "cell0", CONFIG_FORWARD_NONE)
+ inOrder.verify(multicastRoutingCoordinatorService)
+ .applyMulticastRoutingConfig("cell0", "local0", CONFIG_FORWARD_NONE)
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "local0", "wifi0", multicastRoutingConfigMinScope)
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "wifi0", "local0", multicastRoutingConfigSelected)
+
+ localAgent.disconnect()
+ cellAgent.disconnect()
+ wifiAgent.disconnect()
+ }
+
+ @Test
+ fun testMulticastRoutingConfig_UpstreamSelectorWifiToNull() {
+ deps.setBuildSdk(VERSION_V)
+ val cb = TestableNetworkCallback()
+ cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities()
+ .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ .build(), cb)
+ val inOrder = inOrder(multicastRoutingCoordinatorService)
+ val lnc = FromS(LocalNetworkConfig.Builder()
+ .setUpstreamSelector(upstreamSelectorWifi)
+ .setUpstreamMulticastRoutingConfig(multicastRoutingConfigMinScope)
+ .setDownstreamMulticastRoutingConfig(multicastRoutingConfigSelected)
+ .build()
+ )
+ val localAgent = createLocalAgent("local0", lnc)
+ localAgent.connect()
+ val wifiAgent = createWifiAgent("wifi0")
+ wifiAgent.connect()
+ cb.expectAvailableCallbacks(localAgent.network, validated = false)
+ cb.expect<LocalInfoChanged>(localAgent.network) {
+ it.info.upstreamNetwork == wifiAgent.network
+ }
+
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "local0", "wifi0", multicastRoutingConfigMinScope)
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "wifi0", "local0", multicastRoutingConfigSelected)
+
+ sendLocalNetworkConfig(localAgent, null, multicastRoutingConfigMinScope,
+ multicastRoutingConfigSelected)
+ cb.expect<LocalInfoChanged>(localAgent.network) {
+ it.info.upstreamNetwork == null
+ }
+
+ // upstream should have been switched to null
+ inOrder.verify(multicastRoutingCoordinatorService)
+ .applyMulticastRoutingConfig("local0", "wifi0", CONFIG_FORWARD_NONE)
+ inOrder.verify(multicastRoutingCoordinatorService)
+ .applyMulticastRoutingConfig("wifi0", "local0", CONFIG_FORWARD_NONE)
+ inOrder.verify(multicastRoutingCoordinatorService, never()).applyMulticastRoutingConfig(
+ eq("local0"), any(), eq(multicastRoutingConfigMinScope))
+ inOrder.verify(multicastRoutingCoordinatorService, never()).applyMulticastRoutingConfig(
+ any(), eq("local0"), eq(multicastRoutingConfigSelected))
+
+ localAgent.disconnect()
+ wifiAgent.disconnect()
+ }
+
+
@Test
fun testUnregisterUpstreamAfterReplacement_SameIfaceName() {
doTestUnregisterUpstreamAfterReplacement(true)
@@ -196,11 +478,10 @@
val localAgent = Agent(nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
lp = lp("local0"),
lnc = FromS(LocalNetworkConfig.Builder()
- .setUpstreamSelector(NetworkRequest.Builder()
- .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
- .addTransportType(TRANSPORT_WIFI)
- .build())
- .build()),
+ .setUpstreamSelector(upstreamSelectorWifi)
+ .setUpstreamMulticastRoutingConfig(multicastRoutingConfigMinScope)
+ .setDownstreamMulticastRoutingConfig(multicastRoutingConfigSelected)
+ .build()),
score = FromS(NetworkScore.Builder()
.setKeepConnectedReason(KEEP_CONNECTED_LOCAL_NETWORK)
.build())
@@ -219,10 +500,15 @@
}
clearInvocations(netd)
- val inOrder = inOrder(netd)
+ clearInvocations(multicastRoutingCoordinatorService)
+ val inOrder = inOrder(netd, multicastRoutingCoordinatorService)
wifiAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
waitForIdle()
inOrder.verify(netd).ipfwdRemoveInterfaceForward("local0", "wifi0")
+ inOrder.verify(multicastRoutingCoordinatorService)
+ .applyMulticastRoutingConfig("local0", "wifi0", CONFIG_FORWARD_NONE)
+ inOrder.verify(multicastRoutingCoordinatorService)
+ .applyMulticastRoutingConfig("wifi0", "local0", CONFIG_FORWARD_NONE)
inOrder.verify(netd).networkDestroy(wifiAgent.network.netId)
val wifiIface2 = if (sameIfaceName) "wifi0" else "wifi1"
@@ -235,9 +521,16 @@
cb.expect<Lost> { it.network == wifiAgent.network }
inOrder.verify(netd).ipfwdAddInterfaceForward("local0", wifiIface2)
- if (sameIfaceName) {
- inOrder.verify(netd, never()).ipfwdRemoveInterfaceForward(any(), any())
- }
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ "local0", wifiIface2, multicastRoutingConfigMinScope)
+ inOrder.verify(multicastRoutingCoordinatorService).applyMulticastRoutingConfig(
+ wifiIface2, "local0", multicastRoutingConfigSelected)
+
+ inOrder.verify(netd, never()).ipfwdRemoveInterfaceForward(any(), any())
+ inOrder.verify(multicastRoutingCoordinatorService, never())
+ .applyMulticastRoutingConfig("local0", "wifi0", CONFIG_FORWARD_NONE)
+ inOrder.verify(multicastRoutingCoordinatorService, never())
+ .applyMulticastRoutingConfig("wifi0", "local0", CONFIG_FORWARD_NONE)
}
@Test
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt
index 526ec9d..df0a2cc 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt
@@ -63,6 +63,7 @@
private const val PACKAGE_UID = 123
private const val TIMEOUT_MS = 250L
+@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner::class)
@SmallTest
@IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt
new file mode 100644
index 0000000..35f8ae5
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import android.os.Build
+import android.os.Process
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.argThat
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class CSNetworkRequestStateStatsMetricsTests : CSTest() {
+ private val CELL_INTERNET_NOT_METERED_NC = NetworkCapabilities.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
+ .build().setRequestorUidAndPackageName(Process.myUid(), context.getPackageName())
+
+ private val CELL_INTERNET_NOT_METERED_NR = NetworkRequest.Builder()
+ .setCapabilities(CELL_INTERNET_NOT_METERED_NC).build()
+
+ @Before
+ fun setup() {
+ waitForIdle()
+ clearInvocations(networkRequestStateStatsMetrics)
+ }
+
+ @Test
+ fun testRequestTypeNRProduceMetrics() {
+ cm.requestNetwork(CELL_INTERNET_NOT_METERED_NR, TestableNetworkCallback())
+ waitForIdle()
+
+ verify(networkRequestStateStatsMetrics).onNetworkRequestReceived(
+ argThat{req -> req.networkCapabilities.equals(
+ CELL_INTERNET_NOT_METERED_NR.networkCapabilities)})
+ }
+
+ @Test
+ fun testListenTypeNRProduceNoMetrics() {
+ cm.registerNetworkCallback(CELL_INTERNET_NOT_METERED_NR, TestableNetworkCallback())
+ waitForIdle()
+ verify(networkRequestStateStatsMetrics, never()).onNetworkRequestReceived(any())
+ }
+
+ @Test
+ fun testRemoveRequestTypeNRProduceMetrics() {
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(CELL_INTERNET_NOT_METERED_NR, cb)
+
+ waitForIdle()
+ clearInvocations(networkRequestStateStatsMetrics)
+
+ cm.unregisterNetworkCallback(cb)
+ waitForIdle()
+ verify(networkRequestStateStatsMetrics).onNetworkRequestRemoved(
+ argThat{req -> req.networkCapabilities.equals(
+ CELL_INTERNET_NOT_METERED_NR.networkCapabilities)})
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkFallbackTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkFallbackTest.kt
new file mode 100644
index 0000000..9024641
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkFallbackTest.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.net.IpPrefix
+import android.net.INetd
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.NativeNetworkConfig
+import android.net.NativeNetworkType
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkScore
+import android.net.NetworkCapabilities.TRANSPORT_SATELLITE
+import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
+import android.net.RouteInfo
+import android.net.UidRange
+import android.net.UidRangeParcel
+import android.net.VpnManager
+import android.net.netd.aidl.NativeUidRangeConfig
+import android.os.Build
+import android.os.UserHandle
+import android.util.ArraySet
+import com.android.net.module.util.CollectionUtils
+import com.android.server.ConnectivityService.PREFERENCE_ORDER_SATELLITE_FALLBACK
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.visibleOnHandlerThread
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+private const val SECONDARY_USER = 10
+private val SECONDARY_USER_HANDLE = UserHandle(SECONDARY_USER)
+private const val TEST_PACKAGE_UID = 123
+private const val TEST_PACKAGE_UID2 = 321
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class CSSatelliteNetworkPreferredTest : CSTest() {
+ /**
+ * Test createMultiLayerNrisFromSatelliteNetworkPreferredUids returns correct
+ * NetworkRequestInfo.
+ */
+ @Test
+ fun testCreateMultiLayerNrisFromSatelliteNetworkPreferredUids() {
+ // Verify that empty uid set should not create any NRI for it.
+ val nrisNoUid = service.createMultiLayerNrisFromSatelliteNetworkFallbackUids(emptySet())
+ Assert.assertEquals(0, nrisNoUid.size.toLong())
+ val uid1 = PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID)
+ val uid2 = PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID2)
+ val uid3 = SECONDARY_USER_HANDLE.getUid(TEST_PACKAGE_UID)
+ assertCreateMultiLayerNrisFromSatelliteNetworkPreferredUids(mutableSetOf(uid1))
+ assertCreateMultiLayerNrisFromSatelliteNetworkPreferredUids(mutableSetOf(uid1, uid3))
+ assertCreateMultiLayerNrisFromSatelliteNetworkPreferredUids(mutableSetOf(uid1, uid2))
+ }
+
+ /**
+ * Test that SATELLITE_NETWORK_PREFERENCE_UIDS changes will send correct net id and uid ranges
+ * to netd.
+ */
+ @Test
+ fun testSatelliteNetworkPreferredUidsChanged() {
+ val netdInOrder = inOrder(netd)
+
+ val satelliteAgent = createSatelliteAgent("satellite0")
+ satelliteAgent.connect()
+
+ val satelliteNetId = satelliteAgent.network.netId
+ netdInOrder.verify(netd).networkCreate(
+ nativeNetworkConfigPhysical(satelliteNetId, INetd.PERMISSION_NONE))
+
+ val uid1 = PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID)
+ val uid2 = PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID2)
+ val uid3 = SECONDARY_USER_HANDLE.getUid(TEST_PACKAGE_UID)
+
+ // Initial satellite network preferred uids status.
+ setAndUpdateSatelliteNetworkPreferredUids(setOf())
+ netdInOrder.verify(netd, never()).networkAddUidRangesParcel(any())
+ netdInOrder.verify(netd, never()).networkRemoveUidRangesParcel(any())
+
+ // Set SATELLITE_NETWORK_PREFERENCE_UIDS setting and verify that net id and uid ranges
+ // send to netd
+ var uids = mutableSetOf(uid1, uid2, uid3)
+ val uidRanges1 = toUidRangeStableParcels(uidRangesForUids(uids))
+ val config1 = NativeUidRangeConfig(
+ satelliteNetId, uidRanges1,
+ PREFERENCE_ORDER_SATELLITE_FALLBACK
+ )
+ setAndUpdateSatelliteNetworkPreferredUids(uids)
+ netdInOrder.verify(netd).networkAddUidRangesParcel(config1)
+ netdInOrder.verify(netd, never()).networkRemoveUidRangesParcel(any())
+
+ // Set SATELLITE_NETWORK_PREFERENCE_UIDS setting again and verify that old rules are removed
+ // and new rules are added.
+ uids = mutableSetOf(uid1)
+ val uidRanges2: Array<UidRangeParcel?> = toUidRangeStableParcels(uidRangesForUids(uids))
+ val config2 = NativeUidRangeConfig(
+ satelliteNetId, uidRanges2,
+ PREFERENCE_ORDER_SATELLITE_FALLBACK
+ )
+ setAndUpdateSatelliteNetworkPreferredUids(uids)
+ netdInOrder.verify(netd).networkRemoveUidRangesParcel(config1)
+ netdInOrder.verify(netd).networkAddUidRangesParcel(config2)
+ }
+
+ private fun assertCreateMultiLayerNrisFromSatelliteNetworkPreferredUids(uids: Set<Int>) {
+ val nris: Set<ConnectivityService.NetworkRequestInfo> =
+ service.createMultiLayerNrisFromSatelliteNetworkFallbackUids(uids)
+ val nri = nris.iterator().next()
+ // Verify that one NRI is created with multilayer requests. Because one NRI can contain
+ // multiple uid ranges, so it only need create one NRI here.
+ assertEquals(1, nris.size.toLong())
+ assertTrue(nri.isMultilayerRequest)
+ assertEquals(nri.uids, uidRangesForUids(uids))
+ assertEquals(PREFERENCE_ORDER_SATELLITE_FALLBACK, nri.mPreferenceOrder)
+ }
+
+ private fun setAndUpdateSatelliteNetworkPreferredUids(uids: Set<Int>) {
+ visibleOnHandlerThread(csHandler) {
+ deps.satelliteNetworkFallbackUidUpdate!!.accept(uids)
+ }
+ }
+
+ private fun nativeNetworkConfigPhysical(netId: Int, permission: Int) =
+ NativeNetworkConfig(netId, NativeNetworkType.PHYSICAL, permission,
+ false /* secure */, VpnManager.TYPE_VPN_NONE, false /* excludeLocalRoutes */)
+
+ private fun createSatelliteAgent(name: String): CSAgentWrapper {
+ return Agent(score = keepScore(), lp = lp(name),
+ nc = nc(TRANSPORT_SATELLITE, NET_CAPABILITY_INTERNET)
+ )
+ }
+
+ private fun toUidRangeStableParcels(ranges: Set<UidRange>): Array<UidRangeParcel?> {
+ val stableRanges = arrayOfNulls<UidRangeParcel>(ranges.size)
+ for ((index, range) in ranges.withIndex()) {
+ stableRanges[index] = UidRangeParcel(range.start, range.stop)
+ }
+ return stableRanges
+ }
+
+ private fun uidRangesForUids(vararg uids: Int): Set<UidRange> {
+ val ranges = ArraySet<UidRange>()
+ for (uid in uids) {
+ ranges.add(UidRange(uid, uid))
+ }
+ return ranges
+ }
+
+ private fun uidRangesForUids(uids: Collection<Int>): Set<UidRange> {
+ return uidRangesForUids(*CollectionUtils.toIntArray(uids))
+ }
+
+ private fun nc(transport: Int, vararg caps: Int) = NetworkCapabilities.Builder().apply {
+ addTransportType(transport)
+ caps.forEach {
+ addCapability(it)
+ }
+ // Useful capabilities for everybody
+ addCapability(NET_CAPABILITY_NOT_RESTRICTED)
+ addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ addCapability(NET_CAPABILITY_NOT_ROAMING)
+ addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ }.build()
+
+ private fun lp(iface: String) = LinkProperties().apply {
+ interfaceName = iface
+ addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
+ addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
+ }
+
+ // This allows keeping all the networks connected without having to file individual requests
+ // for them.
+ private fun keepScore() = FromS(
+ NetworkScore.Builder().setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST).build()
+ )
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
index d41c742..d7343b1 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
@@ -19,6 +19,8 @@
import android.content.Context
import android.net.ConnectivityManager
import android.net.INetworkMonitor
+import android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_DNS
+import android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTP
import android.net.INetworkMonitorCallbacks
import android.net.LinkProperties
import android.net.LocalNetworkConfig
@@ -75,10 +77,15 @@
) : TestableNetworkCallback.HasNetwork {
private val TAG = "CSAgent${nextAgentId()}"
private val VALIDATION_RESULT_INVALID = 0
+ private val NO_PROBE_RESULT = 0
private val VALIDATION_TIMESTAMP = 1234L
private val agent: NetworkAgent
private val nmCallbacks: INetworkMonitorCallbacks
val networkMonitor = mock<INetworkMonitor>()
+ private var nmValidationRedirectUrl: String? = null
+ private var nmValidationResult = NO_PROBE_RESULT
+ private var nmProbesCompleted = NO_PROBE_RESULT
+ private var nmProbesSucceeded = NO_PROBE_RESULT
override val network: Network get() = agent.network!!
@@ -120,10 +127,10 @@
}
nmCallbacks.notifyProbeStatusChanged(0 /* completed */, 0 /* succeeded */)
val p = NetworkTestResultParcelable()
- p.result = VALIDATION_RESULT_INVALID
- p.probesAttempted = 0
- p.probesSucceeded = 0
- p.redirectUrl = null
+ p.result = nmValidationResult
+ p.probesAttempted = nmProbesCompleted
+ p.probesSucceeded = nmProbesSucceeded
+ p.redirectUrl = nmValidationRedirectUrl
p.timestampMillis = VALIDATION_TIMESTAMP
nmCallbacks.notifyNetworkTestedWithExtras(p)
}
@@ -171,4 +178,26 @@
fun sendLocalNetworkConfig(lnc: LocalNetworkConfig) = agent.sendLocalNetworkConfig(lnc)
fun sendNetworkCapabilities(nc: NetworkCapabilities) = agent.sendNetworkCapabilities(nc)
+
+ fun connectWithCaptivePortal(redirectUrl: String) {
+ setCaptivePortal(redirectUrl)
+ connect()
+ }
+
+ fun setProbesStatus(probesCompleted: Int, probesSucceeded: Int) {
+ nmProbesCompleted = probesCompleted
+ nmProbesSucceeded = probesSucceeded
+ }
+
+ fun setCaptivePortal(redirectUrl: String) {
+ nmValidationResult = VALIDATION_RESULT_INVALID
+ nmValidationRedirectUrl = redirectUrl
+ // Suppose the portal is found when NetworkMonitor probes NETWORK_VALIDATION_PROBE_HTTP
+ // in the beginning. Because NETWORK_VALIDATION_PROBE_HTTP is the decisive probe for captive
+ // portal, considering the NETWORK_VALIDATION_PROBE_HTTPS hasn't probed yet and set only
+ // DNS and HTTP probes completed.
+ setProbesStatus(
+ NETWORK_VALIDATION_PROBE_DNS or NETWORK_VALIDATION_PROBE_HTTP /* probesCompleted */,
+ VALIDATION_RESULT_INVALID /* probesSucceeded */)
+ }
}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
index 958c4f2..595ca47 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -16,6 +16,7 @@
package com.android.server
+import android.app.AlarmManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@@ -46,8 +47,10 @@
import android.os.Bundle
import android.os.Handler
import android.os.HandlerThread
+import android.os.Process
import android.os.UserHandle
import android.os.UserManager
+import android.permission.PermissionManager.PermissionResult
import android.telephony.TelephonyManager
import android.testing.TestableContext
import android.util.ArraySet
@@ -61,20 +64,30 @@
import com.android.server.connectivity.CarrierPrivilegeAuthenticator
import com.android.server.connectivity.ClatCoordinator
import com.android.server.connectivity.ConnectivityFlags
+import com.android.server.connectivity.MulticastRoutingCoordinatorService
import com.android.server.connectivity.MultinetworkPolicyTracker
import com.android.server.connectivity.MultinetworkPolicyTrackerTestDependencies
+import com.android.server.connectivity.NetworkRequestStateStatsMetrics
import com.android.server.connectivity.ProxyTracker
+import com.android.server.connectivity.SatelliteAccessController
import com.android.testutils.visibleOnHandlerThread
import com.android.testutils.waitForIdle
import java.util.concurrent.Executors
+import java.util.concurrent.LinkedBlockingQueue
+import java.util.concurrent.TimeUnit
+import java.util.function.Consumer
+import java.util.function.BiConsumer
+import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.fail
+import org.junit.After
+import org.junit.Before
import org.mockito.AdditionalAnswers.delegatesTo
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
-internal const val HANDLER_TIMEOUT_MS = 2_000
+internal const val HANDLER_TIMEOUT_MS = 2_000L
internal const val BROADCAST_TIMEOUT_MS = 3_000L
internal const val TEST_PACKAGE_NAME = "com.android.test.package"
internal const val WIFI_WOL_IFNAME = "test_wlan_wol"
@@ -129,10 +142,12 @@
// permissions using static contexts.
val enabledFeatures = HashMap<String, Boolean>().also {
it[ConnectivityFlags.NO_REMATCH_ALL_REQUESTS_ON_REGISTER] = true
+ it[ConnectivityFlags.REQUEST_RESTRICTED_WIFI] = true
it[ConnectivityService.KEY_DESTROY_FROZEN_SOCKETS_VERSION] = true
it[ConnectivityService.DELAY_DESTROY_FROZEN_SOCKETS_VERSION] = true
it[ConnectivityService.ALLOW_SYSUI_CONNECTIVITY_REPORTS] = true
it[ConnectivityService.LOG_BPF_RC] = true
+ it[ConnectivityService.ALLOW_SATALLITE_NETWORK_FALLBACK] = true
}
fun enableFeature(f: String) = enabledFeatures.set(f, true)
fun disableFeature(f: String) = enabledFeatures.set(f, false)
@@ -157,8 +172,8 @@
val netd = mock<INetd>()
val bpfNetMaps = mock<BpfNetMaps>()
val clatCoordinator = mock<ClatCoordinator>()
+ val networkRequestStateStatsMetrics = mock<NetworkRequestStateStatsMetrics>()
val proxyTracker = ProxyTracker(context, mock<Handler>(), 16 /* EVENT_PROXY_HAS_CHANGED */)
- val alarmManager = makeMockAlarmManager()
val systemConfigManager = makeMockSystemConfigManager()
val batteryStats = mock<IBatteryStats>()
val batteryManager = BatteryStatsManager(batteryStats)
@@ -166,10 +181,36 @@
doReturn(true).`when`(it).isDataCapable()
}
+ val multicastRoutingCoordinatorService = mock<MulticastRoutingCoordinatorService>()
+ val satelliteAccessController = mock<SatelliteAccessController>()
+
val deps = CSDeps()
- val service = makeConnectivityService(context, netd, deps).also { it.systemReadyInternal() }
- val cm = ConnectivityManager(context, service)
- val csHandler = Handler(csHandlerThread.looper)
+
+ // Initializations that start threads are done from setUp to avoid thread leak
+ lateinit var alarmHandlerThread: HandlerThread
+ lateinit var alarmManager: AlarmManager
+ lateinit var service: ConnectivityService
+ lateinit var cm: ConnectivityManager
+ lateinit var csHandler: Handler
+
+ @Before
+ fun setUp() {
+ alarmHandlerThread = HandlerThread("TestAlarmManager").also { it.start() }
+ alarmManager = makeMockAlarmManager(alarmHandlerThread)
+ service = makeConnectivityService(context, netd, deps).also { it.systemReadyInternal() }
+ cm = ConnectivityManager(context, service)
+ // csHandler initialization must be after makeConnectivityService since ConnectivityService
+ // constructor starts csHandlerThread
+ csHandler = Handler(csHandlerThread.looper)
+ }
+
+ @After
+ fun tearDown() {
+ csHandlerThread.quitSafely()
+ csHandlerThread.join()
+ alarmHandlerThread.quitSafely()
+ alarmHandlerThread.join()
+ }
inner class CSDeps : ConnectivityService.Dependencies() {
override fun getResources(ctx: Context) = connResources
@@ -179,12 +220,26 @@
override fun makeHandlerThread(tag: String) = csHandlerThread
override fun makeProxyTracker(context: Context, connServiceHandler: Handler) = proxyTracker
+ override fun makeMulticastRoutingCoordinatorService(handler: Handler) =
+ this@CSTest.multicastRoutingCoordinatorService
override fun makeCarrierPrivilegeAuthenticator(
context: Context,
- tm: TelephonyManager
+ tm: TelephonyManager,
+ requestRestrictedWifiEnabled: Boolean,
+ listener: BiConsumer<Int, Int>
) = if (SdkLevel.isAtLeastT()) mock<CarrierPrivilegeAuthenticator>() else null
+ var satelliteNetworkFallbackUidUpdate: Consumer<Set<Int>>? = null
+ override fun makeSatelliteAccessController(
+ context: Context,
+ updateSatelliteNetworkFallackUid: Consumer<Set<Int>>?,
+ csHandlerThread: Handler
+ ): SatelliteAccessController? {
+ satelliteNetworkFallbackUidUpdate = updateSatelliteNetworkFallackUid
+ return satelliteAccessController
+ }
+
private inner class AOOKTDeps(c: Context) : AutomaticOnOffKeepaliveTracker.Dependencies(c) {
override fun isTetheringFeatureNotChickenedOut(name: String): Boolean {
return isFeatureEnabled(context, name)
@@ -197,6 +252,9 @@
MultinetworkPolicyTracker(c, h, r,
MultinetworkPolicyTrackerTestDependencies(connResources.get()))
+ override fun makeNetworkRequestStateStatsMetrics(c: Context) =
+ this@CSTest.networkRequestStateStatsMetrics
+
// All queried features must be mocked, because the test cannot hold the
// READ_DEVICE_CONFIG permission and device config utils use static methods for
// checking permissions.
@@ -247,13 +305,65 @@
val pacProxyManager = mock<PacProxyManager>()
val networkPolicyManager = mock<NetworkPolicyManager>()
+ // Map of permission name -> PermissionManager.Permission_{GRANTED|DENIED} constant
+ // For permissions granted across the board, the key is only the permission name.
+ // For permissions only granted to a combination of uid/pid, the key
+ // is "<permission name>,<pid>,<uid>". PID+UID permissions have priority over generic ones.
+ private val mMockedPermissions: HashMap<String, Int> = HashMap()
+ private val mStartedActivities = LinkedBlockingQueue<Intent>()
override fun getPackageManager() = this@CSTest.packageManager
override fun getContentResolver() = this@CSTest.contentResolver
- // TODO : buff up the capabilities of this permission scheme to allow checking for
- // permission rejections
- override fun checkPermission(permission: String, pid: Int, uid: Int) = PERMISSION_GRANTED
- override fun checkCallingOrSelfPermission(permission: String) = PERMISSION_GRANTED
+ // If the permission result does not set in the mMockedPermissions, it will be
+ // considered as PERMISSION_GRANTED as existing design to prevent breaking other tests.
+ override fun checkPermission(permission: String, pid: Int, uid: Int) =
+ checkMockedPermission(permission, pid, uid, PERMISSION_GRANTED)
+
+ override fun enforceCallingOrSelfPermission(permission: String, message: String?) {
+ // If the permission result does not set in the mMockedPermissions, it will be
+ // considered as PERMISSION_GRANTED as existing design to prevent breaking other tests.
+ val granted = checkMockedPermission(permission, Process.myPid(), Process.myUid(),
+ PERMISSION_GRANTED)
+ if (!granted.equals(PERMISSION_GRANTED)) {
+ throw SecurityException("[Test] permission denied: " + permission)
+ }
+ }
+
+ // If the permission result does not set in the mMockedPermissions, it will be
+ // considered as PERMISSION_GRANTED as existing design to prevent breaking other tests.
+ override fun checkCallingOrSelfPermission(permission: String) =
+ checkMockedPermission(permission, Process.myPid(), Process.myUid(), PERMISSION_GRANTED)
+
+ private fun checkMockedPermission(permission: String, pid: Int, uid: Int, default: Int):
+ Int {
+ val processSpecificKey = "$permission,$pid,$uid"
+ return mMockedPermissions[processSpecificKey]
+ ?: mMockedPermissions[permission] ?: default
+ }
+
+ /**
+ * Mock checks for the specified permission, and have them behave as per `granted` or
+ * `denied`.
+ *
+ * This will apply to all calls no matter what the checked UID and PID are.
+ *
+ * @param granted One of {@link PackageManager#PermissionResult}.
+ */
+ fun setPermission(permission: String, @PermissionResult granted: Int) {
+ mMockedPermissions.put(permission, granted)
+ }
+
+ /**
+ * Mock checks for the specified permission, and have them behave as per `granted` or
+ * `denied`.
+ *
+ * This will only apply to the passed UID and PID.
+ *
+ * @param granted One of {@link PackageManager#PermissionResult}.
+ */
+ fun setPermission(permission: String, pid: Int, uid: Int, @PermissionResult granted: Int) {
+ mMockedPermissions.put("$permission,$pid,$uid", granted)
+ }
// Necessary for MultinetworkPolicyTracker, which tries to register a receiver for
// all users. The test can't do that since it doesn't hold INTERACT_ACROSS_USERS.
@@ -311,6 +421,16 @@
) {
orderedBroadcastAsUserHistory.add(intent)
}
+
+ override fun startActivityAsUser(intent: Intent, handle: UserHandle) {
+ mStartedActivities.put(intent)
+ }
+
+ fun expectStartActivityIntent(timeoutMs: Long = HANDLER_TIMEOUT_MS): Intent {
+ val intent = mStartedActivities.poll(timeoutMs, TimeUnit.MILLISECONDS)
+ assertNotNull(intent, "Did not receive sign-in intent after " + timeoutMs + "ms")
+ return intent
+ }
}
// Utility methods for subclasses to use
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
index c1828b2..8ff790c 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
@@ -53,6 +53,7 @@
import com.android.modules.utils.build.SdkLevel
import com.android.server.ConnectivityService.Dependencies
import com.android.server.connectivity.ConnectivityResources
+import kotlin.test.fail
import org.mockito.ArgumentMatchers
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyInt
@@ -64,7 +65,6 @@
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.doNothing
import org.mockito.Mockito.doReturn
-import kotlin.test.fail
internal inline fun <reified T> mock() = Mockito.mock(T::class.java)
internal inline fun <reified T> any() = any(T::class.java)
@@ -128,8 +128,8 @@
}
private val UNREASONABLY_LONG_ALARM_WAIT_MS = 1000
-internal fun makeMockAlarmManager() = mock<AlarmManager>().also { am ->
- val alrmHdlr = HandlerThread("TestAlarmManager").also { it.start() }.threadHandler
+internal fun makeMockAlarmManager(handlerThread: HandlerThread) = mock<AlarmManager>().also { am ->
+ val alrmHdlr = handlerThread.threadHandler
doAnswer {
val (_, date, _, wakeupMsg, handler) = it.arguments
wakeupMsg as WakeupMessage
diff --git a/tests/unit/java/com/android/server/net/BpfInterfaceMapHelperTest.java b/tests/unit/java/com/android/server/net/BpfInterfaceMapHelperTest.java
new file mode 100644
index 0000000..7b3bea3
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/BpfInterfaceMapHelperTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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 com.android.server.net;
+
+import static android.system.OsConstants.EPERM;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.spy;
+
+import android.os.Build;
+import android.system.ErrnoException;
+import android.util.IndentingPrintWriter;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
+import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.Struct.S32;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.TestBpfMap;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+@SmallTest
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+public final class BpfInterfaceMapHelperTest {
+ private static final int TEST_INDEX = 1;
+ private static final int TEST_INDEX2 = 2;
+ private static final String TEST_INTERFACE_NAME = "test1";
+ private static final String TEST_INTERFACE_NAME2 = "test2";
+
+ private BaseNetdUnsolicitedEventListener mListener;
+ private BpfInterfaceMapHelper mUpdater;
+ private IBpfMap<S32, InterfaceMapValue> mBpfMap =
+ spy(new TestBpfMap<>(S32.class, InterfaceMapValue.class));
+
+ private class TestDependencies extends BpfInterfaceMapHelper.Dependencies {
+ @Override
+ public IBpfMap<S32, InterfaceMapValue> getInterfaceMap() {
+ return mBpfMap;
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mUpdater = new BpfInterfaceMapHelper(new TestDependencies());
+ }
+
+ @Test
+ public void testGetIfNameByIndex() throws Exception {
+ mBpfMap.updateEntry(new S32(TEST_INDEX), new InterfaceMapValue(TEST_INTERFACE_NAME));
+ assertEquals(TEST_INTERFACE_NAME, mUpdater.getIfNameByIndex(TEST_INDEX));
+ }
+
+ @Test
+ public void testGetIfNameByIndexNoEntry() {
+ assertNull(mUpdater.getIfNameByIndex(TEST_INDEX));
+ }
+
+ @Test
+ public void testGetIfNameByIndexException() throws Exception {
+ doThrow(new ErrnoException("", EPERM)).when(mBpfMap).getValue(new S32(TEST_INDEX));
+ assertNull(mUpdater.getIfNameByIndex(TEST_INDEX));
+ }
+
+ private void assertDumpContains(final String dump, final String message) {
+ assertTrue(String.format("dump(%s) does not contain '%s'", dump, message),
+ dump.contains(message));
+ }
+
+ private String getDump() {
+ final StringWriter sw = new StringWriter();
+ mUpdater.dump(new IndentingPrintWriter(new PrintWriter(sw), " "));
+ return sw.toString();
+ }
+
+ @Test
+ public void testDump() throws ErrnoException {
+ mBpfMap.updateEntry(new S32(TEST_INDEX), new InterfaceMapValue(TEST_INTERFACE_NAME));
+ mBpfMap.updateEntry(new S32(TEST_INDEX2), new InterfaceMapValue(TEST_INTERFACE_NAME2));
+
+ final String dump = getDump();
+ assertDumpContains(dump, "IfaceIndexNameMap: OK");
+ assertDumpContains(dump, "ifaceIndex=1 ifaceName=test1");
+ assertDumpContains(dump, "ifaceIndex=2 ifaceName=test2");
+ }
+}
diff --git a/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java b/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java
deleted file mode 100644
index c730856..0000000
--- a/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java
+++ /dev/null
@@ -1,174 +0,0 @@
-/*
- * 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 com.android.server.net;
-
-import static android.system.OsConstants.EPERM;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.net.INetd;
-import android.net.MacAddress;
-import android.os.Build;
-import android.os.Handler;
-import android.os.test.TestLooper;
-import android.system.ErrnoException;
-import android.util.IndentingPrintWriter;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
-import com.android.net.module.util.IBpfMap;
-import com.android.net.module.util.InterfaceParams;
-import com.android.net.module.util.Struct.S32;
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRunner;
-import com.android.testutils.TestBpfMap;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.io.PrintWriter;
-import java.io.StringWriter;
-
-@SmallTest
-@RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
-public final class BpfInterfaceMapUpdaterTest {
- private static final int TEST_INDEX = 1;
- private static final int TEST_INDEX2 = 2;
- private static final String TEST_INTERFACE_NAME = "test1";
- private static final String TEST_INTERFACE_NAME2 = "test2";
-
- private final TestLooper mLooper = new TestLooper();
- private BaseNetdUnsolicitedEventListener mListener;
- private BpfInterfaceMapUpdater mUpdater;
- private IBpfMap<S32, InterfaceMapValue> mBpfMap =
- spy(new TestBpfMap<>(S32.class, InterfaceMapValue.class));
- @Mock private INetd mNetd;
- @Mock private Context mContext;
-
- private class TestDependencies extends BpfInterfaceMapUpdater.Dependencies {
- @Override
- public IBpfMap<S32, InterfaceMapValue> getInterfaceMap() {
- return mBpfMap;
- }
-
- @Override
- public InterfaceParams getInterfaceParams(String ifaceName) {
- if (ifaceName.equals(TEST_INTERFACE_NAME)) {
- return new InterfaceParams(TEST_INTERFACE_NAME, TEST_INDEX,
- MacAddress.ALL_ZEROS_ADDRESS);
- } else if (ifaceName.equals(TEST_INTERFACE_NAME2)) {
- return new InterfaceParams(TEST_INTERFACE_NAME2, TEST_INDEX2,
- MacAddress.ALL_ZEROS_ADDRESS);
- }
-
- return null;
- }
-
- @Override
- public INetd getINetd(Context ctx) {
- return mNetd;
- }
- }
-
- @Before
- public void setUp() throws Exception {
- MockitoAnnotations.initMocks(this);
- when(mNetd.interfaceGetList()).thenReturn(new String[] {TEST_INTERFACE_NAME});
- mUpdater = new BpfInterfaceMapUpdater(mContext, new Handler(mLooper.getLooper()),
- new TestDependencies());
- }
-
- private void verifyStartUpdater() throws Exception {
- mUpdater.start();
- mLooper.dispatchAll();
- final ArgumentCaptor<BaseNetdUnsolicitedEventListener> listenerCaptor =
- ArgumentCaptor.forClass(BaseNetdUnsolicitedEventListener.class);
- verify(mNetd).registerUnsolicitedEventListener(listenerCaptor.capture());
- mListener = listenerCaptor.getValue();
- verify(mBpfMap).updateEntry(eq(new S32(TEST_INDEX)),
- eq(new InterfaceMapValue(TEST_INTERFACE_NAME)));
- }
-
- @Test
- public void testUpdateInterfaceMap() throws Exception {
- verifyStartUpdater();
-
- mListener.onInterfaceAdded(TEST_INTERFACE_NAME2);
- mLooper.dispatchAll();
- verify(mBpfMap).updateEntry(eq(new S32(TEST_INDEX2)),
- eq(new InterfaceMapValue(TEST_INTERFACE_NAME2)));
-
- // Check that when onInterfaceRemoved is called, nothing happens.
- mListener.onInterfaceRemoved(TEST_INTERFACE_NAME);
- mLooper.dispatchAll();
- verifyNoMoreInteractions(mBpfMap);
- }
-
- @Test
- public void testGetIfNameByIndex() throws Exception {
- mBpfMap.updateEntry(new S32(TEST_INDEX), new InterfaceMapValue(TEST_INTERFACE_NAME));
- assertEquals(TEST_INTERFACE_NAME, mUpdater.getIfNameByIndex(TEST_INDEX));
- }
-
- @Test
- public void testGetIfNameByIndexNoEntry() {
- assertNull(mUpdater.getIfNameByIndex(TEST_INDEX));
- }
-
- @Test
- public void testGetIfNameByIndexException() throws Exception {
- doThrow(new ErrnoException("", EPERM)).when(mBpfMap).getValue(new S32(TEST_INDEX));
- assertNull(mUpdater.getIfNameByIndex(TEST_INDEX));
- }
-
- private void assertDumpContains(final String dump, final String message) {
- assertTrue(String.format("dump(%s) does not contain '%s'", dump, message),
- dump.contains(message));
- }
-
- private String getDump() {
- final StringWriter sw = new StringWriter();
- mUpdater.dump(new IndentingPrintWriter(new PrintWriter(sw), " "));
- return sw.toString();
- }
-
- @Test
- public void testDump() throws ErrnoException {
- mBpfMap.updateEntry(new S32(TEST_INDEX), new InterfaceMapValue(TEST_INTERFACE_NAME));
- mBpfMap.updateEntry(new S32(TEST_INDEX2), new InterfaceMapValue(TEST_INTERFACE_NAME2));
-
- final String dump = getDump();
- assertDumpContains(dump, "IfaceIndexNameMap: OK");
- assertDumpContains(dump, "ifaceIndex=1 ifaceName=test1");
- assertDumpContains(dump, "ifaceIndex=2 ifaceName=test2");
- }
-}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 7a4dfed..2be74db 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -64,9 +64,12 @@
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static android.text.format.DateUtils.WEEK_IN_MILLIS;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doThrow;
import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_RAT_CHANGED;
import static com.android.server.net.NetworkStatsEventLogger.PollEvent.pollReasonNameOf;
import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_POLL;
+import static com.android.server.net.NetworkStatsService.NETSTATS_FASTDATAINPUT_FALLBACKS_COUNTER_NAME;
+import static com.android.server.net.NetworkStatsService.NETSTATS_FASTDATAINPUT_SUCCESSES_COUNTER_NAME;
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;
@@ -83,6 +86,7 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -95,6 +99,7 @@
import android.app.AlarmManager;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.net.DataUsageRequest;
@@ -122,12 +127,15 @@
import android.os.IBinder;
import android.os.Looper;
import android.os.PowerManager;
+import android.os.Process;
import android.os.SimpleClock;
+import android.os.UserHandle;
import android.provider.Settings;
import android.system.ErrnoException;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.ArrayMap;
+import android.util.IndentingPrintWriter;
import android.util.Pair;
import androidx.annotation.Nullable;
@@ -169,7 +177,6 @@
import java.io.File;
import java.io.FileDescriptor;
-import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.file.Files;
@@ -242,6 +249,7 @@
private static @Mock WifiInfo sWifiInfo;
private @Mock INetd mNetd;
private @Mock TetheringManager mTetheringManager;
+ private @Mock PackageManager mPm;
private @Mock NetworkStatsFactory mStatsFactory;
@NonNull
private final TestNetworkStatsSettings mSettings =
@@ -251,7 +259,7 @@
private @Mock AlarmManager mAlarmManager;
@Mock
private NetworkStatsSubscriptionsMonitor mNetworkStatsSubscriptionsMonitor;
- private @Mock BpfInterfaceMapUpdater mBpfInterfaceMapUpdater;
+ private @Mock BpfInterfaceMapHelper mBpfInterfaceMapHelper;
private HandlerThread mHandlerThread;
@Mock
private LocationPermissionChecker mLocationPermissionChecker;
@@ -284,9 +292,14 @@
private @Mock PersistentInt mImportLegacyAttemptsCounter;
private @Mock PersistentInt mImportLegacySuccessesCounter;
private @Mock PersistentInt mImportLegacyFallbacksCounter;
+ private int mFastDataInputTargetAttempts = 0;
+ private @Mock PersistentInt mFastDataInputSuccessesCounter;
+ private @Mock PersistentInt mFastDataInputFallbacksCounter;
+ private String mCompareStatsResult = null;
private @Mock Resources mResources;
private Boolean mIsDebuggable;
private HandlerThread mObserverHandlerThread;
+ final TestDependencies mDeps = new TestDependencies();
private class MockContext extends BroadcastInterceptingContext {
private final Context mBaseContext;
@@ -297,6 +310,16 @@
}
@Override
+ public PackageManager getPackageManager() {
+ return mPm;
+ }
+
+ @Override
+ public Context createContextAsUser(UserHandle user, int flags) {
+ return this;
+ }
+
+ @Override
public Object getSystemService(String name) {
if (Context.TELEPHONY_SERVICE.equals(name)) return mTelephonyManager;
if (Context.TETHERING_SERVICE.equals(name)) return mTetheringManager;
@@ -369,7 +392,6 @@
powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
mHandlerThread = new HandlerThread("NetworkStatsServiceTest-HandlerThread");
- final NetworkStatsService.Dependencies deps = makeDependencies();
// Create a separate thread for observers to run on. This thread cannot be the same
// as the handler thread, because the observer callback is fired on this thread, and
// it should not be blocked by client code. Additionally, creating the observers
@@ -384,7 +406,7 @@
}
};
mService = new NetworkStatsService(mServiceContext, mNetd, mAlarmManager, wakeLock,
- mClock, mSettings, mStatsFactory, statsObservers, deps);
+ mClock, mSettings, mStatsFactory, statsObservers, mDeps);
mElapsedRealtime = 0L;
@@ -420,135 +442,155 @@
any(), tetheringEventCbCaptor.capture());
mTetheringEventCallback = tetheringEventCbCaptor.getValue();
+ doReturn(Process.myUid()).when(mPm)
+ .getPackageUid(eq(mServiceContext.getPackageName()), anyInt());
+
mUsageCallback = new TestableUsageCallback(mUsageCallbackBinder);
}
- @NonNull
- private NetworkStatsService.Dependencies makeDependencies() {
- return new NetworkStatsService.Dependencies() {
- @Override
- public File getLegacyStatsDir() {
- return mLegacyStatsDir;
- }
+ class TestDependencies extends NetworkStatsService.Dependencies {
+ private int mCompareStatsInvocation = 0;
- @Override
- public File getOrCreateStatsDir() {
- return mStatsDir;
- }
+ @Override
+ public File getLegacyStatsDir() {
+ return mLegacyStatsDir;
+ }
- @Override
- public boolean getStoreFilesInApexData() {
- return mStoreFilesInApexData;
- }
+ @Override
+ public File getOrCreateStatsDir() {
+ return mStatsDir;
+ }
- @Override
- public int getImportLegacyTargetAttempts() {
- return mImportLegacyTargetAttempts;
- }
+ @Override
+ public boolean getStoreFilesInApexData() {
+ return mStoreFilesInApexData;
+ }
- @Override
- 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
+ public int getImportLegacyTargetAttempts() {
+ return mImportLegacyTargetAttempts;
+ }
- @Override
- public NetworkStatsCollection readPlatformCollection(
- @NonNull String prefix, long bucketDuration) {
- return mPlatformNetworkStatsCollection.get(prefix);
- }
+ @Override
+ public int getUseFastDataInputTargetAttempts() {
+ return mFastDataInputTargetAttempts;
+ }
- @Override
- public HandlerThread makeHandlerThread() {
- return mHandlerThread;
- }
+ @Override
+ public String compareStats(NetworkStatsCollection a, NetworkStatsCollection b,
+ boolean allowKeyChange) {
+ mCompareStatsInvocation++;
+ return mCompareStatsResult;
+ }
- @Override
- public NetworkStatsSubscriptionsMonitor makeSubscriptionsMonitor(
- @NonNull Context context, @NonNull Executor executor,
- @NonNull NetworkStatsService service) {
+ int getCompareStatsInvocation() {
+ return mCompareStatsInvocation;
+ }
- return mNetworkStatsSubscriptionsMonitor;
+ @Override
+ public PersistentInt createPersistentCounter(@NonNull Path dir, @NonNull String name) {
+ 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;
+ case NETSTATS_FASTDATAINPUT_SUCCESSES_COUNTER_NAME:
+ return mFastDataInputSuccessesCounter;
+ case NETSTATS_FASTDATAINPUT_FALLBACKS_COUNTER_NAME:
+ return mFastDataInputFallbacksCounter;
+ default:
+ throw new IllegalArgumentException("Unknown counter name: " + name);
}
+ }
- @Override
- public ContentObserver makeContentObserver(Handler handler,
- NetworkStatsSettings settings, NetworkStatsSubscriptionsMonitor monitor) {
- mHandler = handler;
- return mContentObserver = super.makeContentObserver(handler, settings, monitor);
- }
+ @Override
+ public NetworkStatsCollection readPlatformCollection(
+ @NonNull String prefix, long bucketDuration) {
+ return mPlatformNetworkStatsCollection.get(prefix);
+ }
- @Override
- public LocationPermissionChecker makeLocationPermissionChecker(final Context context) {
- return mLocationPermissionChecker;
- }
+ @Override
+ public HandlerThread makeHandlerThread() {
+ return mHandlerThread;
+ }
- @Override
- public BpfInterfaceMapUpdater makeBpfInterfaceMapUpdater(
- @NonNull Context ctx, @NonNull Handler handler) {
- return mBpfInterfaceMapUpdater;
- }
+ @Override
+ public NetworkStatsSubscriptionsMonitor makeSubscriptionsMonitor(
+ @NonNull Context context, @NonNull Executor executor,
+ @NonNull NetworkStatsService service) {
- @Override
- public IBpfMap<S32, U8> getUidCounterSetMap() {
- return mUidCounterSetMap;
- }
+ return mNetworkStatsSubscriptionsMonitor;
+ }
- @Override
- public IBpfMap<CookieTagMapKey, CookieTagMapValue> getCookieTagMap() {
- return mCookieTagMap;
- }
+ @Override
+ public ContentObserver makeContentObserver(Handler handler,
+ NetworkStatsSettings settings, NetworkStatsSubscriptionsMonitor monitor) {
+ mHandler = handler;
+ return mContentObserver = super.makeContentObserver(handler, settings, monitor);
+ }
- @Override
- public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapA() {
- return mStatsMapA;
- }
+ @Override
+ public LocationPermissionChecker makeLocationPermissionChecker(final Context context) {
+ return mLocationPermissionChecker;
+ }
- @Override
- public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapB() {
- return mStatsMapB;
- }
+ @Override
+ public BpfInterfaceMapHelper makeBpfInterfaceMapHelper() {
+ return mBpfInterfaceMapHelper;
+ }
- @Override
- public IBpfMap<UidStatsMapKey, StatsMapValue> getAppUidStatsMap() {
- return mAppUidStatsMap;
- }
+ @Override
+ public IBpfMap<S32, U8> getUidCounterSetMap() {
+ return mUidCounterSetMap;
+ }
- @Override
- public IBpfMap<S32, StatsMapValue> getIfaceStatsMap() {
- return mIfaceStatsMap;
- }
+ @Override
+ public IBpfMap<CookieTagMapKey, CookieTagMapValue> getCookieTagMap() {
+ return mCookieTagMap;
+ }
- @Override
- public boolean isDebuggable() {
- return mIsDebuggable == Boolean.TRUE;
- }
+ @Override
+ public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapA() {
+ return mStatsMapA;
+ }
- @Override
- public BpfNetMaps makeBpfNetMaps(Context ctx) {
- return mBpfNetMaps;
- }
+ @Override
+ public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapB() {
+ return mStatsMapB;
+ }
- @Override
- public SkDestroyListener makeSkDestroyListener(
- IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap, Handler handler) {
- return mSkDestroyListener;
- }
+ @Override
+ public IBpfMap<UidStatsMapKey, StatsMapValue> getAppUidStatsMap() {
+ return mAppUidStatsMap;
+ }
- @Override
- public boolean supportEventLogger(@NonNull Context cts) {
- return true;
- }
- };
+ @Override
+ public IBpfMap<S32, StatsMapValue> getIfaceStatsMap() {
+ return mIfaceStatsMap;
+ }
+
+ @Override
+ public boolean isDebuggable() {
+ return mIsDebuggable == Boolean.TRUE;
+ }
+
+ @Override
+ public BpfNetMaps makeBpfNetMaps(Context ctx) {
+ return mBpfNetMaps;
+ }
+
+ @Override
+ public SkDestroyListener makeSkDestroyListener(
+ IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap, Handler handler) {
+ return mSkDestroyListener;
+ }
+
+ @Override
+ public boolean supportEventLogger(@NonNull Context cts) {
+ return true;
+ }
}
@After
@@ -1568,7 +1610,7 @@
// Register and verify request and that binder was called
DataUsageRequest request = mService.registerUsageCallback(
- mServiceContext.getOpPackageName(), inputRequest, mUsageCallback);
+ mServiceContext.getPackageName(), inputRequest, mUsageCallback);
assertTrue(request.requestId > 0);
assertTrue(Objects.equals(sTemplateWifi, request.template));
long minThresholdInBytes = 2 * 1024 * 1024; // 2 MB
@@ -2166,6 +2208,71 @@
}
@Test
+ public void testAdoptFastDataInput_featureDisabled() throws Exception {
+ // Boot through serviceReady() with flag disabled, verify the persistent
+ // counters are not increased.
+ mFastDataInputTargetAttempts = 0;
+ doReturn(0).when(mFastDataInputSuccessesCounter).get();
+ doReturn(0).when(mFastDataInputFallbacksCounter).get();
+ mService.systemReady();
+ verify(mFastDataInputSuccessesCounter, never()).set(anyInt());
+ verify(mFastDataInputFallbacksCounter, never()).set(anyInt());
+ assertEquals(0, mDeps.getCompareStatsInvocation());
+ }
+
+ @Test
+ public void testAdoptFastDataInput_noRetryAfterFail() throws Exception {
+ // Boot through serviceReady(), verify the service won't retry unexpectedly
+ // since the target attempt remains the same.
+ mFastDataInputTargetAttempts = 1;
+ doReturn(0).when(mFastDataInputSuccessesCounter).get();
+ doReturn(1).when(mFastDataInputFallbacksCounter).get();
+ mService.systemReady();
+ verify(mFastDataInputSuccessesCounter, never()).set(anyInt());
+ verify(mFastDataInputFallbacksCounter, never()).set(anyInt());
+ }
+
+ @Test
+ public void testAdoptFastDataInput_noRetryAfterSuccess() throws Exception {
+ // Boot through serviceReady(), verify the service won't retry unexpectedly
+ // since the target attempt remains the same.
+ mFastDataInputTargetAttempts = 1;
+ doReturn(1).when(mFastDataInputSuccessesCounter).get();
+ doReturn(0).when(mFastDataInputFallbacksCounter).get();
+ mService.systemReady();
+ verify(mFastDataInputSuccessesCounter, never()).set(anyInt());
+ verify(mFastDataInputFallbacksCounter, never()).set(anyInt());
+ }
+
+ @Test
+ public void testAdoptFastDataInput_hasDiff() throws Exception {
+ // Boot through serviceReady() with flag enabled and assumes the stats are
+ // failed to compare, verify the fallbacks counter is increased.
+ mockDefaultSettings();
+ doReturn(0).when(mFastDataInputSuccessesCounter).get();
+ doReturn(0).when(mFastDataInputFallbacksCounter).get();
+ mFastDataInputTargetAttempts = 1;
+ mCompareStatsResult = "Has differences";
+ mService.systemReady();
+ verify(mFastDataInputSuccessesCounter, never()).set(anyInt());
+ verify(mFastDataInputFallbacksCounter).set(1);
+ }
+
+ @Test
+ public void testAdoptFastDataInput_noDiff() throws Exception {
+ // Boot through serviceReady() with target attempts increased,
+ // assumes there was a previous failure,
+ // and assumes the stats are successfully compared,
+ // verify the successes counter is increased.
+ mFastDataInputTargetAttempts = 2;
+ doReturn(1).when(mFastDataInputFallbacksCounter).get();
+ mCompareStatsResult = null;
+ mService.systemReady();
+ verify(mFastDataInputSuccessesCounter).set(1);
+ verify(mFastDataInputFallbacksCounter, never()).set(anyInt());
+ }
+
+ @Test
public void testStatsFactoryRemoveUids() throws Exception {
// pretend that network comes online
mockDefaultSettings();
@@ -2230,7 +2337,8 @@
final DropBoxManager dropBox = mock(DropBoxManager.class);
return new NetworkStatsRecorder(new FileRotator(
directory, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
- observer, dropBox, prefix, config.bucketDuration, includeTags, wipeOnError);
+ observer, dropBox, prefix, config.bucketDuration, includeTags, wipeOnError,
+ false /* useFastDataInput */, directory);
}
private NetworkStatsCollection getLegacyCollection(String prefix, boolean includeTags) {
@@ -2673,13 +2781,13 @@
@Test
public void testDumpStatsMap() throws ErrnoException {
- doReturn("wlan0").when(mBpfInterfaceMapUpdater).getIfNameByIndex(10 /* index */);
+ doReturn("wlan0").when(mBpfInterfaceMapHelper).getIfNameByIndex(10 /* index */);
doTestDumpStatsMap("wlan0");
}
@Test
public void testDumpStatsMapUnknownInterface() throws ErrnoException {
- doReturn(null).when(mBpfInterfaceMapUpdater).getIfNameByIndex(10 /* index */);
+ doReturn(null).when(mBpfInterfaceMapHelper).getIfNameByIndex(10 /* index */);
doTestDumpStatsMap("unknown");
}
@@ -2694,13 +2802,13 @@
@Test
public void testDumpIfaceStatsMap() throws Exception {
- doReturn("wlan0").when(mBpfInterfaceMapUpdater).getIfNameByIndex(10 /* index */);
+ doReturn("wlan0").when(mBpfInterfaceMapHelper).getIfNameByIndex(10 /* index */);
doTestDumpIfaceStatsMap("wlan0");
}
@Test
public void testDumpIfaceStatsMapUnknownInterface() throws Exception {
- doReturn(null).when(mBpfInterfaceMapUpdater).getIfNameByIndex(10 /* index */);
+ doReturn(null).when(mBpfInterfaceMapHelper).getIfNameByIndex(10 /* index */);
doTestDumpIfaceStatsMap("unknown");
}
@@ -2713,4 +2821,48 @@
final String dump = getDump();
assertDumpContains(dump, pollReasonNameOf(POLL_REASON_RAT_CHANGED));
}
+
+ @Test
+ public void testEnforcePackageNameMatchesUid() throws Exception {
+ final String testMyPackageName = "test.package.myname";
+ final String testRedPackageName = "test.package.red";
+ final String testInvalidPackageName = "test.package.notfound";
+
+ doReturn(UID_RED).when(mPm).getPackageUid(eq(testRedPackageName), anyInt());
+ doReturn(Process.myUid()).when(mPm).getPackageUid(eq(testMyPackageName), anyInt());
+ doThrow(new PackageManager.NameNotFoundException()).when(mPm)
+ .getPackageUid(eq(testInvalidPackageName), anyInt());
+
+ assertThrows(SecurityException.class, () ->
+ mService.openSessionForUsageStats(0 /* flags */, testRedPackageName));
+ assertThrows(SecurityException.class, () ->
+ mService.openSessionForUsageStats(0 /* flags */, testInvalidPackageName));
+ assertThrows(NullPointerException.class, () ->
+ mService.openSessionForUsageStats(0 /* flags */, null));
+ // Verify package name belongs to ourselves does not throw.
+ mService.openSessionForUsageStats(0 /* flags */, testMyPackageName);
+
+ long thresholdInBytes = 10 * 1024 * 1024; // 10 MB
+ DataUsageRequest request = new DataUsageRequest(
+ 2 /* requestId */, sTemplateImsi1, thresholdInBytes);
+ assertThrows(SecurityException.class, () ->
+ mService.registerUsageCallback(testRedPackageName, request, mUsageCallback));
+ assertThrows(SecurityException.class, () ->
+ mService.registerUsageCallback(testInvalidPackageName, request, mUsageCallback));
+ assertThrows(NullPointerException.class, () ->
+ mService.registerUsageCallback(null, request, mUsageCallback));
+ mService.registerUsageCallback(testMyPackageName, request, mUsageCallback);
+ }
+
+ @Test
+ public void testDumpSkDestroyListenerLogs() throws ErrnoException {
+ doAnswer((invocation) -> {
+ final IndentingPrintWriter ipw = (IndentingPrintWriter) invocation.getArgument(0);
+ ipw.println("Log for testing");
+ return null;
+ }).when(mSkDestroyListener).dump(any());
+
+ final String dump = getDump();
+ assertDumpContains(dump, "Log for testing");
+ }
}
diff --git a/tests/unit/java/com/android/server/net/SkDestroyListenerTest.kt b/tests/unit/java/com/android/server/net/SkDestroyListenerTest.kt
new file mode 100644
index 0000000..18785e5
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/SkDestroyListenerTest.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net
+
+import android.os.Handler
+import android.os.HandlerThread
+import com.android.net.module.util.SharedLog
+import com.android.testutils.DevSdkIgnoreRunner
+import java.io.PrintWriter
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mock
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+class SkDestroyListenerTest {
+ @Mock lateinit var sharedLog: SharedLog
+ val handlerThread = HandlerThread("SkDestroyListenerTest")
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ handlerThread.start()
+ }
+
+ @After
+ fun tearDown() {
+ handlerThread.quitSafely()
+ handlerThread.join()
+ }
+
+ @Test
+ fun testDump() {
+ doReturn(sharedLog).`when`(sharedLog).forSubComponent(any())
+
+ val handler = Handler(handlerThread.looper)
+ val skDestroylistener = SkDestroyListener(null /* cookieTagMap */, handler, sharedLog)
+ val pw = PrintWriter(System.out)
+ skDestroylistener.dump(pw)
+
+ verify(sharedLog).reverseDump(pw)
+ }
+}
diff --git a/tests/unit/java/com/android/server/net/TrafficStatsRateLimitCacheTest.kt b/tests/unit/java/com/android/server/net/TrafficStatsRateLimitCacheTest.kt
new file mode 100644
index 0000000..27e6f96
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/TrafficStatsRateLimitCacheTest.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net
+
+import android.net.NetworkStats
+import com.android.testutils.DevSdkIgnoreRunner
+import java.time.Clock
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.`when`
+
+@RunWith(DevSdkIgnoreRunner::class)
+class TrafficStatsRateLimitCacheTest {
+ companion object {
+ private const val expiryDurationMs = 1000L
+ }
+
+ private val clock = mock(Clock::class.java)
+ private val entry = mock(NetworkStats.Entry::class.java)
+ private val cache = TrafficStatsRateLimitCache(clock, expiryDurationMs)
+
+ @Test
+ fun testGet_returnsEntryIfNotExpired() {
+ cache.put("iface", 2, entry)
+ `when`(clock.millis()).thenReturn(500L) // Set clock to before expiry
+ val result = cache.get("iface", 2)
+ assertEquals(entry, result)
+ }
+
+ @Test
+ fun testGet_returnsNullIfExpired() {
+ cache.put("iface", 2, entry)
+ `when`(clock.millis()).thenReturn(2000L) // Set clock to after expiry
+ assertNull(cache.get("iface", 2))
+ }
+
+ @Test
+ fun testGet_returnsNullForNonExistentKey() {
+ val result = cache.get("otherIface", 99)
+ assertNull(result)
+ }
+
+ @Test
+ fun testPutAndGet_retrievesCorrectEntryForDifferentKeys() {
+ val entry1 = mock(NetworkStats.Entry::class.java)
+ val entry2 = mock(NetworkStats.Entry::class.java)
+
+ cache.put("iface1", 2, entry1)
+ cache.put("iface2", 4, entry2)
+
+ assertEquals(entry1, cache.get("iface1", 2))
+ assertEquals(entry2, cache.get("iface2", 4))
+ }
+
+ @Test
+ fun testPut_overridesExistingEntry() {
+ val entry1 = mock(NetworkStats.Entry::class.java)
+ val entry2 = mock(NetworkStats.Entry::class.java)
+
+ cache.put("iface", 2, entry1)
+ cache.put("iface", 2, entry2) // Put with the same key
+
+ assertEquals(entry2, cache.get("iface", 2))
+ }
+
+ @Test
+ fun testClear() {
+ cache.put("iface", 2, entry)
+ cache.clear()
+ assertNull(cache.get("iface", 2))
+ }
+}
diff --git a/tests/unit/jni/Android.bp b/tests/unit/jni/Android.bp
index 616da81..57a157d 100644
--- a/tests/unit/jni/Android.bp
+++ b/tests/unit/jni/Android.bp
@@ -1,4 +1,5 @@
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/tests/unit/vpn-jarjar-rules.txt b/tests/unit/vpn-jarjar-rules.txt
index 1a6bddc..f74eab8 100644
--- a/tests/unit/vpn-jarjar-rules.txt
+++ b/tests/unit/vpn-jarjar-rules.txt
@@ -1,4 +1,2 @@
# Only keep classes imported by ConnectivityServiceTest
-keep com.android.server.connectivity.Vpn
keep com.android.server.connectivity.VpnProfileStore
-keep com.android.server.net.LockdownVpnTracker
diff --git a/thread/TEST_MAPPING b/thread/TEST_MAPPING
index 30aeca5..ebbb9af 100644
--- a/thread/TEST_MAPPING
+++ b/thread/TEST_MAPPING
@@ -2,11 +2,14 @@
"presubmit": [
{
"name": "CtsThreadNetworkTestCases"
+ },
+ {
+ "name": "ThreadNetworkUnitTests"
+ },
+ {
+ "name": "ThreadNetworkIntegrationTests"
}
],
"postsubmit": [
- {
- "name": "ThreadNetworkUnitTests"
- }
]
}
diff --git a/thread/apex/Android.bp b/thread/apex/Android.bp
index 28854f2..edf000a 100644
--- a/thread/apex/Android.bp
+++ b/thread/apex/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_thread_network",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/thread/apex/ot-daemon.34rc b/thread/apex/ot-daemon.34rc
index 1eb1294..25060d1 100644
--- a/thread/apex/ot-daemon.34rc
+++ b/thread/apex/ot-daemon.34rc
@@ -21,4 +21,5 @@
user thread_network
group thread_network inet system
seclabel u:r:ot_daemon:s0
+ socket ot-daemon/thread-wpan.sock stream 0666 thread_network thread_network
override
diff --git a/thread/demoapp/Android.bp b/thread/demoapp/Android.bp
index da7a5f8..fcfd469 100644
--- a/thread/demoapp/Android.bp
+++ b/thread/demoapp/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_thread_network",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/thread/framework/Android.bp b/thread/framework/Android.bp
index cc598d8..846253c 100644
--- a/thread/framework/Android.bp
+++ b/thread/framework/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_thread_network",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/thread/framework/java/android/net/thread/IStateCallback.aidl b/thread/framework/java/android/net/thread/IStateCallback.aidl
index d7cbda9..9d0a571 100644
--- a/thread/framework/java/android/net/thread/IStateCallback.aidl
+++ b/thread/framework/java/android/net/thread/IStateCallback.aidl
@@ -22,4 +22,5 @@
oneway interface IStateCallback {
void onDeviceRoleChanged(int deviceRole);
void onPartitionIdChanged(long partitionId);
+ void onThreadEnableStateChanged(int enabledState);
}
diff --git a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
index 89dcd39..485e25d 100644
--- a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
+++ b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
@@ -38,6 +38,10 @@
void scheduleMigration(in PendingOperationalDataset pendingOpDataset, in IOperationReceiver receiver);
void leave(in IOperationReceiver receiver);
+ void setTestNetworkAsUpstream(in String testNetworkInterfaceName, in IOperationReceiver receiver);
+
int getThreadVersion();
void createRandomizedDataset(String networkName, IActiveOperationalDatasetReceiver receiver);
+
+ void setEnabled(boolean enabled, in IOperationReceiver receiver);
}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index 34b0b06..db761a3 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -31,6 +31,7 @@
import android.os.RemoteException;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -67,6 +68,15 @@
/** The device is a Thread Leader. */
public static final int DEVICE_ROLE_LEADER = 4;
+ /** The Thread radio is disabled. */
+ public static final int STATE_DISABLED = 0;
+
+ /** The Thread radio is enabled. */
+ public static final int STATE_ENABLED = 1;
+
+ /** The Thread radio is being disabled. */
+ public static final int STATE_DISABLING = 2;
+
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef({
@@ -78,6 +88,13 @@
})
public @interface DeviceRole {}
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ prefix = {"STATE_"},
+ value = {STATE_DISABLED, STATE_ENABLED, STATE_DISABLING})
+ public @interface EnabledState {}
+
/** Thread standard version 1.3. */
public static final int THREAD_VERSION_1_3 = 4;
@@ -105,6 +122,40 @@
mControllerService = controllerService;
}
+ /**
+ * Enables/Disables the radio of this ThreadNetworkController. The requested enabled state will
+ * be persistent and survives device reboots.
+ *
+ * <p>When Thread is in {@code STATE_DISABLED}, {@link ThreadNetworkController} APIs which
+ * require the Thread radio will fail with error code {@link
+ * ThreadNetworkException#ERROR_THREAD_DISABLED}. When Thread is in {@code STATE_DISABLING},
+ * {@link ThreadNetworkController} APIs that return a {@link ThreadNetworkException} will fail
+ * with error code {@link ThreadNetworkException#ERROR_BUSY}.
+ *
+ * <p>On success, {@link OutcomeReceiver#onResult} of {@code receiver} is called. It indicates
+ * the operation has completed. But there maybe subsequent calls to update the enabled state,
+ * callers of this method should use {@link #registerStateCallback} to subscribe to the Thread
+ * enabled state changes.
+ *
+ * <p>On failure, {@link OutcomeReceiver#onError} of {@code receiver} will be invoked with a
+ * specific error in {@link ThreadNetworkException#ERROR_}.
+ *
+ * @param enabled {@code true} for enabling Thread
+ * @param executor the executor to execute {@code receiver}
+ * @param receiver the receiver to receive result of this operation
+ */
+ @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+ public void setEnabled(
+ boolean enabled,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+ try {
+ mControllerService.setEnabled(enabled, new OperationReceiverProxy(executor, receiver));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
/** Returns the Thread version this device is operating on. */
@ThreadVersion
public int getThreadVersion() {
@@ -169,6 +220,16 @@
* @param partitionId the new Thread partition ID
*/
default void onPartitionIdChanged(long partitionId) {}
+
+ /**
+ * The Thread enabled state has changed.
+ *
+ * <p>The Thread enabled state can be set with {@link setEnabled}, it may also be updated by
+ * airplane mode or admin control.
+ *
+ * @param enabledState the new Thread enabled state
+ */
+ default void onThreadEnableStateChanged(@EnabledState int enabledState) {}
}
private static final class StateCallbackProxy extends IStateCallback.Stub {
@@ -199,6 +260,16 @@
Binder.restoreCallingIdentity(identity);
}
}
+
+ @Override
+ public void onThreadEnableStateChanged(@EnabledState int enabled) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mCallback.onThreadEnableStateChanged(enabled));
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
}
/**
@@ -499,6 +570,32 @@
}
}
+ /**
+ * Sets to use a specified test network as the upstream.
+ *
+ * @param testNetworkInterfaceName The name of the test network interface. When it's null,
+ * forbids using test network as an upstream.
+ * @param executor the executor to execute {@code receiver}
+ * @param receiver the receiver to receive result of this operation
+ * @hide
+ */
+ @VisibleForTesting
+ @RequiresPermission(
+ allOf = {"android.permission.THREAD_NETWORK_PRIVILEGED", permission.NETWORK_SETTINGS})
+ public void setTestNetworkAsUpstream(
+ @Nullable String testNetworkInterfaceName,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+ requireNonNull(executor, "executor cannot be null");
+ requireNonNull(receiver, "receiver cannot be null");
+ try {
+ mControllerService.setTestNetworkAsUpstream(
+ testNetworkInterfaceName, new OperationReceiverProxy(executor, receiver));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
private static <T> void propagateError(
Executor executor,
OutcomeReceiver<T, ThreadNetworkException> receiver,
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkException.java b/thread/framework/java/android/net/thread/ThreadNetworkException.java
index c5e1e97..4def0fb 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkException.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkException.java
@@ -47,6 +47,8 @@
ERROR_REJECTED_BY_PEER,
ERROR_RESPONSE_BAD_FORMAT,
ERROR_RESOURCE_EXHAUSTED,
+ ERROR_UNKNOWN,
+ ERROR_THREAD_DISABLED,
})
public @interface ErrorCode {}
@@ -87,8 +89,9 @@
/**
* The operation failed because required preconditions were not satisfied. For example, trying
- * to schedule a network migration when this device is not attached will receive this error. The
- * caller should not retry the same operation before the precondition is satisfied.
+ * to schedule a network migration when this device is not attached will receive this error or
+ * enable Thread while User Resitration has disabled it. The caller should not retry the same
+ * operation before the precondition is satisfied.
*/
public static final int ERROR_FAILED_PRECONDITION = 6;
@@ -122,11 +125,42 @@
*/
public static final int ERROR_RESOURCE_EXHAUSTED = 10;
+ /**
+ * The operation failed because of an unknown error in the system. This typically indicates that
+ * the caller doesn't understand error codes added in newer Android versions.
+ */
+ public static final int ERROR_UNKNOWN = 11;
+
+ /**
+ * The operation failed because the Thread radio is disabled by {@link
+ * ThreadNetworkController#setEnabled}, airplane mode or device admin. The caller should retry
+ * only after Thread is enabled.
+ */
+ public static final int ERROR_THREAD_DISABLED = 12;
+
+ private static final int ERROR_MIN = ERROR_INTERNAL_ERROR;
+ private static final int ERROR_MAX = ERROR_THREAD_DISABLED;
+
private final int mErrorCode;
- /** Creates a new {@link ThreadNetworkException} object with given error code and message. */
- public ThreadNetworkException(@ErrorCode int errorCode, @NonNull String errorMessage) {
- super(requireNonNull(errorMessage, "errorMessage cannot be null"));
+ /**
+ * Creates a new {@link ThreadNetworkException} object with given error code and message.
+ *
+ * @throws IllegalArgumentException if {@code errorCode} is not a value in {@link #ERROR_}
+ * @throws NullPointerException if {@code message} is {@code null}
+ */
+ public ThreadNetworkException(@ErrorCode int errorCode, @NonNull String message) {
+ super(requireNonNull(message, "message cannot be null"));
+ if (errorCode < ERROR_MIN || errorCode > ERROR_MAX) {
+ throw new IllegalArgumentException(
+ "errorCode cannot be "
+ + errorCode
+ + " (allowedRange = ["
+ + ERROR_MIN
+ + ", "
+ + ERROR_MAX
+ + "])");
+ }
this.mErrorCode = errorCode;
}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkManager.java b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
index 28012a7..150b759 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkManager.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
@@ -79,6 +79,17 @@
public static final String PERMISSION_THREAD_NETWORK_PRIVILEGED =
"android.permission.THREAD_NETWORK_PRIVILEGED";
+ /**
+ * This user restriction specifies if Thread network is disallowed on the device. If Thread
+ * network is disallowed it cannot be turned on via Settings.
+ *
+ * <p>this is a mirror of {@link UserManager#DISALLOW_THREAD_NETWORK} which is not available on
+ * Android U devices.
+ *
+ * @hide
+ */
+ public static final String DISALLOW_THREAD_NETWORK = "no_thread_network";
+
@NonNull private final Context mContext;
@NonNull private final List<ThreadNetworkController> mUnmodifiableControllerServices;
diff --git a/thread/scripts/make-pretty.sh b/thread/scripts/make-pretty.sh
index e4bd459..c176bfa 100755
--- a/thread/scripts/make-pretty.sh
+++ b/thread/scripts/make-pretty.sh
@@ -3,5 +3,7 @@
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
GOOGLE_JAVA_FORMAT=$SCRIPT_DIR/../../../../../prebuilts/tools/common/google-java-format/google-java-format
+ANDROID_BP_FORMAT=$SCRIPT_DIR/../../../../../prebuilts/build-tools/linux-x86/bin/bpfmt
$GOOGLE_JAVA_FORMAT --aosp -i $(find $SCRIPT_DIR/../ -name "*.java")
+$ANDROID_BP_FORMAT -w $(find $SCRIPT_DIR/../ -name "*.bp")
diff --git a/thread/service/Android.bp b/thread/service/Android.bp
index 35ae3c2..6e2fac1 100644
--- a/thread/service/Android.bp
+++ b/thread/service/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_thread_network",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -35,17 +36,18 @@
libs: [
"framework-connectivity-pre-jarjar",
"framework-connectivity-t-pre-jarjar",
+ "framework-location.stubs.module_lib",
+ "framework-wifi",
"service-connectivity-pre-jarjar",
+ "ServiceConnectivityResources",
],
static_libs: [
+ "modules-utils-shell-command-handler",
"net-utils-device-common",
"net-utils-device-common-netlink",
"ot-daemon-aidl-java",
],
apex_available: ["com.android.tethering"],
- optimize: {
- proguard_flags_files: ["proguard.flags"],
- },
}
cc_library_shared {
diff --git a/thread/service/java/com/android/server/thread/ActiveOperationalDatasetReceiverWrapper.java b/thread/service/java/com/android/server/thread/ActiveOperationalDatasetReceiverWrapper.java
new file mode 100644
index 0000000..e3b4e1a
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ActiveOperationalDatasetReceiverWrapper.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.thread;
+
+import static android.net.thread.ThreadNetworkException.ERROR_UNAVAILABLE;
+
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.IActiveOperationalDatasetReceiver;
+import android.os.RemoteException;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A {@link IActiveOperationalDatasetReceiver} wrapper which makes it easier to invoke the
+ * callbacks.
+ */
+final class ActiveOperationalDatasetReceiverWrapper {
+ private final IActiveOperationalDatasetReceiver mReceiver;
+
+ private static final Object sPendingReceiversLock = new Object();
+
+ @GuardedBy("sPendingReceiversLock")
+ private static final Set<ActiveOperationalDatasetReceiverWrapper> sPendingReceivers =
+ new HashSet<>();
+
+ public ActiveOperationalDatasetReceiverWrapper(IActiveOperationalDatasetReceiver receiver) {
+ this.mReceiver = receiver;
+
+ synchronized (sPendingReceiversLock) {
+ sPendingReceivers.add(this);
+ }
+ }
+
+ public static void onOtDaemonDied() {
+ synchronized (sPendingReceiversLock) {
+ for (ActiveOperationalDatasetReceiverWrapper receiver : sPendingReceivers) {
+ try {
+ receiver.mReceiver.onError(ERROR_UNAVAILABLE, "Thread daemon died");
+ } catch (RemoteException e) {
+ // The client is dead, do nothing
+ }
+ }
+ sPendingReceivers.clear();
+ }
+ }
+
+ public void onSuccess(ActiveOperationalDataset dataset) {
+ synchronized (sPendingReceiversLock) {
+ sPendingReceivers.remove(this);
+ }
+
+ try {
+ mReceiver.onSuccess(dataset);
+ } catch (RemoteException e) {
+ // The client is dead, do nothing
+ }
+ }
+
+ public void onError(int errorCode, String errorMessage) {
+ synchronized (sPendingReceiversLock) {
+ sPendingReceivers.remove(this);
+ }
+
+ try {
+ mReceiver.onError(errorCode, errorMessage);
+ } catch (RemoteException e) {
+ // The client is dead, do nothing
+ }
+ }
+}
diff --git a/thread/service/java/com/android/server/thread/InfraInterfaceController.java b/thread/service/java/com/android/server/thread/InfraInterfaceController.java
index d7c49a0..be54cbc 100644
--- a/thread/service/java/com/android/server/thread/InfraInterfaceController.java
+++ b/thread/service/java/com/android/server/thread/InfraInterfaceController.java
@@ -36,8 +36,7 @@
* @return an ICMPv6 socket file descriptor on the Infrastructure network interface.
* @throws IOException when fails to create the socket.
*/
- public static ParcelFileDescriptor createIcmp6Socket(String infraInterfaceName)
- throws IOException {
+ public ParcelFileDescriptor createIcmp6Socket(String infraInterfaceName) throws IOException {
return ParcelFileDescriptor.adoptFd(nativeCreateIcmp6Socket(infraInterfaceName));
}
diff --git a/thread/service/java/com/android/server/thread/NsdPublisher.java b/thread/service/java/com/android/server/thread/NsdPublisher.java
new file mode 100644
index 0000000..c74c023
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/NsdPublisher.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.thread;
+
+import static android.net.nsd.NsdManager.PROTOCOL_DNS_SD;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.net.nsd.NsdManager;
+import android.net.nsd.NsdServiceInfo;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.thread.openthread.DnsTxtAttribute;
+import com.android.server.thread.openthread.INsdPublisher;
+import com.android.server.thread.openthread.INsdStatusReceiver;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.HashSet;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Implementation of {@link INsdPublisher}.
+ *
+ * <p>This class provides API for service registration and discovery over mDNS. This class is a
+ * proxy between ot-daemon and NsdManager.
+ *
+ * <p>All the data members of this class MUST be accessed in the {@code mHandler}'s Thread except
+ * {@code mHandler} itself.
+ *
+ * <p>TODO: b/323300118 - Remove the following mechanism when the race condition in NsdManager is
+ * fixed.
+ *
+ * <p>There's always only one running registration job at any timepoint. All other pending jobs are
+ * queued in {@code mRegistrationJobs}. When a registration job is complete (i.e. the according
+ * method in {@link NsdManager.RegistrationListener} is called), it will start the next registration
+ * job in the queue.
+ */
+public final class NsdPublisher extends INsdPublisher.Stub {
+ // TODO: b/321883491 - specify network for mDNS operations
+ private static final String TAG = NsdPublisher.class.getSimpleName();
+ private final NsdManager mNsdManager;
+ private final Handler mHandler;
+ private final Executor mExecutor;
+ private final SparseArray<RegistrationListener> mRegistrationListeners = new SparseArray<>(0);
+ private final Deque<Runnable> mRegistrationJobs = new ArrayDeque<>();
+
+ @VisibleForTesting
+ public NsdPublisher(NsdManager nsdManager, Handler handler) {
+ mNsdManager = nsdManager;
+ mHandler = handler;
+ mExecutor = runnable -> mHandler.post(runnable);
+ }
+
+ public static NsdPublisher newInstance(Context context, Handler handler) {
+ return new NsdPublisher(context.getSystemService(NsdManager.class), handler);
+ }
+
+ @Override
+ public void registerService(
+ String hostname,
+ String name,
+ String type,
+ List<String> subTypeList,
+ int port,
+ List<DnsTxtAttribute> txt,
+ INsdStatusReceiver receiver,
+ int listenerId) {
+ postRegistrationJob(
+ () -> {
+ NsdServiceInfo serviceInfo =
+ buildServiceInfoForService(
+ hostname, name, type, subTypeList, port, txt);
+ registerInternal(serviceInfo, receiver, listenerId, "service");
+ });
+ }
+
+ private static NsdServiceInfo buildServiceInfoForService(
+ String hostname,
+ String name,
+ String type,
+ List<String> subTypeList,
+ int port,
+ List<DnsTxtAttribute> txt) {
+ NsdServiceInfo serviceInfo = new NsdServiceInfo();
+
+ serviceInfo.setServiceName(name);
+ if (!TextUtils.isEmpty(hostname)) {
+ serviceInfo.setHostname(hostname);
+ }
+ serviceInfo.setServiceType(type);
+ serviceInfo.setPort(port);
+ serviceInfo.setSubtypes(new HashSet<>(subTypeList));
+ for (DnsTxtAttribute attribute : txt) {
+ serviceInfo.setAttribute(attribute.name, attribute.value);
+ }
+
+ return serviceInfo;
+ }
+
+ private void registerInternal(
+ NsdServiceInfo serviceInfo,
+ INsdStatusReceiver receiver,
+ int listenerId,
+ String registrationType) {
+ checkOnHandlerThread();
+ Log.i(
+ TAG,
+ "Registering "
+ + registrationType
+ + ". Listener ID: "
+ + listenerId
+ + ", serviceInfo: "
+ + serviceInfo);
+ RegistrationListener listener = new RegistrationListener(serviceInfo, listenerId, receiver);
+ mRegistrationListeners.append(listenerId, listener);
+ try {
+ mNsdManager.registerService(serviceInfo, PROTOCOL_DNS_SD, mExecutor, listener);
+ } catch (IllegalArgumentException e) {
+ Log.i(TAG, "Failed to register service. serviceInfo: " + serviceInfo, e);
+ listener.onRegistrationFailed(serviceInfo, NsdManager.FAILURE_INTERNAL_ERROR);
+ }
+ }
+
+ public void unregister(INsdStatusReceiver receiver, int listenerId) {
+ postRegistrationJob(() -> unregisterInternal(receiver, listenerId));
+ }
+
+ public void unregisterInternal(INsdStatusReceiver receiver, int listenerId) {
+ checkOnHandlerThread();
+ RegistrationListener registrationListener = mRegistrationListeners.get(listenerId);
+ if (registrationListener == null) {
+ Log.w(
+ TAG,
+ "Failed to unregister service."
+ + " Listener ID: "
+ + listenerId
+ + " The registrationListener is empty.");
+
+ return;
+ }
+ Log.i(
+ TAG,
+ "Unregistering service."
+ + " Listener ID: "
+ + listenerId
+ + " serviceInfo: "
+ + registrationListener.mServiceInfo);
+ registrationListener.addUnregistrationReceiver(receiver);
+ mNsdManager.unregisterService(registrationListener);
+ }
+
+ private void checkOnHandlerThread() {
+ if (mHandler.getLooper().getThread() != Thread.currentThread()) {
+ throw new IllegalStateException(
+ "Not running on handler Thread: " + Thread.currentThread().getName());
+ }
+ }
+
+ /** On ot-daemon died, unregister all registrations. */
+ public void onOtDaemonDied() {
+ checkOnHandlerThread();
+ for (int i = 0; i < mRegistrationListeners.size(); ++i) {
+ try {
+ mNsdManager.unregisterService(mRegistrationListeners.valueAt(i));
+ } catch (IllegalArgumentException e) {
+ Log.i(
+ TAG,
+ "Failed to unregister."
+ + " Listener ID: "
+ + mRegistrationListeners.keyAt(i)
+ + " serviceInfo: "
+ + mRegistrationListeners.valueAt(i).mServiceInfo,
+ e);
+ }
+ }
+ mRegistrationListeners.clear();
+ }
+
+ // TODO: b/323300118 - Remove this mechanism when the race condition in NsdManager is fixed.
+ /** Fetch the first job from the queue and run it. See the class doc for more details. */
+ private void peekAndRun() {
+ if (mRegistrationJobs.isEmpty()) {
+ return;
+ }
+ Runnable job = mRegistrationJobs.getFirst();
+ job.run();
+ }
+
+ // TODO: b/323300118 - Remove this mechanism when the race condition in NsdManager is fixed.
+ /**
+ * Pop the first job from the queue and run the next job. See the class doc for more details.
+ */
+ private void popAndRunNext() {
+ if (mRegistrationJobs.isEmpty()) {
+ Log.i(TAG, "No registration jobs when trying to pop and run next.");
+ return;
+ }
+ mRegistrationJobs.removeFirst();
+ peekAndRun();
+ }
+
+ private void postRegistrationJob(Runnable registrationJob) {
+ mHandler.post(
+ () -> {
+ mRegistrationJobs.addLast(registrationJob);
+ if (mRegistrationJobs.size() == 1) {
+ peekAndRun();
+ }
+ });
+ }
+
+ private final class RegistrationListener implements NsdManager.RegistrationListener {
+ private final NsdServiceInfo mServiceInfo;
+ private final int mListenerId;
+ private final INsdStatusReceiver mRegistrationReceiver;
+ private final List<INsdStatusReceiver> mUnregistrationReceivers;
+
+ RegistrationListener(
+ @NonNull NsdServiceInfo serviceInfo,
+ int listenerId,
+ @NonNull INsdStatusReceiver registrationReceiver) {
+ mServiceInfo = serviceInfo;
+ mListenerId = listenerId;
+ mRegistrationReceiver = registrationReceiver;
+ mUnregistrationReceivers = new ArrayList<>();
+ }
+
+ void addUnregistrationReceiver(@NonNull INsdStatusReceiver unregistrationReceiver) {
+ mUnregistrationReceivers.add(unregistrationReceiver);
+ }
+
+ @Override
+ public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
+ checkOnHandlerThread();
+ mRegistrationListeners.remove(mListenerId);
+ Log.i(
+ TAG,
+ "Failed to register listener ID: "
+ + mListenerId
+ + " error code: "
+ + errorCode
+ + " serviceInfo: "
+ + serviceInfo);
+ try {
+ mRegistrationReceiver.onError(errorCode);
+ } catch (RemoteException ignored) {
+ // do nothing if the client is dead
+ }
+ popAndRunNext();
+ }
+
+ @Override
+ public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
+ checkOnHandlerThread();
+ for (INsdStatusReceiver receiver : mUnregistrationReceivers) {
+ Log.i(
+ TAG,
+ "Failed to unregister."
+ + "Listener ID: "
+ + mListenerId
+ + ", error code: "
+ + errorCode
+ + ", serviceInfo: "
+ + serviceInfo);
+ try {
+ receiver.onError(errorCode);
+ } catch (RemoteException ignored) {
+ // do nothing if the client is dead
+ }
+ }
+ popAndRunNext();
+ }
+
+ @Override
+ public void onServiceRegistered(NsdServiceInfo serviceInfo) {
+ checkOnHandlerThread();
+ Log.i(
+ TAG,
+ "Registered successfully. "
+ + "Listener ID: "
+ + mListenerId
+ + ", serviceInfo: "
+ + serviceInfo);
+ try {
+ mRegistrationReceiver.onSuccess();
+ } catch (RemoteException ignored) {
+ // do nothing if the client is dead
+ }
+ popAndRunNext();
+ }
+
+ @Override
+ public void onServiceUnregistered(NsdServiceInfo serviceInfo) {
+ checkOnHandlerThread();
+ for (INsdStatusReceiver receiver : mUnregistrationReceivers) {
+ Log.i(
+ TAG,
+ "Unregistered successfully. "
+ + "Listener ID: "
+ + mListenerId
+ + ", serviceInfo: "
+ + serviceInfo);
+ try {
+ receiver.onSuccess();
+ } catch (RemoteException ignored) {
+ // do nothing if the client is dead
+ }
+ }
+ mRegistrationListeners.remove(mListenerId);
+ popAndRunNext();
+ }
+ }
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 60c97bf..0623b87 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -14,8 +14,8 @@
package com.android.server.thread;
+import static android.Manifest.permission.NETWORK_SETTINGS;
import static android.net.MulticastRoutingConfig.CONFIG_FORWARD_NONE;
-import static android.net.MulticastRoutingConfig.FORWARD_NONE;
import static android.net.MulticastRoutingConfig.FORWARD_SELECTED;
import static android.net.MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE;
import static android.net.thread.ActiveOperationalDataset.CHANNEL_PAGE_24_GHZ;
@@ -26,6 +26,9 @@
import static android.net.thread.ActiveOperationalDataset.MESH_LOCAL_PREFIX_FIRST_BYTE;
import static android.net.thread.ActiveOperationalDataset.SecurityPolicy.DEFAULT_ROTATION_TIME_HOURS;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_DETACHED;
+import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
+import static android.net.thread.ThreadNetworkController.STATE_DISABLING;
+import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
import static android.net.thread.ThreadNetworkController.THREAD_VERSION_1_3;
import static android.net.thread.ThreadNetworkException.ERROR_ABORTED;
import static android.net.thread.ThreadNetworkException.ERROR_BUSY;
@@ -34,33 +37,44 @@
import static android.net.thread.ThreadNetworkException.ERROR_REJECTED_BY_PEER;
import static android.net.thread.ThreadNetworkException.ERROR_RESOURCE_EXHAUSTED;
import static android.net.thread.ThreadNetworkException.ERROR_RESPONSE_BAD_FORMAT;
+import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
import static android.net.thread.ThreadNetworkException.ERROR_TIMEOUT;
import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_CHANNEL;
-import static android.net.thread.ThreadNetworkException.ErrorCode;
+import static android.net.thread.ThreadNetworkManager.DISALLOW_THREAD_NETWORK;
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_ABORT;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_BUSY;
-import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_DETACHED;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_FAILED_PRECONDITION;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_INVALID_STATE;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_NO_BUFS;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_PARSE;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_REASSEMBLY_TIMEOUT;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_REJECTED;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_RESPONSE_TIMEOUT;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_THREAD_DISABLED;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_UNSUPPORTED_CHANNEL;
+import static com.android.server.thread.openthread.IOtDaemon.OT_STATE_DISABLED;
+import static com.android.server.thread.openthread.IOtDaemon.OT_STATE_DISABLING;
+import static com.android.server.thread.openthread.IOtDaemon.OT_STATE_ENABLED;
import static com.android.server.thread.openthread.IOtDaemon.TUN_IF_NAME;
import android.Manifest.permission;
import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.net.ConnectivityManager;
-import android.net.IpPrefix;
+import android.net.InetAddresses;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.LocalNetworkConfig;
-import android.net.MulticastRoutingConfig;
import android.net.LocalNetworkInfo;
+import android.net.MulticastRoutingConfig;
import android.net.Network;
import android.net.NetworkAgent;
import android.net.NetworkAgentConfig;
@@ -68,7 +82,7 @@
import android.net.NetworkProvider;
import android.net.NetworkRequest;
import android.net.NetworkScore;
-import android.net.RouteInfo;
+import android.net.TestNetworkSpecifier;
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
import android.net.thread.IActiveOperationalDatasetReceiver;
@@ -80,23 +94,28 @@
import android.net.thread.PendingOperationalDataset;
import android.net.thread.ThreadNetworkController;
import android.net.thread.ThreadNetworkController.DeviceRole;
+import android.net.thread.ThreadNetworkException.ErrorCode;
+import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.os.SystemClock;
+import android.os.UserManager;
import android.util.Log;
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.ServiceManagerWrapper;
+import com.android.server.thread.openthread.BackboneRouterState;
+import com.android.server.thread.openthread.BorderRouterConfigurationParcel;
+import com.android.server.thread.openthread.IChannelMasksReceiver;
import com.android.server.thread.openthread.IOtDaemon;
import com.android.server.thread.openthread.IOtDaemonCallback;
import com.android.server.thread.openthread.IOtStatusReceiver;
import com.android.server.thread.openthread.Ipv6AddressInfo;
import com.android.server.thread.openthread.OtDaemonState;
-import com.android.server.thread.openthread.BorderRouterConfigurationParcel;
import java.io.IOException;
import java.net.Inet6Address;
@@ -105,6 +124,7 @@
import java.security.SecureRandom;
import java.time.Instant;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
@@ -115,10 +135,11 @@
*
* <p>Threading model: This class is not Thread-safe and should only be accessed from the
* ThreadNetworkService class. Additional attention should be paid to handle the threading code
- * correctly: 1. All member fields other than `mHandler` and `mContext` MUST be accessed from
- * `mHandlerThread` 2. In the @Override methods, the actual work MUST be dispatched to the
+ * correctly: 1. All member fields other than `mHandler` and `mContext` MUST be accessed from the
+ * thread of `mHandler` 2. In the @Override methods, the actual work MUST be dispatched to the
* HandlerThread except for arguments or permissions checking
*/
+@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
final class ThreadNetworkControllerService extends IThreadNetworkController.Stub {
private static final String TAG = "ThreadNetworkService";
@@ -127,82 +148,81 @@
private final Context mContext;
private final Handler mHandler;
- // Below member fields can only be accessed from the handler thread (`mHandlerThread`). In
+ // Below member fields can only be accessed from the handler thread (`mHandler`). In
// particular, the constructor does not run on the handler thread, so it must not touch any of
// the non-final fields, nor must it mutate any of the non-final fields inside these objects.
- private final HandlerThread mHandlerThread;
private final NetworkProvider mNetworkProvider;
private final Supplier<IOtDaemon> mOtDaemonSupplier;
private final ConnectivityManager mConnectivityManager;
private final TunInterfaceController mTunIfController;
- private final LinkProperties mLinkProperties = new LinkProperties();
+ private final InfraInterfaceController mInfraIfController;
+ private final NsdPublisher mNsdPublisher;
private final OtDaemonCallbackProxy mOtDaemonCallbackProxy = new OtDaemonCallbackProxy();
- // TODO(b/308310823): read supported channel from Thread dameon
- private final int mSupportedChannelMask = 0x07FFF800; // from channel 11 to 26
+ @Nullable private IOtDaemon mOtDaemon;
+ @Nullable private NetworkAgent mNetworkAgent;
+ @Nullable private NetworkAgent mTestNetworkAgent;
- private IOtDaemon mOtDaemon;
- private NetworkAgent mNetworkAgent;
private MulticastRoutingConfig mUpstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
private MulticastRoutingConfig mDownstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
private Network mUpstreamNetwork;
- private final NetworkRequest mUpstreamNetworkRequest;
+ private NetworkRequest mUpstreamNetworkRequest;
+ private UpstreamNetworkCallback mUpstreamNetworkCallback;
+ private TestNetworkSpecifier mUpstreamTestNetworkSpecifier;
private final HashMap<Network, String> mNetworkToInterface;
- private final LocalNetworkConfig mLocalNetworkConfig;
+ private final ThreadPersistentSettings mPersistentSettings;
+ private final UserManager mUserManager;
+ private boolean mUserRestricted;
private BorderRouterConfigurationParcel mBorderRouterConfig;
@VisibleForTesting
ThreadNetworkControllerService(
Context context,
- HandlerThread handlerThread,
+ Handler handler,
NetworkProvider networkProvider,
Supplier<IOtDaemon> otDaemonSupplier,
ConnectivityManager connectivityManager,
- TunInterfaceController tunIfController) {
+ TunInterfaceController tunIfController,
+ InfraInterfaceController infraIfController,
+ ThreadPersistentSettings persistentSettings,
+ NsdPublisher nsdPublisher,
+ UserManager userManager) {
mContext = context;
- mHandlerThread = handlerThread;
- mHandler = new Handler(handlerThread.getLooper());
+ mHandler = handler;
mNetworkProvider = networkProvider;
mOtDaemonSupplier = otDaemonSupplier;
mConnectivityManager = connectivityManager;
mTunIfController = tunIfController;
- mUpstreamNetworkRequest =
- new NetworkRequest.Builder()
- .clearCapabilities()
- .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
- .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
- .build();
- mLocalNetworkConfig =
- new LocalNetworkConfig.Builder()
- .setUpstreamSelector(mUpstreamNetworkRequest)
- .build();
+ mInfraIfController = infraIfController;
+ mUpstreamNetworkRequest = newUpstreamNetworkRequest();
mNetworkToInterface = new HashMap<Network, String>();
mBorderRouterConfig = new BorderRouterConfigurationParcel();
+ mPersistentSettings = persistentSettings;
+ mNsdPublisher = nsdPublisher;
+ mUserManager = userManager;
}
- public static ThreadNetworkControllerService newInstance(Context context) {
+ public static ThreadNetworkControllerService newInstance(
+ Context context, ThreadPersistentSettings persistentSettings) {
HandlerThread handlerThread = new HandlerThread("ThreadHandlerThread");
handlerThread.start();
+ Handler handler = new Handler(handlerThread.getLooper());
NetworkProvider networkProvider =
new NetworkProvider(context, handlerThread.getLooper(), "ThreadNetworkProvider");
return new ThreadNetworkControllerService(
context,
- handlerThread,
+ handler,
networkProvider,
() -> IOtDaemon.Stub.asInterface(ServiceManagerWrapper.waitForService("ot_daemon")),
context.getSystemService(ConnectivityManager.class),
- new TunInterfaceController(TUN_IF_NAME));
- }
-
- private static NetworkCapabilities newNetworkCapabilities() {
- return new NetworkCapabilities.Builder()
- .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
- .build();
+ new TunInterfaceController(TUN_IF_NAME),
+ new InfraInterfaceController(),
+ persistentSettings,
+ NsdPublisher.newInstance(context, handler),
+ context.getSystemService(UserManager.class));
}
private static Inet6Address bytesToInet6Address(byte[] addressBytes) {
@@ -237,6 +257,28 @@
LinkAddress.LIFETIME_PERMANENT /* expirationTime */);
}
+ private NetworkRequest newUpstreamNetworkRequest() {
+ NetworkRequest.Builder builder = new NetworkRequest.Builder().clearCapabilities();
+
+ if (mUpstreamTestNetworkSpecifier != null) {
+ return builder.addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+ .setNetworkSpecifier(mUpstreamTestNetworkSpecifier)
+ .build();
+ }
+ return builder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .build();
+ }
+
+ private LocalNetworkConfig newLocalNetworkConfig() {
+ return new LocalNetworkConfig.Builder()
+ .setUpstreamMulticastRoutingConfig(mUpstreamMulticastRoutingConfig)
+ .setDownstreamMulticastRoutingConfig(mDownstreamMulticastRoutingConfig)
+ .setUpstreamSelector(mUpstreamNetworkRequest)
+ .build();
+ }
+
private void initializeOtDaemon() {
try {
getOtDaemon();
@@ -246,6 +288,8 @@
}
private IOtDaemon getOtDaemon() throws RemoteException {
+ checkOnHandlerThread();
+
if (mOtDaemon != null) {
return mOtDaemon;
}
@@ -254,19 +298,23 @@
if (otDaemon == null) {
throw new RemoteException("Internal error: failed to start OT daemon");
}
- otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
- otDaemon.initialize(mTunIfController.getTunFd());
+ otDaemon.initialize(mTunIfController.getTunFd(), isEnabled(), mNsdPublisher);
otDaemon.registerStateCallback(mOtDaemonCallbackProxy, -1);
+ otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
mOtDaemon = otDaemon;
return mOtDaemon;
}
- // TODO(b/309792480): restarts the OT daemon service
private void onOtDaemonDied() {
- Log.w(TAG, "OT daemon became dead, clean up...");
+ checkOnHandlerThread();
+ Log.w(TAG, "OT daemon is dead, clean up and restart it...");
+
OperationReceiverWrapper.onOtDaemonDied();
mOtDaemonCallbackProxy.onOtDaemonDied();
+ mTunIfController.onOtDaemonDied();
+ mNsdPublisher.onOtDaemonDied();
mOtDaemon = null;
+ initializeOtDaemon();
}
public void initialize() {
@@ -279,56 +327,182 @@
throw new IllegalStateException(
"Failed to create Thread tunnel interface", e);
}
- mLinkProperties.setInterfaceName(TUN_IF_NAME);
- mLinkProperties.setMtu(TunInterfaceController.MTU);
mConnectivityManager.registerNetworkProvider(mNetworkProvider);
requestUpstreamNetwork();
-
+ requestThreadNetwork();
+ mUserRestricted = isThreadUserRestricted();
+ registerUserRestrictionsReceiver();
initializeOtDaemon();
});
}
- private void requestUpstreamNetwork() {
- mConnectivityManager.registerNetworkCallback(
- mUpstreamNetworkRequest,
- new ConnectivityManager.NetworkCallback() {
- @Override
- public void onAvailable(@NonNull Network network) {
- Log.i(TAG, "onAvailable: " + network);
- }
+ public void setEnabled(boolean isEnabled, @NonNull IOperationReceiver receiver) {
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
- @Override
- public void onLost(@NonNull Network network) {
- Log.i(TAG, "onLost: " + network);
- }
+ mHandler.post(
+ () ->
+ setEnabledInternal(
+ isEnabled,
+ true /* persist */,
+ new OperationReceiverWrapper(receiver)));
+ }
+ private void setEnabledInternal(
+ boolean isEnabled, boolean persist, @NonNull OperationReceiverWrapper receiver) {
+ if (isEnabled && isThreadUserRestricted()) {
+ receiver.onError(
+ ERROR_FAILED_PRECONDITION,
+ "Cannot enable Thread: forbidden by user restriction");
+ return;
+ }
+
+ if (persist) {
+ // The persistent setting keeps the desired enabled state, thus it's set regardless
+ // the otDaemon set enabled state operation succeeded or not, so that it can recover
+ // to the desired value after reboot.
+ mPersistentSettings.put(ThreadPersistentSettings.THREAD_ENABLED.key, isEnabled);
+ }
+
+ try {
+ getOtDaemon().setThreadEnabled(isEnabled, newOtStatusReceiver(receiver));
+ } catch (RemoteException e) {
+ Log.e(TAG, "otDaemon.setThreadEnabled failed", e);
+ receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+ }
+ }
+
+ private void registerUserRestrictionsReceiver() {
+ mContext.registerReceiver(
+ new BroadcastReceiver() {
@Override
- public void onLinkPropertiesChanged(
- @NonNull Network network, @NonNull LinkProperties linkProperties) {
- Log.i(
- TAG,
- String.format(
- "onLinkPropertiesChanged: {network: %s, interface: %s}",
- network, linkProperties.getInterfaceName()));
- mNetworkToInterface.put(network, linkProperties.getInterfaceName());
- if (network.equals(mUpstreamNetwork)) {
- enableBorderRouting(mNetworkToInterface.get(mUpstreamNetwork));
- }
+ public void onReceive(Context context, Intent intent) {
+ onUserRestrictionsChanged(isThreadUserRestricted());
}
},
+ new IntentFilter(UserManager.ACTION_USER_RESTRICTIONS_CHANGED),
+ null /* broadcastPermission */,
mHandler);
}
+ private void onUserRestrictionsChanged(boolean newUserRestrictedState) {
+ checkOnHandlerThread();
+ if (mUserRestricted == newUserRestrictedState) {
+ return;
+ }
+ Log.i(
+ TAG,
+ "Thread user restriction changed: "
+ + mUserRestricted
+ + " -> "
+ + newUserRestrictedState);
+ mUserRestricted = newUserRestrictedState;
+
+ final boolean isEnabled = isEnabled();
+ final IOperationReceiver receiver =
+ new IOperationReceiver.Stub() {
+ @Override
+ public void onSuccess() {
+ Log.d(
+ TAG,
+ (isEnabled ? "Enabled" : "Disabled")
+ + " Thread due to user restriction change");
+ }
+
+ @Override
+ public void onError(int otError, String messages) {
+ Log.e(
+ TAG,
+ "Failed to "
+ + (isEnabled ? "enable" : "disable")
+ + " Thread for user restriction change");
+ }
+ };
+ // Do not save the user restriction state to persistent settings so that the user
+ // configuration won't be overwritten
+ setEnabledInternal(isEnabled, false /* persist */, new OperationReceiverWrapper(receiver));
+ }
+
+ /** Returns {@code true} if Thread is set enabled. */
+ private boolean isEnabled() {
+ return !mUserRestricted && mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED);
+ }
+
+ /** Returns {@code true} if Thread has been restricted for the user. */
+ private boolean isThreadUserRestricted() {
+ return mUserManager.hasUserRestriction(DISALLOW_THREAD_NETWORK);
+ }
+
+ private void requestUpstreamNetwork() {
+ if (mUpstreamNetworkCallback != null) {
+ throw new AssertionError("The upstream network request is already there.");
+ }
+ mUpstreamNetworkCallback = new UpstreamNetworkCallback();
+ mConnectivityManager.registerNetworkCallback(
+ mUpstreamNetworkRequest, mUpstreamNetworkCallback, mHandler);
+ }
+
+ private void cancelRequestUpstreamNetwork() {
+ if (mUpstreamNetworkCallback == null) {
+ throw new AssertionError("The upstream network request null.");
+ }
+ mNetworkToInterface.clear();
+ mConnectivityManager.unregisterNetworkCallback(mUpstreamNetworkCallback);
+ mUpstreamNetworkCallback = null;
+ }
+
+ private final class UpstreamNetworkCallback extends ConnectivityManager.NetworkCallback {
+ @Override
+ public void onAvailable(@NonNull Network network) {
+ checkOnHandlerThread();
+ Log.i(TAG, "Upstream network available: " + network);
+ }
+
+ @Override
+ public void onLost(@NonNull Network network) {
+ checkOnHandlerThread();
+ Log.i(TAG, "Upstream network lost: " + network);
+
+ // TODO: disable border routing when upsteam network disconnected
+ }
+
+ @Override
+ public void onLinkPropertiesChanged(
+ @NonNull Network network, @NonNull LinkProperties linkProperties) {
+ checkOnHandlerThread();
+
+ String existingIfName = mNetworkToInterface.get(network);
+ String newIfName = linkProperties.getInterfaceName();
+ if (Objects.equals(existingIfName, newIfName)) {
+ return;
+ }
+ Log.i(TAG, "Upstream network changed: " + existingIfName + " -> " + newIfName);
+ mNetworkToInterface.put(network, newIfName);
+
+ // TODO: disable border routing if netIfName is null
+ if (network.equals(mUpstreamNetwork)) {
+ enableBorderRouting(mNetworkToInterface.get(mUpstreamNetwork));
+ }
+ }
+ }
+
private final class ThreadNetworkCallback extends ConnectivityManager.NetworkCallback {
@Override
public void onAvailable(@NonNull Network network) {
- Log.i(TAG, "onAvailable: Thread network Available");
+ checkOnHandlerThread();
+ Log.i(TAG, "Thread network available: " + network);
}
@Override
public void onLocalNetworkInfoChanged(
@NonNull Network network, @NonNull LocalNetworkInfo localNetworkInfo) {
- Log.i(TAG, "onLocalNetworkInfoChanged: " + localNetworkInfo);
+ checkOnHandlerThread();
+ Log.i(
+ TAG,
+ "LocalNetworkInfo of Thread network changed: {threadNetwork: "
+ + network
+ + ", localNetworkInfo: "
+ + localNetworkInfo
+ + "}");
if (localNetworkInfo.getUpstreamNetwork() == null) {
mUpstreamNetwork = null;
return;
@@ -345,35 +519,54 @@
private void requestThreadNetwork() {
mConnectivityManager.registerNetworkCallback(
new NetworkRequest.Builder()
+ // clearCapabilities() is needed to remove forbidden capabilities and UID
+ // requirement.
.clearCapabilities()
.addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
- .removeForbiddenCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
.build(),
new ThreadNetworkCallback(),
mHandler);
}
+ /** Injects a {@link NetworkAgent} for testing. */
+ @VisibleForTesting
+ void setTestNetworkAgent(@Nullable NetworkAgent testNetworkAgent) {
+ mTestNetworkAgent = testNetworkAgent;
+ }
+
+ private NetworkAgent newNetworkAgent() {
+ if (mTestNetworkAgent != null) {
+ return mTestNetworkAgent;
+ }
+
+ final NetworkCapabilities netCaps =
+ new NetworkCapabilities.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build();
+ final NetworkScore score =
+ new NetworkScore.Builder()
+ .setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK)
+ .build();
+ return new NetworkAgent(
+ mContext,
+ mHandler.getLooper(),
+ TAG,
+ netCaps,
+ mTunIfController.getLinkProperties(),
+ newLocalNetworkConfig(),
+ score,
+ new NetworkAgentConfig.Builder().build(),
+ mNetworkProvider) {};
+ }
+
private void registerThreadNetwork() {
if (mNetworkAgent != null) {
return;
}
- NetworkCapabilities netCaps = newNetworkCapabilities();
- NetworkScore score =
- new NetworkScore.Builder()
- .setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK)
- .build();
- requestThreadNetwork();
- mNetworkAgent =
- new NetworkAgent(
- mContext,
- mHandlerThread.getLooper(),
- TAG,
- netCaps,
- mLinkProperties,
- mLocalNetworkConfig,
- score,
- new NetworkAgentConfig.Builder().build(),
- mNetworkProvider) {};
+
+ mNetworkAgent = newNetworkAgent();
mNetworkAgent.register();
mNetworkAgent.markConnected();
Log.i(TAG, "Registered Thread network");
@@ -392,46 +585,6 @@
mNetworkAgent = null;
}
- private void updateTunInterfaceAddress(LinkAddress linkAddress, boolean isAdded) {
- try {
- if (isAdded) {
- mTunIfController.addAddress(linkAddress);
- } else {
- mTunIfController.removeAddress(linkAddress);
- }
- } catch (IOException e) {
- Log.e(
- TAG,
- String.format(
- "Failed to %s Thread tun interface address %s",
- (isAdded ? "add" : "remove"), linkAddress),
- e);
- }
- }
-
- private void updateNetworkLinkProperties(LinkAddress linkAddress, boolean isAdded) {
- RouteInfo routeInfo =
- new RouteInfo(
- new IpPrefix(linkAddress.getAddress(), 64),
- null,
- TUN_IF_NAME,
- RouteInfo.RTN_UNICAST,
- TunInterfaceController.MTU);
- if (isAdded) {
- mLinkProperties.addLinkAddress(linkAddress);
- mLinkProperties.addRoute(routeInfo);
- } else {
- mLinkProperties.removeLinkAddress(linkAddress);
- mLinkProperties.removeRoute(routeInfo);
- }
-
- // The Thread daemon can send link property updates before the networkAgent is
- // registered
- if (mNetworkAgent != null) {
- mNetworkAgent.sendLinkProperties(mLinkProperties);
- }
- }
-
@Override
public int getThreadVersion() {
return THREAD_VERSION_1_3;
@@ -440,26 +593,51 @@
@Override
public void createRandomizedDataset(
String networkName, IActiveOperationalDatasetReceiver receiver) {
- mHandler.post(
- () -> {
- ActiveOperationalDataset dataset =
- createRandomizedDatasetInternal(
- networkName,
- mSupportedChannelMask,
- Instant.now(),
- new Random(),
- new SecureRandom());
- try {
- receiver.onSuccess(dataset);
- } catch (RemoteException e) {
- // The client is dead, do nothing
- }
- });
+ ActiveOperationalDatasetReceiverWrapper receiverWrapper =
+ new ActiveOperationalDatasetReceiverWrapper(receiver);
+ mHandler.post(() -> createRandomizedDatasetInternal(networkName, receiverWrapper));
}
- private static ActiveOperationalDataset createRandomizedDatasetInternal(
+ private void createRandomizedDatasetInternal(
+ String networkName, @NonNull ActiveOperationalDatasetReceiverWrapper receiver) {
+ checkOnHandlerThread();
+
+ try {
+ getOtDaemon().getChannelMasks(newChannelMasksReceiver(networkName, receiver));
+ } catch (RemoteException e) {
+ Log.e(TAG, "otDaemon.getChannelMasks failed", e);
+ receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+ }
+ }
+
+ private IChannelMasksReceiver newChannelMasksReceiver(
+ String networkName, ActiveOperationalDatasetReceiverWrapper receiver) {
+ return new IChannelMasksReceiver.Stub() {
+ @Override
+ public void onSuccess(int supportedChannelMask, int preferredChannelMask) {
+ ActiveOperationalDataset dataset =
+ createRandomizedDataset(
+ networkName,
+ supportedChannelMask,
+ preferredChannelMask,
+ Instant.now(),
+ new Random(),
+ new SecureRandom());
+
+ receiver.onSuccess(dataset);
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) {
+ receiver.onError(otErrorToAndroidError(errorCode), errorMessage);
+ }
+ };
+ }
+
+ private static ActiveOperationalDataset createRandomizedDataset(
String networkName,
int supportedChannelMask,
+ int preferredChannelMask,
Instant now,
Random random,
SecureRandom secureRandom) {
@@ -469,6 +647,7 @@
final SparseArray<byte[]> channelMask = new SparseArray<>(1);
channelMask.put(CHANNEL_PAGE_24_GHZ, channelMaskToByteArray(supportedChannelMask));
+ final int channel = selectChannel(supportedChannelMask, preferredChannelMask, random);
final byte[] securityFlags = new byte[] {(byte) 0xff, (byte) 0xf8};
@@ -479,7 +658,7 @@
.setExtendedPanId(newRandomBytes(random, LENGTH_EXTENDED_PAN_ID))
.setPanId(panId)
.setNetworkName(networkName)
- .setChannel(CHANNEL_PAGE_24_GHZ, selectRandomChannel(supportedChannelMask, random))
+ .setChannel(CHANNEL_PAGE_24_GHZ, channel)
.setChannelMask(channelMask)
.setPskc(newRandomBytes(secureRandom, LENGTH_PSKC))
.setNetworkKey(newRandomBytes(secureRandom, LENGTH_NETWORK_KEY))
@@ -488,6 +667,18 @@
.build();
}
+ private static int selectChannel(
+ int supportedChannelMask, int preferredChannelMask, Random random) {
+ // If the preferred channel mask is not empty, select a random channel from it, otherwise
+ // choose one from the supported channel mask.
+ preferredChannelMask = preferredChannelMask & supportedChannelMask;
+ if (preferredChannelMask == 0) {
+ preferredChannelMask = supportedChannelMask;
+ }
+
+ return selectRandomChannel(preferredChannelMask, random);
+ }
+
private static byte[] newRandomBytes(Random random, int length) {
byte[] result = new byte[length];
random.nextBytes(result);
@@ -524,29 +715,29 @@
return -1;
}
- private void enforceAllCallingPermissionsGranted(String... permissions) {
+ private void enforceAllPermissionsGranted(String... permissions) {
for (String permission : permissions) {
- mContext.enforceCallingPermission(
+ mContext.enforceCallingOrSelfPermission(
permission, "Permission " + permission + " is missing");
}
}
@Override
public void registerStateCallback(IStateCallback stateCallback) throws RemoteException {
- enforceAllCallingPermissionsGranted(permission.ACCESS_NETWORK_STATE);
+ enforceAllPermissionsGranted(permission.ACCESS_NETWORK_STATE);
mHandler.post(() -> mOtDaemonCallbackProxy.registerStateCallback(stateCallback));
}
@Override
public void unregisterStateCallback(IStateCallback stateCallback) throws RemoteException {
- enforceAllCallingPermissionsGranted(permission.ACCESS_NETWORK_STATE);
+ enforceAllPermissionsGranted(permission.ACCESS_NETWORK_STATE);
mHandler.post(() -> mOtDaemonCallbackProxy.unregisterStateCallback(stateCallback));
}
@Override
public void registerOperationalDatasetCallback(IOperationalDatasetCallback callback)
throws RemoteException {
- enforceAllCallingPermissionsGranted(
+ enforceAllPermissionsGranted(
permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
mHandler.post(() -> mOtDaemonCallbackProxy.registerDatasetCallback(callback));
}
@@ -554,13 +745,13 @@
@Override
public void unregisterOperationalDatasetCallback(IOperationalDatasetCallback callback)
throws RemoteException {
- enforceAllCallingPermissionsGranted(
+ enforceAllPermissionsGranted(
permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
mHandler.post(() -> mOtDaemonCallbackProxy.unregisterDatasetCallback(callback));
}
private void checkOnHandlerThread() {
- if (Looper.myLooper() != mHandlerThread.getLooper()) {
+ if (Looper.myLooper() != mHandler.getLooper()) {
Log.wtf(TAG, "Must be on the handler thread!");
}
}
@@ -587,9 +778,6 @@
return ERROR_ABORTED;
case OT_ERROR_BUSY:
return ERROR_BUSY;
- case OT_ERROR_DETACHED:
- case OT_ERROR_INVALID_STATE:
- return ERROR_FAILED_PRECONDITION;
case OT_ERROR_NO_BUFS:
return ERROR_RESOURCE_EXHAUSTED;
case OT_ERROR_PARSE:
@@ -601,6 +789,11 @@
return ERROR_REJECTED_BY_PEER;
case OT_ERROR_UNSUPPORTED_CHANNEL:
return ERROR_UNSUPPORTED_CHANNEL;
+ case OT_ERROR_THREAD_DISABLED:
+ return ERROR_THREAD_DISABLED;
+ case OT_ERROR_FAILED_PRECONDITION:
+ return ERROR_FAILED_PRECONDITION;
+ case OT_ERROR_INVALID_STATE:
default:
return ERROR_INTERNAL_ERROR;
}
@@ -609,7 +802,7 @@
@Override
public void join(
@NonNull ActiveOperationalDataset activeDataset, @NonNull IOperationReceiver receiver) {
- enforceAllCallingPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
OperationReceiverWrapper receiverWrapper = new OperationReceiverWrapper(receiver);
mHandler.post(() -> joinInternal(activeDataset, receiverWrapper));
@@ -633,7 +826,7 @@
public void scheduleMigration(
@NonNull PendingOperationalDataset pendingDataset,
@NonNull IOperationReceiver receiver) {
- enforceAllCallingPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
OperationReceiverWrapper receiverWrapper = new OperationReceiverWrapper(receiver);
mHandler.post(() -> scheduleMigrationInternal(pendingDataset, receiverWrapper));
@@ -656,7 +849,7 @@
@Override
public void leave(@NonNull IOperationReceiver receiver) throws RemoteException {
- enforceAllCallingPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
mHandler.post(() -> leaveInternal(new OperationReceiverWrapper(receiver)));
}
@@ -672,16 +865,74 @@
}
}
+ /**
+ * Sets the country code.
+ *
+ * @param countryCode 2 characters string country code (as defined in ISO 3166) to set.
+ * @param receiver the receiver to receive result of this operation
+ */
+ @RequiresPermission(PERMISSION_THREAD_NETWORK_PRIVILEGED)
+ public void setCountryCode(@NonNull String countryCode, @NonNull IOperationReceiver receiver) {
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ OperationReceiverWrapper receiverWrapper = new OperationReceiverWrapper(receiver);
+ mHandler.post(() -> setCountryCodeInternal(countryCode, receiverWrapper));
+ }
+
+ private void setCountryCodeInternal(
+ String countryCode, @NonNull OperationReceiverWrapper receiver) {
+ checkOnHandlerThread();
+
+ try {
+ getOtDaemon().setCountryCode(countryCode, newOtStatusReceiver(receiver));
+ } catch (RemoteException e) {
+ Log.e(TAG, "otDaemon.setCountryCode failed", e);
+ receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+ }
+ }
+
+ @Override
+ public void setTestNetworkAsUpstream(
+ @Nullable String testNetworkInterfaceName, @NonNull IOperationReceiver receiver) {
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED, NETWORK_SETTINGS);
+
+ Log.i(TAG, "setTestNetworkAsUpstream: " + testNetworkInterfaceName);
+ mHandler.post(() -> setTestNetworkAsUpstreamInternal(testNetworkInterfaceName, receiver));
+ }
+
+ private void setTestNetworkAsUpstreamInternal(
+ @Nullable String testNetworkInterfaceName, @NonNull IOperationReceiver receiver) {
+ checkOnHandlerThread();
+
+ TestNetworkSpecifier testNetworkSpecifier = null;
+ if (testNetworkInterfaceName != null) {
+ testNetworkSpecifier = new TestNetworkSpecifier(testNetworkInterfaceName);
+ }
+
+ if (!Objects.equals(mUpstreamTestNetworkSpecifier, testNetworkSpecifier)) {
+ cancelRequestUpstreamNetwork();
+ mUpstreamTestNetworkSpecifier = testNetworkSpecifier;
+ mUpstreamNetworkRequest = newUpstreamNetworkRequest();
+ requestUpstreamNetwork();
+ sendLocalNetworkConfig();
+ }
+ try {
+ receiver.onSuccess();
+ } catch (RemoteException ignored) {
+ // do nothing if the client is dead
+ }
+ }
+
private void enableBorderRouting(String infraIfName) {
if (mBorderRouterConfig.isBorderRoutingEnabled
&& infraIfName.equals(mBorderRouterConfig.infraInterfaceName)) {
return;
}
- Log.i(TAG, "enableBorderRouting on AIL: " + infraIfName);
+ Log.i(TAG, "Enable border routing on AIL: " + infraIfName);
try {
mBorderRouterConfig.infraInterfaceName = infraIfName;
mBorderRouterConfig.infraInterfaceIcmp6Socket =
- InfraInterfaceController.createIcmp6Socket(infraIfName);
+ mInfraIfController.createIcmp6Socket(infraIfName);
mBorderRouterConfig.isBorderRoutingEnabled = true;
mOtDaemon.configureBorderRouter(
@@ -708,7 +959,7 @@
private void handleThreadInterfaceStateChanged(boolean isUp) {
try {
mTunIfController.setInterfaceUp(isUp);
- Log.d(TAG, "Thread network interface becomes " + (isUp ? "up" : "down"));
+ Log.i(TAG, "Thread TUN interface becomes " + (isUp ? "up" : "down"));
} catch (IOException e) {
Log.e(TAG, "Failed to handle Thread interface state changes", e);
}
@@ -716,13 +967,13 @@
private void handleDeviceRoleChanged(@DeviceRole int deviceRole) {
if (ThreadNetworkController.isAttached(deviceRole)) {
- Log.d(TAG, "Attached to the Thread network");
+ Log.i(TAG, "Attached to the Thread network");
// This is an idempotent method which can be called for multiple times when the device
// is already attached (e.g. going from Child to Router)
registerThreadNetwork();
} else {
- Log.d(TAG, "Detached from the Thread network");
+ Log.i(TAG, "Detached from the Thread network");
// This is an idempotent method which can be called for multiple times when the device
// is already detached or stopped
@@ -739,104 +990,66 @@
}
LinkAddress linkAddress = newLinkAddress(addressInfo);
- Log.d(TAG, (isAdded ? "Adding" : "Removing") + " address " + linkAddress);
+ if (isAdded) {
+ mTunIfController.addAddress(linkAddress);
+ } else {
+ mTunIfController.removeAddress(linkAddress);
+ }
- updateTunInterfaceAddress(linkAddress, isAdded);
- updateNetworkLinkProperties(linkAddress, isAdded);
- }
-
- private boolean isMulticastForwardingEnabled() {
- return !(mUpstreamMulticastRoutingConfig.getForwardingMode() == FORWARD_NONE
- && mDownstreamMulticastRoutingConfig.getForwardingMode() == FORWARD_NONE);
+ // The OT daemon can send link property updates before the networkAgent is
+ // registered
+ if (mNetworkAgent != null) {
+ mNetworkAgent.sendLinkProperties(mTunIfController.getLinkProperties());
+ }
}
private void sendLocalNetworkConfig() {
if (mNetworkAgent == null) {
return;
}
- final LocalNetworkConfig.Builder configBuilder = new LocalNetworkConfig.Builder();
- LocalNetworkConfig localNetworkConfig =
- configBuilder
- .setUpstreamMulticastRoutingConfig(mUpstreamMulticastRoutingConfig)
- .setDownstreamMulticastRoutingConfig(mDownstreamMulticastRoutingConfig)
- .setUpstreamSelector(mUpstreamNetworkRequest)
- .build();
+ final LocalNetworkConfig localNetworkConfig = newLocalNetworkConfig();
mNetworkAgent.sendLocalNetworkConfig(localNetworkConfig);
- Log.d(
- TAG,
- "Sent localNetworkConfig with upstreamConfig "
- + mUpstreamMulticastRoutingConfig
- + " downstreamConfig"
- + mDownstreamMulticastRoutingConfig);
+ Log.d(TAG, "Sent localNetworkConfig: " + localNetworkConfig);
}
- private void handleMulticastForwardingStateChanged(boolean isEnabled) {
- if (isMulticastForwardingEnabled() == isEnabled) {
- return;
- }
- if (isEnabled) {
+ private void handleMulticastForwardingChanged(BackboneRouterState state) {
+ MulticastRoutingConfig upstreamMulticastRoutingConfig;
+ MulticastRoutingConfig downstreamMulticastRoutingConfig;
+
+ if (state.multicastForwardingEnabled) {
// When multicast forwarding is enabled, setup upstream forwarding to any address
// with minimal scope 4
// setup downstream forwarding with addresses subscribed from Thread network
- mUpstreamMulticastRoutingConfig =
+ upstreamMulticastRoutingConfig =
new MulticastRoutingConfig.Builder(FORWARD_WITH_MIN_SCOPE, 4).build();
- mDownstreamMulticastRoutingConfig =
- new MulticastRoutingConfig.Builder(FORWARD_SELECTED).build();
+ downstreamMulticastRoutingConfig =
+ buildDownstreamMulticastRoutingConfigSelected(state.listeningAddresses);
} else {
// When multicast forwarding is disabled, set both upstream and downstream
// forwarding config to FORWARD_NONE.
- mUpstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
- mDownstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
+ upstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
+ downstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
}
+
+ if (upstreamMulticastRoutingConfig.equals(mUpstreamMulticastRoutingConfig)
+ && downstreamMulticastRoutingConfig.equals(mDownstreamMulticastRoutingConfig)) {
+ return;
+ }
+
+ mUpstreamMulticastRoutingConfig = upstreamMulticastRoutingConfig;
+ mDownstreamMulticastRoutingConfig = downstreamMulticastRoutingConfig;
sendLocalNetworkConfig();
- Log.d(
- TAG,
- "Sent updated localNetworkConfig with multicast forwarding "
- + (isEnabled ? "enabled" : "disabled"));
}
- private void handleMulticastForwardingAddressChanged(byte[] addressBytes, boolean isAdded) {
- Inet6Address address = bytesToInet6Address(addressBytes);
- MulticastRoutingConfig newDownstreamConfig;
- MulticastRoutingConfig.Builder builder;
-
- if (mDownstreamMulticastRoutingConfig.getForwardingMode() !=
- MulticastRoutingConfig.FORWARD_SELECTED) {
- Log.e(
- TAG,
- "Ignore multicast listening address updates when downstream multicast "
- + "forwarding mode is not FORWARD_SELECTED");
- // Don't update the address set if downstream multicast forwarding is disabled.
- return;
- }
- if (isAdded ==
- mDownstreamMulticastRoutingConfig.getListeningAddresses().contains(address)) {
- return;
- }
-
- builder = new MulticastRoutingConfig.Builder(FORWARD_SELECTED);
- for (Inet6Address listeningAddress :
- mDownstreamMulticastRoutingConfig.getListeningAddresses()) {
- builder.addListeningAddress(listeningAddress);
- }
-
- if (isAdded) {
+ private MulticastRoutingConfig buildDownstreamMulticastRoutingConfigSelected(
+ List<String> listeningAddresses) {
+ MulticastRoutingConfig.Builder builder =
+ new MulticastRoutingConfig.Builder(FORWARD_SELECTED);
+ for (String addressStr : listeningAddresses) {
+ Inet6Address address = (Inet6Address) InetAddresses.parseNumericAddress(addressStr);
builder.addListeningAddress(address);
- } else {
- builder.clearListeningAddress(address);
}
-
- newDownstreamConfig = builder.build();
- if (!newDownstreamConfig.equals(mDownstreamMulticastRoutingConfig)) {
- Log.d(
- TAG,
- "Multicast listening address "
- + address.getHostAddress()
- + " is "
- + (isAdded ? "added" : "removed"));
- mDownstreamMulticastRoutingConfig = newDownstreamConfig;
- sendLocalNetworkConfig();
- }
+ return builder.build();
}
private static final class CallbackMetadata {
@@ -861,8 +1074,8 @@
}
/**
- * Handles and forwards Thread daemon callbacks. This class must be accessed from the {@code
- * mHandlerThread}.
+ * Handles and forwards Thread daemon callbacks. This class must be accessed from the thread of
+ * {@code mHandler}.
*/
private final class OtDaemonCallbackProxy extends IOtDaemonCallback.Stub {
private final Map<IStateCallback, CallbackMetadata> mStateCallbacks = new HashMap<>();
@@ -897,6 +1110,15 @@
}
}
+ private void notifyThreadEnabledUpdated(IStateCallback callback, int enabledState) {
+ try {
+ callback.onThreadEnableStateChanged(enabledState);
+ Log.i(TAG, "onThreadEnableStateChanged " + enabledState);
+ } catch (RemoteException ignored) {
+ // do nothing if the client is dead
+ }
+ }
+
public void unregisterStateCallback(IStateCallback callback) {
checkOnHandlerThread();
if (!mStateCallbacks.containsKey(callback)) {
@@ -961,6 +1183,31 @@
}
@Override
+ public void onThreadEnabledChanged(int state) {
+ mHandler.post(() -> onThreadEnabledChangedInternal(state));
+ }
+
+ private void onThreadEnabledChangedInternal(int state) {
+ checkOnHandlerThread();
+ for (IStateCallback callback : mStateCallbacks.keySet()) {
+ notifyThreadEnabledUpdated(callback, otStateToAndroidState(state));
+ }
+ }
+
+ private static int otStateToAndroidState(int state) {
+ switch (state) {
+ case OT_STATE_ENABLED:
+ return STATE_ENABLED;
+ case OT_STATE_DISABLED:
+ return STATE_DISABLED;
+ case OT_STATE_DISABLING:
+ return STATE_DISABLING;
+ default:
+ throw new IllegalArgumentException("Unknown ot state " + state);
+ }
+ }
+
+ @Override
public void onStateChanged(OtDaemonState newState, long listenerId) {
mHandler.post(() -> onStateChangedInternal(newState, listenerId));
}
@@ -970,7 +1217,6 @@
onInterfaceStateChanged(newState.isInterfaceUp);
onDeviceRoleChanged(newState.deviceRole, listenerId);
onPartitionIdChanged(newState.partitionId, listenerId);
- onMulticastForwardingStateChanged(newState.multicastForwardingEnabled);
mState = newState;
ActiveOperationalDataset newActiveDataset;
@@ -1079,19 +1325,14 @@
}
}
- private void onMulticastForwardingStateChanged(boolean isEnabled) {
- checkOnHandlerThread();
- handleMulticastForwardingStateChanged(isEnabled);
- }
-
@Override
public void onAddressChanged(Ipv6AddressInfo addressInfo, boolean isAdded) {
mHandler.post(() -> handleAddressChanged(addressInfo, isAdded));
}
@Override
- public void onMulticastForwardingAddressChanged(byte[] address, boolean isAdded) {
- mHandler.post(() -> handleMulticastForwardingAddressChanged(address, isAdded));
+ public void onBackboneRouterStateChanged(BackboneRouterState state) {
+ mHandler.post(() -> handleMulticastForwardingChanged(state));
}
}
}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java b/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java
new file mode 100644
index 0000000..ffa7b44
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java
@@ -0,0 +1,590 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.thread;
+
+import android.annotation.Nullable;
+import android.annotation.StringDef;
+import android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.location.Address;
+import android.location.Geocoder;
+import android.location.Location;
+import android.location.LocationManager;
+import android.net.thread.IOperationReceiver;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.ActiveCountryCodeChangedCallback;
+import android.os.Build;
+import android.sysprop.ThreadNetworkProperties;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.connectivity.resources.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.ConnectivityResources;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.time.Instant;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Provide functions for making changes to Thread Network country code. This Country Code is from
+ * location, WiFi, telephony or OEM configuration. This class sends Country Code to Thread Network
+ * native layer.
+ *
+ * <p>This class is thread-safe.
+ */
+@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+public class ThreadNetworkCountryCode {
+ private static final String TAG = "ThreadNetworkCountryCode";
+ // To be used when there is no country code available.
+ @VisibleForTesting public static final String DEFAULT_COUNTRY_CODE = "WW";
+
+ // Wait 1 hour between updates.
+ private static final long TIME_BETWEEN_LOCATION_UPDATES_MS = 1000L * 60 * 60 * 1;
+ // Minimum distance before an update is triggered, in meters. We don't need this to be too
+ // exact because all we care about is what country the user is in.
+ private static final float DISTANCE_BETWEEN_LOCALTION_UPDATES_METERS = 5_000.0f;
+
+ /** List of country code sources. */
+ @Retention(RetentionPolicy.SOURCE)
+ @StringDef(
+ prefix = "COUNTRY_CODE_SOURCE_",
+ value = {
+ COUNTRY_CODE_SOURCE_DEFAULT,
+ COUNTRY_CODE_SOURCE_LOCATION,
+ COUNTRY_CODE_SOURCE_OEM,
+ COUNTRY_CODE_SOURCE_OVERRIDE,
+ COUNTRY_CODE_SOURCE_TELEPHONY,
+ COUNTRY_CODE_SOURCE_TELEPHONY_LAST,
+ COUNTRY_CODE_SOURCE_WIFI,
+ })
+ private @interface CountryCodeSource {}
+
+ private static final String COUNTRY_CODE_SOURCE_DEFAULT = "Default";
+ private static final String COUNTRY_CODE_SOURCE_LOCATION = "Location";
+ private static final String COUNTRY_CODE_SOURCE_OEM = "Oem";
+ private static final String COUNTRY_CODE_SOURCE_OVERRIDE = "Override";
+ private static final String COUNTRY_CODE_SOURCE_TELEPHONY = "Telephony";
+ private static final String COUNTRY_CODE_SOURCE_TELEPHONY_LAST = "TelephonyLast";
+ private static final String COUNTRY_CODE_SOURCE_WIFI = "Wifi";
+
+ private static final CountryCodeInfo DEFAULT_COUNTRY_CODE_INFO =
+ new CountryCodeInfo(DEFAULT_COUNTRY_CODE, COUNTRY_CODE_SOURCE_DEFAULT);
+
+ private final ConnectivityResources mResources;
+ private final Context mContext;
+ private final LocationManager mLocationManager;
+ @Nullable private final Geocoder mGeocoder;
+ private final ThreadNetworkControllerService mThreadNetworkControllerService;
+ private final WifiManager mWifiManager;
+ private final TelephonyManager mTelephonyManager;
+ private final SubscriptionManager mSubscriptionManager;
+ private final Map<Integer, TelephonyCountryCodeSlotInfo> mTelephonyCountryCodeSlotInfoMap =
+ new ArrayMap();
+
+ @Nullable private CountryCodeInfo mCurrentCountryCodeInfo;
+ @Nullable private CountryCodeInfo mLocationCountryCodeInfo;
+ @Nullable private CountryCodeInfo mOverrideCountryCodeInfo;
+ @Nullable private CountryCodeInfo mWifiCountryCodeInfo;
+ @Nullable private CountryCodeInfo mTelephonyCountryCodeInfo;
+ @Nullable private CountryCodeInfo mTelephonyLastCountryCodeInfo;
+ @Nullable private CountryCodeInfo mOemCountryCodeInfo;
+
+ /** Container class to store Thread country code information. */
+ private static final class CountryCodeInfo {
+ private String mCountryCode;
+ @CountryCodeSource private String mSource;
+ private final Instant mUpdatedTimestamp;
+
+ /**
+ * Constructs a new {@code CountryCodeInfo} from the given country code, country code source
+ * and country coode created time.
+ *
+ * @param countryCode a String representation of the country code as defined in ISO 3166.
+ * @param countryCodeSource a String representation of country code source.
+ * @param instant a Instant representation of the time when the country code was created.
+ * @throws IllegalArgumentException if {@code countryCode} contains invalid country code.
+ */
+ public CountryCodeInfo(
+ String countryCode, @CountryCodeSource String countryCodeSource, Instant instant) {
+ if (!isValidCountryCode(countryCode)) {
+ throw new IllegalArgumentException("Country code is invalid: " + countryCode);
+ }
+
+ mCountryCode = countryCode;
+ mSource = countryCodeSource;
+ mUpdatedTimestamp = instant;
+ }
+
+ /**
+ * Constructs a new {@code CountryCodeInfo} from the given country code, country code
+ * source. The updated timestamp of the country code will be set to the time when {@code
+ * CountryCodeInfo} was constructed.
+ *
+ * @param countryCode a String representation of the country code as defined in ISO 3166.
+ * @param countryCodeSource a String representation of country code source.
+ * @throws IllegalArgumentException if {@code countryCode} contains invalid country code.
+ */
+ public CountryCodeInfo(String countryCode, @CountryCodeSource String countryCodeSource) {
+ this(countryCode, countryCodeSource, Instant.now());
+ }
+
+ public String getCountryCode() {
+ return mCountryCode;
+ }
+
+ public boolean isCountryCodeMatch(CountryCodeInfo countryCodeInfo) {
+ if (countryCodeInfo == null) {
+ return false;
+ }
+
+ return Objects.equals(countryCodeInfo.mCountryCode, mCountryCode);
+ }
+
+ @Override
+ public String toString() {
+ return "CountryCodeInfo{ mCountryCode: "
+ + mCountryCode
+ + ", mSource: "
+ + mSource
+ + ", mUpdatedTimestamp: "
+ + mUpdatedTimestamp
+ + "}";
+ }
+ }
+
+ /** Container class to store country code per SIM slot. */
+ private static final class TelephonyCountryCodeSlotInfo {
+ public int slotIndex;
+ public String countryCode;
+ public String lastKnownCountryCode;
+ public Instant timestamp;
+
+ @Override
+ public String toString() {
+ return "TelephonyCountryCodeSlotInfo{ slotIndex: "
+ + slotIndex
+ + ", countryCode: "
+ + countryCode
+ + ", lastKnownCountryCode: "
+ + lastKnownCountryCode
+ + ", timestamp: "
+ + timestamp
+ + "}";
+ }
+ }
+
+ private boolean isLocationUseForCountryCodeEnabled() {
+ return mResources
+ .get()
+ .getBoolean(R.bool.config_thread_location_use_for_country_code_enabled);
+ }
+
+ public ThreadNetworkCountryCode(
+ LocationManager locationManager,
+ ThreadNetworkControllerService threadNetworkControllerService,
+ @Nullable Geocoder geocoder,
+ ConnectivityResources resources,
+ WifiManager wifiManager,
+ Context context,
+ TelephonyManager telephonyManager,
+ SubscriptionManager subscriptionManager,
+ @Nullable String oemCountryCode) {
+ mLocationManager = locationManager;
+ mThreadNetworkControllerService = threadNetworkControllerService;
+ mGeocoder = geocoder;
+ mResources = resources;
+ mWifiManager = wifiManager;
+ mContext = context;
+ mTelephonyManager = telephonyManager;
+ mSubscriptionManager = subscriptionManager;
+
+ if (oemCountryCode != null) {
+ mOemCountryCodeInfo = new CountryCodeInfo(oemCountryCode, COUNTRY_CODE_SOURCE_OEM);
+ }
+ }
+
+ public static ThreadNetworkCountryCode newInstance(
+ Context context, ThreadNetworkControllerService controllerService) {
+ return new ThreadNetworkCountryCode(
+ context.getSystemService(LocationManager.class),
+ controllerService,
+ Geocoder.isPresent() ? new Geocoder(context) : null,
+ new ConnectivityResources(context),
+ context.getSystemService(WifiManager.class),
+ context,
+ context.getSystemService(TelephonyManager.class),
+ context.getSystemService(SubscriptionManager.class),
+ ThreadNetworkProperties.country_code().orElse(null));
+ }
+
+ /** Sets up this country code module to listen to location country code changes. */
+ public synchronized void initialize() {
+ registerGeocoderCountryCodeCallback();
+ registerWifiCountryCodeCallback();
+ registerTelephonyCountryCodeCallback();
+ updateTelephonyCountryCodeFromSimCard();
+ updateCountryCode(false /* forceUpdate */);
+ }
+
+ private synchronized void registerGeocoderCountryCodeCallback() {
+ if ((mGeocoder != null) && isLocationUseForCountryCodeEnabled()) {
+ mLocationManager.requestLocationUpdates(
+ LocationManager.PASSIVE_PROVIDER,
+ TIME_BETWEEN_LOCATION_UPDATES_MS,
+ DISTANCE_BETWEEN_LOCALTION_UPDATES_METERS,
+ location -> setCountryCodeFromGeocodingLocation(location));
+ }
+ }
+
+ private synchronized void geocodeListener(List<Address> addresses) {
+ if (addresses != null && !addresses.isEmpty()) {
+ String countryCode = addresses.get(0).getCountryCode();
+
+ if (isValidCountryCode(countryCode)) {
+ Log.d(TAG, "Set location country code to: " + countryCode);
+ mLocationCountryCodeInfo =
+ new CountryCodeInfo(countryCode, COUNTRY_CODE_SOURCE_LOCATION);
+ } else {
+ Log.d(TAG, "Received invalid location country code");
+ mLocationCountryCodeInfo = null;
+ }
+
+ updateCountryCode(false /* forceUpdate */);
+ }
+ }
+
+ private synchronized void setCountryCodeFromGeocodingLocation(@Nullable Location location) {
+ if ((location == null) || (mGeocoder == null)) return;
+
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
+ Log.wtf(
+ TAG,
+ "Unexpected call to set country code from the Geocoding location, "
+ + "Thread code never runs under T or lower.");
+ return;
+ }
+
+ mGeocoder.getFromLocation(
+ location.getLatitude(),
+ location.getLongitude(),
+ 1 /* maxResults */,
+ this::geocodeListener);
+ }
+
+ private synchronized void registerWifiCountryCodeCallback() {
+ if (mWifiManager != null) {
+ mWifiManager.registerActiveCountryCodeChangedCallback(
+ r -> r.run(), new WifiCountryCodeCallback());
+ }
+ }
+
+ private class WifiCountryCodeCallback implements ActiveCountryCodeChangedCallback {
+ @Override
+ public void onActiveCountryCodeChanged(String countryCode) {
+ Log.d(TAG, "Wifi country code is changed to " + countryCode);
+ synchronized ("ThreadNetworkCountryCode.this") {
+ if (isValidCountryCode(countryCode)) {
+ mWifiCountryCodeInfo =
+ new CountryCodeInfo(countryCode, COUNTRY_CODE_SOURCE_WIFI);
+ } else {
+ Log.w(TAG, "WiFi country code " + countryCode + " is invalid");
+ mWifiCountryCodeInfo = null;
+ }
+
+ updateCountryCode(false /* forceUpdate */);
+ }
+ }
+
+ @Override
+ public void onCountryCodeInactive() {
+ Log.d(TAG, "Wifi country code is inactived");
+ synchronized ("ThreadNetworkCountryCode.this") {
+ mWifiCountryCodeInfo = null;
+ updateCountryCode(false /* forceUpdate */);
+ }
+ }
+ }
+
+ private synchronized void registerTelephonyCountryCodeCallback() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ Log.wtf(
+ TAG,
+ "Unexpected call to register the telephony country code changed callback, "
+ + "Thread code never runs under T or lower.");
+ return;
+ }
+
+ BroadcastReceiver broadcastReceiver =
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ int slotIndex =
+ intent.getIntExtra(
+ SubscriptionManager.EXTRA_SLOT_INDEX,
+ SubscriptionManager.INVALID_SIM_SLOT_INDEX);
+ String lastKnownCountryCode = null;
+ String countryCode =
+ intent.getStringExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ lastKnownCountryCode =
+ intent.getStringExtra(
+ TelephonyManager.EXTRA_LAST_KNOWN_NETWORK_COUNTRY);
+ }
+
+ setTelephonyCountryCodeAndLastKnownCountryCode(
+ slotIndex, countryCode, lastKnownCountryCode);
+ }
+ };
+
+ mContext.registerReceiver(
+ broadcastReceiver,
+ new IntentFilter(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED),
+ Context.RECEIVER_EXPORTED);
+ }
+
+ private synchronized void updateTelephonyCountryCodeFromSimCard() {
+ List<SubscriptionInfo> subscriptionInfoList =
+ mSubscriptionManager.getActiveSubscriptionInfoList();
+
+ if (subscriptionInfoList == null) {
+ Log.d(TAG, "No SIM card is found");
+ return;
+ }
+
+ for (SubscriptionInfo subscriptionInfo : subscriptionInfoList) {
+ String countryCode;
+ int slotIndex;
+
+ slotIndex = subscriptionInfo.getSimSlotIndex();
+ try {
+ countryCode = mTelephonyManager.getNetworkCountryIso(slotIndex);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Failed to get country code for slot index:" + slotIndex, e);
+ continue;
+ }
+
+ Log.d(TAG, "Telephony slot " + slotIndex + " country code is " + countryCode);
+ setTelephonyCountryCodeAndLastKnownCountryCode(
+ slotIndex, countryCode, null /* lastKnownCountryCode */);
+ }
+ }
+
+ private synchronized void setTelephonyCountryCodeAndLastKnownCountryCode(
+ int slotIndex, String countryCode, String lastKnownCountryCode) {
+ Log.d(
+ TAG,
+ "Set telephony country code to: "
+ + countryCode
+ + ", last country code to: "
+ + lastKnownCountryCode
+ + " for slotIndex: "
+ + slotIndex);
+
+ TelephonyCountryCodeSlotInfo telephonyCountryCodeInfoSlot =
+ mTelephonyCountryCodeSlotInfoMap.computeIfAbsent(
+ slotIndex, k -> new TelephonyCountryCodeSlotInfo());
+ telephonyCountryCodeInfoSlot.slotIndex = slotIndex;
+ telephonyCountryCodeInfoSlot.timestamp = Instant.now();
+ telephonyCountryCodeInfoSlot.countryCode = countryCode;
+ telephonyCountryCodeInfoSlot.lastKnownCountryCode = lastKnownCountryCode;
+
+ mTelephonyCountryCodeInfo = null;
+ mTelephonyLastCountryCodeInfo = null;
+
+ for (TelephonyCountryCodeSlotInfo slotInfo : mTelephonyCountryCodeSlotInfoMap.values()) {
+ if ((mTelephonyCountryCodeInfo == null) && isValidCountryCode(slotInfo.countryCode)) {
+ mTelephonyCountryCodeInfo =
+ new CountryCodeInfo(
+ slotInfo.countryCode,
+ COUNTRY_CODE_SOURCE_TELEPHONY,
+ slotInfo.timestamp);
+ }
+
+ if ((mTelephonyLastCountryCodeInfo == null)
+ && isValidCountryCode(slotInfo.lastKnownCountryCode)) {
+ mTelephonyLastCountryCodeInfo =
+ new CountryCodeInfo(
+ slotInfo.lastKnownCountryCode,
+ COUNTRY_CODE_SOURCE_TELEPHONY_LAST,
+ slotInfo.timestamp);
+ }
+ }
+
+ updateCountryCode(false /* forceUpdate */);
+ }
+
+ /**
+ * Priority order of country code sources (we stop at the first known country code source):
+ *
+ * <ul>
+ * <li>1. Override country code - Country code forced via shell command (local/automated
+ * testing)
+ * <li>2. Telephony country code - Current country code retrieved via cellular. If there are
+ * multiple SIM's, the country code chosen is non-deterministic if they return different
+ * codes. The first valid country code with the lowest slot number will be used.
+ * <li>3. Wifi country code - Current country code retrieved via wifi (via 80211.ad).
+ * <li>4. Last known telephony country code - Last known country code retrieved via cellular.
+ * If there are multiple SIM's, the country code chosen is non-deterministic if they
+ * return different codes. The first valid last known country code with the lowest slot
+ * number will be used.
+ * <li>5. Location country code - Country code retrieved from LocationManager passive location
+ * provider.
+ * <li>6. OEM country code - Country code retrieved from the system property
+ * `threadnetwork.country_code`.
+ * <li>7. Default country code `WW`.
+ * </ul>
+ *
+ * @return the selected country code information.
+ */
+ private CountryCodeInfo pickCountryCode() {
+ if (mOverrideCountryCodeInfo != null) {
+ return mOverrideCountryCodeInfo;
+ }
+
+ if (mTelephonyCountryCodeInfo != null) {
+ return mTelephonyCountryCodeInfo;
+ }
+
+ if (mWifiCountryCodeInfo != null) {
+ return mWifiCountryCodeInfo;
+ }
+
+ if (mTelephonyLastCountryCodeInfo != null) {
+ return mTelephonyLastCountryCodeInfo;
+ }
+
+ if (mLocationCountryCodeInfo != null) {
+ return mLocationCountryCodeInfo;
+ }
+
+ if (mOemCountryCodeInfo != null) {
+ return mOemCountryCodeInfo;
+ }
+
+ return DEFAULT_COUNTRY_CODE_INFO;
+ }
+
+ private IOperationReceiver newOperationReceiver(CountryCodeInfo countryCodeInfo) {
+ return new IOperationReceiver.Stub() {
+ @Override
+ public void onSuccess() {
+ synchronized ("ThreadNetworkCountryCode.this") {
+ mCurrentCountryCodeInfo = countryCodeInfo;
+ }
+ }
+
+ @Override
+ public void onError(int otError, String message) {
+ Log.e(
+ TAG,
+ "Error "
+ + otError
+ + ": "
+ + message
+ + ". Failed to set country code "
+ + countryCodeInfo);
+ }
+ };
+ }
+
+ /**
+ * Updates country code to the Thread native layer.
+ *
+ * @param forceUpdate Force update the country code even if it was the same as previously cached
+ * value.
+ */
+ @VisibleForTesting
+ public synchronized void updateCountryCode(boolean forceUpdate) {
+ CountryCodeInfo countryCodeInfo = pickCountryCode();
+
+ if (!forceUpdate && countryCodeInfo.isCountryCodeMatch(mCurrentCountryCodeInfo)) {
+ Log.i(TAG, "Ignoring already set country code " + countryCodeInfo.getCountryCode());
+ return;
+ }
+
+ Log.i(TAG, "Set country code: " + countryCodeInfo);
+ mThreadNetworkControllerService.setCountryCode(
+ countryCodeInfo.getCountryCode().toUpperCase(Locale.ROOT),
+ newOperationReceiver(countryCodeInfo));
+ }
+
+ /** Returns the current country code or {@code null} if no country code is set. */
+ @Nullable
+ public synchronized String getCountryCode() {
+ return (mCurrentCountryCodeInfo != null) ? mCurrentCountryCodeInfo.getCountryCode() : null;
+ }
+
+ /**
+ * Returns {@code true} if {@code countryCode} is a valid country code.
+ *
+ * <p>A country code is valid if it consists of 2 alphabets.
+ */
+ public static boolean isValidCountryCode(String countryCode) {
+ return countryCode != null
+ && countryCode.length() == 2
+ && countryCode.chars().allMatch(Character::isLetter);
+ }
+
+ /**
+ * Overrides any existing country code.
+ *
+ * @param countryCode A 2-Character alphabetical country code (as defined in ISO 3166).
+ * @throws IllegalArgumentException if {@code countryCode} is an invalid country code.
+ */
+ public synchronized void setOverrideCountryCode(String countryCode) {
+ if (!isValidCountryCode(countryCode)) {
+ throw new IllegalArgumentException("The override country code is invalid");
+ }
+
+ mOverrideCountryCodeInfo = new CountryCodeInfo(countryCode, COUNTRY_CODE_SOURCE_OVERRIDE);
+ updateCountryCode(true /* forceUpdate */);
+ }
+
+ /** Clears the country code previously set through {@link #setOverrideCountryCode} method. */
+ public synchronized void clearOverrideCountryCode() {
+ mOverrideCountryCodeInfo = null;
+ updateCountryCode(true /* forceUpdate */);
+ }
+
+ /** Dumps the current state of this ThreadNetworkCountryCode object. */
+ public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("---- Dump of ThreadNetworkCountryCode begin ----");
+ pw.println("mOverrideCountryCodeInfo : " + mOverrideCountryCodeInfo);
+ pw.println("mTelephonyCountryCodeSlotInfoMap: " + mTelephonyCountryCodeSlotInfoMap);
+ pw.println("mTelephonyCountryCodeInfo : " + mTelephonyCountryCodeInfo);
+ pw.println("mWifiCountryCodeInfo : " + mWifiCountryCodeInfo);
+ pw.println("mTelephonyLastCountryCodeInfo : " + mTelephonyLastCountryCodeInfo);
+ pw.println("mLocationCountryCodeInfo : " + mLocationCountryCodeInfo);
+ pw.println("mOemCountryCodeInfo : " + mOemCountryCodeInfo);
+ pw.println("mCurrentCountryCodeInfo : " + mCurrentCountryCodeInfo);
+ pw.println("---- Dump of ThreadNetworkCountryCode end ------");
+ }
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkService.java b/thread/service/java/com/android/server/thread/ThreadNetworkService.java
index cc694a1..5664922 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkService.java
@@ -16,13 +16,20 @@
package com.android.server.thread;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.net.thread.IThreadNetworkController;
import android.net.thread.IThreadNetworkManager;
+import android.os.Binder;
+import android.os.ParcelFileDescriptor;
import com.android.server.SystemService;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
import java.util.Collections;
import java.util.List;
@@ -31,22 +38,35 @@
*/
public class ThreadNetworkService extends IThreadNetworkManager.Stub {
private final Context mContext;
+ @Nullable private ThreadNetworkCountryCode mCountryCode;
@Nullable private ThreadNetworkControllerService mControllerService;
+ private final ThreadPersistentSettings mPersistentSettings;
+ @Nullable private ThreadNetworkShellCommand mShellCommand;
/** Creates a new {@link ThreadNetworkService} object. */
public ThreadNetworkService(Context context) {
mContext = context;
+ mPersistentSettings = ThreadPersistentSettings.newInstance(context);
}
/**
- * Called by the service initializer.
+ * Called by {@link com.android.server.ConnectivityServiceInitializer}.
*
* @see com.android.server.SystemService#onBootPhase
*/
public void onBootPhase(int phase) {
- if (phase == SystemService.PHASE_BOOT_COMPLETED) {
- mControllerService = ThreadNetworkControllerService.newInstance(mContext);
+ if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
+ mPersistentSettings.initialize();
+ mControllerService =
+ ThreadNetworkControllerService.newInstance(mContext, mPersistentSettings);
mControllerService.initialize();
+ } else if (phase == SystemService.PHASE_BOOT_COMPLETED) {
+ // Country code initialization is delayed to the BOOT_COMPLETED phase because it will
+ // call into Wi-Fi and Telephony service whose country code module is ready after
+ // PHASE_ACTIVITY_MANAGER_READY and PHASE_THIRD_PARTY_APPS_CAN_START
+ mCountryCode = ThreadNetworkCountryCode.newInstance(mContext, mControllerService);
+ mCountryCode.initialize();
+ mShellCommand = new ThreadNetworkShellCommand(mCountryCode);
}
}
@@ -57,4 +77,40 @@
}
return Collections.singletonList(mControllerService);
}
+
+ @Override
+ public int handleShellCommand(
+ @NonNull ParcelFileDescriptor in,
+ @NonNull ParcelFileDescriptor out,
+ @NonNull ParcelFileDescriptor err,
+ @NonNull String[] args) {
+ if (mShellCommand == null) {
+ return -1;
+ }
+ return mShellCommand.exec(
+ this,
+ in.getFileDescriptor(),
+ out.getFileDescriptor(),
+ err.getFileDescriptor(),
+ args);
+ }
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
+ != PERMISSION_GRANTED) {
+ pw.println(
+ "Permission Denial: can't dump ThreadNetworkService from from pid="
+ + Binder.getCallingPid()
+ + ", uid="
+ + Binder.getCallingUid());
+ return;
+ }
+
+ if (mCountryCode != null) {
+ mCountryCode.dump(fd, pw, args);
+ }
+
+ pw.println();
+ }
}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java b/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java
new file mode 100644
index 0000000..c17c5a7
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.thread;
+
+import android.annotation.Nullable;
+import android.os.Binder;
+import android.os.Process;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.BasicShellCommandHandler;
+
+import java.io.PrintWriter;
+import java.util.List;
+
+/**
+ * Interprets and executes 'adb shell cmd thread_network [args]'.
+ *
+ * <p>To add new commands: - onCommand: Add a case "<command>" execute. Return a 0 if command
+ * executed successfully. - onHelp: add a description string.
+ *
+ * <p>Permissions: currently root permission is required for some commands. Others will enforce the
+ * corresponding API permissions.
+ */
+public class ThreadNetworkShellCommand extends BasicShellCommandHandler {
+ private static final String TAG = "ThreadNetworkShellCommand";
+
+ // These don't require root access.
+ private static final List<String> NON_PRIVILEGED_COMMANDS = List.of("help", "get-country-code");
+
+ @Nullable private final ThreadNetworkCountryCode mCountryCode;
+ @Nullable private PrintWriter mOutputWriter;
+ @Nullable private PrintWriter mErrorWriter;
+
+ ThreadNetworkShellCommand(@Nullable ThreadNetworkCountryCode countryCode) {
+ mCountryCode = countryCode;
+ }
+
+ @VisibleForTesting
+ public void setPrintWriters(PrintWriter outputWriter, PrintWriter errorWriter) {
+ mOutputWriter = outputWriter;
+ mErrorWriter = errorWriter;
+ }
+
+ private PrintWriter getOutputWriter() {
+ return (mOutputWriter != null) ? mOutputWriter : getOutPrintWriter();
+ }
+
+ private PrintWriter getErrorWriter() {
+ return (mErrorWriter != null) ? mErrorWriter : getErrPrintWriter();
+ }
+
+ @Override
+ public int onCommand(String cmd) {
+ // Treat no command as help command.
+ if (TextUtils.isEmpty(cmd)) {
+ cmd = "help";
+ }
+
+ final PrintWriter pw = getOutputWriter();
+ final PrintWriter perr = getErrorWriter();
+
+ // Explicit exclusion from root permission
+ if (!NON_PRIVILEGED_COMMANDS.contains(cmd)) {
+ final int uid = Binder.getCallingUid();
+
+ if (uid != Process.ROOT_UID) {
+ perr.println(
+ "Uid "
+ + uid
+ + " does not have access to "
+ + cmd
+ + " thread command "
+ + "(or such command doesn't exist)");
+ return -1;
+ }
+ }
+
+ switch (cmd) {
+ case "force-country-code":
+ boolean enabled;
+
+ if (mCountryCode == null) {
+ perr.println("Thread country code operations are not supported");
+ return -1;
+ }
+
+ try {
+ enabled = getNextArgRequiredTrueOrFalse("enabled", "disabled");
+ } catch (IllegalArgumentException e) {
+ perr.println("Invalid argument: " + e.getMessage());
+ return -1;
+ }
+
+ if (enabled) {
+ String countryCode = getNextArgRequired();
+ if (!ThreadNetworkCountryCode.isValidCountryCode(countryCode)) {
+ perr.println(
+ "Invalid argument: Country code must be a 2-Character"
+ + " string. But got country code "
+ + countryCode
+ + " instead");
+ return -1;
+ }
+ mCountryCode.setOverrideCountryCode(countryCode);
+ pw.println("Set Thread country code: " + countryCode);
+
+ } else {
+ mCountryCode.clearOverrideCountryCode();
+ }
+ return 0;
+ case "get-country-code":
+ if (mCountryCode == null) {
+ perr.println("Thread country code operations are not supported");
+ return -1;
+ }
+
+ pw.println("Thread country code = " + mCountryCode.getCountryCode());
+ return 0;
+ default:
+ return handleDefaultCommands(cmd);
+ }
+ }
+
+ private static boolean argTrueOrFalse(String arg, String trueString, String falseString) {
+ if (trueString.equals(arg)) {
+ return true;
+ } else if (falseString.equals(arg)) {
+ return false;
+ } else {
+ throw new IllegalArgumentException(
+ "Expected '"
+ + trueString
+ + "' or '"
+ + falseString
+ + "' as next arg but got '"
+ + arg
+ + "'");
+ }
+ }
+
+ private boolean getNextArgRequiredTrueOrFalse(String trueString, String falseString) {
+ String nextArg = getNextArgRequired();
+ return argTrueOrFalse(nextArg, trueString, falseString);
+ }
+
+ private void onHelpNonPrivileged(PrintWriter pw) {
+ pw.println(" get-country-code");
+ pw.println(" Gets country code as a two-letter string");
+ }
+
+ private void onHelpPrivileged(PrintWriter pw) {
+ pw.println(" force-country-code enabled <two-letter code> | disabled ");
+ pw.println(" Sets country code to <two-letter code> or left for normal value");
+ }
+
+ @Override
+ public void onHelp() {
+ final PrintWriter pw = getOutputWriter();
+ pw.println("Thread network commands:");
+ pw.println(" help or -h");
+ pw.println(" Print this help text.");
+ onHelpNonPrivileged(pw);
+ if (Binder.getCallingUid() == Process.ROOT_UID) {
+ onHelpPrivileged(pw);
+ }
+ pw.println();
+ }
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
new file mode 100644
index 0000000..aba4193
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.thread;
+
+import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
+
+import android.annotation.Nullable;
+import android.content.ApexEnvironment;
+import android.content.Context;
+import android.os.PersistableBundle;
+import android.util.AtomicFile;
+import android.util.Log;
+
+import com.android.connectivity.resources.R;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.ConnectivityResources;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Store persistent data for Thread network settings. These are key (string) / value pairs that are
+ * stored in ThreadPersistentSetting.xml file. The values allowed are those that can be serialized
+ * via {@link PersistableBundle}.
+ */
+public class ThreadPersistentSettings {
+ private static final String TAG = "ThreadPersistentSettings";
+ /** File name used for storing settings. */
+ private static final String FILE_NAME = "ThreadPersistentSettings.xml";
+ /** Current config store data version. This will be incremented for any additions. */
+ private static final int CURRENT_SETTINGS_STORE_DATA_VERSION = 1;
+ /**
+ * Stores the version of the data. This can be used to handle migration of data if some
+ * non-backward compatible change introduced.
+ */
+ private static final String VERSION_KEY = "version";
+
+ /******** Thread persistent setting keys ***************/
+ /** Stores the Thread feature toggle state, true for enabled and false for disabled. */
+ public static final Key<Boolean> THREAD_ENABLED = new Key<>("Thread_enabled", true);
+
+ /******** Thread persistent setting keys ***************/
+
+ @GuardedBy("mLock")
+ private final AtomicFile mAtomicFile;
+
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private final PersistableBundle mSettings = new PersistableBundle();
+
+ private final ConnectivityResources mResources;
+
+ public static ThreadPersistentSettings newInstance(Context context) {
+ return new ThreadPersistentSettings(
+ new AtomicFile(new File(getOrCreateThreadNetworkDir(), FILE_NAME)),
+ new ConnectivityResources(context));
+ }
+
+ @VisibleForTesting
+ ThreadPersistentSettings(AtomicFile atomicFile, ConnectivityResources resources) {
+ mAtomicFile = atomicFile;
+ mResources = resources;
+ }
+
+ /** Initialize the settings by reading from the settings file. */
+ public void initialize() {
+ readFromStoreFile();
+ synchronized (mLock) {
+ if (!mSettings.containsKey(THREAD_ENABLED.key)) {
+ Log.i(TAG, "\"thread_enabled\" is missing in settings file, using default value");
+ put(
+ THREAD_ENABLED.key,
+ mResources.get().getBoolean(R.bool.config_thread_default_enabled));
+ }
+ }
+ }
+
+ private void putObject(String key, @Nullable Object value) {
+ synchronized (mLock) {
+ if (value == null) {
+ mSettings.putString(key, null);
+ } else if (value instanceof Boolean) {
+ mSettings.putBoolean(key, (Boolean) value);
+ } else if (value instanceof Integer) {
+ mSettings.putInt(key, (Integer) value);
+ } else if (value instanceof Long) {
+ mSettings.putLong(key, (Long) value);
+ } else if (value instanceof Double) {
+ mSettings.putDouble(key, (Double) value);
+ } else if (value instanceof String) {
+ mSettings.putString(key, (String) value);
+ } else {
+ throw new IllegalArgumentException("Unsupported type " + value.getClass());
+ }
+ }
+ }
+
+ private <T> T getObject(String key, T defaultValue) {
+ Object value;
+ synchronized (mLock) {
+ if (defaultValue instanceof Boolean) {
+ value = mSettings.getBoolean(key, (Boolean) defaultValue);
+ } else if (defaultValue instanceof Integer) {
+ value = mSettings.getInt(key, (Integer) defaultValue);
+ } else if (defaultValue instanceof Long) {
+ value = mSettings.getLong(key, (Long) defaultValue);
+ } else if (defaultValue instanceof Double) {
+ value = mSettings.getDouble(key, (Double) defaultValue);
+ } else if (defaultValue instanceof String) {
+ value = mSettings.getString(key, (String) defaultValue);
+ } else {
+ throw new IllegalArgumentException("Unsupported type " + defaultValue.getClass());
+ }
+ }
+ return (T) value;
+ }
+
+ /**
+ * Store a value to the stored settings.
+ *
+ * @param key One of the settings keys.
+ * @param value Value to be stored.
+ */
+ public <T> void put(String key, @Nullable T value) {
+ putObject(key, value);
+ writeToStoreFile();
+ }
+
+ /**
+ * Retrieve a value from the stored settings.
+ *
+ * @param key One of the settings keys.
+ * @return value stored in settings, defValue if the key does not exist.
+ */
+ public <T> T get(Key<T> key) {
+ return getObject(key.key, key.defaultValue);
+ }
+
+ /**
+ * Base class to store string key and its default value.
+ *
+ * @param <T> Type of the value.
+ */
+ public static class Key<T> {
+ public final String key;
+ public final T defaultValue;
+
+ private Key(String key, T defaultValue) {
+ this.key = key;
+ this.defaultValue = defaultValue;
+ }
+
+ @Override
+ public String toString() {
+ return "[Key: " + key + ", DefaultValue: " + defaultValue + "]";
+ }
+ }
+
+ private void writeToStoreFile() {
+ try {
+ final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ final PersistableBundle bundleToWrite;
+ synchronized (mLock) {
+ bundleToWrite = new PersistableBundle(mSettings);
+ }
+ bundleToWrite.putInt(VERSION_KEY, CURRENT_SETTINGS_STORE_DATA_VERSION);
+ bundleToWrite.writeToStream(outputStream);
+ synchronized (mLock) {
+ writeToAtomicFile(mAtomicFile, outputStream.toByteArray());
+ }
+ } catch (IOException e) {
+ Log.wtf(TAG, "Write to store file failed", e);
+ }
+ }
+
+ private void readFromStoreFile() {
+ try {
+ final byte[] readData;
+ synchronized (mLock) {
+ Log.i(TAG, "Reading from store file: " + mAtomicFile.getBaseFile());
+ readData = readFromAtomicFile(mAtomicFile);
+ }
+ final ByteArrayInputStream inputStream = new ByteArrayInputStream(readData);
+ final PersistableBundle bundleRead = PersistableBundle.readFromStream(inputStream);
+ // Version unused for now. May be needed in the future for handling migrations.
+ bundleRead.remove(VERSION_KEY);
+ synchronized (mLock) {
+ mSettings.putAll(bundleRead);
+ }
+ } catch (FileNotFoundException e) {
+ Log.e(TAG, "No store file to read", e);
+ } catch (IOException e) {
+ Log.e(TAG, "Read from store file failed", e);
+ }
+ }
+
+ /**
+ * Read raw data from the atomic file. Note: This is a copy of {@link AtomicFile#readFully()}
+ * modified to use the passed in {@link InputStream} which was returned using {@link
+ * AtomicFile#openRead()}.
+ */
+ private static byte[] readFromAtomicFile(AtomicFile file) throws IOException {
+ FileInputStream stream = null;
+ try {
+ stream = file.openRead();
+ int pos = 0;
+ int avail = stream.available();
+ byte[] data = new byte[avail];
+ while (true) {
+ int amt = stream.read(data, pos, data.length - pos);
+ if (amt <= 0) {
+ return data;
+ }
+ pos += amt;
+ avail = stream.available();
+ if (avail > data.length - pos) {
+ byte[] newData = new byte[pos + avail];
+ System.arraycopy(data, 0, newData, 0, pos);
+ data = newData;
+ }
+ }
+ } finally {
+ if (stream != null) stream.close();
+ }
+ }
+
+ /** Write the raw data to the atomic file. */
+ private static void writeToAtomicFile(AtomicFile file, byte[] data) throws IOException {
+ // Write the data to the atomic file.
+ FileOutputStream out = null;
+ try {
+ out = file.startWrite();
+ out.write(data);
+ file.finishWrite(out);
+ } catch (IOException e) {
+ if (out != null) {
+ file.failWrite(out);
+ }
+ throw e;
+ }
+ }
+
+ /** Get device protected storage dir for the tethering apex. */
+ private static File getOrCreateThreadNetworkDir() {
+ final File threadnetworkDir;
+ final File apexDataDir =
+ ApexEnvironment.getApexEnvironment(TETHERING_MODULE_NAME)
+ .getDeviceProtectedDataDir();
+ threadnetworkDir = new File(apexDataDir, "thread");
+
+ if (threadnetworkDir.exists() || threadnetworkDir.mkdirs()) {
+ return threadnetworkDir;
+ }
+ throw new IllegalStateException(
+ "Cannot write into thread network data directory: " + threadnetworkDir);
+ }
+}
diff --git a/thread/service/java/com/android/server/thread/TunInterfaceController.java b/thread/service/java/com/android/server/thread/TunInterfaceController.java
index 7223b2a..b29a54f 100644
--- a/thread/service/java/com/android/server/thread/TunInterfaceController.java
+++ b/thread/service/java/com/android/server/thread/TunInterfaceController.java
@@ -17,7 +17,10 @@
package com.android.server.thread;
import android.annotation.Nullable;
+import android.net.IpPrefix;
import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.RouteInfo;
import android.net.util.SocketUtils;
import android.os.ParcelFileDescriptor;
import android.os.SystemClock;
@@ -31,6 +34,7 @@
import java.io.FileDescriptor;
import java.io.IOException;
+import java.io.InterruptedIOException;
/** Controller for virtual/tunnel network interfaces. */
public class TunInterfaceController {
@@ -43,13 +47,21 @@
}
private final String mIfName;
+ private final LinkProperties mLinkProperties = new LinkProperties();
private ParcelFileDescriptor mParcelTunFd;
private FileDescriptor mNetlinkSocket;
private static int sNetlinkSeqNo = 0;
/** Creates a new {@link TunInterfaceController} instance for given interface. */
public TunInterfaceController(String interfaceName) {
- this.mIfName = interfaceName;
+ mIfName = interfaceName;
+ mLinkProperties.setInterfaceName(mIfName);
+ mLinkProperties.setMtu(MTU);
+ }
+
+ /** Returns link properties of the Thread TUN interface. */
+ public LinkProperties getLinkProperties() {
+ return mLinkProperties;
}
/**
@@ -87,13 +99,18 @@
/** Sets the interface up or down according to {@code isUp}. */
public void setInterfaceUp(boolean isUp) throws IOException {
+ if (!isUp) {
+ for (LinkAddress address : mLinkProperties.getAllLinkAddresses()) {
+ removeAddress(address);
+ }
+ }
nativeSetInterfaceUp(mIfName, isUp);
}
private native void nativeSetInterfaceUp(String interfaceName, boolean isUp) throws IOException;
/** Adds a new address to the interface. */
- public void addAddress(LinkAddress address) throws IOException {
+ public void addAddress(LinkAddress address) {
Log.d(TAG, "Adding address " + address + " with flags: " + address.getFlags());
long validLifetimeSeconds;
@@ -121,7 +138,7 @@
byte[] message =
RtNetlinkAddressMessage.newRtmNewAddressMessage(
- sNetlinkSeqNo,
+ sNetlinkSeqNo++,
address.getAddress(),
(short) address.getPrefixLength(),
address.getFlags(),
@@ -131,13 +148,51 @@
preferredLifetimeSeconds);
try {
Os.write(mNetlinkSocket, message, 0, message.length);
- } catch (ErrnoException e) {
- throw new IOException("Failed to send netlink message", e);
+ } catch (ErrnoException | InterruptedIOException e) {
+ Log.e(TAG, "Failed to add address " + address, e);
+ return;
}
+ mLinkProperties.addLinkAddress(address);
+ mLinkProperties.addRoute(getRouteForAddress(address));
}
/** Removes an address from the interface. */
- public void removeAddress(LinkAddress address) throws IOException {
- // TODO(b/263222068): remove address with netlink
+ public void removeAddress(LinkAddress address) {
+ Log.d(TAG, "Removing address " + address);
+ byte[] message =
+ RtNetlinkAddressMessage.newRtmDelAddressMessage(
+ sNetlinkSeqNo++,
+ address.getAddress(),
+ (short) address.getPrefixLength(),
+ Os.if_nametoindex(mIfName));
+
+ // Intentionally update the mLinkProperties before send netlink message because the
+ // address is already removed from ot-daemon and apps can't reach to the address even
+ // when the netlink request below fails
+ mLinkProperties.removeLinkAddress(address);
+ mLinkProperties.removeRoute(getRouteForAddress(address));
+ try {
+ Os.write(mNetlinkSocket, message, 0, message.length);
+ } catch (ErrnoException | InterruptedIOException e) {
+ Log.e(TAG, "Failed to remove address " + address, e);
+ }
+ }
+
+ private RouteInfo getRouteForAddress(LinkAddress linkAddress) {
+ return new RouteInfo(
+ new IpPrefix(linkAddress.getAddress(), linkAddress.getPrefixLength()),
+ null,
+ mIfName,
+ RouteInfo.RTN_UNICAST,
+ MTU);
+ }
+
+ /** Called by {@link ThreadNetworkControllerService} to do clean up when ot-daemon is dead. */
+ public void onOtDaemonDied() {
+ try {
+ setInterfaceUp(false);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to set Thread TUN interface down");
+ }
}
}
diff --git a/thread/service/proguard.flags b/thread/service/proguard.flags
deleted file mode 100644
index 5028982..0000000
--- a/thread/service/proguard.flags
+++ /dev/null
@@ -1,4 +0,0 @@
-# Ensure the callback methods are not stripped
--keepclassmembers class **.ThreadNetworkControllerService$ThreadNetworkCallback {
- *;
-}
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index d91c9db..c1cf0a0 100644
--- a/thread/tests/cts/Android.bp
+++ b/thread/tests/cts/Android.bp
@@ -15,12 +15,13 @@
//
package {
+ default_team: "trendy_team_fwk_thread_network",
default_applicable_licenses: ["Android-Apache-2.0"],
}
-// TODO: add this test to the CTS test suite
android_test {
name: "CtsThreadNetworkTestCases",
+ defaults: ["cts_defaults"],
min_sdk_version: "33",
sdk_version: "test_current",
manifest: "AndroidManifest.xml",
@@ -29,7 +30,9 @@
"src/**/*.java",
],
test_suites: [
+ "cts",
"general-tests",
+ "mcts-tethering",
"mts-tethering",
],
static_libs: [
@@ -39,14 +42,16 @@
"guava",
"guava-android-testlib",
"net-tests-utils",
+ "ThreadNetworkTestUtils",
"truth",
],
libs: [
"android.test.base",
"android.test.runner",
- "framework-connectivity-module-api-stubs-including-flagged"
+ "framework-connectivity-module-api-stubs-including-flagged",
],
// Test coverage system runs on different devices. Need to
// compile for all architectures.
compile_multilib: "both",
+ platform_apis: true,
}
diff --git a/thread/tests/cts/AndroidManifest.xml b/thread/tests/cts/AndroidManifest.xml
index 4370fe3..1541bf5 100644
--- a/thread/tests/cts/AndroidManifest.xml
+++ b/thread/tests/cts/AndroidManifest.xml
@@ -19,6 +19,9 @@
xmlns:android="http://schemas.android.com/apk/res/android"
package="android.net.thread.cts">
+ <uses-permission android:name="android.permission.INTERNET"/>
+ <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
+
<application android:debuggable="true">
<uses-library android:name="android.test.runner" />
</application>
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
index 362ff39..36ce4d5 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -16,24 +16,41 @@
package android.net.thread.cts;
+import static android.Manifest.permission.ACCESS_NETWORK_STATE;
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_CHILD;
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_ROUTER;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_STOPPED;
+import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
+import static android.net.thread.ThreadNetworkController.STATE_DISABLING;
+import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
import static android.net.thread.ThreadNetworkController.THREAD_VERSION_1_3;
import static android.net.thread.ThreadNetworkException.ERROR_ABORTED;
import static android.net.thread.ThreadNetworkException.ERROR_FAILED_PRECONDITION;
import static android.net.thread.ThreadNetworkException.ERROR_REJECTED_BY_PEER;
+import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
import static org.junit.Assume.assumeNotNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import android.Manifest.permission;
import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.nsd.NsdManager;
+import android.net.nsd.NsdServiceInfo;
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.OperationalDatasetTimestamp;
import android.net.thread.PendingOperationalDataset;
@@ -42,17 +59,20 @@
import android.net.thread.ThreadNetworkController.StateCallback;
import android.net.thread.ThreadNetworkException;
import android.net.thread.ThreadNetworkManager;
+import android.net.thread.utils.TapTestNetworkTracker;
import android.os.Build;
+import android.os.HandlerThread;
import android.os.OutcomeReceiver;
+import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest;
+import com.android.net.module.util.ArrayTrackRecord;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.DevSdkIgnoreRunner;
-
-import com.google.common.util.concurrent.SettableFuture;
+import com.android.testutils.FunctionalUtils.ThrowingRunnable;
import org.junit.After;
import org.junit.Before;
@@ -60,65 +80,744 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Random;
import java.util.Set;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Predicate;
/** CTS tests for {@link ThreadNetworkController}. */
@LargeTest
@RunWith(DevSdkIgnoreRunner.class)
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) // Thread is available on only U+
public class ThreadNetworkControllerTest {
- private static final int CALLBACK_TIMEOUT_MILLIS = 1000;
- private static final String PERMISSION_THREAD_NETWORK_PRIVILEGED =
+ private static final int JOIN_TIMEOUT_MILLIS = 30 * 1000;
+ private static final int LEAVE_TIMEOUT_MILLIS = 2_000;
+ private static final int MIGRATION_TIMEOUT_MILLIS = 40 * 1_000;
+ private static final int NETWORK_CALLBACK_TIMEOUT_MILLIS = 10 * 1000;
+ private static final int CALLBACK_TIMEOUT_MILLIS = 1_000;
+ private static final int ENABLED_TIMEOUT_MILLIS = 2_000;
+ private static final int SERVICE_DISCOVERY_TIMEOUT_MILLIS = 30_000;
+ private static final String MESHCOP_SERVICE_TYPE = "_meshcop._udp";
+ private static final String THREAD_NETWORK_PRIVILEGED =
"android.permission.THREAD_NETWORK_PRIVILEGED";
@Rule public DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
private final Context mContext = ApplicationProvider.getApplicationContext();
private ExecutorService mExecutor;
- private ThreadNetworkManager mManager;
+ private ThreadNetworkController mController;
+ private NsdManager mNsdManager;
private Set<String> mGrantedPermissions;
+ private HandlerThread mHandlerThread;
+ private TapTestNetworkTracker mTestNetworkTracker;
@Before
- public void setUp() {
- mExecutor = Executors.newSingleThreadExecutor();
- mManager = mContext.getSystemService(ThreadNetworkManager.class);
- mGrantedPermissions = new HashSet<String>();
+ public void setUp() throws Exception {
+ ThreadNetworkManager manager = mContext.getSystemService(ThreadNetworkManager.class);
+ if (manager != null) {
+ mController = manager.getAllThreadNetworkControllers().get(0);
+ }
// TODO: we will also need it in tearDown(), it's better to have a Rule to skip
// tests if a feature is not available.
- assumeNotNull(mManager);
+ assumeNotNull(mController);
+
+ mGrantedPermissions = new HashSet<String>();
+ mExecutor = Executors.newSingleThreadExecutor();
+ mNsdManager = mContext.getSystemService(NsdManager.class);
+ mHandlerThread = new HandlerThread(this.getClass().getSimpleName());
+ mHandlerThread.start();
+
+ setEnabledAndWait(mController, true);
}
@After
public void tearDown() throws Exception {
- if (mManager != null) {
- leaveAndWait();
+ if (mController == null) {
+ return;
+ }
+ dropAllPermissions();
+ leaveAndWait(mController);
+ tearDownTestNetwork();
+ }
+
+ @Test
+ public void getThreadVersion_returnsAtLeastThreadVersion1P3() {
+ assertThat(mController.getThreadVersion()).isAtLeast(THREAD_VERSION_1_3);
+ }
+
+ @Test
+ public void registerStateCallback_permissionsGranted_returnsCurrentStates() throws Exception {
+ CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+ StateCallback callback = deviceRole::complete;
+
+ try {
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ () -> mController.registerStateCallback(mExecutor, callback));
+
+ assertThat(deviceRole.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS))
+ .isEqualTo(DEVICE_ROLE_STOPPED);
+ } finally {
+ runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(callback));
+ }
+ }
+
+ @Test
+ public void registerStateCallback_returnsUpdatedEnabledStates() throws Exception {
+ CompletableFuture<Void> setFuture1 = new CompletableFuture<>();
+ CompletableFuture<Void> setFuture2 = new CompletableFuture<>();
+ EnabledStateListener listener = new EnabledStateListener(mController);
+
+ try {
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> {
+ mController.setEnabled(false, mExecutor, newOutcomeReceiver(setFuture1));
+ });
+ setFuture1.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> {
+ mController.setEnabled(true, mExecutor, newOutcomeReceiver(setFuture2));
+ });
+ setFuture2.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+
+ listener.expectThreadEnabledState(STATE_ENABLED);
+ listener.expectThreadEnabledState(STATE_DISABLING);
+ listener.expectThreadEnabledState(STATE_DISABLED);
+ listener.expectThreadEnabledState(STATE_ENABLED);
+ } finally {
+ listener.unregisterStateCallback();
+ }
+ }
+
+ @Test
+ public void registerStateCallback_noPermissions_throwsSecurityException() throws Exception {
+ dropAllPermissions();
+
+ assertThrows(
+ SecurityException.class,
+ () -> mController.registerStateCallback(mExecutor, role -> {}));
+ }
+
+ @Test
+ public void registerStateCallback_alreadyRegistered_throwsIllegalArgumentException()
+ throws Exception {
+ grantPermissions(ACCESS_NETWORK_STATE);
+ CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+ StateCallback callback = role -> deviceRole.complete(role);
+
+ mController.registerStateCallback(mExecutor, callback);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mController.registerStateCallback(mExecutor, callback));
+ }
+
+ @Test
+ public void unregisterStateCallback_noPermissions_throwsSecurityException() throws Exception {
+ CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+ StateCallback callback = role -> deviceRole.complete(role);
+ runAsShell(
+ ACCESS_NETWORK_STATE, () -> mController.registerStateCallback(mExecutor, callback));
+
+ try {
dropAllPermissions();
+ assertThrows(
+ SecurityException.class, () -> mController.unregisterStateCallback(callback));
+ } finally {
+ runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(callback));
}
}
- private List<ThreadNetworkController> getAllControllers() {
- return mManager.getAllThreadNetworkControllers();
+ @Test
+ public void unregisterStateCallback_callbackRegistered_success() throws Exception {
+ grantPermissions(ACCESS_NETWORK_STATE);
+ CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+ StateCallback callback = role -> deviceRole.complete(role);
+
+ assertDoesNotThrow(() -> mController.registerStateCallback(mExecutor, callback));
+ mController.unregisterStateCallback(callback);
}
- private void leaveAndWait() throws Exception {
- grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ @Test
+ public void unregisterStateCallback_callbackNotRegistered_throwsIllegalArgumentException()
+ throws Exception {
+ CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+ StateCallback callback = role -> deviceRole.complete(role);
- for (ThreadNetworkController controller : getAllControllers()) {
- SettableFuture<Void> future = SettableFuture.create();
- controller.leave(mExecutor, future::set);
- future.get();
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mController.unregisterStateCallback(callback));
+ }
+
+ @Test
+ public void unregisterStateCallback_alreadyUnregistered_throwsIllegalArgumentException()
+ throws Exception {
+ grantPermissions(ACCESS_NETWORK_STATE);
+ CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+ StateCallback callback = deviceRole::complete;
+ mController.registerStateCallback(mExecutor, callback);
+ mController.unregisterStateCallback(callback);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mController.unregisterStateCallback(callback));
+ }
+
+ @Test
+ public void registerOperationalDatasetCallback_permissionsGranted_returnsCurrentStates()
+ throws Exception {
+ grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+ CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
+ CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
+ var callback = newDatasetCallback(activeFuture, pendingFuture);
+
+ try {
+ mController.registerOperationalDatasetCallback(mExecutor, callback);
+
+ assertThat(activeFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
+ assertThat(pendingFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
+ } finally {
+ mController.unregisterOperationalDatasetCallback(callback);
}
}
+ @Test
+ public void registerOperationalDatasetCallback_noPermissions_throwsSecurityException()
+ throws Exception {
+ dropAllPermissions();
+ CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
+ CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
+ var callback = newDatasetCallback(activeFuture, pendingFuture);
+
+ assertThrows(
+ SecurityException.class,
+ () -> mController.registerOperationalDatasetCallback(mExecutor, callback));
+ }
+
+ @Test
+ public void unregisterOperationalDatasetCallback_callbackRegistered_success() throws Exception {
+ grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+ CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
+ CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
+ var callback = newDatasetCallback(activeFuture, pendingFuture);
+ mController.registerOperationalDatasetCallback(mExecutor, callback);
+
+ assertDoesNotThrow(() -> mController.unregisterOperationalDatasetCallback(callback));
+ }
+
+ @Test
+ public void unregisterOperationalDatasetCallback_noPermissions_throwsSecurityException()
+ throws Exception {
+ CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
+ CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
+ var callback = newDatasetCallback(activeFuture, pendingFuture);
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ THREAD_NETWORK_PRIVILEGED,
+ () -> mController.registerOperationalDatasetCallback(mExecutor, callback));
+
+ try {
+ dropAllPermissions();
+ assertThrows(
+ SecurityException.class,
+ () -> mController.unregisterOperationalDatasetCallback(callback));
+ } finally {
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ THREAD_NETWORK_PRIVILEGED,
+ () -> mController.unregisterOperationalDatasetCallback(callback));
+ }
+ }
+
+ private static <V> OutcomeReceiver<V, ThreadNetworkException> newOutcomeReceiver(
+ CompletableFuture<V> future) {
+ return new OutcomeReceiver<V, ThreadNetworkException>() {
+ @Override
+ public void onResult(V result) {
+ future.complete(result);
+ }
+
+ @Override
+ public void onError(ThreadNetworkException e) {
+ future.completeExceptionally(e);
+ }
+ };
+ }
+
+ @Test
+ public void join_withPrivilegedPermission_success() throws Exception {
+ ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", mController);
+ CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> mController.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture)));
+ joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+
+ assertThat(isAttached(mController)).isTrue();
+ assertThat(getActiveOperationalDataset(mController)).isEqualTo(activeDataset);
+ }
+
+ @Test
+ public void join_withoutPrivilegedPermission_throwsSecurityException() throws Exception {
+ dropAllPermissions();
+ ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", mController);
+
+ assertThrows(
+ SecurityException.class, () -> mController.join(activeDataset, mExecutor, v -> {}));
+ }
+
+ @Test
+ public void join_threadDisabled_failsWithErrorThreadDisabled() throws Exception {
+ setEnabledAndWait(mController, false);
+ ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", mController);
+ CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> mController.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture)));
+
+ var thrown =
+ assertThrows(
+ ExecutionException.class,
+ () -> joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS));
+ var threadException = (ThreadNetworkException) thrown.getCause();
+ assertThat(threadException.getErrorCode()).isEqualTo(ERROR_THREAD_DISABLED);
+ }
+
+ @Test
+ public void join_concurrentRequests_firstOneIsAborted() throws Exception {
+ final byte[] KEY_1 = new byte[] {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
+ final byte[] KEY_2 = new byte[] {2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2};
+ ActiveOperationalDataset activeDataset1 =
+ new ActiveOperationalDataset.Builder(newRandomizedDataset("TestNet", mController))
+ .setNetworkKey(KEY_1)
+ .build();
+ ActiveOperationalDataset activeDataset2 =
+ new ActiveOperationalDataset.Builder(activeDataset1).setNetworkKey(KEY_2).build();
+ CompletableFuture<Void> joinFuture1 = new CompletableFuture<>();
+ CompletableFuture<Void> joinFuture2 = new CompletableFuture<>();
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> {
+ mController.join(activeDataset1, mExecutor, newOutcomeReceiver(joinFuture1));
+ mController.join(activeDataset2, mExecutor, newOutcomeReceiver(joinFuture2));
+ });
+
+ var thrown =
+ assertThrows(
+ ExecutionException.class,
+ () -> joinFuture1.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS));
+ var threadException = (ThreadNetworkException) thrown.getCause();
+ assertThat(threadException.getErrorCode()).isEqualTo(ERROR_ABORTED);
+ joinFuture2.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+ assertThat(isAttached(mController)).isTrue();
+ assertThat(getActiveOperationalDataset(mController)).isEqualTo(activeDataset2);
+ }
+
+ @Test
+ public void leave_withPrivilegedPermission_success() throws Exception {
+ CompletableFuture<Void> leaveFuture = new CompletableFuture<>();
+ joinRandomizedDatasetAndWait(mController);
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> mController.leave(mExecutor, newOutcomeReceiver(leaveFuture)));
+ leaveFuture.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
+
+ assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+ }
+
+ @Test
+ public void leave_withoutPrivilegedPermission_throwsSecurityException() {
+ dropAllPermissions();
+
+ assertThrows(SecurityException.class, () -> mController.leave(mExecutor, v -> {}));
+ }
+
+ @Test
+ public void leave_threadDisabled_success() throws Exception {
+ setEnabledAndWait(mController, false);
+ CompletableFuture<Void> leaveFuture = new CompletableFuture<>();
+
+ leave(mController, newOutcomeReceiver(leaveFuture));
+ leaveFuture.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
+
+ assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+ }
+
+ @Test
+ public void leave_concurrentRequests_bothSuccess() throws Exception {
+ CompletableFuture<Void> leaveFuture1 = new CompletableFuture<>();
+ CompletableFuture<Void> leaveFuture2 = new CompletableFuture<>();
+ joinRandomizedDatasetAndWait(mController);
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> {
+ mController.leave(mExecutor, newOutcomeReceiver(leaveFuture1));
+ mController.leave(mExecutor, newOutcomeReceiver(leaveFuture2));
+ });
+
+ leaveFuture1.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
+ leaveFuture2.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
+ assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+ }
+
+ @Test
+ public void scheduleMigration_withPrivilegedPermission_newDatasetApplied() throws Exception {
+ grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+ ActiveOperationalDataset activeDataset1 =
+ new ActiveOperationalDataset.Builder(newRandomizedDataset("TestNet", mController))
+ .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
+ .setExtendedPanId(new byte[] {1, 1, 1, 1, 1, 1, 1, 1})
+ .build();
+ ActiveOperationalDataset activeDataset2 =
+ new ActiveOperationalDataset.Builder(activeDataset1)
+ .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
+ .setNetworkName("ThreadNet2")
+ .build();
+ PendingOperationalDataset pendingDataset2 =
+ new PendingOperationalDataset(
+ activeDataset2,
+ OperationalDatasetTimestamp.fromInstant(Instant.now()),
+ Duration.ofSeconds(30));
+ CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+ CompletableFuture<Void> migrateFuture = new CompletableFuture<>();
+ mController.join(activeDataset1, mExecutor, newOutcomeReceiver(joinFuture));
+ joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+
+ mController.scheduleMigration(
+ pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture));
+ migrateFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+
+ CompletableFuture<Boolean> dataset2IsApplied = new CompletableFuture<>();
+ CompletableFuture<Boolean> pendingDatasetIsRemoved = new CompletableFuture<>();
+ OperationalDatasetCallback datasetCallback =
+ new OperationalDatasetCallback() {
+ @Override
+ public void onActiveOperationalDatasetChanged(
+ ActiveOperationalDataset activeDataset) {
+ if (activeDataset.equals(activeDataset2)) {
+ dataset2IsApplied.complete(true);
+ }
+ }
+
+ @Override
+ public void onPendingOperationalDatasetChanged(
+ PendingOperationalDataset pendingDataset) {
+ if (pendingDataset == null) {
+ pendingDatasetIsRemoved.complete(true);
+ }
+ }
+ };
+ mController.registerOperationalDatasetCallback(directExecutor(), datasetCallback);
+ try {
+ assertThat(dataset2IsApplied.get(MIGRATION_TIMEOUT_MILLIS, MILLISECONDS)).isTrue();
+ assertThat(pendingDatasetIsRemoved.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isTrue();
+ } finally {
+ mController.unregisterOperationalDatasetCallback(datasetCallback);
+ }
+ }
+
+ @Test
+ public void scheduleMigration_whenNotAttached_failWithPreconditionError() throws Exception {
+ grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+ PendingOperationalDataset pendingDataset =
+ new PendingOperationalDataset(
+ newRandomizedDataset("TestNet", mController),
+ OperationalDatasetTimestamp.fromInstant(Instant.now()),
+ Duration.ofSeconds(30));
+ CompletableFuture<Void> migrateFuture = new CompletableFuture<>();
+
+ mController.scheduleMigration(pendingDataset, mExecutor, newOutcomeReceiver(migrateFuture));
+
+ ThreadNetworkException thrown =
+ (ThreadNetworkException)
+ assertThrows(ExecutionException.class, migrateFuture::get).getCause();
+ assertThat(thrown.getErrorCode()).isEqualTo(ERROR_FAILED_PRECONDITION);
+ }
+
+ @Test
+ public void scheduleMigration_secondRequestHasSmallerTimestamp_rejectedByLeader()
+ throws Exception {
+ grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+ final ActiveOperationalDataset activeDataset =
+ new ActiveOperationalDataset.Builder(newRandomizedDataset("testNet", mController))
+ .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
+ .build();
+ ActiveOperationalDataset activeDataset1 =
+ new ActiveOperationalDataset.Builder(activeDataset)
+ .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
+ .setNetworkName("testNet1")
+ .build();
+ PendingOperationalDataset pendingDataset1 =
+ new PendingOperationalDataset(
+ activeDataset1,
+ new OperationalDatasetTimestamp(100, 0, false),
+ Duration.ofSeconds(30));
+ ActiveOperationalDataset activeDataset2 =
+ new ActiveOperationalDataset.Builder(activeDataset)
+ .setActiveTimestamp(new OperationalDatasetTimestamp(3L, 0, false))
+ .setNetworkName("testNet2")
+ .build();
+ PendingOperationalDataset pendingDataset2 =
+ new PendingOperationalDataset(
+ activeDataset2,
+ new OperationalDatasetTimestamp(20, 0, false),
+ Duration.ofSeconds(30));
+ CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+ CompletableFuture<Void> migrateFuture1 = new CompletableFuture<>();
+ CompletableFuture<Void> migrateFuture2 = new CompletableFuture<>();
+ mController.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
+ joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+
+ mController.scheduleMigration(
+ pendingDataset1, mExecutor, newOutcomeReceiver(migrateFuture1));
+ migrateFuture1.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+ mController.scheduleMigration(
+ pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2));
+
+ ThreadNetworkException thrown =
+ (ThreadNetworkException)
+ assertThrows(ExecutionException.class, migrateFuture2::get).getCause();
+ assertThat(thrown.getErrorCode()).isEqualTo(ERROR_REJECTED_BY_PEER);
+ }
+
+ @Test
+ public void scheduleMigration_secondRequestHasLargerTimestamp_newDatasetApplied()
+ throws Exception {
+ grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+ final ActiveOperationalDataset activeDataset =
+ new ActiveOperationalDataset.Builder(newRandomizedDataset("validName", mController))
+ .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
+ .build();
+ ActiveOperationalDataset activeDataset1 =
+ new ActiveOperationalDataset.Builder(activeDataset)
+ .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
+ .setNetworkName("testNet1")
+ .build();
+ PendingOperationalDataset pendingDataset1 =
+ new PendingOperationalDataset(
+ activeDataset1,
+ new OperationalDatasetTimestamp(100, 0, false),
+ Duration.ofSeconds(30));
+ ActiveOperationalDataset activeDataset2 =
+ new ActiveOperationalDataset.Builder(activeDataset)
+ .setActiveTimestamp(new OperationalDatasetTimestamp(3L, 0, false))
+ .setNetworkName("testNet2")
+ .build();
+ PendingOperationalDataset pendingDataset2 =
+ new PendingOperationalDataset(
+ activeDataset2,
+ new OperationalDatasetTimestamp(200, 0, false),
+ Duration.ofSeconds(30));
+ CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+ CompletableFuture<Void> migrateFuture1 = new CompletableFuture<>();
+ CompletableFuture<Void> migrateFuture2 = new CompletableFuture<>();
+ mController.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
+ joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+
+ mController.scheduleMigration(
+ pendingDataset1, mExecutor, newOutcomeReceiver(migrateFuture1));
+ migrateFuture1.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+ mController.scheduleMigration(
+ pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2));
+ migrateFuture2.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+
+ CompletableFuture<Boolean> dataset2IsApplied = new CompletableFuture<>();
+ CompletableFuture<Boolean> pendingDatasetIsRemoved = new CompletableFuture<>();
+ OperationalDatasetCallback datasetCallback =
+ new OperationalDatasetCallback() {
+ @Override
+ public void onActiveOperationalDatasetChanged(
+ ActiveOperationalDataset activeDataset) {
+ if (activeDataset.equals(activeDataset2)) {
+ dataset2IsApplied.complete(true);
+ }
+ }
+
+ @Override
+ public void onPendingOperationalDatasetChanged(
+ PendingOperationalDataset pendingDataset) {
+ if (pendingDataset == null) {
+ pendingDatasetIsRemoved.complete(true);
+ }
+ }
+ };
+ mController.registerOperationalDatasetCallback(directExecutor(), datasetCallback);
+ try {
+ assertThat(dataset2IsApplied.get(MIGRATION_TIMEOUT_MILLIS, MILLISECONDS)).isTrue();
+ assertThat(pendingDatasetIsRemoved.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isTrue();
+ } finally {
+ mController.unregisterOperationalDatasetCallback(datasetCallback);
+ }
+ }
+
+ @Test
+ public void scheduleMigration_threadDisabled_failsWithErrorThreadDisabled() throws Exception {
+ ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", mController);
+ PendingOperationalDataset pendingDataset =
+ new PendingOperationalDataset(
+ activeDataset,
+ OperationalDatasetTimestamp.fromInstant(Instant.now()),
+ Duration.ofSeconds(30));
+ joinRandomizedDatasetAndWait(mController);
+ CompletableFuture<Void> migrationFuture = new CompletableFuture<>();
+
+ setEnabledAndWait(mController, false);
+
+ scheduleMigration(mController, pendingDataset, newOutcomeReceiver(migrationFuture));
+
+ ThreadNetworkException thrown =
+ (ThreadNetworkException)
+ assertThrows(ExecutionException.class, migrationFuture::get).getCause();
+ assertThat(thrown.getErrorCode()).isEqualTo(ERROR_THREAD_DISABLED);
+ }
+
+ @Test
+ public void createRandomizedDataset_wrongNetworkNameLength_throwsIllegalArgumentException() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mController.createRandomizedDataset("", mExecutor, dataset -> {}));
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ mController.createRandomizedDataset(
+ "ANetNameIs17Bytes", mExecutor, dataset -> {}));
+ }
+
+ @Test
+ public void createRandomizedDataset_validNetworkName_success() throws Exception {
+ ActiveOperationalDataset dataset = newRandomizedDataset("validName", mController);
+
+ assertThat(dataset.getNetworkName()).isEqualTo("validName");
+ assertThat(dataset.getPanId()).isLessThan(0xffff);
+ assertThat(dataset.getChannelMask().size()).isAtLeast(1);
+ assertThat(dataset.getExtendedPanId()).hasLength(8);
+ assertThat(dataset.getNetworkKey()).hasLength(16);
+ assertThat(dataset.getPskc()).hasLength(16);
+ assertThat(dataset.getMeshLocalPrefix().getPrefixLength()).isEqualTo(64);
+ assertThat(dataset.getMeshLocalPrefix().getRawAddress()[0]).isEqualTo((byte) 0xfd);
+ }
+
+ @Test
+ public void setEnabled_permissionsGranted_succeeds() throws Exception {
+ CompletableFuture<Void> setFuture1 = new CompletableFuture<>();
+ CompletableFuture<Void> setFuture2 = new CompletableFuture<>();
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> mController.setEnabled(false, mExecutor, newOutcomeReceiver(setFuture1)));
+ setFuture1.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+ waitForEnabledState(mController, booleanToEnabledState(false));
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> mController.setEnabled(true, mExecutor, newOutcomeReceiver(setFuture2)));
+ setFuture2.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+ waitForEnabledState(mController, booleanToEnabledState(true));
+ }
+
+ @Test
+ public void setEnabled_noPermissions_throwsSecurityException() throws Exception {
+ CompletableFuture<Void> setFuture = new CompletableFuture<>();
+ assertThrows(
+ SecurityException.class,
+ () -> mController.setEnabled(false, mExecutor, newOutcomeReceiver(setFuture)));
+ }
+
+ @Test
+ public void setEnabled_disable_leavesThreadNetwork() throws Exception {
+ joinRandomizedDatasetAndWait(mController);
+ setEnabledAndWait(mController, false);
+ assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+ }
+
+ @Test
+ public void setEnabled_toggleAfterJoin_joinsThreadNetworkAgain() throws Exception {
+ joinRandomizedDatasetAndWait(mController);
+
+ setEnabledAndWait(mController, false);
+ assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+ setEnabledAndWait(mController, true);
+
+ runAsShell(ACCESS_NETWORK_STATE, () -> waitForAttachedState(mController));
+ }
+
+ @Test
+ public void setEnabled_enableFollowedByDisable_allSucceed() throws Exception {
+ joinRandomizedDatasetAndWait(mController);
+ CompletableFuture<Void> setFuture1 = new CompletableFuture<>();
+ CompletableFuture<Void> setFuture2 = new CompletableFuture<>();
+ EnabledStateListener listener = new EnabledStateListener(mController);
+ listener.expectThreadEnabledState(STATE_ENABLED);
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> {
+ mController.setEnabled(true, mExecutor, newOutcomeReceiver(setFuture1));
+ mController.setEnabled(false, mExecutor, newOutcomeReceiver(setFuture2));
+ });
+ setFuture1.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+ setFuture2.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+
+ listener.expectThreadEnabledState(STATE_DISABLING);
+ listener.expectThreadEnabledState(STATE_DISABLED);
+ assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+ // FIXME: this is not called when a exception is thrown after the creation of `listener`
+ listener.unregisterStateCallback();
+ }
+
+ // TODO (b/322437869): add test case to verify when Thread is in DISABLING state, any commands
+ // (join/leave/scheduleMigration/setEnabled) fail with ERROR_BUSY. This is not currently tested
+ // because DISABLING has very short lifecycle, it's not possible to guarantee the command can be
+ // sent before state changes to DISABLED.
+
+ @Test
+ public void threadNetworkCallback_deviceAttached_threadNetworkIsAvailable() throws Exception {
+ CompletableFuture<Network> networkFuture = new CompletableFuture<>();
+ ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
+ NetworkRequest networkRequest =
+ new NetworkRequest.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
+ .build();
+ ConnectivityManager.NetworkCallback networkCallback =
+ new ConnectivityManager.NetworkCallback() {
+ @Override
+ public void onAvailable(Network network) {
+ networkFuture.complete(network);
+ }
+ };
+
+ joinRandomizedDatasetAndWait(mController);
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ () -> cm.registerNetworkCallback(networkRequest, networkCallback));
+
+ assertThat(isAttached(mController)).isTrue();
+ assertThat(networkFuture.get(NETWORK_CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNotNull();
+ }
+
private void grantPermissions(String... permissions) {
for (String permission : permissions) {
mGrantedPermissions.add(permission);
@@ -128,14 +827,79 @@
getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(allPermissions);
}
+ @Test
+ public void meshcopService_threadEnabledButNotJoined_discoveredButNoNetwork() throws Exception {
+ setUpTestNetwork();
+
+ setEnabledAndWait(mController, true);
+ leaveAndWait(mController);
+
+ NsdServiceInfo serviceInfo =
+ expectServiceResolved(
+ MESHCOP_SERVICE_TYPE,
+ SERVICE_DISCOVERY_TIMEOUT_MILLIS,
+ s -> s.getAttributes().get("at") == null);
+
+ Map<String, byte[]> txtMap = serviceInfo.getAttributes();
+
+ assertThat(txtMap.get("rv")).isNotNull();
+ assertThat(txtMap.get("tv")).isNotNull();
+ assertThat(txtMap.get("sb")).isNotNull();
+ }
+
+ @Test
+ public void meshcopService_joinedNetwork_discoveredHasNetwork() throws Exception {
+ setUpTestNetwork();
+
+ String networkName = "TestNet" + new Random().nextInt(10_000);
+ joinRandomizedDatasetAndWait(mController, networkName);
+
+ Predicate<NsdServiceInfo> predicate =
+ serviceInfo ->
+ serviceInfo.getAttributes().get("at") != null
+ && Arrays.equals(
+ serviceInfo.getAttributes().get("nn"),
+ networkName.getBytes(StandardCharsets.UTF_8));
+
+ NsdServiceInfo resolvedService =
+ expectServiceResolved(
+ MESHCOP_SERVICE_TYPE, SERVICE_DISCOVERY_TIMEOUT_MILLIS, predicate);
+
+ Map<String, byte[]> txtMap = resolvedService.getAttributes();
+ assertThat(txtMap.get("rv")).isNotNull();
+ assertThat(txtMap.get("tv")).isNotNull();
+ assertThat(txtMap.get("sb")).isNotNull();
+ assertThat(txtMap.get("id").length).isEqualTo(16);
+ }
+
+ @Test
+ public void meshcopService_threadDisabled_notDiscovered() throws Exception {
+ setUpTestNetwork();
+
+ CompletableFuture<NsdServiceInfo> serviceLostFuture = new CompletableFuture<>();
+ NsdManager.DiscoveryListener listener =
+ discoverForServiceLost(MESHCOP_SERVICE_TYPE, serviceLostFuture);
+ setEnabledAndWait(mController, false);
+ try {
+ serviceLostFuture.get(SERVICE_DISCOVERY_TIMEOUT_MILLIS, MILLISECONDS);
+ } catch (InterruptedException | ExecutionException | TimeoutException ignored) {
+ // It's fine if the service lost event didn't show up. The service may not ever be
+ // advertised.
+ } finally {
+ mNsdManager.stopServiceDiscovery(listener);
+ }
+
+ assertThrows(TimeoutException.class, () -> discoverService(MESHCOP_SERVICE_TYPE));
+ }
+
private static void dropAllPermissions() {
getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
}
private static ActiveOperationalDataset newRandomizedDataset(
String networkName, ThreadNetworkController controller) throws Exception {
- SettableFuture<ActiveOperationalDataset> future = SettableFuture.create();
- controller.createRandomizedDataset(networkName, directExecutor(), future::set);
+ CompletableFuture<ActiveOperationalDataset> future = new CompletableFuture<>();
+ controller.createRandomizedDataset(networkName, directExecutor(), future::complete);
return future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
}
@@ -144,567 +908,302 @@
}
private static int getDeviceRole(ThreadNetworkController controller) throws Exception {
- SettableFuture<Integer> future = SettableFuture.create();
- StateCallback callback = future::set;
- controller.registerStateCallback(directExecutor(), callback);
- int role = future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
- controller.unregisterStateCallback(callback);
- return role;
+ CompletableFuture<Integer> future = new CompletableFuture<>();
+ StateCallback callback = future::complete;
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ () -> controller.registerStateCallback(directExecutor(), callback));
+ try {
+ return future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+ } finally {
+ runAsShell(ACCESS_NETWORK_STATE, () -> controller.unregisterStateCallback(callback));
+ }
+ }
+
+ private static int waitForAttachedState(ThreadNetworkController controller) throws Exception {
+ List<Integer> attachedRoles = new ArrayList<>();
+ attachedRoles.add(DEVICE_ROLE_CHILD);
+ attachedRoles.add(DEVICE_ROLE_ROUTER);
+ attachedRoles.add(DEVICE_ROLE_LEADER);
+ return waitForStateAnyOf(controller, attachedRoles);
}
private static int waitForStateAnyOf(
ThreadNetworkController controller, List<Integer> deviceRoles) throws Exception {
- SettableFuture<Integer> future = SettableFuture.create();
+ CompletableFuture<Integer> future = new CompletableFuture<>();
StateCallback callback =
newRole -> {
if (deviceRoles.contains(newRole)) {
- future.set(newRole);
+ future.complete(newRole);
}
};
controller.registerStateCallback(directExecutor(), callback);
- int role = future.get();
+ int role = future.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
controller.unregisterStateCallback(callback);
return role;
}
+ private static void waitForEnabledState(ThreadNetworkController controller, int state)
+ throws Exception {
+ CompletableFuture<Integer> future = new CompletableFuture<>();
+ StateCallback callback =
+ new ThreadNetworkController.StateCallback() {
+ @Override
+ public void onDeviceRoleChanged(int r) {}
+
+ @Override
+ public void onThreadEnableStateChanged(int enabled) {
+ if (enabled == state) {
+ future.complete(enabled);
+ }
+ }
+ };
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ () -> controller.registerStateCallback(directExecutor(), callback));
+ future.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+ runAsShell(ACCESS_NETWORK_STATE, () -> controller.unregisterStateCallback(callback));
+ }
+
+ private void leave(
+ ThreadNetworkController controller,
+ OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+ runAsShell(THREAD_NETWORK_PRIVILEGED, () -> controller.leave(mExecutor, receiver));
+ }
+
+ private void leaveAndWait(ThreadNetworkController controller) throws Exception {
+ CompletableFuture<Void> future = new CompletableFuture<>();
+ leave(controller, future::complete);
+ future.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
+ }
+
+ private void scheduleMigration(
+ ThreadNetworkController controller,
+ PendingOperationalDataset pendingDataset,
+ OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> controller.scheduleMigration(pendingDataset, mExecutor, receiver));
+ }
+
+ private class EnabledStateListener {
+ private ArrayTrackRecord<Integer> mEnabledStates = new ArrayTrackRecord<>();
+ private final ArrayTrackRecord<Integer>.ReadHead mReadHead = mEnabledStates.newReadHead();
+ ThreadNetworkController mController;
+ StateCallback mCallback =
+ new ThreadNetworkController.StateCallback() {
+ @Override
+ public void onDeviceRoleChanged(int r) {}
+
+ @Override
+ public void onThreadEnableStateChanged(int enabled) {
+ mEnabledStates.add(enabled);
+ }
+ };
+
+ EnabledStateListener(ThreadNetworkController controller) {
+ this.mController = controller;
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ () -> controller.registerStateCallback(mExecutor, mCallback));
+ }
+
+ public void expectThreadEnabledState(int enabled) {
+ assertNotNull(mReadHead.poll(ENABLED_TIMEOUT_MILLIS, e -> (e == enabled)));
+ }
+
+ public void unregisterStateCallback() {
+ runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(mCallback));
+ }
+ }
+
+ private int booleanToEnabledState(boolean enabled) {
+ return enabled ? STATE_ENABLED : STATE_DISABLED;
+ }
+
+ private void setEnabledAndWait(ThreadNetworkController controller, boolean enabled)
+ throws Exception {
+ CompletableFuture<Void> setFuture = new CompletableFuture<>();
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> controller.setEnabled(enabled, mExecutor, newOutcomeReceiver(setFuture)));
+ setFuture.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+ waitForEnabledState(controller, booleanToEnabledState(enabled));
+ }
+
+ private CompletableFuture joinRandomizedDataset(
+ ThreadNetworkController controller, String networkName) throws Exception {
+ ActiveOperationalDataset activeDataset = newRandomizedDataset(networkName, controller);
+ CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture)));
+ return joinFuture;
+ }
+
+ private void joinRandomizedDatasetAndWait(ThreadNetworkController controller) throws Exception {
+ joinRandomizedDatasetAndWait(controller, "TestNet");
+ }
+
+ private void joinRandomizedDatasetAndWait(
+ ThreadNetworkController controller, String networkName) throws Exception {
+ CompletableFuture<Void> joinFuture = joinRandomizedDataset(controller, networkName);
+ joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+ assertThat(isAttached(controller)).isTrue();
+ }
+
private static ActiveOperationalDataset getActiveOperationalDataset(
ThreadNetworkController controller) throws Exception {
- SettableFuture<ActiveOperationalDataset> future = SettableFuture.create();
- OperationalDatasetCallback callback = future::set;
- controller.registerOperationalDatasetCallback(directExecutor(), callback);
- ActiveOperationalDataset dataset = future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
- controller.unregisterOperationalDatasetCallback(callback);
- return dataset;
+ CompletableFuture<ActiveOperationalDataset> future = new CompletableFuture<>();
+ OperationalDatasetCallback callback = future::complete;
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ THREAD_NETWORK_PRIVILEGED,
+ () -> controller.registerOperationalDatasetCallback(directExecutor(), callback));
+ try {
+ return future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+ } finally {
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ THREAD_NETWORK_PRIVILEGED,
+ () -> controller.unregisterOperationalDatasetCallback(callback));
+ }
}
private static PendingOperationalDataset getPendingOperationalDataset(
ThreadNetworkController controller) throws Exception {
- SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
- SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
+ CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
+ CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
controller.registerOperationalDatasetCallback(
directExecutor(), newDatasetCallback(activeFuture, pendingFuture));
- return pendingFuture.get();
+ return pendingFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
}
private static OperationalDatasetCallback newDatasetCallback(
- SettableFuture<ActiveOperationalDataset> activeFuture,
- SettableFuture<PendingOperationalDataset> pendingFuture) {
+ CompletableFuture<ActiveOperationalDataset> activeFuture,
+ CompletableFuture<PendingOperationalDataset> pendingFuture) {
return new OperationalDatasetCallback() {
@Override
public void onActiveOperationalDatasetChanged(
ActiveOperationalDataset activeOpDataset) {
- activeFuture.set(activeOpDataset);
+ activeFuture.complete(activeOpDataset);
}
@Override
public void onPendingOperationalDatasetChanged(
PendingOperationalDataset pendingOpDataset) {
- pendingFuture.set(pendingOpDataset);
+ pendingFuture.complete(pendingOpDataset);
}
};
}
- @Test
- public void getThreadVersion_returnsAtLeastThreadVersion1P3() {
- for (ThreadNetworkController controller : getAllControllers()) {
- assertThat(controller.getThreadVersion()).isAtLeast(THREAD_VERSION_1_3);
+ private static void assertDoesNotThrow(ThrowingRunnable runnable) {
+ try {
+ runnable.run();
+ } catch (Throwable e) {
+ fail("Should not have thrown " + e);
}
}
- @Test
- public void registerStateCallback_permissionsGranted_returnsCurrentStates() throws Exception {
- grantPermissions(permission.ACCESS_NETWORK_STATE);
-
- for (ThreadNetworkController controller : getAllControllers()) {
- SettableFuture<Integer> deviceRole = SettableFuture.create();
- StateCallback callback = deviceRole::set;
-
- try {
- controller.registerStateCallback(mExecutor, callback);
-
- assertThat(deviceRole.get()).isEqualTo(DEVICE_ROLE_STOPPED);
- } finally {
- controller.unregisterStateCallback(callback);
- }
+ // Return the first discovered service instance.
+ private NsdServiceInfo discoverService(String serviceType) throws Exception {
+ CompletableFuture<NsdServiceInfo> serviceInfoFuture = new CompletableFuture<>();
+ NsdManager.DiscoveryListener listener =
+ new DefaultDiscoveryListener() {
+ @Override
+ public void onServiceFound(NsdServiceInfo serviceInfo) {
+ serviceInfoFuture.complete(serviceInfo);
+ }
+ };
+ mNsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener);
+ try {
+ serviceInfoFuture.get(SERVICE_DISCOVERY_TIMEOUT_MILLIS, MILLISECONDS);
+ } finally {
+ mNsdManager.stopServiceDiscovery(listener);
}
+
+ return serviceInfoFuture.get();
}
- @Test
- public void registerStateCallback_noPermissions_throwsSecurityException() throws Exception {
- dropAllPermissions();
-
- for (ThreadNetworkController controller : getAllControllers()) {
- assertThrows(
- SecurityException.class,
- () -> controller.registerStateCallback(mExecutor, role -> {}));
- }
+ private NsdManager.DiscoveryListener discoverForServiceLost(
+ String serviceType, CompletableFuture<NsdServiceInfo> serviceInfoFuture) {
+ NsdManager.DiscoveryListener listener =
+ new DefaultDiscoveryListener() {
+ @Override
+ public void onServiceLost(NsdServiceInfo serviceInfo) {
+ serviceInfoFuture.complete(serviceInfo);
+ }
+ };
+ mNsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener);
+ return listener;
}
- @Test
- public void registerStateCallback_alreadyRegistered_throwsIllegalArgumentException()
+ private NsdServiceInfo expectServiceResolved(
+ String serviceType, int timeoutMilliseconds, Predicate<NsdServiceInfo> predicate)
throws Exception {
- grantPermissions(permission.ACCESS_NETWORK_STATE);
-
- for (ThreadNetworkController controller : getAllControllers()) {
- SettableFuture<Integer> deviceRole = SettableFuture.create();
- StateCallback callback = role -> deviceRole.set(role);
- controller.registerStateCallback(mExecutor, callback);
-
- assertThrows(
- IllegalArgumentException.class,
- () -> controller.registerStateCallback(mExecutor, callback));
+ NsdServiceInfo discoveredServiceInfo = discoverService(serviceType);
+ CompletableFuture<NsdServiceInfo> future = new CompletableFuture<>();
+ NsdManager.ServiceInfoCallback callback =
+ new DefaultServiceInfoCallback() {
+ @Override
+ public void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo) {
+ if (predicate.test(serviceInfo)) {
+ future.complete(serviceInfo);
+ }
+ }
+ };
+ mNsdManager.registerServiceInfoCallback(discoveredServiceInfo, mExecutor, callback);
+ try {
+ return future.get(timeoutMilliseconds, MILLISECONDS);
+ } finally {
+ mNsdManager.unregisterServiceInfoCallback(callback);
}
}
- @Test
- public void unregisterStateCallback_noPermissions_throwsSecurityException() throws Exception {
- for (ThreadNetworkController controller : getAllControllers()) {
- SettableFuture<Integer> deviceRole = SettableFuture.create();
- StateCallback callback = role -> deviceRole.set(role);
- grantPermissions(permission.ACCESS_NETWORK_STATE);
- controller.registerStateCallback(mExecutor, callback);
-
- try {
- dropAllPermissions();
- assertThrows(
- SecurityException.class,
- () -> controller.unregisterStateCallback(callback));
- } finally {
- grantPermissions(permission.ACCESS_NETWORK_STATE);
- controller.unregisterStateCallback(callback);
- }
- }
+ private void setUpTestNetwork() {
+ assertThat(mTestNetworkTracker).isNull();
+ mTestNetworkTracker = new TapTestNetworkTracker(mContext, mHandlerThread.getLooper());
}
- @Test
- public void unregisterStateCallback_callbackRegistered_success() throws Exception {
- grantPermissions(permission.ACCESS_NETWORK_STATE);
- for (ThreadNetworkController controller : getAllControllers()) {
- SettableFuture<Integer> deviceRole = SettableFuture.create();
- StateCallback callback = role -> deviceRole.set(role);
- controller.registerStateCallback(mExecutor, callback);
-
- controller.unregisterStateCallback(callback);
+ private void tearDownTestNetwork() throws InterruptedException {
+ if (mTestNetworkTracker != null) {
+ mTestNetworkTracker.tearDown();
}
+ mHandlerThread.quitSafely();
+ mHandlerThread.join();
}
- @Test
- public void unregisterStateCallback_callbackNotRegistered_throwsIllegalArgumentException()
- throws Exception {
- for (ThreadNetworkController controller : getAllControllers()) {
- SettableFuture<Integer> deviceRole = SettableFuture.create();
- StateCallback callback = role -> deviceRole.set(role);
+ private static class DefaultDiscoveryListener implements NsdManager.DiscoveryListener {
+ @Override
+ public void onStartDiscoveryFailed(String serviceType, int errorCode) {}
- assertThrows(
- IllegalArgumentException.class,
- () -> controller.unregisterStateCallback(callback));
- }
+ @Override
+ public void onStopDiscoveryFailed(String serviceType, int errorCode) {}
+
+ @Override
+ public void onDiscoveryStarted(String serviceType) {}
+
+ @Override
+ public void onDiscoveryStopped(String serviceType) {}
+
+ @Override
+ public void onServiceFound(NsdServiceInfo serviceInfo) {}
+
+ @Override
+ public void onServiceLost(NsdServiceInfo serviceInfo) {}
}
- @Test
- public void unregisterStateCallback_alreadyUnregistered_throwsIllegalArgumentException()
- throws Exception {
- grantPermissions(permission.ACCESS_NETWORK_STATE);
- for (ThreadNetworkController controller : getAllControllers()) {
- SettableFuture<Integer> deviceRole = SettableFuture.create();
- StateCallback callback = deviceRole::set;
- controller.registerStateCallback(mExecutor, callback);
- controller.unregisterStateCallback(callback);
+ private static class DefaultServiceInfoCallback implements NsdManager.ServiceInfoCallback {
+ @Override
+ public void onServiceInfoCallbackRegistrationFailed(int errorCode) {}
- assertThrows(
- IllegalArgumentException.class,
- () -> controller.unregisterStateCallback(callback));
- }
- }
+ @Override
+ public void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo) {}
- @Test
- public void registerOperationalDatasetCallback_permissionsGranted_returnsCurrentStates()
- throws Exception {
- grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ @Override
+ public void onServiceLost() {}
- for (ThreadNetworkController controller : getAllControllers()) {
- SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
- SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
- var callback = newDatasetCallback(activeFuture, pendingFuture);
-
- try {
- controller.registerOperationalDatasetCallback(mExecutor, callback);
-
- assertThat(activeFuture.get()).isNull();
- assertThat(pendingFuture.get()).isNull();
- } finally {
- controller.unregisterOperationalDatasetCallback(callback);
- }
- }
- }
-
- @Test
- public void registerOperationalDatasetCallback_noPermissions_throwsSecurityException()
- throws Exception {
- dropAllPermissions();
-
- for (ThreadNetworkController controller : getAllControllers()) {
- SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
- SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
- var callback = newDatasetCallback(activeFuture, pendingFuture);
-
- assertThrows(
- SecurityException.class,
- () -> controller.registerOperationalDatasetCallback(mExecutor, callback));
- }
- }
-
- @Test
- public void unregisterOperationalDatasetCallback_callbackRegistered_success() throws Exception {
- grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
- for (ThreadNetworkController controller : getAllControllers()) {
- SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
- SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
- var callback = newDatasetCallback(activeFuture, pendingFuture);
- controller.registerOperationalDatasetCallback(mExecutor, callback);
-
- controller.unregisterOperationalDatasetCallback(callback);
- }
- }
-
- @Test
- public void unregisterOperationalDatasetCallback_noPermissions_throwsSecurityException()
- throws Exception {
- dropAllPermissions();
-
- for (ThreadNetworkController controller : getAllControllers()) {
- SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
- SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
- var callback = newDatasetCallback(activeFuture, pendingFuture);
- grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
- controller.registerOperationalDatasetCallback(mExecutor, callback);
-
- try {
- dropAllPermissions();
- assertThrows(
- SecurityException.class,
- () -> controller.unregisterOperationalDatasetCallback(callback));
- } finally {
- grantPermissions(
- permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
- controller.unregisterOperationalDatasetCallback(callback);
- }
- }
- }
-
- private static <V> OutcomeReceiver<V, ThreadNetworkException> newOutcomeReceiver(
- SettableFuture<V> future) {
- return new OutcomeReceiver<V, ThreadNetworkException>() {
- @Override
- public void onResult(V result) {
- future.set(result);
- }
-
- @Override
- public void onError(ThreadNetworkException e) {
- future.setException(e);
- }
- };
- }
-
- @Test
- public void join_withPrivilegedPermission_success() throws Exception {
- grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
- for (ThreadNetworkController controller : getAllControllers()) {
- ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
- SettableFuture<Void> joinFuture = SettableFuture.create();
-
- controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
- joinFuture.get();
-
- grantPermissions(permission.ACCESS_NETWORK_STATE);
- assertThat(isAttached(controller)).isTrue();
- assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset);
- }
- }
-
- @Test
- public void join_withoutPrivilegedPermission_throwsSecurityException() throws Exception {
- dropAllPermissions();
-
- for (ThreadNetworkController controller : getAllControllers()) {
- ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
-
- assertThrows(
- SecurityException.class,
- () -> controller.join(activeDataset, mExecutor, v -> {}));
- }
- }
-
- @Test
- public void join_concurrentRequests_firstOneIsAborted() throws Exception {
- grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
- final byte[] KEY_1 = new byte[] {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
- final byte[] KEY_2 = new byte[] {2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2};
- for (ThreadNetworkController controller : getAllControllers()) {
- ActiveOperationalDataset activeDataset1 =
- new ActiveOperationalDataset.Builder(
- newRandomizedDataset("TestNet", controller))
- .setNetworkKey(KEY_1)
- .build();
- ActiveOperationalDataset activeDataset2 =
- new ActiveOperationalDataset.Builder(activeDataset1)
- .setNetworkKey(KEY_2)
- .build();
- SettableFuture<Void> joinFuture1 = SettableFuture.create();
- SettableFuture<Void> joinFuture2 = SettableFuture.create();
-
- controller.join(activeDataset1, mExecutor, newOutcomeReceiver(joinFuture1));
- controller.join(activeDataset2, mExecutor, newOutcomeReceiver(joinFuture2));
-
- ThreadNetworkException thrown =
- (ThreadNetworkException)
- assertThrows(ExecutionException.class, joinFuture1::get).getCause();
- assertThat(thrown.getErrorCode()).isEqualTo(ERROR_ABORTED);
- joinFuture2.get();
- grantPermissions(permission.ACCESS_NETWORK_STATE);
- assertThat(isAttached(controller)).isTrue();
- assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset2);
- }
- }
-
- @Test
- public void leave_withPrivilegedPermission_success() throws Exception {
- grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
- for (ThreadNetworkController controller : getAllControllers()) {
- ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
- SettableFuture<Void> joinFuture = SettableFuture.create();
- SettableFuture<Void> leaveFuture = SettableFuture.create();
- controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
- joinFuture.get();
-
- controller.leave(mExecutor, newOutcomeReceiver(leaveFuture));
- leaveFuture.get();
-
- grantPermissions(permission.ACCESS_NETWORK_STATE);
- assertThat(getDeviceRole(controller)).isEqualTo(DEVICE_ROLE_STOPPED);
- }
- }
-
- @Test
- public void leave_withoutPrivilegedPermission_throwsSecurityException() {
- dropAllPermissions();
-
- for (ThreadNetworkController controller : getAllControllers()) {
- assertThrows(SecurityException.class, () -> controller.leave(mExecutor, v -> {}));
- }
- }
-
- @Test
- public void leave_concurrentRequests_bothSuccess() throws Exception {
- grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
- for (ThreadNetworkController controller : getAllControllers()) {
- ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
- SettableFuture<Void> joinFuture = SettableFuture.create();
- SettableFuture<Void> leaveFuture1 = SettableFuture.create();
- SettableFuture<Void> leaveFuture2 = SettableFuture.create();
- controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
- joinFuture.get();
-
- controller.leave(mExecutor, newOutcomeReceiver(leaveFuture1));
- controller.leave(mExecutor, newOutcomeReceiver(leaveFuture2));
-
- leaveFuture1.get();
- leaveFuture2.get();
- grantPermissions(permission.ACCESS_NETWORK_STATE);
- assertThat(getDeviceRole(controller)).isEqualTo(DEVICE_ROLE_STOPPED);
- }
- }
-
- @Test
- public void scheduleMigration_withPrivilegedPermission_success() throws Exception {
- grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
- for (ThreadNetworkController controller : getAllControllers()) {
- ActiveOperationalDataset activeDataset1 =
- new ActiveOperationalDataset.Builder(
- newRandomizedDataset("TestNet", controller))
- .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
- .setExtendedPanId(new byte[] {1, 1, 1, 1, 1, 1, 1, 1})
- .build();
- ActiveOperationalDataset activeDataset2 =
- new ActiveOperationalDataset.Builder(activeDataset1)
- .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
- .setNetworkName("ThreadNet2")
- .build();
- PendingOperationalDataset pendingDataset2 =
- new PendingOperationalDataset(
- activeDataset2,
- OperationalDatasetTimestamp.fromInstant(Instant.now()),
- Duration.ofSeconds(30));
- SettableFuture<Void> joinFuture = SettableFuture.create();
- SettableFuture<Void> migrateFuture = SettableFuture.create();
- controller.join(activeDataset1, mExecutor, newOutcomeReceiver(joinFuture));
- joinFuture.get();
-
- controller.scheduleMigration(
- pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture));
-
- migrateFuture.get();
- Thread.sleep(35 * 1000);
- assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset2);
- assertThat(getPendingOperationalDataset(controller)).isNull();
- }
- }
-
- @Test
- public void scheduleMigration_whenNotAttached_failWithPreconditionError() throws Exception {
- grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
- for (ThreadNetworkController controller : getAllControllers()) {
- PendingOperationalDataset pendingDataset =
- new PendingOperationalDataset(
- newRandomizedDataset("TestNet", controller),
- OperationalDatasetTimestamp.fromInstant(Instant.now()),
- Duration.ofSeconds(30));
- SettableFuture<Void> migrateFuture = SettableFuture.create();
-
- controller.scheduleMigration(
- pendingDataset, mExecutor, newOutcomeReceiver(migrateFuture));
-
- ThreadNetworkException thrown =
- (ThreadNetworkException)
- assertThrows(ExecutionException.class, migrateFuture::get).getCause();
- assertThat(thrown.getErrorCode()).isEqualTo(ERROR_FAILED_PRECONDITION);
- }
- }
-
- @Test
- public void scheduleMigration_secondRequestHasSmallerTimestamp_rejectedByLeader()
- throws Exception {
- grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
- for (ThreadNetworkController controller : getAllControllers()) {
- final ActiveOperationalDataset activeDataset =
- new ActiveOperationalDataset.Builder(
- newRandomizedDataset("testNet", controller))
- .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
- .build();
- ActiveOperationalDataset activeDataset1 =
- new ActiveOperationalDataset.Builder(activeDataset)
- .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
- .setNetworkName("testNet1")
- .build();
- PendingOperationalDataset pendingDataset1 =
- new PendingOperationalDataset(
- activeDataset1,
- new OperationalDatasetTimestamp(100, 0, false),
- Duration.ofSeconds(30));
- ActiveOperationalDataset activeDataset2 =
- new ActiveOperationalDataset.Builder(activeDataset)
- .setActiveTimestamp(new OperationalDatasetTimestamp(3L, 0, false))
- .setNetworkName("testNet2")
- .build();
- PendingOperationalDataset pendingDataset2 =
- new PendingOperationalDataset(
- activeDataset2,
- new OperationalDatasetTimestamp(20, 0, false),
- Duration.ofSeconds(30));
- SettableFuture<Void> joinFuture = SettableFuture.create();
- SettableFuture<Void> migrateFuture1 = SettableFuture.create();
- SettableFuture<Void> migrateFuture2 = SettableFuture.create();
- controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
- joinFuture.get();
-
- controller.scheduleMigration(
- pendingDataset1, mExecutor, newOutcomeReceiver(migrateFuture1));
- migrateFuture1.get();
- controller.scheduleMigration(
- pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2));
-
- ThreadNetworkException thrown =
- (ThreadNetworkException)
- assertThrows(ExecutionException.class, migrateFuture2::get).getCause();
- assertThat(thrown.getErrorCode()).isEqualTo(ERROR_REJECTED_BY_PEER);
- }
- }
-
- @Test
- public void scheduleMigration_secondRequestHasLargerTimestamp_success() throws Exception {
- grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
- for (ThreadNetworkController controller : getAllControllers()) {
- final ActiveOperationalDataset activeDataset =
- new ActiveOperationalDataset.Builder(
- newRandomizedDataset("validName", controller))
- .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
- .build();
- ActiveOperationalDataset activeDataset1 =
- new ActiveOperationalDataset.Builder(activeDataset)
- .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
- .setNetworkName("testNet1")
- .build();
- PendingOperationalDataset pendingDataset1 =
- new PendingOperationalDataset(
- activeDataset1,
- new OperationalDatasetTimestamp(100, 0, false),
- Duration.ofSeconds(30));
- ActiveOperationalDataset activeDataset2 =
- new ActiveOperationalDataset.Builder(activeDataset)
- .setActiveTimestamp(new OperationalDatasetTimestamp(3L, 0, false))
- .setNetworkName("testNet2")
- .build();
- PendingOperationalDataset pendingDataset2 =
- new PendingOperationalDataset(
- activeDataset2,
- new OperationalDatasetTimestamp(200, 0, false),
- Duration.ofSeconds(30));
- SettableFuture<Void> joinFuture = SettableFuture.create();
- SettableFuture<Void> migrateFuture1 = SettableFuture.create();
- SettableFuture<Void> migrateFuture2 = SettableFuture.create();
- controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
- joinFuture.get();
-
- controller.scheduleMigration(
- pendingDataset1, mExecutor, newOutcomeReceiver(migrateFuture1));
- migrateFuture1.get();
- controller.scheduleMigration(
- pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2));
-
- migrateFuture2.get();
- Thread.sleep(35 * 1000);
- assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset2);
- assertThat(getPendingOperationalDataset(controller)).isNull();
- }
- }
-
- @Test
- public void createRandomizedDataset_wrongNetworkNameLength_throwsIllegalArgumentException() {
- for (ThreadNetworkController controller : getAllControllers()) {
- assertThrows(
- IllegalArgumentException.class,
- () -> controller.createRandomizedDataset("", mExecutor, dataset -> {}));
-
- assertThrows(
- IllegalArgumentException.class,
- () ->
- controller.createRandomizedDataset(
- "ANetNameIs17Bytes", mExecutor, dataset -> {}));
- }
- }
-
- @Test
- public void createRandomizedDataset_validNetworkName_success() throws Exception {
- for (ThreadNetworkController controller : getAllControllers()) {
- ActiveOperationalDataset dataset = newRandomizedDataset("validName", controller);
-
- assertThat(dataset.getNetworkName()).isEqualTo("validName");
- assertThat(dataset.getPanId()).isLessThan(0xffff);
- assertThat(dataset.getChannelMask().size()).isAtLeast(1);
- assertThat(dataset.getExtendedPanId()).hasLength(8);
- assertThat(dataset.getNetworkKey()).hasLength(16);
- assertThat(dataset.getPskc()).hasLength(16);
- assertThat(dataset.getMeshLocalPrefix().getPrefixLength()).isEqualTo(64);
- assertThat(dataset.getMeshLocalPrefix().getRawAddress()[0]).isEqualTo((byte) 0xfd);
- }
+ @Override
+ public void onServiceInfoCallbackUnregistered() {}
}
}
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkExceptionTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkExceptionTest.java
new file mode 100644
index 0000000..7d9ae81
--- /dev/null
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkExceptionTest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread.cts;
+
+import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
+import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
+import static android.net.thread.ThreadNetworkException.ERROR_UNKNOWN;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.net.thread.ThreadNetworkException;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** CTS tests for {@link ThreadNetworkException}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ThreadNetworkExceptionTest {
+ @Test
+ public void constructor_validValues_valuesAreConnectlySet() throws Exception {
+ ThreadNetworkException errorThreadDisabled =
+ new ThreadNetworkException(ERROR_THREAD_DISABLED, "Thread disabled error!");
+ ThreadNetworkException errorInternalError =
+ new ThreadNetworkException(ERROR_INTERNAL_ERROR, "internal error!");
+
+ assertThat(errorThreadDisabled.getErrorCode()).isEqualTo(ERROR_THREAD_DISABLED);
+ assertThat(errorThreadDisabled.getMessage()).isEqualTo("Thread disabled error!");
+ assertThat(errorInternalError.getErrorCode()).isEqualTo(ERROR_INTERNAL_ERROR);
+ assertThat(errorInternalError.getMessage()).isEqualTo("internal error!");
+ }
+
+ @Test
+ public void constructor_nullMessage_throwsNullPointerException() throws Exception {
+ assertThrows(
+ NullPointerException.class,
+ () -> new ThreadNetworkException(ERROR_UNKNOWN, null /* message */));
+ }
+
+ @Test
+ public void constructor_tooSmallErrorCode_throwsIllegalArgumentException() throws Exception {
+ assertThrows(IllegalArgumentException.class, () -> new ThreadNetworkException(0, "0"));
+ // TODO: add argument check for too large error code when mainline CTS is ready. This was
+ // not added here for CTS forward copatibility.
+ }
+}
diff --git a/thread/tests/integration/Android.bp b/thread/tests/integration/Android.bp
new file mode 100644
index 0000000..6ba192d
--- /dev/null
+++ b/thread/tests/integration/Android.bp
@@ -0,0 +1,60 @@
+//
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ default_team: "trendy_team_fwk_thread_network",
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_defaults {
+ name: "ThreadNetworkIntegrationTestsDefaults",
+ min_sdk_version: "30",
+ static_libs: [
+ "androidx.test.rules",
+ "compatibility-device-util-axt",
+ "guava",
+ "mockito-target-minus-junit4",
+ "net-tests-utils",
+ "net-utils-device-common",
+ "net-utils-device-common-bpf",
+ "testables",
+ "truth",
+ ],
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ "android.test.mock",
+ ],
+}
+
+android_test {
+ name: "ThreadNetworkIntegrationTests",
+ platform_apis: true,
+ manifest: "AndroidManifest.xml",
+ test_config: "AndroidTest.xml",
+ defaults: [
+ "framework-connectivity-test-defaults",
+ "ThreadNetworkIntegrationTestsDefaults",
+ ],
+ test_suites: [
+ "mts-tethering",
+ "general-tests",
+ ],
+ srcs: [
+ "src/**/*.java",
+ ],
+ compile_multilib: "both",
+}
diff --git a/thread/tests/integration/AndroidManifest.xml b/thread/tests/integration/AndroidManifest.xml
new file mode 100644
index 0000000..a049184
--- /dev/null
+++ b/thread/tests/integration/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.thread.tests.integration">
+
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+ <!-- The test need CHANGE_NETWORK_STATE permission to use requestNetwork API to setup test
+ network. Since R shell application don't have such permission, grant permission to the test
+ here. TODO: Remove CHANGE_NETWORK_STATE permission here and use adopt shell permission to
+ obtain CHANGE_NETWORK_STATE for testing once R device is no longer supported. -->
+ <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
+ <uses-permission android:name="android.permission.THREAD_NETWORK_PRIVILEGED"/>
+ <uses-permission android:name="android.permission.NETWORK_SETTINGS"/>
+ <uses-permission android:name="android.permission.INTERNET"/>
+
+ <application android:debuggable="true">
+ <uses-library android:name="android.test.runner" />
+ </application>
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.thread.tests.integration"
+ android:label="Thread integration tests">
+ </instrumentation>
+</manifest>
diff --git a/thread/tests/integration/AndroidTest.xml b/thread/tests/integration/AndroidTest.xml
new file mode 100644
index 0000000..152c1c3
--- /dev/null
+++ b/thread/tests/integration/AndroidTest.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<configuration description="Config for Thread integration tests">
+ <option name="test-tag" value="ThreadNetworkIntegrationTests" />
+ <option name="test-suite-tag" value="apct" />
+
+ <!--
+ Only run tests if the device under test is SDK version 34 (Android 14) or above.
+ -->
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.Sdk34ModuleController" />
+
+ <!-- Run tests in MTS only if the Tethering Mainline module is installed. -->
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+ <option name="mainline-module-package-name" value="com.google.android.tethering" />
+ </object>
+
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+
+ <!-- Install test -->
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="test-file-name" value="ThreadNetworkIntegrationTests.apk" />
+ <option name="check-min-sdk" value="true" />
+ <option name="cleanup-apks" value="true" />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.thread.tests.integration" />
+ </test>
+</configuration>
diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
new file mode 100644
index 0000000..e8ef346
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
@@ -0,0 +1,666 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
+import static android.net.thread.utils.IntegrationTestUtils.JOIN_TIMEOUT;
+import static android.net.thread.utils.IntegrationTestUtils.RESTART_JOIN_TIMEOUT;
+import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv6Packet;
+import static android.net.thread.utils.IntegrationTestUtils.isFromIpv6Source;
+import static android.net.thread.utils.IntegrationTestUtils.isInMulticastGroup;
+import static android.net.thread.utils.IntegrationTestUtils.isSimulatedThreadRadioSupported;
+import static android.net.thread.utils.IntegrationTestUtils.isToIpv6Destination;
+import static android.net.thread.utils.IntegrationTestUtils.newPacketReader;
+import static android.net.thread.utils.IntegrationTestUtils.pollForPacket;
+import static android.net.thread.utils.IntegrationTestUtils.sendUdpMessage;
+import static android.net.thread.utils.IntegrationTestUtils.waitFor;
+
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REPLY_TYPE;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
+import static com.android.testutils.DeviceInfoUtils.isKernelVersionAtLeast;
+import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+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.Assume.assumeNotNull;
+import static org.junit.Assume.assumeTrue;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import android.content.Context;
+import android.net.InetAddresses;
+import android.net.LinkProperties;
+import android.net.MacAddress;
+import android.net.thread.utils.FullThreadDevice;
+import android.net.thread.utils.InfraNetworkDevice;
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.TapPacketReader;
+import com.android.testutils.TestNetworkTracker;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet6Address;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
+
+/** Integration test cases for Thread Border Routing feature. */
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class BorderRoutingTest {
+ private static final String TAG = BorderRoutingTest.class.getSimpleName();
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private ThreadNetworkController mController;
+ private HandlerThread mHandlerThread;
+ private Handler mHandler;
+ private TestNetworkTracker mInfraNetworkTracker;
+ private List<FullThreadDevice> mFtds;
+ private TapPacketReader mInfraNetworkReader;
+ private InfraNetworkDevice mInfraDevice;
+
+ private static final int NUM_FTD = 2;
+ private static final String KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED = "5.15.0";
+ private static final Inet6Address GROUP_ADDR_SCOPE_5 =
+ (Inet6Address) InetAddresses.parseNumericAddress("ff05::1234");
+ private static final Inet6Address GROUP_ADDR_SCOPE_4 =
+ (Inet6Address) InetAddresses.parseNumericAddress("ff04::1234");
+ private static final Inet6Address GROUP_ADDR_SCOPE_3 =
+ (Inet6Address) InetAddresses.parseNumericAddress("ff03::1234");
+
+ // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
+ private static final byte[] DEFAULT_DATASET_TLVS =
+ base16().decode(
+ "0E080000000000010000000300001335060004001FFFE002"
+ + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+ + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+ + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ + "B9D351B40C0402A0FFF8");
+ private static final ActiveOperationalDataset DEFAULT_DATASET =
+ ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
+
+ @Before
+ public void setUp() throws Exception {
+ assumeTrue(isSimulatedThreadRadioSupported());
+ final ThreadNetworkManager manager = mContext.getSystemService(ThreadNetworkManager.class);
+ if (manager != null) {
+ mController = manager.getAllThreadNetworkControllers().get(0);
+ }
+
+ // Run the tests on only devices where the Thread feature is available
+ assumeNotNull(mController);
+
+ mHandlerThread = new HandlerThread(getClass().getSimpleName());
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
+ mFtds = new ArrayList<>();
+
+ setUpInfraNetwork();
+
+ // BR forms a network.
+ startBrLeader();
+
+ // Creates a infra network device.
+ mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
+ startInfraDevice();
+
+ // Create Ftds
+ for (int i = 0; i < NUM_FTD; ++i) {
+ mFtds.add(new FullThreadDevice(15 + i /* node ID */));
+ }
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (mController == null) {
+ return;
+ }
+
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ NETWORK_SETTINGS,
+ () -> {
+ CountDownLatch latch = new CountDownLatch(2);
+ mController.setTestNetworkAsUpstream(
+ null, directExecutor(), v -> latch.countDown());
+ mController.leave(directExecutor(), v -> latch.countDown());
+ latch.await(10, TimeUnit.SECONDS);
+ });
+ tearDownInfraNetwork();
+
+ mHandlerThread.quitSafely();
+ mHandlerThread.join();
+
+ for (var ftd : mFtds) {
+ ftd.destroy();
+ }
+ mFtds.clear();
+ }
+
+ @Test
+ public void unicastRouting_infraDevicePingTheadDeviceOmr_replyReceived() throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ // Let ftd join the network.
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+
+ // Infra device sends an echo request to FTD's OMR.
+ mInfraDevice.sendEchoRequest(ftd.getOmrAddress());
+
+ // Infra device receives an echo reply sent by FTD.
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, null /* srcAddress */));
+ }
+
+ @Test
+ public void unicastRouting_borderRouterSendsUdpToThreadDevice_datagramReceived()
+ throws Exception {
+ assumeTrue(isSimulatedThreadRadioSupported());
+
+ /*
+ * <pre>
+ * Topology:
+ * Thread
+ * Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ // BR forms a network.
+ CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ () -> mController.join(DEFAULT_DATASET, directExecutor(), joinFuture::complete));
+ joinFuture.get(RESTART_JOIN_TIMEOUT.toMillis(), MILLISECONDS);
+
+ // Creates a Full Thread Device (FTD) and lets it join the network.
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ Inet6Address ftdOmr = ftd.getOmrAddress();
+ Inet6Address ftdMlEid = ftd.getMlEid();
+ assertNotNull(ftdMlEid);
+
+ ftd.udpBind(ftdOmr, 12345);
+ sendUdpMessage(ftdOmr, 12345, "aaaaaaaa");
+ assertEquals("aaaaaaaa", ftd.udpReceive());
+
+ ftd.udpBind(ftdMlEid, 12345);
+ sendUdpMessage(ftdMlEid, 12345, "bbbbbbbb");
+ assertEquals("bbbbbbbb", ftd.udpReceive());
+ }
+
+ @Test
+ public void multicastRouting_ftdSubscribedMulticastAddress_infraLinkJoinsMulticastGroup()
+ throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+
+ ftd.subscribeMulticastAddress(GROUP_ADDR_SCOPE_5);
+
+ assertInfraLinkMemberOfGroup(GROUP_ADDR_SCOPE_5);
+ }
+
+ @Test
+ public void
+ multicastRouting_ftdSubscribedScope3MulticastAddress_infraLinkNotJoinMulticastGroup()
+ throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+
+ ftd.subscribeMulticastAddress(GROUP_ADDR_SCOPE_3);
+
+ assertInfraLinkNotMemberOfGroup(GROUP_ADDR_SCOPE_3);
+ }
+
+ @Test
+ public void multicastRouting_ftdSubscribedMulticastAddress_canPingfromInfraLink()
+ throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ subscribeMulticastAddressAndWait(ftd, GROUP_ADDR_SCOPE_5);
+
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
+
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+ }
+
+ @Test
+ public void multicastRouting_inboundForwarding_afterBrRejoinFtdRepliesSubscribedAddress()
+ throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+
+ // TODO (b/327311034): Testing bbr state switch from primary mode to secondary mode and back
+ // to primary mode requires an additional BR in the Thread network. This is not currently
+ // supported, to be implemented when possible.
+ }
+
+ @Test
+ public void multicastRouting_ftdSubscribedScope3MulticastAddress_cannotPingfromInfraLink()
+ throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ ftd.subscribeMulticastAddress(GROUP_ADDR_SCOPE_3);
+
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_3);
+
+ assertNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+ }
+
+ @Test
+ public void multicastRouting_ftdNotSubscribedMulticastAddress_cannotPingFromInfraDevice()
+ throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_4);
+
+ assertNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+ }
+
+ @Test
+ public void multicastRouting_multipleFtdsSubscribedDifferentAddresses_canPingFromInfraDevice()
+ throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device 1
+ * (Cuttlefish)
+ * |
+ * | Thread
+ * |
+ * Full Thread device 2
+ * </pre>
+ */
+
+ FullThreadDevice ftd1 = mFtds.get(0);
+ startFtdChild(ftd1);
+ subscribeMulticastAddressAndWait(ftd1, GROUP_ADDR_SCOPE_5);
+
+ FullThreadDevice ftd2 = mFtds.get(1);
+ startFtdChild(ftd2);
+ subscribeMulticastAddressAndWait(ftd2, GROUP_ADDR_SCOPE_4);
+
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
+
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd1.getOmrAddress()));
+
+ // Verify ping reply from ftd1 and ftd2 separately as the order of replies can't be
+ // predicted.
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_4);
+
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd2.getOmrAddress()));
+ }
+
+ @Test
+ public void multicastRouting_multipleFtdsSubscribedSameAddress_canPingFromInfraDevice()
+ throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device 1
+ * (Cuttlefish)
+ * |
+ * | Thread
+ * |
+ * Full Thread device 2
+ * </pre>
+ */
+
+ FullThreadDevice ftd1 = mFtds.get(0);
+ startFtdChild(ftd1);
+ subscribeMulticastAddressAndWait(ftd1, GROUP_ADDR_SCOPE_5);
+
+ FullThreadDevice ftd2 = mFtds.get(1);
+ startFtdChild(ftd2);
+ subscribeMulticastAddressAndWait(ftd2, GROUP_ADDR_SCOPE_5);
+
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
+
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd1.getOmrAddress()));
+
+ // Send the request twice as the order of replies from ftd1 and ftd2 are not guaranteed
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
+
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd2.getOmrAddress()));
+ }
+
+ @Test
+ public void multicastRouting_outboundForwarding_scopeLargerThan3IsForwarded() throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ Inet6Address ftdOmr = ftd.getOmrAddress();
+
+ ftd.ping(GROUP_ADDR_SCOPE_5);
+ ftd.ping(GROUP_ADDR_SCOPE_4);
+
+ assertNotNull(
+ pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_5));
+ assertNotNull(
+ pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
+ }
+
+ @Test
+ public void multicastRouting_outboundForwarding_scopeSmallerThan4IsNotForwarded()
+ throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+
+ ftd.ping(GROUP_ADDR_SCOPE_3);
+
+ assertNull(
+ pollForPacketOnInfraNetwork(
+ ICMPV6_ECHO_REQUEST_TYPE, ftd.getOmrAddress(), GROUP_ADDR_SCOPE_3));
+ }
+
+ @Test
+ public void multicastRouting_outboundForwarding_llaToScope4IsNotForwarded() throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ Inet6Address ftdLla = ftd.getLinkLocalAddress();
+ assertNotNull(ftdLla);
+
+ ftd.ping(GROUP_ADDR_SCOPE_4, ftdLla, 100 /* size */, 1 /* count */);
+
+ assertNull(
+ pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdLla, GROUP_ADDR_SCOPE_4));
+ }
+
+ @Test
+ public void multicastRouting_outboundForwarding_mlaToScope4IsNotForwarded() throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ List<Inet6Address> ftdMlas = ftd.getMeshLocalAddresses();
+ assertFalse(ftdMlas.isEmpty());
+
+ for (Inet6Address ftdMla : ftdMlas) {
+ ftd.ping(GROUP_ADDR_SCOPE_4, ftdMla, 100 /* size */, 1 /* count */);
+
+ assertNull(
+ pollForPacketOnInfraNetwork(
+ ICMPV6_ECHO_REQUEST_TYPE, ftdMla, GROUP_ADDR_SCOPE_4));
+ }
+ }
+
+ @Test
+ public void multicastRouting_infraNetworkSwitch_ftdRepliesToSubscribedAddress()
+ throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ subscribeMulticastAddressAndWait(ftd, GROUP_ADDR_SCOPE_5);
+ Inet6Address ftdOmr = ftd.getOmrAddress();
+
+ // Destroy infra link and re-create
+ tearDownInfraNetwork();
+ setUpInfraNetwork();
+ mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
+ startInfraDevice();
+
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
+
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftdOmr));
+ }
+
+ @Test
+ public void multicastRouting_infraNetworkSwitch_outboundPacketIsForwarded() throws Exception {
+ assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ Inet6Address ftdOmr = ftd.getOmrAddress();
+
+ // Destroy infra link and re-create
+ tearDownInfraNetwork();
+ setUpInfraNetwork();
+ mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
+ startInfraDevice();
+
+ ftd.ping(GROUP_ADDR_SCOPE_5);
+ ftd.ping(GROUP_ADDR_SCOPE_4);
+
+ assertNotNull(
+ pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_5));
+ assertNotNull(
+ pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
+ }
+
+ private void setUpInfraNetwork() {
+ mInfraNetworkTracker =
+ runAsShell(
+ MANAGE_TEST_NETWORKS,
+ () ->
+ initTestNetwork(
+ mContext, new LinkProperties(), 5000 /* timeoutMs */));
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ NETWORK_SETTINGS,
+ () -> {
+ CompletableFuture<Void> future = new CompletableFuture<>();
+ mController.setTestNetworkAsUpstream(
+ mInfraNetworkTracker.getTestIface().getInterfaceName(),
+ directExecutor(),
+ future::complete);
+ future.get(5, TimeUnit.SECONDS);
+ });
+ }
+
+ private void tearDownInfraNetwork() {
+ runAsShell(MANAGE_TEST_NETWORKS, () -> mInfraNetworkTracker.teardown());
+ }
+
+ private void startBrLeader() throws Exception {
+ CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ () -> mController.join(DEFAULT_DATASET, directExecutor(), joinFuture::complete));
+ joinFuture.get(RESTART_JOIN_TIMEOUT.toSeconds(), TimeUnit.SECONDS);
+ }
+
+ private void startFtdChild(FullThreadDevice ftd) throws Exception {
+ ftd.factoryReset();
+ ftd.joinNetwork(DEFAULT_DATASET);
+ ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+ waitFor(() -> ftd.getOmrAddress() != null, Duration.ofSeconds(60));
+ Inet6Address ftdOmr = ftd.getOmrAddress();
+ assertNotNull(ftdOmr);
+ }
+
+ private void startInfraDevice() throws Exception {
+ mInfraDevice =
+ new InfraNetworkDevice(MacAddress.fromString("1:2:3:4:5:6"), mInfraNetworkReader);
+ mInfraDevice.runSlaac(Duration.ofSeconds(60));
+ assertNotNull(mInfraDevice.ipv6Addr);
+ }
+
+ private void assertInfraLinkMemberOfGroup(Inet6Address address) throws Exception {
+ waitFor(
+ () ->
+ isInMulticastGroup(
+ mInfraNetworkTracker.getTestIface().getInterfaceName(), address),
+ Duration.ofSeconds(3));
+ }
+
+ private void assertInfraLinkNotMemberOfGroup(Inet6Address address) throws Exception {
+ waitFor(
+ () ->
+ !isInMulticastGroup(
+ mInfraNetworkTracker.getTestIface().getInterfaceName(), address),
+ Duration.ofSeconds(3));
+ }
+
+ private void subscribeMulticastAddressAndWait(FullThreadDevice ftd, Inet6Address address)
+ throws Exception {
+ ftd.subscribeMulticastAddress(address);
+
+ assertInfraLinkMemberOfGroup(address);
+ }
+
+ private byte[] pollForPacketOnInfraNetwork(int type, Inet6Address srcAddress) {
+ return pollForPacketOnInfraNetwork(type, srcAddress, null);
+ }
+
+ private byte[] pollForPacketOnInfraNetwork(
+ int type, Inet6Address srcAddress, Inet6Address destAddress) {
+ Predicate<byte[]> filter;
+ filter =
+ p ->
+ (isExpectedIcmpv6Packet(p, type)
+ && (srcAddress == null ? true : isFromIpv6Source(p, srcAddress))
+ && (destAddress == null
+ ? true
+ : isToIpv6Destination(p, destAddress)));
+ return pollForPacket(mInfraNetworkReader, filter);
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
new file mode 100644
index 0000000..70897f0
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_DETACHED;
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_STOPPED;
+import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
+import static android.net.thread.utils.IntegrationTestUtils.CALLBACK_TIMEOUT;
+import static android.net.thread.utils.IntegrationTestUtils.LEAVE_TIMEOUT;
+import static android.net.thread.utils.IntegrationTestUtils.RESTART_JOIN_TIMEOUT;
+import static android.net.thread.utils.IntegrationTestUtils.waitForStateAnyOf;
+
+import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+import static org.junit.Assume.assumeNotNull;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.thread.ThreadNetworkController.StateCallback;
+import android.net.thread.utils.OtDaemonController;
+import android.os.SystemClock;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet6Address;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+/** Tests for E2E Android Thread integration with ot-daemon, ConnectivityService, etc.. */
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class ThreadIntegrationTest {
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private ThreadNetworkController mController;
+ private OtDaemonController mOtCtl;
+
+ // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
+ private static final byte[] DEFAULT_DATASET_TLVS =
+ base16().decode(
+ "0E080000000000010000000300001335060004001FFFE002"
+ + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+ + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+ + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ + "B9D351B40C0402A0FFF8");
+ private static final ActiveOperationalDataset DEFAULT_DATASET =
+ ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
+
+ @Before
+ public void setUp() throws Exception {
+ final ThreadNetworkManager manager = mContext.getSystemService(ThreadNetworkManager.class);
+ if (manager != null) {
+ mController = manager.getAllThreadNetworkControllers().get(0);
+ }
+
+ // Run the tests on only devices where the Thread feature is available
+ assumeNotNull(mController);
+
+ mOtCtl = new OtDaemonController();
+ leaveAndWait(mController);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (mController == null) {
+ return;
+ }
+
+ setTestUpStreamNetworkAndWait(mController, null);
+ leaveAndWait(mController);
+ }
+
+ @Test
+ public void otDaemonRestart_notJoinedAndStopped_deviceRoleIsStopped() throws Exception {
+ leaveAndWait(mController);
+
+ runShellCommand("stop ot-daemon");
+ // TODO(b/323331973): the sleep is needed to workaround the race conditions
+ SystemClock.sleep(200);
+
+ waitForStateAnyOf(mController, List.of(DEVICE_ROLE_STOPPED), CALLBACK_TIMEOUT);
+ }
+
+ @Test
+ public void otDaemonRestart_JoinedNetworkAndStopped_autoRejoined() throws Exception {
+ joinAndWait(mController, DEFAULT_DATASET);
+
+ runShellCommand("stop ot-daemon");
+
+ waitForStateAnyOf(mController, List.of(DEVICE_ROLE_DETACHED), CALLBACK_TIMEOUT);
+ waitForStateAnyOf(mController, List.of(DEVICE_ROLE_LEADER), RESTART_JOIN_TIMEOUT);
+ }
+
+ @Test
+ public void otDaemonFactoryReset_deviceRoleIsStopped() throws Exception {
+ joinAndWait(mController, DEFAULT_DATASET);
+
+ mOtCtl.factoryReset();
+
+ assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+ }
+
+ @Test
+ public void otDaemonFactoryReset_addressesRemoved() throws Exception {
+ joinAndWait(mController, DEFAULT_DATASET);
+
+ mOtCtl.factoryReset();
+ String ifconfig = runShellCommand("ifconfig thread-wpan");
+
+ assertThat(ifconfig).doesNotContain("inet6 addr");
+ }
+
+ @Test
+ public void tunInterface_joinedNetwork_otAddressesAddedToTunInterface() throws Exception {
+ joinAndWait(mController, DEFAULT_DATASET);
+
+ String ifconfig = runShellCommand("ifconfig thread-wpan");
+ List<Inet6Address> otAddresses = mOtCtl.getAddresses();
+ assertThat(otAddresses).isNotEmpty();
+ for (Inet6Address otAddress : otAddresses) {
+ assertThat(ifconfig).contains(otAddress.getHostAddress());
+ }
+ }
+
+ // TODO (b/323300829): add more tests for integration with linux platform and
+ // ConnectivityService
+
+ private static int getDeviceRole(ThreadNetworkController controller) throws Exception {
+ CompletableFuture<Integer> future = new CompletableFuture<>();
+ StateCallback callback = future::complete;
+ controller.registerStateCallback(directExecutor(), callback);
+ try {
+ return future.get(CALLBACK_TIMEOUT.toMillis(), MILLISECONDS);
+ } finally {
+ controller.unregisterStateCallback(callback);
+ }
+ }
+
+ private static void joinAndWait(
+ ThreadNetworkController controller, ActiveOperationalDataset activeDataset)
+ throws Exception {
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ () -> controller.join(activeDataset, directExecutor(), result -> {}));
+ waitForStateAnyOf(controller, List.of(DEVICE_ROLE_LEADER), RESTART_JOIN_TIMEOUT);
+ }
+
+ private static void leaveAndWait(ThreadNetworkController controller) throws Exception {
+ CompletableFuture<Void> future = new CompletableFuture<>();
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ () -> controller.leave(directExecutor(), future::complete));
+ future.get(LEAVE_TIMEOUT.toMillis(), MILLISECONDS);
+ }
+
+ private static void setTestUpStreamNetworkAndWait(
+ ThreadNetworkController controller, @Nullable String networkInterfaceName)
+ throws Exception {
+ CompletableFuture<Void> future = new CompletableFuture<>();
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ NETWORK_SETTINGS,
+ () -> {
+ controller.setTestNetworkAsUpstream(
+ networkInterfaceName, directExecutor(), future::complete);
+ });
+ future.get(CALLBACK_TIMEOUT.toMillis(), MILLISECONDS);
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
new file mode 100644
index 0000000..6cb1675
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.thread.utils;
+
+import static android.net.thread.utils.IntegrationTestUtils.waitFor;
+
+import static com.google.common.io.BaseEncoding.base16;
+
+import static org.junit.Assert.fail;
+
+import android.net.InetAddresses;
+import android.net.IpPrefix;
+import android.net.thread.ActiveOperationalDataset;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.net.Inet6Address;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A class that launches and controls a simulation Full Thread Device (FTD).
+ *
+ * <p>This class launches an `ot-cli-ftd` process and communicates with it via command line input
+ * and output. See <a
+ * href="https://github.com/openthread/openthread/blob/main/src/cli/README.md">this page</a> for
+ * available commands.
+ */
+public final class FullThreadDevice {
+ private final Process mProcess;
+ private final BufferedReader mReader;
+ private final BufferedWriter mWriter;
+
+ private ActiveOperationalDataset mActiveOperationalDataset;
+
+ /**
+ * Constructs a {@link FullThreadDevice} for the given node ID.
+ *
+ * <p>It launches an `ot-cli-ftd` process using the given node ID. The node ID is an integer in
+ * range [1, OPENTHREAD_SIMULATION_MAX_NETWORK_SIZE]. `OPENTHREAD_SIMULATION_MAX_NETWORK_SIZE`
+ * is defined in `external/openthread/examples/platforms/simulation/platform-config.h`.
+ *
+ * @param nodeId the node ID for the simulation Full Thread Device.
+ * @throws IllegalStateException the node ID is already occupied by another simulation Thread
+ * device.
+ */
+ public FullThreadDevice(int nodeId) {
+ try {
+ mProcess = Runtime.getRuntime().exec("/system/bin/ot-cli-ftd " + nodeId);
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to start ot-cli-ftd (id=" + nodeId + ")", e);
+ }
+ mReader = new BufferedReader(new InputStreamReader(mProcess.getInputStream()));
+ mWriter = new BufferedWriter(new OutputStreamWriter(mProcess.getOutputStream()));
+ mActiveOperationalDataset = null;
+ }
+
+ public void destroy() {
+ mProcess.destroy();
+ }
+
+ /**
+ * Returns an OMR (Off-Mesh-Routable) address on this device if any.
+ *
+ * <p>This methods goes through all unicast addresses on the device and returns the first
+ * address which is neither link-local nor mesh-local.
+ */
+ public Inet6Address getOmrAddress() {
+ List<String> addresses = executeCommand("ipaddr");
+ IpPrefix meshLocalPrefix = mActiveOperationalDataset.getMeshLocalPrefix();
+ for (String address : addresses) {
+ if (address.startsWith("fe80:")) {
+ continue;
+ }
+ Inet6Address addr = (Inet6Address) InetAddresses.parseNumericAddress(address);
+ if (!meshLocalPrefix.contains(addr)) {
+ return addr;
+ }
+ }
+ return null;
+ }
+
+ /** Returns the Mesh-local EID address on this device if any. */
+ public Inet6Address getMlEid() {
+ List<String> addresses = executeCommand("ipaddr mleid");
+ return (Inet6Address) InetAddresses.parseNumericAddress(addresses.get(0));
+ }
+
+ /**
+ * Returns the link-local address of the device.
+ *
+ * <p>This methods goes through all unicast addresses on the device and returns the address that
+ * begins with fe80.
+ */
+ public Inet6Address getLinkLocalAddress() {
+ List<String> output = executeCommand("ipaddr linklocal");
+ if (!output.isEmpty() && output.get(0).startsWith("fe80:")) {
+ return (Inet6Address) InetAddresses.parseNumericAddress(output.get(0));
+ }
+ return null;
+ }
+
+ /**
+ * Returns the mesh-local addresses of the device.
+ *
+ * <p>This methods goes through all unicast addresses on the device and returns the address that
+ * begins with mesh-local prefix.
+ */
+ public List<Inet6Address> getMeshLocalAddresses() {
+ List<String> addresses = executeCommand("ipaddr");
+ List<Inet6Address> meshLocalAddresses = new ArrayList<>();
+ IpPrefix meshLocalPrefix = mActiveOperationalDataset.getMeshLocalPrefix();
+ for (String address : addresses) {
+ Inet6Address addr = (Inet6Address) InetAddresses.parseNumericAddress(address);
+ if (meshLocalPrefix.contains(addr)) {
+ meshLocalAddresses.add(addr);
+ }
+ }
+ return meshLocalAddresses;
+ }
+
+ /**
+ * Joins the Thread network using the given {@link ActiveOperationalDataset}.
+ *
+ * @param dataset the Active Operational Dataset
+ */
+ public void joinNetwork(ActiveOperationalDataset dataset) {
+ mActiveOperationalDataset = dataset;
+ executeCommand("dataset set active " + base16().lowerCase().encode(dataset.toThreadTlvs()));
+ executeCommand("ifconfig up");
+ executeCommand("thread start");
+ }
+
+ /** Stops the Thread network radio. */
+ public void stopThreadRadio() {
+ executeCommand("thread stop");
+ executeCommand("ifconfig down");
+ }
+
+ /**
+ * Waits for the Thread device to enter the any state of the given {@link List<String>}.
+ *
+ * @param states the list of states to wait for. Valid states are "disabled", "detached",
+ * "child", "router" and "leader".
+ * @param timeout the time to wait for the expected state before throwing
+ */
+ public void waitForStateAnyOf(List<String> states, Duration timeout) throws TimeoutException {
+ waitFor(() -> states.contains(getState()), timeout);
+ }
+
+ /**
+ * Gets the state of the Thread device.
+ *
+ * @return a string representing the state.
+ */
+ public String getState() {
+ return executeCommand("state").get(0);
+ }
+
+ /** Closes the UDP socket. */
+ public void udpClose() {
+ executeCommand("udp close");
+ }
+
+ /** Opens the UDP socket. */
+ public void udpOpen() {
+ executeCommand("udp open");
+ }
+
+ /** Opens the UDP socket and binds it to a specific address and port. */
+ public void udpBind(Inet6Address address, int port) {
+ udpClose();
+ udpOpen();
+ executeCommand(String.format("udp bind %s %d", address.getHostAddress(), port));
+ }
+
+ /** Returns the message received on the UDP socket. */
+ public String udpReceive() throws IOException {
+ Pattern pattern =
+ Pattern.compile("> (\\d+) bytes from ([\\da-f:]+) (\\d+) ([\\x00-\\x7F]+)");
+ Matcher matcher = pattern.matcher(mReader.readLine());
+ matcher.matches();
+
+ return matcher.group(4);
+ }
+
+ /** Runs the "factoryreset" command on the device. */
+ public void factoryReset() {
+ try {
+ mWriter.write("factoryreset\n");
+ mWriter.flush();
+ // fill the input buffer to avoid truncating next command
+ for (int i = 0; i < 1000; ++i) {
+ mWriter.write("\n");
+ }
+ mWriter.flush();
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to run factoryreset on ot-cli-ftd", e);
+ }
+ }
+
+ public void subscribeMulticastAddress(Inet6Address address) {
+ executeCommand("ipmaddr add " + address.getHostAddress());
+ }
+
+ public void ping(Inet6Address address, Inet6Address source, int size, int count) {
+ String cmd =
+ "ping"
+ + ((source == null) ? "" : (" -I " + source.getHostAddress()))
+ + " "
+ + address.getHostAddress()
+ + " "
+ + size
+ + " "
+ + count;
+ executeCommand(cmd);
+ }
+
+ public void ping(Inet6Address address) {
+ ping(address, null, 100 /* size */, 1 /* count */);
+ }
+
+ private List<String> executeCommand(String command) {
+ try {
+ mWriter.write(command + "\n");
+ mWriter.flush();
+ } catch (IOException e) {
+ throw new IllegalStateException(
+ "Failed to write the command " + command + " to ot-cli-ftd", e);
+ }
+ try {
+ return readUntilDone();
+ } catch (IOException e) {
+ throw new IllegalStateException(
+ "Failed to read the ot-cli-ftd output of command: " + command, e);
+ }
+ }
+
+ private List<String> readUntilDone() throws IOException {
+ ArrayList<String> result = new ArrayList<>();
+ String line;
+ while ((line = mReader.readLine()) != null) {
+ if (line.equals("Done")) {
+ break;
+ }
+ if (line.startsWith("Error:")) {
+ fail("ot-cli-ftd reported an error: " + line);
+ }
+ if (!line.startsWith("> ")) {
+ result.add(line);
+ }
+ }
+ return result;
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/utils/InfraNetworkDevice.java b/thread/tests/integration/src/android/net/thread/utils/InfraNetworkDevice.java
new file mode 100644
index 0000000..72a278c
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/utils/InfraNetworkDevice.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.thread.utils;
+
+import static android.net.thread.utils.IntegrationTestUtils.getRaPios;
+import static android.net.thread.utils.IntegrationTestUtils.pollForPacket;
+import static android.net.thread.utils.IntegrationTestUtils.waitFor;
+
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_ROUTERS_MULTICAST;
+
+import android.net.InetAddresses;
+import android.net.MacAddress;
+
+import com.android.net.module.util.Ipv6Utils;
+import com.android.net.module.util.structs.LlaOption;
+import com.android.net.module.util.structs.PrefixInformationOption;
+import com.android.testutils.TapPacketReader;
+
+import java.io.IOException;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.time.Duration;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * A class that simulates a device on the infrastructure network.
+ *
+ * <p>This class directly interacts with the TUN interface of the test network to pretend there's a
+ * device on the infrastructure network.
+ */
+public final class InfraNetworkDevice {
+ // The MAC address of this device.
+ public final MacAddress macAddr;
+ // The packet reader of the TUN interface of the test network.
+ public final TapPacketReader packetReader;
+ // The IPv6 address generated by SLAAC for the device.
+ public Inet6Address ipv6Addr;
+
+ /**
+ * Constructs an InfraNetworkDevice with the given {@link MAC address} and {@link
+ * TapPacketReader}.
+ *
+ * @param macAddr the MAC address of the device
+ * @param packetReader the packet reader of the TUN interface of the test network.
+ */
+ public InfraNetworkDevice(MacAddress macAddr, TapPacketReader packetReader) {
+ this.macAddr = macAddr;
+ this.packetReader = packetReader;
+ }
+
+ /**
+ * Sends an ICMPv6 echo request message to the given {@link Inet6Address}.
+ *
+ * @param dstAddr the destination address of the packet.
+ * @throws IOException when it fails to send the packet.
+ */
+ public void sendEchoRequest(Inet6Address dstAddr) throws IOException {
+ ByteBuffer icmp6Packet = Ipv6Utils.buildEchoRequestPacket(ipv6Addr, dstAddr);
+ packetReader.sendResponse(icmp6Packet);
+ }
+
+ /**
+ * Sends an ICMPv6 Router Solicitation (RS) message to all routers on the network.
+ *
+ * @throws IOException when it fails to send the packet.
+ */
+ public void sendRsPacket() throws IOException {
+ ByteBuffer slla = LlaOption.build((byte) ICMPV6_ND_OPTION_SLLA, macAddr);
+ ByteBuffer rs =
+ Ipv6Utils.buildRsPacket(
+ (Inet6Address) InetAddresses.parseNumericAddress("fe80::1"),
+ IPV6_ADDR_ALL_ROUTERS_MULTICAST,
+ slla);
+ packetReader.sendResponse(rs);
+ }
+
+ /**
+ * Runs SLAAC to generate an IPv6 address for the device.
+ *
+ * <p>The devices sends an RS message, processes the received RA messages and generates an IPv6
+ * address if there's any available Prefix Information Option (PIO). For now it only generates
+ * one address in total and doesn't track the expiration.
+ *
+ * @param timeoutSeconds the number of seconds to wait for.
+ * @throws TimeoutException when the device fails to generate a SLAAC address in given timeout.
+ */
+ public void runSlaac(Duration timeout) throws TimeoutException {
+ waitFor(() -> (ipv6Addr = runSlaac()) != null, timeout);
+ }
+
+ private Inet6Address runSlaac() {
+ try {
+ sendRsPacket();
+
+ final byte[] raPacket = pollForPacket(packetReader, p -> !getRaPios(p).isEmpty());
+
+ final List<PrefixInformationOption> options = getRaPios(raPacket);
+
+ for (PrefixInformationOption pio : options) {
+ if (pio.validLifetime > 0 && pio.preferredLifetime > 0) {
+ final byte[] addressBytes = pio.prefix;
+ addressBytes[addressBytes.length - 1] = (byte) (new Random()).nextInt();
+ addressBytes[addressBytes.length - 2] = (byte) (new Random()).nextInt();
+ return (Inet6Address) InetAddress.getByAddress(addressBytes);
+ }
+ }
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to generate an address by SLAAC", e);
+ }
+ return null;
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
new file mode 100644
index 0000000..74251a6
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.thread.utils;
+
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+
+import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_PIO;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
+
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+import android.net.TestNetworkInterface;
+import android.net.thread.ThreadNetworkController;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.structs.Icmpv6Header;
+import com.android.net.module.util.structs.Ipv6Header;
+import com.android.net.module.util.structs.PrefixInformationOption;
+import com.android.net.module.util.structs.RaHeader;
+import com.android.testutils.HandlerUtils;
+import com.android.testutils.TapPacketReader;
+
+import com.google.common.util.concurrent.SettableFuture;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.nio.ByteBuffer;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+/** Static utility methods relating to Thread integration tests. */
+public final class IntegrationTestUtils {
+ // The timeout of join() after restarting ot-daemon. The device needs to send 6 Link Request
+ // every 5 seconds, followed by 4 Parent Request every second. So this value needs to be 40
+ // seconds to be safe
+ public static final Duration RESTART_JOIN_TIMEOUT = Duration.ofSeconds(40);
+ public static final Duration JOIN_TIMEOUT = Duration.ofSeconds(30);
+ public static final Duration LEAVE_TIMEOUT = Duration.ofSeconds(2);
+ public static final Duration CALLBACK_TIMEOUT = Duration.ofSeconds(1);
+
+ private IntegrationTestUtils() {}
+
+ /** Returns whether the device supports simulated Thread radio. */
+ public static boolean isSimulatedThreadRadioSupported() {
+ // The integration test uses SIMULATION Thread radio so that it only supports CuttleFish.
+ return SystemProperties.get("ro.product.model").startsWith("Cuttlefish");
+ }
+
+ /**
+ * Waits for the given {@link Supplier} to be true until given timeout.
+ *
+ * @param condition the condition to check
+ * @param timeout the time to wait for the condition before throwing
+ * @throws TimeoutException if the condition is still not met when the timeout expires
+ */
+ public static void waitFor(Supplier<Boolean> condition, Duration timeout)
+ throws TimeoutException {
+ final long intervalMills = 1000;
+ final long timeoutMills = timeout.toMillis();
+
+ for (long i = 0; i < timeoutMills; i += intervalMills) {
+ if (condition.get()) {
+ return;
+ }
+ SystemClock.sleep(intervalMills);
+ }
+ if (condition.get()) {
+ return;
+ }
+ throw new TimeoutException("The condition failed to become true in " + timeout);
+ }
+
+ /**
+ * Creates a {@link TapPacketReader} given the {@link TestNetworkInterface} and {@link Handler}.
+ *
+ * @param testNetworkInterface the TUN interface of the test network
+ * @param handler the handler to process the packets
+ * @return the {@link TapPacketReader}
+ */
+ public static TapPacketReader newPacketReader(
+ TestNetworkInterface testNetworkInterface, Handler handler) {
+ FileDescriptor fd = testNetworkInterface.getFileDescriptor().getFileDescriptor();
+ final TapPacketReader reader =
+ new TapPacketReader(handler, fd, testNetworkInterface.getMtu());
+ handler.post(() -> reader.start());
+ HandlerUtils.waitForIdle(handler, 5000 /* timeout in milliseconds */);
+ return reader;
+ }
+
+ /**
+ * Waits for the Thread module to enter any state of the given {@code deviceRoles}.
+ *
+ * @param controller the {@link ThreadNetworkController}
+ * @param deviceRoles the desired device roles. See also {@link
+ * ThreadNetworkController.DeviceRole}
+ * @param timeout the time to wait for the expected state before throwing
+ * @return the {@link ThreadNetworkController.DeviceRole} after waiting
+ * @throws TimeoutException if the device hasn't become any of expected roles until the timeout
+ * expires
+ */
+ public static int waitForStateAnyOf(
+ ThreadNetworkController controller, List<Integer> deviceRoles, Duration timeout)
+ throws TimeoutException {
+ SettableFuture<Integer> future = SettableFuture.create();
+ ThreadNetworkController.StateCallback callback =
+ newRole -> {
+ if (deviceRoles.contains(newRole)) {
+ future.set(newRole);
+ }
+ };
+ controller.registerStateCallback(directExecutor(), callback);
+ try {
+ return future.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
+ } catch (InterruptedException | ExecutionException e) {
+ throw new TimeoutException(
+ String.format(
+ "The device didn't become an expected role in %s: %s",
+ timeout, e.getMessage()));
+ } finally {
+ controller.unregisterStateCallback(callback);
+ }
+ }
+
+ /**
+ * Polls for a packet from a given {@link TapPacketReader} that satisfies the {@code filter}.
+ *
+ * @param packetReader a TUN packet reader
+ * @param filter the filter to be applied on the packet
+ * @return the first IPv6 packet that satisfies the {@code filter}. If it has waited for more
+ * than 3000ms to read the next packet, the method will return null
+ */
+ public static byte[] pollForPacket(TapPacketReader packetReader, Predicate<byte[]> filter) {
+ byte[] packet;
+ while ((packet = packetReader.poll(3000 /* timeoutMs */, filter)) != null) {
+ return packet;
+ }
+ return null;
+ }
+
+ /** Returns {@code true} if {@code packet} is an ICMPv6 packet of given {@code type}. */
+ public static boolean isExpectedIcmpv6Packet(byte[] packet, int type) {
+ if (packet == null) {
+ return false;
+ }
+ ByteBuffer buf = ByteBuffer.wrap(packet);
+ try {
+ if (Struct.parse(Ipv6Header.class, buf).nextHeader != (byte) IPPROTO_ICMPV6) {
+ return false;
+ }
+ return Struct.parse(Icmpv6Header.class, buf).type == (short) type;
+ } catch (IllegalArgumentException ignored) {
+ // It's fine that the passed in packet is malformed because it's could be sent
+ // by anybody.
+ }
+ return false;
+ }
+
+ public static boolean isFromIpv6Source(byte[] packet, Inet6Address src) {
+ if (packet == null) {
+ return false;
+ }
+ ByteBuffer buf = ByteBuffer.wrap(packet);
+ try {
+ return Struct.parse(Ipv6Header.class, buf).srcIp.equals(src);
+ } catch (IllegalArgumentException ignored) {
+ // It's fine that the passed in packet is malformed because it's could be sent
+ // by anybody.
+ }
+ return false;
+ }
+
+ public static boolean isToIpv6Destination(byte[] packet, Inet6Address dest) {
+ if (packet == null) {
+ return false;
+ }
+ ByteBuffer buf = ByteBuffer.wrap(packet);
+ try {
+ return Struct.parse(Ipv6Header.class, buf).dstIp.equals(dest);
+ } catch (IllegalArgumentException ignored) {
+ // It's fine that the passed in packet is malformed because it's could be sent
+ // by anybody.
+ }
+ return false;
+ }
+
+ /** Returns the Prefix Information Options (PIO) extracted from an ICMPv6 RA message. */
+ public static List<PrefixInformationOption> getRaPios(byte[] raMsg) {
+ final ArrayList<PrefixInformationOption> pioList = new ArrayList<>();
+
+ if (raMsg == null) {
+ return pioList;
+ }
+
+ final ByteBuffer buf = ByteBuffer.wrap(raMsg);
+ final Ipv6Header ipv6Header = Struct.parse(Ipv6Header.class, buf);
+ if (ipv6Header.nextHeader != (byte) IPPROTO_ICMPV6) {
+ return pioList;
+ }
+
+ final Icmpv6Header icmpv6Header = Struct.parse(Icmpv6Header.class, buf);
+ if (icmpv6Header.type != (short) ICMPV6_ROUTER_ADVERTISEMENT) {
+ return pioList;
+ }
+
+ Struct.parse(RaHeader.class, buf);
+ while (buf.position() < raMsg.length) {
+ final int currentPos = buf.position();
+ final int type = Byte.toUnsignedInt(buf.get());
+ final int length = Byte.toUnsignedInt(buf.get());
+ if (type == ICMPV6_ND_OPTION_PIO) {
+ final ByteBuffer pioBuf =
+ ByteBuffer.wrap(
+ buf.array(),
+ currentPos,
+ Struct.getSize(PrefixInformationOption.class));
+ final PrefixInformationOption pio =
+ Struct.parse(PrefixInformationOption.class, pioBuf);
+ pioList.add(pio);
+
+ // Move ByteBuffer position to the next option.
+ buf.position(currentPos + Struct.getSize(PrefixInformationOption.class));
+ } else {
+ // The length is in units of 8 octets.
+ buf.position(currentPos + (length * 8));
+ }
+ }
+ return pioList;
+ }
+
+ /**
+ * Sends a UDP message to a destination.
+ *
+ * @param dstAddress the IP address of the destination
+ * @param dstPort the port of the destination
+ * @param message the message in UDP payload
+ * @throws IOException if failed to send the message
+ */
+ public static void sendUdpMessage(InetAddress dstAddress, int dstPort, String message)
+ throws IOException {
+ SocketAddress dstSockAddr = new InetSocketAddress(dstAddress, dstPort);
+
+ try (DatagramSocket socket = new DatagramSocket()) {
+ socket.connect(dstSockAddr);
+
+ byte[] msgBytes = message.getBytes();
+ DatagramPacket packet = new DatagramPacket(msgBytes, msgBytes.length);
+
+ socket.send(packet);
+ }
+ }
+
+ public static boolean isInMulticastGroup(String interfaceName, Inet6Address address) {
+ final String cmd = "ip -6 maddr show dev " + interfaceName;
+ final String output = runShellCommandOrThrow(cmd);
+ final String addressStr = address.getHostAddress();
+ for (final String line : output.split("\\n")) {
+ if (line.contains(addressStr)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java b/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
new file mode 100644
index 0000000..4a06fe8
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread.utils;
+
+import android.net.InetAddresses;
+import android.os.SystemClock;
+
+import com.android.compatibility.common.util.SystemUtil;
+
+import java.net.Inet6Address;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Wrapper of the "/system/bin/ot-ctl" which can be used to send CLI commands to ot-daemon to
+ * control its behavior.
+ *
+ * <p>Note that this class takes root privileged to run.
+ */
+public final class OtDaemonController {
+ private static final String OT_CTL = "/system/bin/ot-ctl";
+
+ /**
+ * Factory resets ot-daemon.
+ *
+ * <p>This will erase all persistent data written into apexdata/com.android.apex/ot-daemon and
+ * restart the ot-daemon service.
+ */
+ public void factoryReset() {
+ executeCommand("factoryreset");
+
+ // TODO(b/323164524): ot-ctl is a separate process so that the tests can't depend on the
+ // time sequence. Here needs to wait for system server to receive the ot-daemon death
+ // signal and take actions.
+ // A proper fix is to replace "ot-ctl" with "cmd thread_network ot-ctl" which is
+ // synchronized with the system server
+ SystemClock.sleep(500);
+ }
+
+ /** Returns the list of IPv6 addresses on ot-daemon. */
+ public List<Inet6Address> getAddresses() {
+ String output = executeCommand("ipaddr");
+ return Arrays.asList(output.split("\n")).stream()
+ .map(String::trim)
+ .filter(str -> !str.equals("Done"))
+ .map(addr -> InetAddresses.parseNumericAddress(addr))
+ .map(inetAddr -> (Inet6Address) inetAddr)
+ .toList();
+ }
+
+ public String executeCommand(String cmd) {
+ return SystemUtil.runShellCommand(OT_CTL + " " + cmd);
+ }
+}
diff --git a/thread/tests/unit/Android.bp b/thread/tests/unit/Android.bp
index 8092693..3365cd0 100644
--- a/thread/tests/unit/Android.bp
+++ b/thread/tests/unit/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_thread_network",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -29,22 +30,38 @@
],
test_suites: [
"general-tests",
+ "mts-tethering",
],
static_libs: [
- "androidx.test.ext.junit",
- "compatibility-device-util-axt",
+ "frameworks-base-testutils",
"framework-connectivity-pre-jarjar",
"framework-connectivity-t-pre-jarjar",
+ "framework-location.stubs.module_lib",
"guava",
"guava-android-testlib",
- "mockito-target-minus-junit4",
+ "mockito-target-extended-minus-junit4",
"net-tests-utils",
+ "ot-daemon-aidl-java",
+ "ot-daemon-testing",
+ "service-connectivity-pre-jarjar",
+ "service-thread-pre-jarjar",
"truth",
+ "service-thread-pre-jarjar",
],
libs: [
"android.test.base",
"android.test.runner",
+ "ServiceConnectivityResources",
+ "framework-wifi",
],
+ jni_libs: [
+ "libservice-thread-jni",
+
+ // these are needed for Extended Mockito
+ "libdexmakerjvmtiagent",
+ "libstaticjvmtiagent",
+ ],
+ jni_uses_platform_apis: true,
jarjar_rules: ":connectivity-jarjar-rules",
// Test coverage system runs on different devices. Need to
// compile for all architectures.
diff --git a/thread/tests/unit/AndroidTest.xml b/thread/tests/unit/AndroidTest.xml
index 597c6a8..d16e423 100644
--- a/thread/tests/unit/AndroidTest.xml
+++ b/thread/tests/unit/AndroidTest.xml
@@ -19,6 +19,18 @@
<option name="test-tag" value="ThreadNetworkUnitTests" />
<option name="test-suite-tag" value="apct" />
+ <!--
+ Only run tests if the device under test is SDK version 34 (Android 14) or above.
+ -->
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.Sdk34ModuleController" />
+
+ <!-- Run tests in MTS only if the Tethering Mainline module is installed. -->
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+ <option name="mainline-module-package-name" value="com.google.android.tethering" />
+ </object>
+
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="test-file-name" value="ThreadNetworkUnitTests.apk" />
<option name="check-min-sdk" value="true" />
@@ -30,5 +42,8 @@
<option name="hidden-api-checks" value="false"/>
<!-- Ignores tests introduced by guava-android-testlib -->
<option name="exclude-annotation" value="org.junit.Ignore"/>
+ <!-- Ignores tests introduced by frameworks-base-testutils -->
+ <option name="exclude-filter" value="android.os.test.TestLooperTest"/>
+ <option name="exclude-filter" value="com.android.test.filters.SelectTestTests"/>
</test>
</configuration>
diff --git a/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java b/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java
index 7284968..e92dcb9 100644
--- a/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java
+++ b/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java
@@ -33,12 +33,8 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import java.security.SecureRandom;
-import java.util.Random;
-
/** Unit tests for {@link ActiveOperationalDataset}. */
@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -62,9 +58,6 @@
+ "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ "B9D351B40C0402A0FFF8");
- @Mock private Random mockRandom;
- @Mock private SecureRandom mockSecureRandom;
-
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
diff --git a/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
index 2f120b2..75eb043 100644
--- a/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
+++ b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
@@ -28,11 +28,6 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
-import android.net.thread.IActiveOperationalDatasetReceiver;
-import android.net.thread.IOperationReceiver;
-import android.net.thread.IOperationalDatasetCallback;
-import android.net.thread.IStateCallback;
-import android.net.thread.IThreadNetworkController;
import android.net.thread.ThreadNetworkController.OperationalDatasetCallback;
import android.net.thread.ThreadNetworkController.StateCallback;
import android.os.Binder;
@@ -111,6 +106,11 @@
return (IOperationReceiver) invocation.getArguments()[1];
}
+ private static IOperationReceiver getSetTestNetworkAsUpstreamReceiver(
+ InvocationOnMock invocation) {
+ return (IOperationReceiver) invocation.getArguments()[1];
+ }
+
private static IActiveOperationalDatasetReceiver getCreateDatasetReceiver(
InvocationOnMock invocation) {
return (IActiveOperationalDatasetReceiver) invocation.getArguments()[1];
@@ -359,4 +359,27 @@
assertThat(errorCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
assertThat(errorCallbackUid.get()).isEqualTo(Process.myUid());
}
+
+ @Test
+ public void setTestNetworkAsUpstream_callbackIsInvokedWithCallingAppIdentity()
+ throws Exception {
+ setBinderUid(SYSTEM_UID);
+
+ AtomicInteger callbackUid = new AtomicInteger(0);
+
+ doAnswer(
+ invoke -> {
+ getSetTestNetworkAsUpstreamReceiver(invoke).onSuccess();
+ return null;
+ })
+ .when(mMockService)
+ .setTestNetworkAsUpstream(anyString(), any(IOperationReceiver.class));
+ mController.setTestNetworkAsUpstream(
+ null, Runnable::run, v -> callbackUid.set(Binder.getCallingUid()));
+ mController.setTestNetworkAsUpstream(
+ new String("test0"), Runnable::run, v -> callbackUid.set(Binder.getCallingUid()));
+
+ assertThat(callbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(callbackUid.get()).isEqualTo(Process.myUid());
+ }
}
diff --git a/thread/tests/unit/src/android/net/thread/ThreadNetworkExceptionTest.java b/thread/tests/unit/src/android/net/thread/ThreadNetworkExceptionTest.java
new file mode 100644
index 0000000..f62b437
--- /dev/null
+++ b/thread/tests/unit/src/android/net/thread/ThreadNetworkExceptionTest.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link ThreadNetworkException} to cover what is not covered in CTS tests. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ThreadNetworkExceptionTest {
+ @Test
+ public void constructor_tooLargeErrorCode_throwsIllegalArgumentException() throws Exception {
+ // TODO (b/323791003): move this test case to cts/ThreadNetworkExceptionTest when mainline
+ // CTS is ready.
+ assertThrows(IllegalArgumentException.class, () -> new ThreadNetworkException(13, "13"));
+ }
+}
diff --git a/thread/tests/unit/src/com/android/server/thread/BinderUtil.java b/thread/tests/unit/src/com/android/server/thread/BinderUtil.java
new file mode 100644
index 0000000..3614bce
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/BinderUtil.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.thread;
+
+import android.os.Binder;
+
+/** Utilities for faking the calling uid in Binder. */
+public class BinderUtil {
+ /**
+ * Fake the calling uid in Binder.
+ *
+ * @param uid the calling uid that Binder should return from now on
+ */
+ public static void setUid(int uid) {
+ Binder.restoreCallingIdentity((((long) uid) << 32) | Binder.getCallingPid());
+ }
+}
diff --git a/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java b/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
new file mode 100644
index 0000000..8aea0a3
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
@@ -0,0 +1,367 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.thread;
+
+import static android.net.nsd.NsdManager.FAILURE_INTERNAL_ERROR;
+import static android.net.nsd.NsdManager.PROTOCOL_DNS_SD;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.net.nsd.NsdManager;
+import android.net.nsd.NsdServiceInfo;
+import android.os.Handler;
+import android.os.test.TestLooper;
+
+import com.android.server.thread.openthread.DnsTxtAttribute;
+import com.android.server.thread.openthread.INsdStatusReceiver;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+
+/** Unit tests for {@link NsdPublisher}. */
+public final class NsdPublisherTest {
+ @Mock private NsdManager mMockNsdManager;
+
+ @Mock private INsdStatusReceiver mRegistrationReceiver;
+ @Mock private INsdStatusReceiver mUnregistrationReceiver;
+
+ private TestLooper mTestLooper;
+ private NsdPublisher mNsdPublisher;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void registerService_nsdManagerSucceeds_serviceRegistrationSucceeds() throws Exception {
+ prepareTest();
+
+ DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
+ DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
+
+ mNsdPublisher.registerService(
+ null,
+ "MyService",
+ "_test._tcp",
+ List.of("_subtype1", "_subtype2"),
+ 12345,
+ List.of(txt1, txt2),
+ mRegistrationReceiver,
+ 16 /* listenerId */);
+
+ mTestLooper.dispatchAll();
+
+ ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
+ ArgumentCaptor.forClass(NsdServiceInfo.class);
+ ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
+ ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
+
+ verify(mMockNsdManager, times(1))
+ .registerService(
+ actualServiceInfoCaptor.capture(),
+ eq(PROTOCOL_DNS_SD),
+ any(),
+ actualRegistrationListenerCaptor.capture());
+
+ NsdServiceInfo actualServiceInfo = actualServiceInfoCaptor.getValue();
+ NsdManager.RegistrationListener actualRegistrationListener =
+ actualRegistrationListenerCaptor.getValue();
+
+ actualRegistrationListener.onServiceRegistered(actualServiceInfo);
+ mTestLooper.dispatchAll();
+
+ assertThat(actualServiceInfo.getServiceName()).isEqualTo("MyService");
+ assertThat(actualServiceInfo.getServiceType()).isEqualTo("_test._tcp");
+ assertThat(actualServiceInfo.getSubtypes()).isEqualTo(Set.of("_subtype1", "_subtype2"));
+ assertThat(actualServiceInfo.getPort()).isEqualTo(12345);
+ assertThat(actualServiceInfo.getAttributes().size()).isEqualTo(2);
+ assertThat(actualServiceInfo.getAttributes().get("key1"))
+ .isEqualTo(new byte[] {(byte) 0x01, (byte) 0x02});
+ assertThat(actualServiceInfo.getAttributes().get("key2"))
+ .isEqualTo(new byte[] {(byte) 0x03});
+
+ verify(mRegistrationReceiver, times(1)).onSuccess();
+ }
+
+ @Test
+ public void registerService_nsdManagerFails_serviceRegistrationFails() throws Exception {
+ prepareTest();
+
+ DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
+ DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
+
+ mNsdPublisher.registerService(
+ null,
+ "MyService",
+ "_test._tcp",
+ List.of("_subtype1", "_subtype2"),
+ 12345,
+ List.of(txt1, txt2),
+ mRegistrationReceiver,
+ 16 /* listenerId */);
+
+ mTestLooper.dispatchAll();
+
+ ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
+ ArgumentCaptor.forClass(NsdServiceInfo.class);
+ ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
+ ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
+
+ verify(mMockNsdManager, times(1))
+ .registerService(
+ actualServiceInfoCaptor.capture(),
+ eq(PROTOCOL_DNS_SD),
+ any(Executor.class),
+ actualRegistrationListenerCaptor.capture());
+
+ NsdServiceInfo actualServiceInfo = actualServiceInfoCaptor.getValue();
+ NsdManager.RegistrationListener actualRegistrationListener =
+ actualRegistrationListenerCaptor.getValue();
+
+ actualRegistrationListener.onRegistrationFailed(actualServiceInfo, FAILURE_INTERNAL_ERROR);
+ mTestLooper.dispatchAll();
+
+ assertThat(actualServiceInfo.getServiceName()).isEqualTo("MyService");
+ assertThat(actualServiceInfo.getServiceType()).isEqualTo("_test._tcp");
+ assertThat(actualServiceInfo.getSubtypes()).isEqualTo(Set.of("_subtype1", "_subtype2"));
+ assertThat(actualServiceInfo.getPort()).isEqualTo(12345);
+ assertThat(actualServiceInfo.getAttributes().size()).isEqualTo(2);
+ assertThat(actualServiceInfo.getAttributes().get("key1"))
+ .isEqualTo(new byte[] {(byte) 0x01, (byte) 0x02});
+ assertThat(actualServiceInfo.getAttributes().get("key2"))
+ .isEqualTo(new byte[] {(byte) 0x03});
+
+ verify(mRegistrationReceiver, times(1)).onError(FAILURE_INTERNAL_ERROR);
+ }
+
+ @Test
+ public void registerService_nsdManagerThrows_serviceRegistrationFails() throws Exception {
+ prepareTest();
+
+ DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
+ DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
+
+ doThrow(new IllegalArgumentException("NsdManager fails"))
+ .when(mMockNsdManager)
+ .registerService(any(), anyInt(), any(Executor.class), any());
+
+ mNsdPublisher.registerService(
+ null,
+ "MyService",
+ "_test._tcp",
+ List.of("_subtype1", "_subtype2"),
+ 12345,
+ List.of(txt1, txt2),
+ mRegistrationReceiver,
+ 16 /* listenerId */);
+ mTestLooper.dispatchAll();
+
+ verify(mRegistrationReceiver, times(1)).onError(FAILURE_INTERNAL_ERROR);
+ }
+
+ @Test
+ public void unregisterService_nsdManagerSucceeds_serviceUnregistrationSucceeds()
+ throws Exception {
+ prepareTest();
+
+ DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
+ DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
+
+ mNsdPublisher.registerService(
+ null,
+ "MyService",
+ "_test._tcp",
+ List.of("_subtype1", "_subtype2"),
+ 12345,
+ List.of(txt1, txt2),
+ mRegistrationReceiver,
+ 16 /* listenerId */);
+
+ mTestLooper.dispatchAll();
+
+ ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
+ ArgumentCaptor.forClass(NsdServiceInfo.class);
+ ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
+ ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
+
+ verify(mMockNsdManager, times(1))
+ .registerService(
+ actualServiceInfoCaptor.capture(),
+ eq(PROTOCOL_DNS_SD),
+ any(Executor.class),
+ actualRegistrationListenerCaptor.capture());
+
+ NsdServiceInfo actualServiceInfo = actualServiceInfoCaptor.getValue();
+ NsdManager.RegistrationListener actualRegistrationListener =
+ actualRegistrationListenerCaptor.getValue();
+
+ actualRegistrationListener.onServiceRegistered(actualServiceInfo);
+ mNsdPublisher.unregister(mUnregistrationReceiver, 16 /* listenerId */);
+ mTestLooper.dispatchAll();
+ verify(mMockNsdManager, times(1)).unregisterService(actualRegistrationListener);
+
+ actualRegistrationListener.onServiceUnregistered(actualServiceInfo);
+ mTestLooper.dispatchAll();
+ verify(mUnregistrationReceiver, times(1)).onSuccess();
+ }
+
+ @Test
+ public void unregisterService_nsdManagerFails_serviceUnregistrationFails() throws Exception {
+ prepareTest();
+
+ DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
+ DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
+
+ mNsdPublisher.registerService(
+ null,
+ "MyService",
+ "_test._tcp",
+ List.of("_subtype1", "_subtype2"),
+ 12345,
+ List.of(txt1, txt2),
+ mRegistrationReceiver,
+ 16 /* listenerId */);
+
+ mTestLooper.dispatchAll();
+
+ ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
+ ArgumentCaptor.forClass(NsdServiceInfo.class);
+ ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
+ ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
+
+ verify(mMockNsdManager, times(1))
+ .registerService(
+ actualServiceInfoCaptor.capture(),
+ eq(PROTOCOL_DNS_SD),
+ any(Executor.class),
+ actualRegistrationListenerCaptor.capture());
+
+ NsdServiceInfo actualServiceInfo = actualServiceInfoCaptor.getValue();
+ NsdManager.RegistrationListener actualRegistrationListener =
+ actualRegistrationListenerCaptor.getValue();
+
+ actualRegistrationListener.onServiceRegistered(actualServiceInfo);
+ mNsdPublisher.unregister(mUnregistrationReceiver, 16 /* listenerId */);
+ mTestLooper.dispatchAll();
+ verify(mMockNsdManager, times(1)).unregisterService(actualRegistrationListener);
+
+ actualRegistrationListener.onUnregistrationFailed(
+ actualServiceInfo, FAILURE_INTERNAL_ERROR);
+ mTestLooper.dispatchAll();
+ verify(mUnregistrationReceiver, times(1)).onError(0);
+ }
+
+ @Test
+ public void onOtDaemonDied_unregisterAll() {
+ prepareTest();
+
+ DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
+ DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
+
+ ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
+ ArgumentCaptor.forClass(NsdServiceInfo.class);
+ ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
+ ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
+
+ mNsdPublisher.registerService(
+ null,
+ "MyService",
+ "_test._tcp",
+ List.of("_subtype1", "_subtype2"),
+ 12345,
+ List.of(txt1, txt2),
+ mRegistrationReceiver,
+ 16 /* listenerId */);
+ mTestLooper.dispatchAll();
+
+ verify(mMockNsdManager, times(1))
+ .registerService(
+ actualServiceInfoCaptor.capture(),
+ eq(PROTOCOL_DNS_SD),
+ any(Executor.class),
+ actualRegistrationListenerCaptor.capture());
+ NsdManager.RegistrationListener actualListener1 =
+ actualRegistrationListenerCaptor.getValue();
+ actualListener1.onServiceRegistered(actualServiceInfoCaptor.getValue());
+
+ mNsdPublisher.registerService(
+ null,
+ "MyService2",
+ "_test._udp",
+ Collections.emptyList(),
+ 11111,
+ Collections.emptyList(),
+ mRegistrationReceiver,
+ 17 /* listenerId */);
+
+ mTestLooper.dispatchAll();
+
+ verify(mMockNsdManager, times(2))
+ .registerService(
+ actualServiceInfoCaptor.capture(),
+ eq(PROTOCOL_DNS_SD),
+ any(Executor.class),
+ actualRegistrationListenerCaptor.capture());
+ NsdManager.RegistrationListener actualListener2 =
+ actualRegistrationListenerCaptor.getAllValues().get(1);
+ actualListener2.onServiceRegistered(actualServiceInfoCaptor.getValue());
+
+ mNsdPublisher.onOtDaemonDied();
+ mTestLooper.dispatchAll();
+
+ verify(mMockNsdManager, times(1)).unregisterService(actualListener1);
+ verify(mMockNsdManager, times(1)).unregisterService(actualListener2);
+ }
+
+ private static DnsTxtAttribute makeTxtAttribute(String name, List<Integer> value) {
+ DnsTxtAttribute txtAttribute = new DnsTxtAttribute();
+
+ txtAttribute.name = name;
+ txtAttribute.value = new byte[value.size()];
+
+ for (int i = 0; i < value.size(); ++i) {
+ txtAttribute.value[i] = value.get(i).byteValue();
+ }
+
+ return txtAttribute;
+ }
+
+ // @Before and @Test run in different threads. NsdPublisher requires the jobs are run on the
+ // thread looper, so TestLooper needs to be created inside each test case to install the
+ // correct looper.
+ private void prepareTest() {
+ mTestLooper = new TestLooper();
+ Handler handler = new Handler(mTestLooper.getLooper());
+ mNsdPublisher = new NsdPublisher(mMockNsdManager, handler);
+ }
+}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
new file mode 100644
index 0000000..60a5f2b
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.thread;
+
+import static android.net.thread.ActiveOperationalDataset.CHANNEL_PAGE_24_GHZ;
+import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
+import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
+import static android.net.thread.ThreadNetworkException.ERROR_FAILED_PRECONDITION;
+import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
+import static android.net.thread.ThreadNetworkManager.DISALLOW_THREAD_NETWORK;
+import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
+
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_INVALID_STATE;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+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.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.NetworkAgent;
+import android.net.NetworkProvider;
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.IActiveOperationalDatasetReceiver;
+import android.net.thread.IOperationReceiver;
+import android.net.thread.ThreadNetworkException;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.UserManager;
+import android.os.test.TestLooper;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.thread.openthread.testing.FakeOtDaemon;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicReference;
+
+/** Unit tests for {@link ThreadNetworkControllerService}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ThreadNetworkControllerServiceTest {
+ // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset new":
+ // Active Timestamp: 1
+ // Channel: 19
+ // Channel Mask: 0x07FFF800
+ // Ext PAN ID: ACC214689BC40BDF
+ // Mesh Local Prefix: fd64:db12:25f4:7e0b::/64
+ // Network Key: F26B3153760F519A63BAFDDFFC80D2AF
+ // Network Name: OpenThread-d9a0
+ // PAN ID: 0xD9A0
+ // PSKc: A245479C836D551B9CA557F7B9D351B4
+ // Security Policy: 672 onrcb
+ private static final byte[] DEFAULT_ACTIVE_DATASET_TLVS =
+ base16().decode(
+ "0E080000000000010000000300001335060004001FFFE002"
+ + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+ + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+ + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ + "B9D351B40C0402A0FFF8");
+ private static final ActiveOperationalDataset DEFAULT_ACTIVE_DATASET =
+ ActiveOperationalDataset.fromThreadTlvs(DEFAULT_ACTIVE_DATASET_TLVS);
+ private static final String DEFAULT_NETWORK_NAME = "thread-wpan0";
+ private static final int OT_ERROR_NONE = 0;
+ private static final int DEFAULT_SUPPORTED_CHANNEL_MASK = 0x07FFF800; // from channel 11 to 26
+ private static final int DEFAULT_PREFERRED_CHANNEL_MASK = 0x00000800; // channel 11
+ private static final int DEFAULT_SELECTED_CHANNEL = 11;
+ private static final byte[] DEFAULT_SUPPORTED_CHANNEL_MASK_ARRAY = base16().decode("001FFFE0");
+
+ @Mock private ConnectivityManager mMockConnectivityManager;
+ @Mock private NetworkAgent mMockNetworkAgent;
+ @Mock private TunInterfaceController mMockTunIfController;
+ @Mock private ParcelFileDescriptor mMockTunFd;
+ @Mock private InfraInterfaceController mMockInfraIfController;
+ @Mock private ThreadPersistentSettings mMockPersistentSettings;
+ @Mock private NsdPublisher mMockNsdPublisher;
+ @Mock private UserManager mMockUserManager;
+ @Mock private IBinder mIBinder;
+ private Context mContext;
+ private TestLooper mTestLooper;
+ private FakeOtDaemon mFakeOtDaemon;
+ private ThreadNetworkControllerService mService;
+ @Captor private ArgumentCaptor<ActiveOperationalDataset> mActiveDatasetCaptor;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mContext = spy(ApplicationProvider.getApplicationContext());
+ doNothing()
+ .when(mContext)
+ .enforceCallingOrSelfPermission(
+ eq(PERMISSION_THREAD_NETWORK_PRIVILEGED), anyString());
+
+ mTestLooper = new TestLooper();
+ final Handler handler = new Handler(mTestLooper.getLooper());
+ NetworkProvider networkProvider =
+ new NetworkProvider(mContext, mTestLooper.getLooper(), "ThreadNetworkProvider");
+
+ mFakeOtDaemon = new FakeOtDaemon(handler);
+ when(mMockTunIfController.getTunFd()).thenReturn(mMockTunFd);
+
+ when(mMockPersistentSettings.get(any())).thenReturn(true);
+ when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
+
+ mService =
+ new ThreadNetworkControllerService(
+ mContext,
+ handler,
+ networkProvider,
+ () -> mFakeOtDaemon,
+ mMockConnectivityManager,
+ mMockTunIfController,
+ mMockInfraIfController,
+ mMockPersistentSettings,
+ mMockNsdPublisher,
+ mMockUserManager);
+ mService.setTestNetworkAgent(mMockNetworkAgent);
+ }
+
+ @Test
+ public void initialize_tunInterfaceAndNsdPublisherSetToOtDaemon() throws Exception {
+ when(mMockTunIfController.getTunFd()).thenReturn(mMockTunFd);
+
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ verify(mMockTunIfController, times(1)).createTunInterface();
+ assertThat(mFakeOtDaemon.getTunFd()).isEqualTo(mMockTunFd);
+ assertThat(mFakeOtDaemon.getNsdPublisher()).isEqualTo(mMockNsdPublisher);
+ }
+
+ @Test
+ public void join_otDaemonRemoteFailure_returnsInternalError() throws Exception {
+ mService.initialize();
+ final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+ mFakeOtDaemon.setJoinException(new RemoteException("ot-daemon join() throws"));
+
+ mService.join(DEFAULT_ACTIVE_DATASET, mockReceiver);
+ mTestLooper.dispatchAll();
+
+ verify(mockReceiver, never()).onSuccess();
+ verify(mockReceiver, times(1)).onError(eq(ERROR_INTERNAL_ERROR), anyString());
+ }
+
+ @Test
+ public void join_succeed_threadNetworkRegistered() throws Exception {
+ mService.initialize();
+ final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+
+ mService.join(DEFAULT_ACTIVE_DATASET, mockReceiver);
+ // Here needs to call Testlooper#dispatchAll twices because TestLooper#moveTimeForward
+ // operates on only currently enqueued messages but the delayed message is posted from
+ // another Handler task.
+ mTestLooper.dispatchAll();
+ mTestLooper.moveTimeForward(FakeOtDaemon.JOIN_DELAY.toMillis() + 100);
+ mTestLooper.dispatchAll();
+
+ verify(mockReceiver, times(1)).onSuccess();
+ verify(mMockNetworkAgent, times(1)).register();
+ }
+
+ @Test
+ public void userRestriction_initWithUserRestricted_threadIsDisabled() {
+ when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
+
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
+ }
+
+ @Test
+ public void userRestriction_initWithUserNotRestricted_threadIsEnabled() {
+ when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
+
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
+ }
+
+ @Test
+ public void userRestriction_userBecomesRestricted_stateIsDisabledButNotPersisted() {
+ AtomicReference<BroadcastReceiver> receiverRef = new AtomicReference<>();
+ when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
+ doAnswer(
+ invocation -> {
+ receiverRef.set((BroadcastReceiver) invocation.getArguments()[0]);
+ return null;
+ })
+ .when(mContext)
+ .registerReceiver(any(BroadcastReceiver.class), any(), any(), any());
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
+ receiverRef.get().onReceive(mContext, new Intent());
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
+ verify(mMockPersistentSettings, never())
+ .put(eq(ThreadPersistentSettings.THREAD_ENABLED.key), eq(false));
+ }
+
+ @Test
+ public void userRestriction_userBecomesNotRestricted_stateIsEnabledButNotPersisted() {
+ AtomicReference<BroadcastReceiver> receiverRef = new AtomicReference<>();
+ when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
+ doAnswer(
+ invocation -> {
+ receiverRef.set((BroadcastReceiver) invocation.getArguments()[0]);
+ return null;
+ })
+ .when(mContext)
+ .registerReceiver(any(BroadcastReceiver.class), any(), any(), any());
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
+ receiverRef.get().onReceive(mContext, new Intent());
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
+ verify(mMockPersistentSettings, never())
+ .put(eq(ThreadPersistentSettings.THREAD_ENABLED.key), eq(true));
+ }
+
+ @Test
+ public void userRestriction_setEnabledWhenUserRestricted_failedPreconditionError() {
+ when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
+ mService.initialize();
+
+ CompletableFuture<Void> setEnabledFuture = new CompletableFuture<>();
+ mService.setEnabled(true, newOperationReceiver(setEnabledFuture));
+ mTestLooper.dispatchAll();
+
+ var thrown = assertThrows(ExecutionException.class, () -> setEnabledFuture.get());
+ ThreadNetworkException failure = (ThreadNetworkException) thrown.getCause();
+ assertThat(failure.getErrorCode()).isEqualTo(ERROR_FAILED_PRECONDITION);
+ }
+
+ private static IOperationReceiver newOperationReceiver(CompletableFuture<Void> future) {
+ return new IOperationReceiver.Stub() {
+ @Override
+ public void onSuccess() {
+ future.complete(null);
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) {
+ future.completeExceptionally(new ThreadNetworkException(errorCode, errorMessage));
+ }
+ };
+ }
+
+ @Test
+ public void createRandomizedDataset_succeed_activeDatasetCreated() throws Exception {
+ final IActiveOperationalDatasetReceiver mockReceiver =
+ mock(IActiveOperationalDatasetReceiver.class);
+ mFakeOtDaemon.setChannelMasks(
+ DEFAULT_SUPPORTED_CHANNEL_MASK, DEFAULT_PREFERRED_CHANNEL_MASK);
+ mFakeOtDaemon.setChannelMasksReceiverOtError(OT_ERROR_NONE);
+
+ mService.createRandomizedDataset(DEFAULT_NETWORK_NAME, mockReceiver);
+ mTestLooper.dispatchAll();
+
+ verify(mockReceiver, never()).onError(anyInt(), anyString());
+ verify(mockReceiver, times(1)).onSuccess(mActiveDatasetCaptor.capture());
+ ActiveOperationalDataset activeDataset = mActiveDatasetCaptor.getValue();
+ assertThat(activeDataset.getNetworkName()).isEqualTo(DEFAULT_NETWORK_NAME);
+ assertThat(activeDataset.getChannelMask().size()).isEqualTo(1);
+ assertThat(activeDataset.getChannelMask().get(CHANNEL_PAGE_24_GHZ))
+ .isEqualTo(DEFAULT_SUPPORTED_CHANNEL_MASK_ARRAY);
+ assertThat(activeDataset.getChannel()).isEqualTo(DEFAULT_SELECTED_CHANNEL);
+ }
+
+ @Test
+ public void createRandomizedDataset_otDaemonRemoteFailure_returnsPreconditionError()
+ throws Exception {
+ final IActiveOperationalDatasetReceiver mockReceiver =
+ mock(IActiveOperationalDatasetReceiver.class);
+ mFakeOtDaemon.setChannelMasksReceiverOtError(OT_ERROR_INVALID_STATE);
+ when(mockReceiver.asBinder()).thenReturn(mIBinder);
+
+ mService.createRandomizedDataset(DEFAULT_NETWORK_NAME, mockReceiver);
+ mTestLooper.dispatchAll();
+
+ verify(mockReceiver, never()).onSuccess(any(ActiveOperationalDataset.class));
+ verify(mockReceiver, times(1)).onError(eq(ERROR_INTERNAL_ERROR), anyString());
+ }
+}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java
new file mode 100644
index 0000000..5ca6511
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java
@@ -0,0 +1,469 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.thread;
+
+import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
+
+import static com.android.server.thread.ThreadNetworkCountryCode.DEFAULT_COUNTRY_CODE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyDouble;
+import static org.mockito.Mockito.anyFloat;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.location.Address;
+import android.location.Geocoder;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.net.thread.IOperationReceiver;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.ActiveCountryCodeChangedCallback;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+
+import androidx.annotation.Nullable;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.connectivity.resources.R;
+import com.android.server.connectivity.ConnectivityResources;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.stubbing.Answer;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+
+/** Unit tests for {@link ThreadNetworkCountryCode}. */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ThreadNetworkCountryCodeTest {
+ private static final String TEST_COUNTRY_CODE_US = "US";
+ private static final String TEST_COUNTRY_CODE_CN = "CN";
+ private static final String TEST_COUNTRY_CODE_INVALID = "INVALID";
+ private static final String TEST_WIFI_DEFAULT_COUNTRY_CODE = "00";
+ private static final int TEST_SIM_SLOT_INDEX_0 = 0;
+ private static final int TEST_SIM_SLOT_INDEX_1 = 1;
+
+ @Mock Context mContext;
+ @Mock LocationManager mLocationManager;
+ @Mock Geocoder mGeocoder;
+ @Mock ThreadNetworkControllerService mThreadNetworkControllerService;
+ @Mock PackageManager mPackageManager;
+ @Mock Location mLocation;
+ @Mock Resources mResources;
+ @Mock ConnectivityResources mConnectivityResources;
+ @Mock WifiManager mWifiManager;
+ @Mock SubscriptionManager mSubscriptionManager;
+ @Mock TelephonyManager mTelephonyManager;
+ @Mock List<SubscriptionInfo> mSubscriptionInfoList;
+ @Mock SubscriptionInfo mSubscriptionInfo0;
+ @Mock SubscriptionInfo mSubscriptionInfo1;
+
+ private ThreadNetworkCountryCode mThreadNetworkCountryCode;
+ private boolean mErrorSetCountryCode;
+
+ @Captor private ArgumentCaptor<LocationListener> mLocationListenerCaptor;
+ @Captor private ArgumentCaptor<Geocoder.GeocodeListener> mGeocodeListenerCaptor;
+ @Captor private ArgumentCaptor<IOperationReceiver> mOperationReceiverCaptor;
+ @Captor private ArgumentCaptor<ActiveCountryCodeChangedCallback> mWifiCountryCodeReceiverCaptor;
+ @Captor private ArgumentCaptor<BroadcastReceiver> mTelephonyCountryCodeReceiverCaptor;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ when(mConnectivityResources.get()).thenReturn(mResources);
+ when(mResources.getBoolean(anyInt())).thenReturn(true);
+
+ when(mSubscriptionManager.getActiveSubscriptionInfoList())
+ .thenReturn(mSubscriptionInfoList);
+ Iterator<SubscriptionInfo> iteratorMock = mock(Iterator.class);
+ when(mSubscriptionInfoList.size()).thenReturn(2);
+ when(mSubscriptionInfoList.iterator()).thenReturn(iteratorMock);
+ when(iteratorMock.hasNext()).thenReturn(true).thenReturn(true).thenReturn(false);
+ when(iteratorMock.next()).thenReturn(mSubscriptionInfo0).thenReturn(mSubscriptionInfo1);
+ when(mSubscriptionInfo0.getSimSlotIndex()).thenReturn(TEST_SIM_SLOT_INDEX_0);
+ when(mSubscriptionInfo1.getSimSlotIndex()).thenReturn(TEST_SIM_SLOT_INDEX_1);
+
+ when(mLocation.getLatitude()).thenReturn(0.0);
+ when(mLocation.getLongitude()).thenReturn(0.0);
+
+ Answer setCountryCodeCallback =
+ invocation -> {
+ Object[] args = invocation.getArguments();
+ IOperationReceiver cb = (IOperationReceiver) args[1];
+
+ if (mErrorSetCountryCode) {
+ cb.onError(ERROR_INTERNAL_ERROR, new String("Invalid country code"));
+ } else {
+ cb.onSuccess();
+ }
+ return new Object();
+ };
+
+ doAnswer(setCountryCodeCallback)
+ .when(mThreadNetworkControllerService)
+ .setCountryCode(any(), any(IOperationReceiver.class));
+
+ mThreadNetworkCountryCode = newCountryCodeWithOemSource(null);
+ }
+
+ private ThreadNetworkCountryCode newCountryCodeWithOemSource(@Nullable String oemCountryCode) {
+ return new ThreadNetworkCountryCode(
+ mLocationManager,
+ mThreadNetworkControllerService,
+ mGeocoder,
+ mConnectivityResources,
+ mWifiManager,
+ mContext,
+ mTelephonyManager,
+ mSubscriptionManager,
+ oemCountryCode);
+ }
+
+ private static Address newAddress(String countryCode) {
+ Address address = new Address(Locale.ROOT);
+ address.setCountryCode(countryCode);
+ return address;
+ }
+
+ @Test
+ public void threadNetworkCountryCode_invalidOemCountryCode_illegalArgumentExceptionIsThrown() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> newCountryCodeWithOemSource(TEST_COUNTRY_CODE_INVALID));
+ }
+
+ @Test
+ public void initialize_defaultCountryCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(DEFAULT_COUNTRY_CODE);
+ }
+
+ @Test
+ public void initialize_oemCountryCodeAvailable_oemCountryCodeIsUsed() {
+ mThreadNetworkCountryCode = newCountryCodeWithOemSource(TEST_COUNTRY_CODE_US);
+
+ mThreadNetworkCountryCode.initialize();
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_US);
+ }
+
+ @Test
+ public void initialize_locationUseIsDisabled_locationFunctionIsNotCalled() {
+ when(mResources.getBoolean(R.bool.config_thread_location_use_for_country_code_enabled))
+ .thenReturn(false);
+
+ mThreadNetworkCountryCode.initialize();
+
+ verifyNoMoreInteractions(mGeocoder);
+ verifyNoMoreInteractions(mLocationManager);
+ }
+
+ @Test
+ public void locationCountryCode_locationChanged_locationCountryCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+
+ verify(mLocationManager)
+ .requestLocationUpdates(
+ anyString(), anyLong(), anyFloat(), mLocationListenerCaptor.capture());
+ mLocationListenerCaptor.getValue().onLocationChanged(mLocation);
+ verify(mGeocoder)
+ .getFromLocation(
+ anyDouble(), anyDouble(), anyInt(), mGeocodeListenerCaptor.capture());
+ mGeocodeListenerCaptor.getValue().onGeocode(List.of(newAddress(TEST_COUNTRY_CODE_US)));
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_US);
+ }
+
+ @Test
+ public void wifiCountryCode_bothWifiAndLocationAreAvailable_wifiCountryCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+ verify(mLocationManager)
+ .requestLocationUpdates(
+ anyString(), anyLong(), anyFloat(), mLocationListenerCaptor.capture());
+ mLocationListenerCaptor.getValue().onLocationChanged(mLocation);
+ verify(mGeocoder)
+ .getFromLocation(
+ anyDouble(), anyDouble(), anyInt(), mGeocodeListenerCaptor.capture());
+
+ Address mockAddress = mock(Address.class);
+ when(mockAddress.getCountryCode()).thenReturn(TEST_COUNTRY_CODE_US);
+ List<Address> addresses = List.of(mockAddress);
+ mGeocodeListenerCaptor.getValue().onGeocode(addresses);
+
+ verify(mWifiManager)
+ .registerActiveCountryCodeChangedCallback(
+ any(), mWifiCountryCodeReceiverCaptor.capture());
+ mWifiCountryCodeReceiverCaptor.getValue().onActiveCountryCodeChanged(TEST_COUNTRY_CODE_CN);
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_CN);
+ }
+
+ @Test
+ public void wifiCountryCode_wifiCountryCodeIsActive_wifiCountryCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+
+ verify(mWifiManager)
+ .registerActiveCountryCodeChangedCallback(
+ any(), mWifiCountryCodeReceiverCaptor.capture());
+ mWifiCountryCodeReceiverCaptor.getValue().onActiveCountryCodeChanged(TEST_COUNTRY_CODE_US);
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_US);
+ }
+
+ @Test
+ public void wifiCountryCode_wifiDefaultCountryCodeIsActive_wifiCountryCodeIsNotUsed() {
+ mThreadNetworkCountryCode.initialize();
+
+ verify(mWifiManager)
+ .registerActiveCountryCodeChangedCallback(
+ any(), mWifiCountryCodeReceiverCaptor.capture());
+ mWifiCountryCodeReceiverCaptor
+ .getValue()
+ .onActiveCountryCodeChanged(TEST_WIFI_DEFAULT_COUNTRY_CODE);
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode())
+ .isNotEqualTo(TEST_WIFI_DEFAULT_COUNTRY_CODE);
+ }
+
+ @Test
+ public void wifiCountryCode_wifiCountryCodeIsInactive_defaultCountryCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+ verify(mWifiManager)
+ .registerActiveCountryCodeChangedCallback(
+ any(), mWifiCountryCodeReceiverCaptor.capture());
+ mWifiCountryCodeReceiverCaptor.getValue().onActiveCountryCodeChanged(TEST_COUNTRY_CODE_US);
+
+ mWifiCountryCodeReceiverCaptor.getValue().onCountryCodeInactive();
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode())
+ .isEqualTo(ThreadNetworkCountryCode.DEFAULT_COUNTRY_CODE);
+ }
+
+ @Test
+ public void telephonyCountryCode_bothTelephonyAndLocationAvailable_telephonyCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+ verify(mLocationManager)
+ .requestLocationUpdates(
+ anyString(), anyLong(), anyFloat(), mLocationListenerCaptor.capture());
+ mLocationListenerCaptor.getValue().onLocationChanged(mLocation);
+ verify(mGeocoder)
+ .getFromLocation(
+ anyDouble(), anyDouble(), anyInt(), mGeocodeListenerCaptor.capture());
+ mGeocodeListenerCaptor.getValue().onGeocode(List.of(newAddress(TEST_COUNTRY_CODE_US)));
+
+ verify(mContext)
+ .registerReceiver(
+ mTelephonyCountryCodeReceiverCaptor.capture(),
+ any(),
+ eq(Context.RECEIVER_EXPORTED));
+ Intent intent =
+ new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED)
+ .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, TEST_COUNTRY_CODE_CN)
+ .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_0);
+ mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent);
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_CN);
+ }
+
+ @Test
+ public void telephonyCountryCode_locationIsAvailable_lastKnownTelephonyCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+ verify(mLocationManager)
+ .requestLocationUpdates(
+ anyString(), anyLong(), anyFloat(), mLocationListenerCaptor.capture());
+ mLocationListenerCaptor.getValue().onLocationChanged(mLocation);
+ verify(mGeocoder)
+ .getFromLocation(
+ anyDouble(), anyDouble(), anyInt(), mGeocodeListenerCaptor.capture());
+ mGeocodeListenerCaptor.getValue().onGeocode(List.of(newAddress(TEST_COUNTRY_CODE_US)));
+
+ verify(mContext)
+ .registerReceiver(
+ mTelephonyCountryCodeReceiverCaptor.capture(),
+ any(),
+ eq(Context.RECEIVER_EXPORTED));
+ Intent intent =
+ new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED)
+ .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, "")
+ .putExtra(
+ TelephonyManager.EXTRA_LAST_KNOWN_NETWORK_COUNTRY,
+ TEST_COUNTRY_CODE_US)
+ .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_0);
+ mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent);
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_US);
+ }
+
+ @Test
+ public void telephonyCountryCode_lastKnownCountryCodeAvailable_telephonyCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+ verify(mContext)
+ .registerReceiver(
+ mTelephonyCountryCodeReceiverCaptor.capture(),
+ any(),
+ eq(Context.RECEIVER_EXPORTED));
+ Intent intent0 =
+ new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED)
+ .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, "")
+ .putExtra(
+ TelephonyManager.EXTRA_LAST_KNOWN_NETWORK_COUNTRY,
+ TEST_COUNTRY_CODE_US)
+ .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_0);
+ mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent0);
+
+ verify(mContext)
+ .registerReceiver(
+ mTelephonyCountryCodeReceiverCaptor.capture(),
+ any(),
+ eq(Context.RECEIVER_EXPORTED));
+ Intent intent1 =
+ new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED)
+ .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, TEST_COUNTRY_CODE_CN)
+ .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_1);
+ mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent1);
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_CN);
+ }
+
+ @Test
+ public void telephonyCountryCode_multipleSims_firstSimIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+ verify(mContext)
+ .registerReceiver(
+ mTelephonyCountryCodeReceiverCaptor.capture(),
+ any(),
+ eq(Context.RECEIVER_EXPORTED));
+ Intent intent1 =
+ new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED)
+ .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, TEST_COUNTRY_CODE_CN)
+ .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_1);
+ mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent1);
+
+ Intent intent0 =
+ new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED)
+ .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, TEST_COUNTRY_CODE_CN)
+ .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_0);
+ mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent0);
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_CN);
+ }
+
+ @Test
+ public void updateCountryCode_noForceUpdateDefaultCountryCode_noCountryCodeIsUpdated() {
+ mThreadNetworkCountryCode.initialize();
+ clearInvocations(mThreadNetworkControllerService);
+
+ mThreadNetworkCountryCode.updateCountryCode(false /* forceUpdate */);
+
+ verify(mThreadNetworkControllerService, never()).setCountryCode(any(), any());
+ }
+
+ @Test
+ public void updateCountryCode_forceUpdateDefaultCountryCode_countryCodeIsUpdated() {
+ mThreadNetworkCountryCode.initialize();
+ clearInvocations(mThreadNetworkControllerService);
+
+ mThreadNetworkCountryCode.updateCountryCode(true /* forceUpdate */);
+
+ verify(mThreadNetworkControllerService)
+ .setCountryCode(eq(DEFAULT_COUNTRY_CODE), mOperationReceiverCaptor.capture());
+ }
+
+ @Test
+ public void setOverrideCountryCode_defaultCountryCodeAvailable_overrideCountryCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+
+ mThreadNetworkCountryCode.setOverrideCountryCode(TEST_COUNTRY_CODE_CN);
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_CN);
+ }
+
+ @Test
+ public void clearOverrideCountryCode_defaultCountryCodeAvailable_defaultCountryCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+ mThreadNetworkCountryCode.setOverrideCountryCode(TEST_COUNTRY_CODE_CN);
+
+ mThreadNetworkCountryCode.clearOverrideCountryCode();
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(DEFAULT_COUNTRY_CODE);
+ }
+
+ @Test
+ public void setCountryCodeFailed_defaultCountryCodeAvailable_countryCodeIsNotUpdated() {
+ mThreadNetworkCountryCode.initialize();
+
+ mErrorSetCountryCode = true;
+ mThreadNetworkCountryCode.setOverrideCountryCode(TEST_COUNTRY_CODE_CN);
+
+ verify(mThreadNetworkControllerService)
+ .setCountryCode(eq(TEST_COUNTRY_CODE_CN), mOperationReceiverCaptor.capture());
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(DEFAULT_COUNTRY_CODE);
+ }
+
+ @Test
+ public void dump_allCountryCodeInfoAreDumped() {
+ StringWriter stringWriter = new StringWriter();
+ PrintWriter printWriter = new PrintWriter(stringWriter);
+
+ mThreadNetworkCountryCode.dump(new FileDescriptor(), printWriter, null);
+ String outputString = stringWriter.toString();
+
+ assertThat(outputString).contains("mOverrideCountryCodeInfo");
+ assertThat(outputString).contains("mTelephonyCountryCodeSlotInfoMap");
+ assertThat(outputString).contains("mTelephonyCountryCodeInfo");
+ assertThat(outputString).contains("mWifiCountryCodeInfo");
+ assertThat(outputString).contains("mTelephonyLastCountryCodeInfo");
+ assertThat(outputString).contains("mLocationCountryCodeInfo");
+ assertThat(outputString).contains("mOemCountryCodeInfo");
+ assertThat(outputString).contains("mCurrentCountryCodeInfo");
+ }
+}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java
new file mode 100644
index 0000000..c7e0eca
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.thread;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.contains;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.validateMockitoUsage;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.os.Binder;
+import android.os.Process;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/** Unit tests for {@link ThreadNetworkShellCommand}. */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ThreadNetworkShellCommandTest {
+ private static final String TAG = "ThreadNetworkShellCommandTTest";
+ @Mock ThreadNetworkService mThreadNetworkService;
+ @Mock ThreadNetworkCountryCode mThreadNetworkCountryCode;
+ @Mock PrintWriter mErrorWriter;
+ @Mock PrintWriter mOutputWriter;
+
+ ThreadNetworkShellCommand mThreadNetworkShellCommand;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mThreadNetworkShellCommand = new ThreadNetworkShellCommand(mThreadNetworkCountryCode);
+ mThreadNetworkShellCommand.setPrintWriters(mOutputWriter, mErrorWriter);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ validateMockitoUsage();
+ }
+
+ @Test
+ public void getCountryCode_executeInUnrootedShell_allowed() {
+ BinderUtil.setUid(Process.SHELL_UID);
+ when(mThreadNetworkCountryCode.getCountryCode()).thenReturn("US");
+
+ mThreadNetworkShellCommand.exec(
+ new Binder(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new String[] {"get-country-code"});
+
+ verify(mOutputWriter).println(contains("US"));
+ }
+
+ @Test
+ public void forceSetCountryCodeEnabled_executeInUnrootedShell_notAllowed() {
+ BinderUtil.setUid(Process.SHELL_UID);
+
+ mThreadNetworkShellCommand.exec(
+ new Binder(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new String[] {"force-country-code", "enabled", "US"});
+
+ verify(mThreadNetworkCountryCode, never()).setOverrideCountryCode(eq("US"));
+ verify(mErrorWriter).println(contains("force-country-code"));
+ }
+
+ @Test
+ public void forceSetCountryCodeEnabled_executeInRootedShell_allowed() {
+ BinderUtil.setUid(Process.ROOT_UID);
+
+ mThreadNetworkShellCommand.exec(
+ new Binder(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new String[] {"force-country-code", "enabled", "US"});
+
+ verify(mThreadNetworkCountryCode).setOverrideCountryCode(eq("US"));
+ }
+
+ @Test
+ public void forceSetCountryCodeDisabled_executeInUnrootedShell_notAllowed() {
+ BinderUtil.setUid(Process.SHELL_UID);
+
+ mThreadNetworkShellCommand.exec(
+ new Binder(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new String[] {"force-country-code", "disabled"});
+
+ verify(mThreadNetworkCountryCode, never()).setOverrideCountryCode(any());
+ verify(mErrorWriter).println(contains("force-country-code"));
+ }
+
+ @Test
+ public void forceSetCountryCodeDisabled_executeInRootedShell_allowed() {
+ BinderUtil.setUid(Process.ROOT_UID);
+
+ mThreadNetworkShellCommand.exec(
+ new Binder(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new String[] {"force-country-code", "disabled"});
+
+ verify(mThreadNetworkCountryCode).clearOverrideCountryCode();
+ }
+}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
new file mode 100644
index 0000000..49b002a
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.thread;
+
+import static com.android.server.thread.ThreadPersistentSettings.THREAD_ENABLED;
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.validateMockitoUsage;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.res.Resources;
+import android.os.PersistableBundle;
+import android.util.AtomicFile;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+import com.android.connectivity.resources.R;
+import com.android.server.connectivity.ConnectivityResources;
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link ThreadPersistentSettings}. */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ThreadPersistentSettingsTest {
+ @Mock private AtomicFile mAtomicFile;
+ @Mock Resources mResources;
+ @Mock ConnectivityResources mConnectivityResources;
+
+ private ThreadPersistentSettings mThreadPersistentSettings;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ when(mConnectivityResources.get()).thenReturn(mResources);
+ when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(true);
+
+ FileOutputStream fos = mock(FileOutputStream.class);
+ when(mAtomicFile.startWrite()).thenReturn(fos);
+ mThreadPersistentSettings =
+ new ThreadPersistentSettings(mAtomicFile, mConnectivityResources);
+ }
+
+ /** Called after each test */
+ @After
+ public void tearDown() {
+ validateMockitoUsage();
+ }
+
+ @Test
+ public void initialize_readsFromFile() throws Exception {
+ byte[] data = createXmlForParsing(THREAD_ENABLED.key, false);
+ setupAtomicFileMockForRead(data);
+
+ mThreadPersistentSettings.initialize();
+
+ assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isFalse();
+ }
+
+ @Test
+ public void initialize_ThreadDisabledInResources_returnsThreadDisabled() throws Exception {
+ when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(false);
+ setupAtomicFileMockForRead(new byte[0]);
+
+ mThreadPersistentSettings.initialize();
+
+ assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isFalse();
+ }
+
+ @Test
+ public void initialize_ThreadDisabledInResourcesButEnabledInXml_returnsThreadEnabled()
+ throws Exception {
+ when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(false);
+ byte[] data = createXmlForParsing(THREAD_ENABLED.key, true);
+ setupAtomicFileMockForRead(data);
+
+ mThreadPersistentSettings.initialize();
+
+ assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isTrue();
+ }
+
+ @Test
+ public void put_ThreadFeatureEnabledTrue_returnsTrue() throws Exception {
+ mThreadPersistentSettings.put(THREAD_ENABLED.key, true);
+
+ assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isTrue();
+ // Confirm that file writes have been triggered.
+ verify(mAtomicFile).startWrite();
+ verify(mAtomicFile).finishWrite(any());
+ }
+
+ @Test
+ public void put_ThreadFeatureEnabledFalse_returnsFalse() throws Exception {
+ mThreadPersistentSettings.put(THREAD_ENABLED.key, false);
+
+ assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isFalse();
+ // Confirm that file writes have been triggered.
+ verify(mAtomicFile).startWrite();
+ verify(mAtomicFile).finishWrite(any());
+ }
+
+ private byte[] createXmlForParsing(String key, Boolean value) throws Exception {
+ PersistableBundle bundle = new PersistableBundle();
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ bundle.putBoolean(key, value);
+ bundle.writeToStream(outputStream);
+ return outputStream.toByteArray();
+ }
+
+ private void setupAtomicFileMockForRead(byte[] dataToRead) throws Exception {
+ FileInputStream is = mock(FileInputStream.class);
+ when(mAtomicFile.openRead()).thenReturn(is);
+ when(is.available()).thenReturn(dataToRead.length).thenReturn(0);
+ doAnswer(
+ invocation -> {
+ byte[] data = invocation.getArgument(0);
+ int pos = invocation.getArgument(1);
+ if (pos == dataToRead.length) return 0; // read complete.
+ System.arraycopy(dataToRead, 0, data, 0, dataToRead.length);
+ return dataToRead.length;
+ })
+ .when(is)
+ .read(any(), anyInt(), anyInt());
+ }
+}
diff --git a/thread/tests/utils/Android.bp b/thread/tests/utils/Android.bp
new file mode 100644
index 0000000..24e9bb9
--- /dev/null
+++ b/thread/tests/utils/Android.bp
@@ -0,0 +1,37 @@
+//
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ default_team: "trendy_team_fwk_thread_network",
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+ name: "ThreadNetworkTestUtils",
+ min_sdk_version: "30",
+ static_libs: [
+ "compatibility-device-util-axt",
+ "net-tests-utils",
+ "net-utils-device-common",
+ "net-utils-device-common-bpf",
+ ],
+ srcs: [
+ "src/**/*.java",
+ ],
+ defaults: [
+ "framework-connectivity-test-defaults",
+ ],
+}
diff --git a/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java b/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java
new file mode 100644
index 0000000..43f177d
--- /dev/null
+++ b/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.thread.utils;
+
+import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
+import static android.net.InetAddresses.parseNumericAddress;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.SOCK_DGRAM;
+
+import static com.android.testutils.RecorderCallback.CallbackEntry.LINK_PROPERTIES_CHANGED;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.TestNetworkInterface;
+import android.net.TestNetworkManager;
+import android.net.TestNetworkSpecifier;
+import android.os.Looper;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import com.android.compatibility.common.util.PollingCheck;
+import com.android.testutils.TestableNetworkAgent;
+import com.android.testutils.TestableNetworkCallback;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** A class that can create/destroy a test network based on TAP interface. */
+public final class TapTestNetworkTracker {
+ private static final Duration TIMEOUT = Duration.ofSeconds(2);
+ private final Context mContext;
+ private final Looper mLooper;
+ private TestNetworkInterface mInterface;
+ private TestableNetworkAgent mAgent;
+ private final TestableNetworkCallback mNetworkCallback;
+ private final ConnectivityManager mConnectivityManager;
+
+ /**
+ * Constructs a {@link TapTestNetworkTracker}.
+ *
+ * <p>It creates a TAP interface (e.g. testtap0) and registers a test network using that
+ * interface. It also requests the test network by {@link ConnectivityManager#requestNetwork} so
+ * the test network won't be automatically turned down by {@link
+ * com.android.server.ConnectivityService}.
+ */
+ public TapTestNetworkTracker(Context context, Looper looper) {
+ mContext = context;
+ mLooper = looper;
+ mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
+ mNetworkCallback = new TestableNetworkCallback();
+ runAsShell(MANAGE_TEST_NETWORKS, this::setUpTestNetwork);
+ }
+
+ /** Tears down the test network. */
+ public void tearDown() {
+ runAsShell(MANAGE_TEST_NETWORKS, this::tearDownTestNetwork);
+ }
+
+ /** Returns the interface name of the test network. */
+ public String getInterfaceName() {
+ return mInterface.getInterfaceName();
+ }
+
+ private void setUpTestNetwork() throws Exception {
+ mInterface = mContext.getSystemService(TestNetworkManager.class).createTapInterface();
+
+ mConnectivityManager.requestNetwork(newNetworkRequest(), mNetworkCallback);
+
+ LinkProperties lp = new LinkProperties();
+ lp.setInterfaceName(getInterfaceName());
+ mAgent =
+ new TestableNetworkAgent(
+ mContext,
+ mLooper,
+ newNetworkCapabilities(),
+ lp,
+ new NetworkAgentConfig.Builder().build());
+ final Network network = mAgent.register();
+ mAgent.markConnected();
+
+ PollingCheck.check(
+ "No usable address on interface",
+ TIMEOUT.toMillis(),
+ () -> hasUsableAddress(network, getInterfaceName()));
+
+ lp.setLinkAddresses(makeLinkAddresses());
+ mAgent.sendLinkProperties(lp);
+ mNetworkCallback.eventuallyExpect(
+ LINK_PROPERTIES_CHANGED,
+ TIMEOUT.toMillis(),
+ l -> !l.getLp().getAddresses().isEmpty());
+ }
+
+ private void tearDownTestNetwork() throws IOException {
+ mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
+ mAgent.unregister();
+ mInterface.getFileDescriptor().close();
+ mAgent.waitForIdle(TIMEOUT.toMillis());
+ }
+
+ private NetworkRequest newNetworkRequest() {
+ return new NetworkRequest.Builder()
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .addTransportType(TRANSPORT_TEST)
+ .setNetworkSpecifier(new TestNetworkSpecifier(getInterfaceName()))
+ .build();
+ }
+
+ private NetworkCapabilities newNetworkCapabilities() {
+ return new NetworkCapabilities()
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .addTransportType(TRANSPORT_TEST)
+ .setNetworkSpecifier(new TestNetworkSpecifier(getInterfaceName()));
+ }
+
+ private List<LinkAddress> makeLinkAddresses() {
+ List<LinkAddress> linkAddresses = new ArrayList<>();
+ List<InterfaceAddress> interfaceAddresses = Collections.emptyList();
+
+ try {
+ interfaceAddresses =
+ NetworkInterface.getByName(getInterfaceName()).getInterfaceAddresses();
+ } catch (SocketException ignored) {
+ // Ignore failures when getting the addresses.
+ }
+
+ for (InterfaceAddress address : interfaceAddresses) {
+ linkAddresses.add(
+ new LinkAddress(address.getAddress(), address.getNetworkPrefixLength()));
+ }
+
+ return linkAddresses;
+ }
+
+ private static boolean hasUsableAddress(Network network, String interfaceName) {
+ try {
+ if (NetworkInterface.getByName(interfaceName).getInterfaceAddresses().isEmpty()) {
+ return false;
+ }
+ } catch (SocketException e) {
+ return false;
+ }
+ // Check if the link-local address can be used. Address flags are not available without
+ // elevated permissions, so check that bindSocket works.
+ try {
+ FileDescriptor sock = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP);
+ network.bindSocket(sock);
+ Os.connect(sock, parseNumericAddress("ff02::fb%" + interfaceName), 12345);
+ Os.close(sock);
+ } catch (ErrnoException | IOException e) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/tools/Android.bp b/tools/Android.bp
index 3ce76f6..9216b5b 100644
--- a/tools/Android.bp
+++ b/tools/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -41,6 +42,7 @@
name: "jarjar-rules-generator-testjavalib",
srcs: ["testdata/java/**/*.java"],
libs: ["unsupportedappusage"],
+ sdk_version: "core_platform",
visibility: ["//visibility:private"],
}
@@ -55,6 +57,7 @@
static_libs: [
"framework-connectivity.stubs.module_lib",
],
+ sdk_version: "module_current",
// Not strictly necessary but specified as this MUST not have generate
// a dex jar as that will break the tests.
compile_dex: false,
@@ -66,6 +69,7 @@
static_libs: [
"framework-connectivity-t.stubs.module_lib",
],
+ sdk_version: "module_current",
// Not strictly necessary but specified as this MUST not have generate
// a dex jar as that will break the tests.
compile_dex: false,